Обход отношения ManyToMany для добавления в контекст в Django DetailView - PullRequest
1 голос
/ 01 апреля 2020

Недавно я задал похожий вопрос о другом проекте. В этом случае (см. Обход нескольких внешних ключей в Django DetailView ), я смог проследить за потоком внешних ключей, чтобы установить необходимое соединение при определении представления. В этом случае мне нужно пройти через таблицу соединений ManyToMany, и я зашел в тупик. Вот диаграмма ER соответствующей части моей модели.

┌────────────┐        ┌────────────┐       ┌───────────────┐       ┌────────────┐
│            │       ╱│            │      ╱│               │╲      │            │
│  Company   │─────┼──│  Product   │────┼──│ PersonProduct │──┼────│   Person   │
│            │       ╲│            │      ╲│               │╱      │            │
└────────────┘        └────────────┘       └───────────────┘       └────────────┘

Цель этого веб-сайта - соединить профессиональных лучников с оборудованием и компаниями, которые их спонсируют, и отобразить эти связи. В этом конкретном случае у меня есть подробный вид для компании. Я успешно настроил его для отображения всех продуктов, которые компания продает. Теперь я хочу отобразить список всех лучников, которые используют один или несколько продуктов этой компании.

Я не могу понять, как пройти весь путь от компании до люди, которые используют эти продукты.

Вот модель, представление и шаблон, с которым я работаю.

# models.py

from django.db import models
from datetime import date
from django.urls import reverse
from django.utils import timezone


def calculateAge(dob):
    today = date.today()
    try:
        birthday = dob.replace(year=today.year)

        # raised when birth date is February 29
    # and the current year is not a leap year 
    except ValueError:
        birthday = dob.replace(year=today.year,
                               month=dob.month + 1, day=1)

    if birthday > today:
        return today.year - dob.year - 1
    else:
        return today.year - dob.year


class Product(models.Model):
    product_type = models.ForeignKey(
        'ProductType',
        on_delete=models.CASCADE,
    )
    product_company = models.ForeignKey(
        "Company",
        on_delete=models.CASCADE,
    )
    name = models.CharField(
        "Product Name",
        max_length=75,
        help_text="""If you don't want to enter a specific product, enter the same
        value as the product type.""",
    )
    slug = models.SlugField(null=False, unique=True)
    product_url = models.URLField("Product URL", blank=True)
    is_for_sale = models.BooleanField(
        "Currently For Sale",
        default=True,
    )
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['product_company']

    def __str__(self):
        return "%s %s (%s)" % (self.product_company, self.name, self.product_type.name)

    def get_absolute_url(self):
        return reverse('product_detail', kwargs={'slug': self.slug})


class Person(models.Model):
    first_name = models.CharField(max_length=50)
    middle_name = models.CharField(max_length=50, blank=True)
    last_name = models.CharField(max_length=75, db_index=True)
    name_suffix = models.ForeignKey(
        'NameSuffix',
        null=True,
        blank=True,
        on_delete=models.CASCADE,
    )
    nickname = models.CharField(max_length=75, blank=True)
    slug = models.SlugField(null=False, unique=True)
    products = models.ManyToManyField(
        Product,
        through="PersonProduct",
        through_fields=('person', 'product'),
        related_name="people",
    )
    headshot = models.ImageField(
        upload_to='headshots/',
        blank=True,
        default='headshots/missing.png',
    )
    gender = models.ForeignKey(
        'Gender',
        on_delete=models.CASCADE,
    )
    country = models.ForeignKey(
        'Country',
        on_delete=models.CASCADE,
    )
    dob = models.DateField("Date of Birth", blank=True, null=True)
    print_surname_first = models.BooleanField(
        "Print surname first",
        default=False,
    )
    twitter_username = models.CharField(
        "Twitter",
        max_length=20,
        unique=True,
        blank=True,
        null=True,
        help_text='Please use the username only. No "@" is required.',
    )
    instagram_username = models.CharField(
        "Instagram",
        max_length=30,
        unique=True,
        blank=True,
        null=True,
        help_text='Username only.',
    )
    facebook_username = models.CharField(
        "Facebook",
        max_length=30,
        unique=True,
        blank=True,
        null=True,
        help_text='Username only.',
    )
    is_retired = models.BooleanField("Retired", default=False)
    is_deceased = models.BooleanField("Deceased", default=False)
    # products = models.ManyToManyField('Product', through='PersonProduct')
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name_plural = "People"
        ordering = ['last_name']

    def __str__(self):
        if self.print_surname_first:
            return "%s %s" % (self.last_name, self.first_name)
        else:
            return "%s %s" % (self.first_name, self.last_name)

    def get_absolute_url(self):
        return reverse('person_detail', kwargs={'slug': self.slug})

    @property
    def age(self):
        return calculateAge(self.dob)

    @property
    def name_with_nickname(self):
        if self.nickname:
            if self.print_surname_first:
                return "%s “%s” %s" % (self.last_name, self.nickname, self.first_name)
            else:
                return "%s “%s” %s" % (self.first_name, self.nickname, self.last_name)
        else:
            return self

    @property
    def twitter_url(self):
        return "https://twitter.com/%s" % (self.twitter_username)

    @property
    def instagram_url(self):
        return "https://www.instagram.com/%s" % (self.instagram_username)

    @property
    def facebook_url(self):
        return "https://www.facebook.com/%s/" % (self.facebook_username)


