0

I have the three models User (django.contrib.auth), Screening and User_Screening. The User_Screening is a m2m table with the extra field status.

#models.py
from django.db import models
from django.contrib.auth.models import User

class Screening(models.Model):
    title = models.CharField(max_length=255)
    start = models.DateTimeField()
    user_relation = models.ManyToManyField(User, blank=True,
        through='User_Status')

class User_Status(models.Model):
    ATTENDING = 'c'
    NOT_ATTENDING = 'n'
    PROJECTION = 'p'
    STATUS_CHOICES = (
        (ATTENDING, 'attending'),
        (NOT_ATTENDING, 'not attending'),
        (PROJECTING, 'projecting'),
    )
    screening = models.ForeignKey(Screening)
    user = models.ForeignKey(User)
    status = models.CharField(max_length=1, choices=STATUS_CHOICES)

Now I want to make a view, which shows all upcoming screenings. So far, so easy:

#views.py
@login_required()
def index(request):
    current_screenings = Screening.objects.filter(start__gte=timezone.now())
    context = {'current_screenings': current_screenings}
    return render(request, 'schedule/index.html', context)

In this view, logged in users should be able, to update their status (from the User_Screening table). It could also be, that the user does not yet have a record for this screening, so one should be created.

I don't understand, how I could archive a form dropdown field for each screening, where the user can select his status. (Either ? if no status is set yet, attending, not attending or projection)

From what I understand I need multiple forms, that are aware what screening they are related to.

Also, Formsets seem not to work, because I can't always fill a form with initial data, as there could be records missing for some or all screenings. Furthermore I would not know, which form belongs to which of the screening objects.

Update: What I want to end up with in HTML is something like this:

<form>
  <h1>Current Screening 1</h1>
    <select onchange="submit()" name="screening_user" id="s1">
      <option value="att">Attending</option>
      <option value="not_att">Not Attending</option>
      <option selected="selected" value="pro">Projection</option>
    </select>
  <h1>Current Screening 2</h1>
    <select onchange="submit()" name="screening_user" id="s2">
      <!-- The 'Please Select' option is only visible, if the user does not
        have a relation in 'User_Screening' for this screening -->
      <option selected="selected" value="none">Please Select</option>
      <option value="att">Attending</option>
      <option value="not_att">Not Attending</option>
      <option value="pro">Projection</option>
    </select>
  <!-- More Screenings -->
  <h1>Current Screening n</h1>
    <!-- select for screening n -->
</form>

Therefore a changing amount of forms is needed, from the same form with preloaded data according to the logged in user.

Marvin Dickhaus
  • 785
  • 12
  • 27

3 Answers3

0

If a screening has a m2m relation to Users, than the attending users can be in that list. If not in attending... Well, than they are not attending! Does that make sense?

class Screening(models.Model):
    title = models.CharField(max_length=255)
    date = models.DateTimeField()
    attending = models.ManyToManyField(User)

Form:

class ScreeningForm(ModelForm):
    class Meta:
        model = Screening
        fieds = ['attending', ]

Formset:

ScreeningFormSet = modelformset_factory(Screenig, max_num=1)
formset = ScreeningFormSet(Screening=Screening.objects.filter(date__gte=now))
allcaps
  • 10,945
  • 1
  • 33
  • 54
  • Okay, I might have to clarify, that there are more than just the `status` 'coming' and 'not coming'. I shortened them for this question. Also: If I have a relation with the status 'not coming', I know that the user actively decided not to come, which I do need, for email reminding purposes. – Marvin Dickhaus Feb 18 '14 at 14:40
0

On one hand you could send the form data via an ajax request. In that request you would simply send one form and process the data. You would not need any formsets. Depending on your usecase this may add unnecessary traffic to your server.

Another solution would be to add another STATUS_CHOICE like 'nothing selected' as default value for the form that is used if there is no entry for the Screening User combination in the db. In the POST handler of your view you you can then just check if the form data is set to this value. In this case you simply ignore the form. If it is another value, then you set the db entry accordingly.

7tupel
  • 151
  • 2
  • 2
  • 5
  • What I really want to archive is to have the same form multiple times in one view (like the title suggests). I updated my question to clarify, what html output I'd expect. – Marvin Dickhaus Feb 19 '14 at 10:26
  • i don't see a problem there. just create a list of forms in your view and create a template that iterates over that list of forms and renders them. when a form changes onChange() is called and the form data including the id of the form is submitted to a according view and processed there. – 7tupel Feb 19 '14 at 12:02
