add text fields and exclusive options for participants
authorJan Szejko <janek37@gmail.com>
Thu, 28 Dec 2017 12:54:13 +0000 (13:54 +0100)
committerJan Szejko <janek37@gmail.com>
Thu, 28 Dec 2017 12:54:13 +0000 (13:54 +0100)
stage2/admin.py
stage2/forms.py
stage2/migrations/0011_auto__add_fieldoptionset__add_fieldoption__add_field_assignment_field_.py [new file with mode: 0644]
stage2/models.py
stage2/templates/stage2/participant.html
stage2/urls.py
stage2/views.py

index 66fb2f9..c2adfb4 100644 (file)
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
 from django.contrib import admin
-from .models import Participant, Assignment
-
+from .models import Participant, Assignment, FieldOptionSet, FieldOption
 
 admin.site.register(Assignment)
 admin.site.register(Participant)
+admin.site.register(FieldOptionSet)
+admin.site.register(FieldOption)
\ No newline at end of file
index 92af3a0..1017347 100644 (file)
@@ -3,10 +3,12 @@ from django import forms
 from django.conf import settings
 from django.template.defaultfilters import filesizeformat
 
-from stage2.models import Attachment, Mark
+from stage2.models import Attachment, Mark, FieldOptionSet, FieldOption
 
 
 class AttachmentForm(forms.ModelForm):
+    assignment_id = forms.CharField(widget=forms.HiddenInput)
+
     class Meta:
         model = Attachment
         fields = ['file']
@@ -14,6 +16,7 @@ class AttachmentForm(forms.ModelForm):
     def __init__(self, assignment, file_no, label, extensions=None, *args, **kwargs):
         prefix = 'att%s-%s' % (assignment.id, file_no)
         super(AttachmentForm, self).__init__(*args, prefix=prefix, **kwargs)
+        self.fields['assignment_id'].initial = assignment.id
         self.fields['file'].label = label
         if extensions:
             self.fields['file'].widget.attrs = {'data-ext': '|'.join(extensions)}
@@ -30,6 +33,53 @@ class AttachmentForm(forms.ModelForm):
         return file
 
 
+class AssignmentFieldForm(forms.Form):
+    value = forms.CharField()
+    assignment_id = forms.CharField(widget=forms.HiddenInput)
+
+    def __init__(self, label, field_no, options, answer, *args, **kwargs):
+        prefix = 'field%s-%s' % (answer.id, field_no)
+        super(AssignmentFieldForm, self).__init__(prefix=prefix, *args, **kwargs)
+        self.answer = answer
+        self.label = label
+        self.fields['value'].label = label
+        self.type = options['type']
+        self.fields['assignment_id'].initial = answer.assignment.id
+        if self.type == 'options':
+            option_set = FieldOptionSet.objects.get(name=options['option_set'])
+            self.fields['value'].widget = forms.Select(choices=option_set.choices(answer))
+            options = answer.fieldoption_set.all()
+            if options:
+                self.fields['value'].initial = options.get().id
+        else:
+            value = answer.field_values.get(label)
+            self.fields['value'].initial = value or ''
+
+    def clean_value(self):
+        if self.type == 'options':
+            option = FieldOption.objects.get(id=int(self.cleaned_data['value']))
+            if option.answer != self.answer and option.answer is not None:
+                raise forms.ValidationError(u'Ta opcja została już wybrana przez kogoś innego.')
+            return option
+        return self.cleaned_data['value']
+
+    def save(self):
+        value = self.cleaned_data['value']
+        if self.type == 'options':
+            option = value
+            if option.answer != self.answer:
+                # not thread-safe :/
+                assert not option.answer
+                for opt in self.answer.fieldoption_set.all():
+                    opt.answer = None
+                    opt.save()
+                option.answer = self.answer
+                option.save()
+        else:
+            self.answer.field_values[self.label] = value
+            self.answer.save()
+
+
 class MarkForm(forms.ModelForm):
     class Meta:
         model = Mark