class NameSuffix(models.Model):
    suffix = models.CharField("Suffix", max_length=15, unique=True)

    class Meta:
        verbose_name_plural = "Name Suffixes"

    def __str__(self):
        return "%s" % (self.suffix)


class Gender(models.Model):
    """
    See reference to ISD 5218:2004 for the standard definition of human sexes commonly
    used in database applications.

    https://www.iso.org/standard/36266.html
    """
    abbrev = models.CharField("Abbreviation", max_length=1, unique=True)
    name = models.CharField(max_length=30, unique=True)

    def __str__(self):
        return "%s" % (self.name)


class Country(models.Model):
    """
    See reference to ISO 3166 for reference to internationally recognized country
    codes.

    https://www.iso.org/iso-3166-country-codes.html
    """
    code_2 = models.CharField(
        "Two-character code",
        max_length=2,
        unique=True,
        help_text="""See <a href="https://www.iso.org/iso-3166-country-codes.html">
        ISO&nbsp;3166</a> to find official country codes and names."""
    )
    code_3 = models.CharField("Three-character code", max_length=3, unique=True)
    short_name = models.CharField(max_length=50, unique=True)
    full_name = models.CharField(max_length=100, unique=True)
    flag = models.CharField(
        max_length=2,
        default='',
        help_text='Please use flag emoji.')

    class Meta:
        verbose_name_plural = "Countries"
        ordering = ['short_name']

    def __str__(self):
        return "%s" % (self.short_name)


class GearSetType(models.Model):
    """
    Contains types of gear sets such at 'Field Archery' or 'Indoor NFAA'.
    """
    name = models.CharField("Name", max_length=75, unique=True)
    parent = models.ForeignKey(
        'self',
        null=True,
        blank=True,
        on_delete=models.CASCADE,
        default=6,
     )
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name_plural = "Gear Set Types"

    def __str__(self):
        return "%s" % (self.name)


class PersonProduct(models.Model):
    person = models.ForeignKey(
        Person,
        on_delete=models.CASCADE,
        db_index=True,
        related_name="person_products",
    )
    product = models.ForeignKey(
        Product,
        on_delete=models.CASCADE,
        related_name="person_products",
        db_index=True,
    )
    gear_set_type = models.ForeignKey(
        GearSetType,
        on_delete=models.CASCADE,
        db_index=True,
    )
    created = models.DateTimeField(default=timezone.now)

    class Meta:
        verbose_name_plural = "Person-Products"

    def __str__(self):
        return "%s - %s (%s)" % (self.person, self.product, self.gear_set_type)


class Activity(models.Model):
    name = models.CharField("Name", max_length=30, unique=True)
    products = models.ManyToManyField(Product, through='ActivityProduct')

    class Meta:
        verbose_name_plural = "Activities"

    def __str__(self):
        return "%s" % (self.name)


class ActivityProduct(models.Model):
    activity = models.ForeignKey(Activity, on_delete=models.CASCADE)
    product = models.ForeignKey(Product, on_delete=models.CASCADE)

    class Meta:
        verbose_name_plural = "Activity-Products"

    def __str__(self):
        return "%s (%s)" % (self.product, self.activity)


class ProductType(models.Model):
    name = models.CharField("Product Type", max_length=25, unique=True)
    category = models.ForeignKey(
        "ProductCategory",
        on_delete=models.CASCADE,
        blank=True,
        null=True,
    )
    display_sort_priority = models.PositiveIntegerField(
        help_text='Enter an integer to determine display order. Use 1 for highest priority.',
        blank=False,
        null=False,
        default=1,
    )
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name_plural = "Product Types"
        ordering = ['display_sort_priority']

    def __str__(self):
        return "%s" % (self.name)


