2

I have a queryset of profiles:

Model:

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

View:

Profile.objects.select_related('user')

Each user/profile can register for multiple events per day:

Models:

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)
    ....

Given a Date how can I annotate (?I think that's what I want?) one Registration object per block (filtered on the user/profile and the Event__Date)

In the end, what I'm trying to output in my template is something like this:

For Date: 19 Dec 2016

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

EDIT

Attempt 1. Here's my first attempt to accomplish this. I suspect this is horribly inefficient and would be extremely slow in production, but at least it works. If anyone can provide a more efficient and elegant solution, it would be appreciated! (Note that this also includes a filter on the User's Profile model for homeroom_teacher that was not included in the original question, but I've left here because this is the code that's working)

Registration model manager

class 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

Template Note that block.constant_string() --> "ABLOCK", "BBLOCK", etc, this is hardcoded in the block.constant_string() method and I'm not sure how to get around this either.

{% 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 %}
43Tesseracts
  • 4,617
  • 8
  • 48
  • 94

2 Answers2

1

Probably you don't need to use annotate but can use regroup tag inside template.

Going from the end. All the information which you want to display on the page can be retrieved from Registration model.

Get all the registrations filtered by date of the event:

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

Afterwards you have to regroup by user in template.

However I see following problems:

  1. I am not sure that regroup tag is working correctly with querysets. So probably you have to convert data into list. However, I have found this answer in which querysets are used.
  2. Even if you will regroup you need some logic in template to define the block for the event.
Community
  • 1
  • 1
Alexander Tyapkov
  • 4,837
  • 5
  • 39
  • 65
  • I think the problem with starting at Registrations is that it won't pick up Users that have no registrations. For example `user1` in my output wouldn't appear. – 43Tesseracts Dec 20 '16 at 18:09
  • @43Tesseracts yes, I was also thinking about solution which provides users to the tempate but it also became not so elegant in the end. Probably some kind of models refactoring should help. I will think about it later. – Alexander Tyapkov Dec 20 '16 at 18:28
1

To solve the problem of the harcoded names, I'd slightly modify your solution to look like this:

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)

I'd take it out from the model manager and put it in views.py or a separate file (like table.py). Seems cleaner to me, but that's just an opinion - you could put this code in the model manager and it would run anyway.

Then in your template:

{% 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 %}
Marco Lavagnino
  • 1,140
  • 12
  • 31
  • Are you able to comment on the differences in efficiency between your code and mine? Yours LOOKS much nicer that's for sure. Is there only one db call in yours? – 43Tesseracts Dec 20 '16 at 19:56
  • 1
    @43Tesseracts my snippet returns generator that yields a tuple with an object User and a list of Event/None values. That makes it a little bit more efficient since it doesn't load the whole table into the memory, but one row at a time. Regarding db hits, it hits the db once per table cell, the same as yours. I think I could refactor it to use one hit per row using list comprehensions, but it would use more memory and processing time overall. – Marco Lavagnino Dec 20 '16 at 21:01
  • One more question: Why get a separate list for block_ids rather than just use `for block in Block.objects.all()`? – 43Tesseracts Dec 26 '16 at 04:45
  • @43Tesseracts I did it so I don't have to populate a whole instance just to know it's id. – Marco Lavagnino Dec 27 '16 at 19:10