From cced2ec09ed232c6044bfc2c8e98fb6af6503ba2 Mon Sep 17 00:00:00 2001 From: Jan Szejko Date: Thu, 28 Dec 2017 13:54:13 +0100 Subject: [PATCH 1/1] add text fields and exclusive options for participants --- stage2/admin.py | 5 +- stage2/forms.py | 52 +++++- ...ieldoption__add_field_assignment_field_.py | 160 ++++++++++++++++++ stage2/models.py | 36 +++- stage2/templates/stage2/participant.html | 14 +- stage2/urls.py | 2 - stage2/views.py | 87 +++++++--- 7 files changed, 319 insertions(+), 37 deletions(-) create mode 100644 stage2/migrations/0011_auto__add_fieldoptionset__add_fieldoption__add_field_assignment_field_.py diff --git a/stage2/admin.py b/stage2/admin.py index 66fb2f9..c2adfb4 100644 --- a/stage2/admin.py +++ b/stage2/admin.py @@ -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 diff --git a/stage2/forms.py b/stage2/forms.py index 92af3a0..1017347 100644 --- a/stage2/forms.py +++ b/stage2/forms.py @@ -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 index 0000000..e55d8f7 --- /dev/null +++ b/stage2/migrations/0011_auto__add_fieldoptionset__add_fieldoption__add_field_assignment_field_.py @@ -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 diff --git a/stage2/models.py b/stage2/models.py index 924b153..c21c53a 100644 --- a/stage2/models.py +++ b/stage2/models.py @@ -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) diff --git a/stage2/templates/stage2/participant.html b/stage2/templates/stage2/participant.html index 64a6a1a..2ed6d62 100644 --- a/stage2/templates/stage2/participant.html +++ b/stage2/templates/stage2/participant.html @@ -12,10 +12,18 @@ {% for assignment in assignments %}

{{ assignment.title }} (do {{ assignment.deadline }})

Zobacz treść zadania -
+ {% csrf_token %} - {% for form, attachment in assignment.forms %} + {% for form in assignment.field_forms %} + {{ form.assignment_id }} +

{{ form.value.label }}:

+ {{ form.value.errors }} +

+ {% if assignment.is_active %}{{ form.value }}{% else %}{{ form.value.initial }}{% endif %} +

+ {% endfor %} + {% for form, attachment in assignment.attachment_forms %} + {{ form.assignment_id }}

{{ form.file.label }}:

{% if assignment.is_active %}{{ form.file }}{% endif %} diff --git a/stage2/urls.py b/stage2/urls.py index 98fe2e1..158027b 100644 --- a/stage2/urls.py +++ b/stage2/urls.py @@ -5,8 +5,6 @@ from stage2 import views urlpatterns = ( url(r'^uczestnik/(?P[0-9]*)/(?P.*)/$', views.participant_view, name='stage2_participant'), - url(r'^upload/(?P[0-9]*)/(?P[0-9]*)/(?P.*)/$', views.upload, - name='stage2_upload'), url(r'^plik/(?P[0-9]*)/(?P[0-9]*)/(?P[0-9]*)/(?P.*)/$', views.get_file, name='stage2_participant_file'), url(r'^zadania/$', views.assignment_list, name='stage2_assignments'), diff --git a/stage2/views.py b/stage2/views.py index 58bdf97..093a824 100644 --- a/stage2/views.py +++ b/stage2/views.py @@ -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()) -- 2.20.1