0

With some help from #django on feenode, I solved my problem. In the end, I stuck with formsets.

Considering the models.py from my question I had to change User_Status slightly, adding a NO_STATUS choice for the Select-Widget if no relation yet exist for the screening. Note that NO_STATUS is not a choice for the model.CharField!

#models.py
class User_Status(models.Model):
NO_STATUS = '?'
PROJECTIONIST = 'p'
ATTENDING = 'c'
NOT_ATTENDING = 'n'
STATUS_CHOICES = [
    (ATTENDING, 'Anwesend'),
    (NOT_ATTENDING, 'Nicht anwesend'),
    (PROJECTIONIST, 'Vorführer'),
]
STATUS_CHOICES_AND_EMPTY = [(NO_STATUS, 'Please choose')] + STATUS_CHOICES
screening = models.ForeignKey(Screening)
user = models.ForeignKey(User)
status = models.CharField(max_length=1, choices=STATUS_CHOICES,
    default=ATTENDING)

Next up, the form. The modified __init__ takes care, that 'Please choose' is only a valid choice, if that is set as the initial value for status. Otherwise, the choice is just not displayed.

#forms.py
class ScreeningUserStatusForm(forms.Form):
    screening_id = forms.IntegerField(min_value=1)
    status = forms.ChoiceField(choices=User_Status.STATUS_CHOICES_AND_EMPTY, 
        widget=forms.Select(attrs={"onChange":'submit()'}))

    def __init__(self, *args, **kwargs):
        super(ScreeningUserStatusForm, self).__init__(*args, **kwargs)
        if self['status'].value() != User_Status.NO_STATUS:
            #Once, a status is selected, the status should not be unset.
            self.fields['status'].choices=User_Status.STATUS_CHOICES

Finally the view, that uses a formset to put all current screenings in it.

def update_user_status(screening, user, status):
    #Get old status, if already exists.
    new_status = User_Status.objects.get_or_create(screening=screening,
        user=user)

    # Add to selected status
    new_status.status = status 
    new_status.save()

@login_required()
def index(request):
    """
    displays all upcoming screenings
    """

    # Get current screenings
    current_screening_set = Screening.objects.filter(start__gte=timezone.now() - datetime.timedelta(hours=24)).order_by('start')
    current_screening_list = current_screening_set.values('id')

    ScreeningFormSet = formset_factory(ScreeningUserStatusForm, extra=0)

    if request.method == 'POST':
        #Get a formset bound to data from POST
        formset = ScreeningFormSet(request.POST, request.FILES)
        if formset.is_valid():
            for form in formset.cleaned_data:
                s = get_object_or_404(Screening, pk=form['screening_id'])
                if form['status'] != User_Status.NO_STATUS:
                    update_user_status(screening=s, user=request.user, status=form['status'])
    else:
        #create a fresh formset
        for form_data in current_screening_list:
            screening = Screening.objects.get(pk=form_data['id'])
            status = User_Status.objects.filter(user=request.user, screening=screening)
            if status.count() != 1:
                form_data['status'] = u'?'
            else:
                form_data['status'] = status.first().status
            form_data['screening_id'] = form_data['id']

        formset = ScreeningFormSet(initial=current_screening_list)

    forms_and_curr_screenings = zip(formset.forms, current_screening_set)

    context = {'formset' : formset, 'current_screenings' : forms_and_curr_screenings}
    return render(request, 'schedule/index.html', context)

The formset.forms are zipped together with the current_screening_set, to provide additional data to each from. formset is additionally given to the template for the management_form.

A template could look like this

<!-- index.html -->
{% if current_screenings %}
    <form method="post">
    {{ formset.management_form }}
    {% csrf_token %}
    <table>
      <thead>
        <tr>
          <th>Screening</th>
          <th>My Status</th>
        </tr>
      </thead>
      <tbody>
      {% for form, screening in current_screenings %}
        <tr>
          <td>{{ screening }}</a></td>
          <td>
            {{ form.screening_id.as_hidden }}
            {{ form.status }}
          </td>
        </tr>
      {% endfor %}
      </tbody>
    </table>
  </form>
{% endif %}
Marvin Dickhaus
  • 785
  • 12
  • 27