class ProductCategory(models.Model):
    """
    Contains categories of products such as 'Bow' and 'Arrow'.
    """
    name = models.CharField("Name", max_length=50, unique=True)
    display_sort_priority = models.PositiveIntegerField(
        help_text='Enter an integer to determine display order. Use 1 for highest priority.',
        blank=False,
        null=False,
        default=1,
    )

    class Meta:
        verbose_name_plural = "Product Categories"
        ordering = ['display_sort_priority']

    def __str__(self):
        return "%s" % (self.name)


class Company(models.Model):
    short_name = models.CharField("Company Short Name", max_length=50)
    full_name = models.CharField("Company Full Name", max_length=75, blank=True)
    slug = models.SlugField(null=False, unique=True)
    address1 = models.CharField("Address (line 1)", max_length=75, blank=True)
    address2 = models.CharField("Address (line 2)", max_length=75, blank=True)
    city = models.CharField(max_length=30, blank=True)
    state = models.CharField(max_length=20, blank=True)
    postal_code = models.CharField(max_length=40, blank=True)
    country = models.ForeignKey(Country, on_delete=models.CASCADE)
    url = models.URLField("Homepage URL", blank=True)
    twitter_username = models.CharField(
        "Twitter",
        max_length=20,
        unique=True,
        blank=True,
        null=True,
        help_text='Please use the username only. No "@" is required.',
    )
    instagram_username = models.CharField(
        "Instagram",
        max_length=30,
        unique=True,
        blank=True,
        null=True,
        help_text='Username only.',
    )
    facebook_username = models.CharField(
        "Facebook",
        max_length=30,
        unique=True,
        blank=True,
        null=True,
        help_text='Username only.',
    )
    youtube_username = models.CharField(
        "YouTube",
        max_length=30,
        unique=True,
        blank=True,
        null=True,
        help_text='Username only.',
    )
    phone_info = models.CharField(max_length=30, blank=True)
    phone_support = models.CharField(max_length=30, blank=True)
    email_info = models.EmailField(blank=True)
    email_sales = models.EmailField(blank=True)
    email_support = models.EmailField(blank=True)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name_plural = "Companies"
        ordering = ['short_name']

    def __str__(self):
        return "%s" % (self.short_name)

    def get_absolute_url(self):
        return reverse('company_detail', kwargs={'slug': self.slug})

    @property
    def address_one_line(self):
        address = ""
        if self.address1:
            address = f"{self.address1}"
        if self.address2:
            address = f"{address}, {self.address2}"
        if self.city:
            address = f"{address}, {self.city}"
        if self.state:
            address = f"{address}, {self.state}"
        if self.postal_code:
            address = f"{address} {self.postal_code}"
        return address

    @property
    def twitter_url(self):
        return "https://twitter.com/%s" % (self.twitter_username)

    @property
    def instagram_url(self):
        return "https://www.instagram.com/%s" % (self.instagram_username)

    @property
    def facebook_url(self):
        return "https://www.facebook.com/%s/" % (self.facebook_username)

    @property
    def youtube_url(self):
        return "https://www.youtube.com/user/%s" % (self.youtube_username)

# views.py
class CompanyDetailView(DetailView):
    model = Company
    queryset = Company.objects.all()
    template_name = 'company_detail.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        company = self.get_object()
        context['company'] = company
        context['products'] = Product.objects.filter(product_company=company)
        context['pros'] = PersonProduct.objects.filter() # I'm trying to access all the people who use a product from this company
        return context
# company_detail.html
{% extends 'base.html' %}
{% load fontawesome_5 %}

{% block content %}

<div class="company-detail">
    <h2>{{ company }} <a href="{{ company.url }}">{%  fa5_icon 'link' 'fas' %}</a></h2>
    <p>{{ company.address_one_line }}</p>
</div>

<div class="product-list">
<h3>Products</h3>
    <ul>
    {% for product in products %}
        <li><a href="{{ product.get_absolute_url }}">{{ product.name }}</a></li>
    {% endfor %}
    </ul>
</div>

<div class="user-list">
<h3>Pros</h3>
    <ul>
    {% for pro in pros %}
        <li>{{ pro }}</li>
    {% endfor %}
    </ul>
</div>

{% endblock content %}

Есть предложения?

1 Ответ

1 голос
/ 01 апреля 2020

В общем случае метод будет следующим:

company = Company.objects.first()
Person.objects.filter(products__product_company=company)

Итак, вот ваш новый get_context_data.

def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)
    company = self.get_object()
    context['company'] = company
    context['products'] = Product.objects.filter(product_company=company)
    context['pros'] = Person.objects.filter(products__product_company=company).distinct() 
    return context

Я заметил, что ваш пример пытается использовать PersonProduct. Если вы хотите использовать это, то вам нужно:

PersonProduct.objects.filter(product__product_company=company)
...