diff --git a/stage2/migrations/0011_auto__add_fieldoptionset__add_fieldoption__add_field_assignment_field_.py b/stage2/migrations/0011_auto__add_fieldoptionset__add_fieldoption__add_field_assignment_field_.py
new file mode 100644 (file)
index 0000000..e55d8f7
--- /dev/null
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'FieldOptionSet'
+        db.create_table(u'stage2_fieldoptionset', (
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)),
+        ))
+        db.send_create_signal(u'stage2', ['FieldOptionSet'])
+
+        # Adding model 'FieldOption'
+        db.create_table(u'stage2_fieldoption', (
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('set', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['stage2.FieldOptionSet'])),
+            ('value', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('answer', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['stage2.Answer'], null=True)),
+        ))
+        db.send_create_signal(u'stage2', ['FieldOption'])
+
+        # Adding field 'Assignment.field_descriptions'
+        db.add_column(u'stage2_assignment', 'field_descriptions',
+                      self.gf('jsonfield.fields.JSONField')(default='[]'),
+                      keep_default=False)
+
+        # Adding field 'Answer.field_values'
+        db.add_column(u'stage2_answer', 'field_values',
+                      self.gf('jsonfield.fields.JSONField')(default='{}'),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting model 'FieldOptionSet'
+        db.delete_table(u'stage2_fieldoptionset')
+
+        # Deleting model 'FieldOption'
+        db.delete_table(u'stage2_fieldoption')
+
+        # Deleting field 'Assignment.field_descriptions'
+        db.delete_column(u'stage2_assignment', 'field_descriptions')
+
+        # Deleting field 'Answer.field_values'
+        db.delete_column(u'stage2_answer', 'field_values')
+
+
+    models = {
+        u'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        u'auth.permission': {
+            'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        u'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        u'contact.contact': {
+            'Meta': {'ordering': "('-created_at',)", 'object_name': 'Contact'},
+            'body': ('jsonfield.fields.JSONField', [], {}),
+            'contact': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'form_tag': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'})
+        },
+        u'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        u'stage2.answer': {
+            'Meta': {'unique_together': "(['participant', 'assignment'],)", 'object_name': 'Answer'},
+            'assignment': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['stage2.Assignment']"}),
+            'complete': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'field_values': ('jsonfield.fields.JSONField', [], {'default': "'{}'"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'need_arbiter': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'participant': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['stage2.Participant']"})
+        },
+        u'stage2.assignment': {
+            'Meta': {'ordering': "['deadline', 'title']", 'object_name': 'Assignment'},
+            'arbiters': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'stage2_arbitrated'", 'blank': 'True', 'to': u"orm['auth.User']"}),
+            'content': ('django.db.models.fields.TextField', [], {}),
+            'content_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
+            'deadline': ('django.db.models.fields.DateTimeField', [], {}),
+            'experts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'stage2_assignments'", 'symmetrical': 'False', 'to': u"orm['auth.User']"}),
+            'field_descriptions': ('jsonfield.fields.JSONField', [], {'default': "'[]'"}),
+            'file_descriptions': ('jsonfield.fields.JSONField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'max_points': ('django.db.models.fields.IntegerField', [], {}),
+            'supervisors': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'stage2_supervised'", 'symmetrical': 'False', 'to': u"orm['auth.User']"}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+        },
+        u'stage2.attachment': {
+            'Meta': {'ordering': "['file_no']", 'object_name': 'Attachment'},
+            'answer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['stage2.Answer']"}),
+            'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'file_no': ('django.db.models.fields.IntegerField', [], {}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        u'stage2.fieldoption': {
+            'Meta': {'object_name': 'FieldOption'},
+            'answer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['stage2.Answer']", 'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'set': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['stage2.FieldOptionSet']"}),
+            'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        u'stage2.fieldoptionset': {
+            'Meta': {'object_name': 'FieldOptionSet'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'})
+        },
+        u'stage2.mark': {
+            'Meta': {'unique_together': "(['expert', 'answer'],)", 'object_name': 'Mark'},
+            'answer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['stage2.Answer']"}),
+            'expert': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'points': ('django.db.models.fields.DecimalField', [], {'max_digits': '3', 'decimal_places': '1'})
+        },
+        u'stage2.participant': {
+            'Meta': {'object_name': 'Participant'},
+            'complete_set': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'contact': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contact.Contact']", 'null': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '100'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
+            'key_sent': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['stage2']
\ No newline at end of file
index 924b153..c21c53a 100644 (file)
@@ -69,7 +69,8 @@ class Assignment(models.Model):
         settings.AUTH_USER_MODEL, blank=True, verbose_name=_('arbiters'), related_name='stage2_arbitrated')
     supervisors = models.ManyToManyField(
         settings.AUTH_USER_MODEL, verbose_name=_('supervisors'), related_name='stage2_supervised')
-    file_descriptions = JSONField(_('file descriptions'))
+    file_descriptions = JSONField(_('file descriptions'), default=[], blank=True)
+    field_descriptions = JSONField(_('field descriptions'), default=[], blank=True)
 
     class Meta:
         ordering = ['deadline', 'title']
@@ -94,6 +95,7 @@ class Assignment(models.Model):
 class Answer(models.Model):
     participant = models.ForeignKey(Participant)
     assignment = models.ForeignKey(Assignment)
+    field_values = JSONField(_('field values'), default={})
     # useful redundancy
     complete = models.BooleanField(default=False)
     need_arbiter = models.BooleanField(default=False)
@@ -124,6 +126,38 @@ class Answer(models.Model):
         return self.mark_set.aggregate(avg=models.Avg('points'))['avg']
 
 
+class FieldOptionSet(models.Model):
+    name = models.CharField(verbose_name=_('nazwa'), max_length=32, db_index=True)
+
+    class Meta:
+        verbose_name = _('option set')
+        verbose_name_plural = _('option sets')
+
+    def __unicode__(self):
+        return self.name
+
+    def choices(self, answer):
+        return [('', '--------')] + [
+            (option.id, option.value)
+            for option in self.fieldoption_set.extra(
+                where=['answer_id is null or answer_id = %s'],
+                params=[answer.id])]
+
+
+class FieldOption(models.Model):
+    set = models.ForeignKey(FieldOptionSet, verbose_name=_('zestaw'))
+    value = models.CharField(verbose_name=_('value'), max_length=255)
+    answer = models.ForeignKey(Answer, verbose_name=_('answer'), null=True, blank=True)
+
+    class Meta:
+        ordering = ['set', 'value']
+        verbose_name = _('option')
+        verbose_name_plural = _('options')
+
+    def __unicode__(self):
+        return self.value
+
+
 def attachment_path(instance, filename):
     answer = instance.answer
     return 'stage2/attachment/%s/%s/%s/%s' % (answer.participant_id, answer.assignment_id, instance.file_no, filename)
index 64a6a1a..2ed6d62 100644 (file)
   {% for assignment in assignments %}
     <h2>{{ assignment.title }} (do {{ assignment.deadline }})</h2>
     <a href="{{ assignment.content_url }}"><strong>Zobacz treść zadania</strong></a>
-    <form method="POST" action="{% url 'stage2_upload' assignment.id participant.id participant.key %}"
-          enctype="multipart/form-data">
+    <form method="POST" action="" enctype="multipart/form-data">
       {% csrf_token %}
-      {% for form, attachment in assignment.forms %}
+      {% for form in assignment.field_forms %}
+        {{ form.assignment_id }}
+        <p><strong>{{ form.value.label }}:</strong></p>
+        {{ form.value.errors }}
+        <p>
+          {% if assignment.is_active %}{{ form.value }}{% else %}{{ form.value.initial }}{% endif %}
+        </p>
+      {% endfor %}
+      {% for form, attachment in assignment.attachment_forms %}
+        {{ form.assignment_id }}
         <p><strong>{{ form.file.label }}:</strong></p>
         <p>
           {% if assignment.is_active %}{{ form.file }}{% endif %}
index 98fe2e1..158027b 100644 (file)
@@ -5,8 +5,6 @@ from stage2 import views
 
 urlpatterns = (
     url(r'^uczestnik/(?P<participant_id>[0-9]*)/(?P<key>.*)/$', views.participant_view, name='stage2_participant'),
-    url(r'^upload/(?P<assignment_id>[0-9]*)/(?P<participant_id>[0-9]*)/(?P<key>.*)/$', views.upload,
-        name='stage2_upload'),
     url(r'^plik/(?P<assignment_id>[0-9]*)/(?P<file_no>[0-9]*)/(?P<participant_id>[0-9]*)/(?P<key>.*)/$',
         views.get_file, name='stage2_participant_file'),
     url(r'^zadania/$', views.assignment_list, name='stage2_assignments'),
index 58bdf97..093a824 100644 (file)
@@ -10,18 +10,29 @@ from django.views.decorators.cache import never_cache
 from django.views.decorators.http import require_POST
 from unidecode import unidecode
 
-from stage2.forms import AttachmentForm, MarkForm
+from stage2.forms import AttachmentForm, MarkForm, AssignmentFieldForm
 from stage2.models import Participant, Assignment, Answer, Attachment, Mark
 
 
-def all_assignments(participant):
+def all_assignments(participant, sent_forms):
     assignments = Assignment.objects.all()
+    if sent_forms:
+        sent_assignment, field_forms, attachment_forms = sent_forms
+    else:
+        sent_assignment = field_forms = attachment_forms = None
     for assignment in assignments:
-        assignment.answer = assignment.answer_set.filter(participant=participant).first()
-        assignment.forms = [
-            (AttachmentForm(assignment=assignment, file_no=i, label=label, extensions=ext),
-             assignment.answer.attachment_set.filter(file_no=i).first() if assignment.answer else None)
-            for i, (label, ext) in enumerate(assignment.file_descriptions, 1)]
+        assignment.answer, created = Answer.objects.get_or_create(participant=participant, assignment=assignment)
+        if assignment == sent_assignment:
+            assignment.field_forms = field_forms
+            assignment.attachment_forms = attachment_forms
+        else:
+            assignment.field_forms = [
+                AssignmentFieldForm(label=label, field_no=i, options=options, answer=assignment.answer)
+                for i, (label, options) in enumerate(assignment.field_descriptions, 1)]
+            assignment.attachment_forms = [
+                (AttachmentForm(assignment=assignment, file_no=i, label=label, extensions=ext),
+                 assignment.answer.attachment_set.filter(file_no=i).first() if assignment.answer else None)
+                for i, (label, ext) in enumerate(assignment.file_descriptions, 1)]
     return assignments
 
 
@@ -30,34 +41,54 @@ def participant_view(request, participant_id, key):
     participant = get_object_or_404(Participant, id=participant_id)
     if not participant.check(key):
         raise Http404
+    if request.POST:
+        # ugly :/
+        assignment_id = None
+        for post_key, value in request.POST.iteritems():
+            if post_key.endswith('assignment_id'):
+                assignment_id = int(value)
+        assert assignment_id
+
+        assignment = get_object_or_404(Assignment, id=assignment_id)
+        now = timezone.now()
+        if assignment.deadline < now:
+            raise Http404  # TODO za późno
+        all_valid = True
+        attachment_forms = []
+        field_forms = []
+        for i, (label, ext) in enumerate(assignment.file_descriptions, 1):
+            answer, created = Answer.objects.get_or_create(participant=participant, assignment=assignment)
+            attachment, created = Attachment.objects.get_or_create(answer=answer, file_no=i)
+            form = AttachmentForm(
+                data=request.POST, files=request.FILES,
+                assignment=assignment, file_no=i, label=label, instance=attachment, extensions=ext)
+            if form.is_valid():
+                form.save()
+            else:
+                all_valid = False
+            attachment_forms.append(form)
+        for i, (label, options) in enumerate(assignment.field_descriptions, 1):
+            answer = Answer.objects.get(participant=participant, assignment=assignment)
+            form = AssignmentFieldForm(data=request.POST, label=label, field_no=i, options=options, answer=answer)
+            if form.is_valid():
+                form.save()
+            else:
+                all_valid = False
+            field_forms.append(form)
+        if all_valid:
+            return HttpResponseRedirect(reverse('stage2_participant', args=(participant_id, key)))
+        else:
+            sent_forms = (assignment, field_forms, attachment_forms)
+    else:
+        sent_forms = None
     response = render(request, 'stage2/participant.html', {
         'participant': participant,
-        'assignments': all_assignments(participant)})
+        'assignments': all_assignments(participant, sent_forms)})
     # not needed in Django 1.8
     patch_cache_control(response, no_cache=True, no_store=True, must_revalidate=True)
     return response
 
 
-@require_POST
-def upload(request, assignment_id, participant_id, key):
-    participant = get_object_or_404(Participant, id=participant_id)
-    if not participant.check(key):
-        raise Http404
-    assignment = get_object_or_404(Assignment, id=assignment_id)
-    now = timezone.now()
-    if assignment.deadline < now:
-        raise Http404  # TODO za późno
-    for i, (label, ext) in enumerate(assignment.file_descriptions, 1):
-        answer, created = Answer.objects.get_or_create(participant=participant, assignment=assignment)
-        attachment, created = Attachment.objects.get_or_create(answer=answer, file_no=i)
-        form = AttachmentForm(
-            data=request.POST, files=request.FILES,
-            assignment=assignment, file_no=i, label=label, instance=attachment, extensions=ext)
-        if form.is_valid():
-            form.save()
-    return HttpResponseRedirect(reverse('stage2_participant', args=(participant_id, key)))
-
-
 def attachment_download(attachment):
     response = HttpResponse(content_type='application/force-download')
     response.write(attachment.file.read())