Аннотирование связанных и мультифильтрованных объектов в Django

У меня есть набор профилей:

Модель:

class Profile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, unique=True)
    ...

Вид:

Profile.objects.select_related('user')

Каждый пользователь/профиль может регистрироваться на несколько событий в день:

Модели:

class Event(models.Model):

    title = models.CharField(max_length=120)
    date = models.DateField(default=default_event_date)
    ...


class Registration(models.Model):

    event = models.ForeignKey(Event)
    student = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    block = models.ForeignKey(Block, on_delete=models.CASCADE)
    ....

Учитывая дату, как я могу аннотировать (? Я думаю, это то, что я хочу?) Один объект регистрации на блок (отфильтрованный по пользователю/профилю и Event__Date)

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

For Date: 19 Dec 2016

User/Profile    Block A      Block B   ...
user1           None         None
user2           Event1       Event2
user3           Event3       None
...

ИЗМЕНИТЬ

Попытка 1. Вот моя первая попытка сделать это. Я подозреваю, что это ужасно неэффективно и будет очень медленным в производстве, но, по крайней мере, это работает. Если кто-то может предоставить более эффективное и элегантное решение, мы будем признательны! (Обратите внимание, что это также включает фильтр модели профиля пользователя для homeroom_teacher, который не был включен в исходный вопрос, но я оставил здесь, потому что это рабочий код)

Менеджер модели регистрации

класс RegistrationManager(models.Manager):

def homeroom_registration_check(self, event_date, homeroom_teacher):
    students = User.objects.all().filter(is_staff=False, profile__homeroom_teacher=homeroom_teacher)
    students = students.values('id', 'username', 'first_name', 'last_name')
    # convert to list of dicts so I can add 'annotate' dict elements
    students = list(students) 

    # get queryset with events? optimization for less hits on db
    registrations_qs = self.get_queryset().filter(event__date=event_date, student__profile__homeroom_teacher=homeroom_teacher)

    # append each students' dict with registration data
    for student in students:
        user_regs_qs = registrations_qs.filter(student_id=student['id'])

        for block in Block.objects.all():
            # add a new key:value for each block
            try:
                reg = user_regs_qs.get(block=block)
                student[block.constant_string()] = str(reg.event)
            except ObjectDoesNotExist:
                student[block.constant_string()] = None

    return students

Шаблон Обратите внимание, что block.constant_string() --> "ABLOCK", "BBLOCK" и т. д., это жестко закодировано в методе block.constant_string(), и я не уверен, как обойти это либо.

{% for student in students %}
  <tr >
    <td>{{ student.username }}</td>
    <td>{{ student.first_name }}</td>
    <td>{{ student.last_name }}</td>
    <td>{{ student.ABLOCK|default_if_none:'-' }}</td>
    <td>{{ student.BBLOCK|default_if_none:'-' }}</td>
  </tr>
{% endfor %}

person 43Tesseracts    schedule 19.12.2016    source источник


Ответы (2)


Чтобы решить проблему закодированных имен, я бы немного изменил ваше решение, чтобы оно выглядело так:

def homeroom_registration_check(event_date, homeroom_teacher):
    students = User.objects.filter(
        is_staff=False,
        profile__homeroom_teacher=homeroom_teacher,
    )
    block_ids = Block.objects.values('id')
    for student in students:
        table_row = []
        for block_id in block_ids:
            try:
                reg = Registration.objects.get(
                    student=student,
                    block=block_id,
                    event__date=event_date,
                )
                table_row.append(reg.event)
            except ObjectDoesNotExist:
                table_row.append(None)
        yield (student, table_row)

Я бы взял его из диспетчера моделей и поместил в views.py или в отдельный файл (например, table.py). Мне кажется чище, но это всего лишь мнение - вы можете поместить этот код в менеджер моделей, и он все равно будет работать.

Затем в вашем шаблоне:

{% for student, row in homeroom_reg_check %}
    <tr>
        <td>{{ student.username }}</td>
        <td>{{ student.other_data }}</td>
        {% for event in row %}
            <td>{{ event.name|default_if_none:'-' }}</td>
        {% endfor %}
    </tr>
{% endfor %}
person Marco Lavagnino    schedule 20.12.2016
comment
Можете ли вы прокомментировать различия в эффективности между вашим кодом и моим? Ваш ВЫГЛЯДИТ намного лучше, это точно. У вас только один вызов db? - person 43Tesseracts; 20.12.2016
comment
@43Tesseracts мой фрагмент возвращает генератор, который дает кортеж с объектом User и списком значений Event/None. Это делает его немного более эффективным, поскольку он загружает в память не всю таблицу, а одну строку за раз. Что касается обращений к БД, оно попадает в БД один раз на ячейку таблицы, так же, как и у вас. Я думаю, что мог бы реорганизовать его, чтобы использовать одно обращение к строке с использованием списков, но в целом это потребовало бы больше памяти и времени обработки. - person Marco Lavagnino; 21.12.2016
comment
Еще один вопрос: зачем создавать отдельный список для block_id, а не просто использовать for block in Block.objects.all()? - person 43Tesseracts; 26.12.2016
comment
@ 43Tesseracts Я сделал это, поэтому мне не нужно заполнять весь экземпляр, чтобы узнать его идентификатор. - person Marco Lavagnino; 27.12.2016

Возможно, вам не нужно использовать аннотации, но вы можете использовать перегруппировать внутри шаблона.

Идем с конца. Всю информацию, которую вы хотите отобразить на странице, можно получить из модели регистрации.

Получите все регистрации, отфильтрованные по дате события:

registrations = Registration.objects.filter(event__date = date)

После этого вам необходимо перегруппировать пользователя в шаблоне.

Однако я вижу следующие проблемы:

  1. Я не уверен, что тег regroup правильно работает с наборами запросов. Поэтому, вероятно, вам нужно преобразовать данные в список. Однако я нашел этот ответ, в котором наборы запросов используются.
  2. Даже если вы перегруппируете, вам понадобится некоторая логика в шаблоне, чтобы определить блок для события.
person Alexander Tyapkov    schedule 20.12.2016
comment
Я думаю, что проблема с запуском с регистраций заключается в том, что она не будет подбирать пользователей, у которых нет регистраций. Например, user1 в моем выводе не появится. - person 43Tesseracts; 20.12.2016
comment
@ 43Tesseracts да, я тоже думал о решении, которое предоставляет пользователям шаблон, но в конце концов оно тоже стало не таким элегантным. Вероятно, должен помочь какой-то рефакторинг моделей. Я подумаю об этом позже. - person Alexander Tyapkov; 20.12.2016