From b7fbbcfd919b7cf3d69da333a7e3f7c09eacd7e8 Mon Sep 17 00:00:00 2001 From: Jan Szejko Date: Thu, 8 Feb 2018 15:28:09 +0100 Subject: [PATCH] marks split into criteria + anonymization + stats for supervisors --- stage2/admin.py | 5 +- stage2/forms.py | 24 +-- ...ique_markcriterion_assignment_order__ad.py | 171 +++++++++++++++++ stage2/models.py | 65 +++++-- stage2/templates/stage2/answer_list.html | 25 ++- stage2/urls.py | 4 +- stage2/views.py | 181 +++++++++++------- 7 files changed, 372 insertions(+), 103 deletions(-) create mode 100644 stage2/migrations/0013_auto__add_markcriterion__add_unique_markcriterion_assignment_order__ad.py diff --git a/stage2/admin.py b/stage2/admin.py index c2adfb4..4fca262 100644 --- a/stage2/admin.py +++ b/stage2/admin.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- from django.contrib import admin -from .models import Participant, Assignment, FieldOptionSet, FieldOption +from .models import Participant, Assignment, FieldOptionSet, FieldOption, MarkCriterion admin.site.register(Assignment) admin.site.register(Participant) admin.site.register(FieldOptionSet) -admin.site.register(FieldOption) \ No newline at end of file +admin.site.register(FieldOption) +admin.site.register(MarkCriterion) diff --git a/stage2/forms.py b/stage2/forms.py index b1bcac9..2ef10e5 100644 --- a/stage2/forms.py +++ b/stage2/forms.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- +from decimal import Decimal + from django import forms from django.conf import settings from django.template.defaultfilters import filesizeformat +from django.utils.safestring import mark_safe from stage2.models import Attachment, Mark, FieldOptionSet, FieldOption @@ -91,6 +94,8 @@ class AssignmentFieldForm(forms.Form): class MarkForm(forms.ModelForm): + answer_id = forms.CharField(widget=forms.HiddenInput) + class Meta: model = Mark fields = ['points'] @@ -98,15 +103,12 @@ class MarkForm(forms.ModelForm): 'points': forms.TextInput(attrs={'type': 'number', 'min': 0, 'step': '0.5'}) } - def __init__(self, answer, *args, **kwargs): + def __init__(self, answer, criterion, *args, **kwargs): super(MarkForm, self).__init__(*args, **kwargs) - self.answer = answer - self.fields['points'].widget.attrs['max'] = answer.assignment.max_points - - def clean_points(self): - points = self.cleaned_data['points'] - if points > self.answer.assignment.max_points: - raise forms.ValidationError('Too many points for this assignment') - if points < 0: - raise forms.ValidationError('Points cannot be negative') - return points + self.fields['answer_id'].initial = answer.id + points_field = self.fields['points'] + points_field.label = mark_safe(criterion.form_label()) + points_field.help_text = '(max %s)' % criterion.max_points + points_field.min_value = Decimal(0) + points_field.max_value = Decimal(criterion.max_points) + points_field.widget.attrs['max'] = criterion.max_points diff --git a/stage2/migrations/0013_auto__add_markcriterion__add_unique_markcriterion_assignment_order__ad.py b/stage2/migrations/0013_auto__add_markcriterion__add_unique_markcriterion_assignment_order__ad.py new file mode 100644 index 0000000..0bb1bb6 --- /dev/null +++ b/stage2/migrations/0013_auto__add_markcriterion__add_unique_markcriterion_assignment_order__ad.py @@ -0,0 +1,171 @@ +# -*- 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): + # Removing unique constraint on 'Mark', fields ['expert', 'answer'] + db.delete_unique(u'stage2_mark', ['expert_id', 'answer_id']) + + # Adding model 'MarkCriterion' + db.create_table(u'stage2_markcriterion', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('assignment', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['stage2.Assignment'])), + ('order', self.gf('django.db.models.fields.IntegerField')()), + ('label', self.gf('django.db.models.fields.CharField')(max_length=1024)), + ('max_points', self.gf('django.db.models.fields.IntegerField')()), + )) + db.send_create_signal(u'stage2', ['MarkCriterion']) + + # Adding unique constraint on 'MarkCriterion', fields ['assignment', 'order'] + db.create_unique(u'stage2_markcriterion', ['assignment_id', 'order']) + + # Adding field 'Mark.criterion' + db.add_column(u'stage2_mark', 'criterion', + self.gf('django.db.models.fields.related.ForeignKey')(default=0, to=orm['stage2.MarkCriterion']), + keep_default=False) + + # Adding unique constraint on 'Mark', fields ['expert', 'criterion'] + db.create_unique(u'stage2_mark', ['expert_id', 'criterion_id']) + + + def backwards(self, orm): + # Removing unique constraint on 'Mark', fields ['expert', 'criterion'] + db.delete_unique(u'stage2_mark', ['expert_id', 'criterion_id']) + + # Removing unique constraint on 'MarkCriterion', fields ['assignment', 'order'] + db.delete_unique(u'stage2_markcriterion', ['assignment_id', 'order']) + + # Deleting model 'MarkCriterion' + db.delete_table(u'stage2_markcriterion') + + # Deleting field 'Mark.criterion' + db.delete_column(u'stage2_mark', 'criterion_id') + + # Adding unique constraint on 'Mark', fields ['expert', 'answer'] + db.create_unique(u'stage2_mark', ['expert_id', 'answer_id']) + + + 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'}), + 'experts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'stage2_assigned_answers'", 'symmetrical': 'False', 'to': u"orm['auth.User']"}), + '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': '[]', 'blank': 'True'}), + 'file_descriptions': ('jsonfield.fields.JSONField', [], {'default': '[]', 'blank': 'True'}), + 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': {'ordering': "['set', 'value']", 'object_name': 'FieldOption'}, + 'answer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['stage2.Answer']", 'null': 'True', 'blank': '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', 'criterion'],)", 'object_name': 'Mark'}, + 'answer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['stage2.Answer']"}), + 'criterion': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['stage2.MarkCriterion']"}), + '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.markcriterion': { + 'Meta': {'ordering': "['order']", 'unique_together': "(['assignment', 'order'],)", 'object_name': 'MarkCriterion'}, + 'assignment': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['stage2.Assignment']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'label': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'max_points': ('django.db.models.fields.IntegerField', [], {}), + 'order': ('django.db.models.fields.IntegerField', [], {}) + }, + 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 7b791b2..8407ef2 100644 --- a/stage2/models.py +++ b/stage2/models.py @@ -8,7 +8,7 @@ from django.core.urlresolvers import reverse from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver -from django.utils.html import format_html +from django.utils.html import format_html, strip_tags from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from jsonfield import JSONField @@ -81,7 +81,9 @@ class Assignment(models.Model): def __unicode__(self): return self.title - def available_answers(self, expert): + def available_answers(self, expert, marked=False): + if marked: + return self.answer_set.filter(mark__expert=expert).order_by('id').distinct('id') answers = self.answer_set.exclude(mark__expert=expert) assigned_to_expert = self.answer_set.filter(experts=expert).exists() is_supervisor = expert in self.supervisors.all() @@ -106,15 +108,46 @@ class Assignment(models.Model): for field_desc in self.field_descriptions: field_name, params = field_desc if params['type'] == 'options': - field_count = FieldOption.objects.filter(answer__in=list(answers), set__name=params['option_set']).count() + field_count = FieldOption.objects.filter( + answer__in=list(answers), set__name=params['option_set']).count() else: # text, link field_count = sum(1 for answer in answers if answer.field_values.get(field_name)) yield field_name, field_count + def expert_counts(self): + for expert in self.experts.all(): + assigned_count = self.answer_set.filter(experts=expert).count() + marked_count = self.available_answers(expert, marked=True).count() + if assigned_count != 0 or marked_count != 0: + yield expert, marked_count, assigned_count + + def complete_answers(self): + return self.answer_set.filter(complete=True) + + def needing_arbiter(self): + return self.answer_set.filter(need_arbiter=True) + def is_active(self): return self.deadline >= timezone.now() +class MarkCriterion(models.Model): + assignment = models.ForeignKey(Assignment) + order = models.IntegerField() + label = models.CharField(max_length=1024) + max_points = models.IntegerField() + + class Meta: + ordering = ['order'] + unique_together = ['assignment', 'order'] + + def __unicode__(self): + return strip_tags(self.form_label()) + + def form_label(self): + return '%s. %s' % (self.order, self.label) + + class Answer(models.Model): participant = models.ForeignKey(Participant) assignment = models.ForeignKey(Assignment) @@ -140,14 +173,21 @@ class Answer(models.Model): value = format_html(u'{url}', url=value) yield field_name, value + def total_points(self): + criterion_count = self.assignment.markcriterion_set.count() + for expert in self.experts.all(): + marks = self.mark_set.filter(expert=expert) + if len(marks) == criterion_count: + yield sum(mark.points for mark in marks) + def update_complete(self): - marks = self.mark_set.all() - if len(marks) < 2: + total_points = list(self.total_points()) + if len(total_points) < 2: complete = False need_arbiter = False - elif len(marks) == 2: - mark1, mark2 = marks - complete = abs(mark1.points - mark2.points) < 0.2 * self.assignment.max_points + elif len(total_points) == 2: + points1, points2 = total_points + complete = abs(points1 - points2) < 0.2 * self.assignment.max_points need_arbiter = not complete else: complete = True @@ -157,10 +197,10 @@ class Answer(models.Model): self.save() def score(self): - marks = self.mark_set.all() - if len(marks) < 2: + total_marks = list(self.total_points()) + if len(total_marks) < 2: return None - return self.mark_set.aggregate(avg=models.Avg('points'))['avg'] + return sum(total_marks) / len(total_marks) # unrelated to `complete' attribute, but whatever def is_complete(self): @@ -234,10 +274,11 @@ class Attachment(models.Model): class Mark(models.Model): expert = models.ForeignKey(settings.AUTH_USER_MODEL) answer = models.ForeignKey(Answer) + criterion = models.ForeignKey(MarkCriterion) points = models.DecimalField(verbose_name=_('points'), max_digits=3, decimal_places=1) class Meta: - unique_together = ['expert', 'answer'] + unique_together = ['expert', 'answer', 'criterion'] @receiver(post_save, sender=Mark, dispatch_uid='update_answer') diff --git a/stage2/templates/stage2/answer_list.html b/stage2/templates/stage2/answer_list.html index c0f7dc8..d8f3dce 100644 --- a/stage2/templates/stage2/answer_list.html +++ b/stage2/templates/stage2/answer_list.html @@ -21,8 +21,21 @@ {% endfor %} {% endif %} + {% if supervisor %} + + {% endif %} {% for answer in answers %} -

{{ answer.participant }}

+ {% if supervisor %} +

{{ answer.participant }}

+ {% else %} +

Uczestnik {{ answer.participant.id }}

+ {% endif %} {% for label, value in answer.fields %}

{{ label }}: {{ value }} @@ -40,10 +53,14 @@

{% endfor %} {% if not assignment.is_active %} -
+ {% csrf_token %} - {{ answer.form.as_p }} - + + {% for form in answer.forms %} + {{ form.as_table }} + {% endfor %} + +
{% endif %} {% endfor %} diff --git a/stage2/urls.py b/stage2/urls.py index 158027b..c717793 100644 --- a/stage2/urls.py +++ b/stage2/urls.py @@ -9,8 +9,8 @@ urlpatterns = ( views.get_file, name='stage2_participant_file'), url(r'^zadania/$', views.assignment_list, name='stage2_assignments'), url(r'^zadania/(?P[0-9]*)/$', views.answer_list, name='stage2_answer_list'), - url(r'^zadania/(?P[0-9]*)/ocenione/$', views.marked_answer_list, name='stage2_marked_answers'), + url(r'^zadania/(?P[0-9]*)/ocenione/$', views.answer_list, + kwargs={'marked': True}, name='stage2_marked_answers'), url(r'^plik/(?P[0-9]*)/$', views.expert_download, name='stage2_expert_download'), - url(r'^mark/(?P[0-9]*)/$', views.mark_answer, name='stage2_mark_answer'), url(r'^csv-results/', views.csv_results, name='stage2_csv_results'), ) diff --git a/stage2/views.py b/stage2/views.py index 825be9e..a010ef4 100644 --- a/stage2/views.py +++ b/stage2/views.py @@ -7,11 +7,10 @@ from django.shortcuts import get_object_or_404, render from django.utils import timezone from django.utils.cache import patch_cache_control 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, AssignmentFieldForm -from stage2.models import Participant, Assignment, Answer, Attachment, Mark, FieldOption +from stage2.models import Participant, Assignment, Answer, Attachment, Mark def all_assignments(participant, sent_forms): @@ -52,30 +51,10 @@ def participant_view(request, participant_id, key): 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 HttpResponseForbidden('Not Allowed') + attachments_valid, attachment_forms = get_attachment_forms(assignment, participant, request) + fields_valid, field_forms = get_field_forms(assignment, participant, request) + if attachments_valid and fields_valid: return HttpResponseRedirect(reverse('stage2_participant', args=(participant_id, key))) else: sent_forms = (assignment, field_forms, attachment_forms) @@ -89,6 +68,37 @@ def participant_view(request, participant_id, key): return response +def get_attachment_forms(assignment, participant, request): + all_valid = True + attachment_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) + return all_valid, attachment_forms + + +def get_field_forms(assignment, participant, request): + all_valid = True + field_forms = [] + 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) + return all_valid, field_forms + + def attachment_download(attachment): response = HttpResponse(content_type='application/force-download') response.write(attachment.file.read()) @@ -112,14 +122,15 @@ def get_file(request, assignment_id, file_no, participant_id, key): @login_required def assignment_list(request): - assignments = request.user.stage2_assignments.all() + expert = request.user + assignments = expert.stage2_assignments.all() if not assignments: return HttpResponseForbidden('Not allowed') for assignment in assignments: - assignment.marked_count = Mark.objects.filter(expert=request.user, answer__assignment=assignment).count() - assignment.to_mark_count = assignment.available_answers(request.user).count() - assignment.supervisor = request.user in assignment.supervisors.all() - assignment.arbiter_count = assignment.answer_set.filter(need_arbiter=True).count() + assignment.marked_count = assignment.available_answers(expert, marked=True).count() + assignment.to_mark_count = assignment.available_answers(expert).count() + assignment.supervisor = expert in assignment.supervisors.all() + assignment.arbiter_count = assignment.needing_arbiter().count() non_empty_assignments = [ass for ass in assignments if ass.marked_count > 0 or ass.to_mark_count > 0] if len(non_empty_assignments) == 1 and non_empty_assignments[0].to_mark_count > 0: @@ -127,49 +138,94 @@ def assignment_list(request): return render(request, 'stage2/assignment_list.html', {'assignments': assignments}) -def available_answers(assignment, expert, answer_with_errors=None, form_with_errors=None, marked=False): +def available_answers(assignment, expert, sent_forms=None, marked=False): if marked: - answers = Answer.objects.filter(mark__expert=expert, assignment=assignment) + answers = assignment.available_answers(expert, marked=True) + else: + answers = assignment.available_answers(expert).order_by('participant__last_name') + answers = answers.prefetch_related('attachment_set') + if sent_forms: + sent_answer_id, mark_forms = sent_forms else: - answers = assignment.available_answers(expert) - answers = answers.order_by('participant__last_name').prefetch_related('attachment_set') + sent_answer_id = mark_forms = None for answer in answers: attachments = answer.attachment_set.all() attachments_by_file_no = {attachment.file_no: attachment for attachment in attachments} answer.attachments = [ (desc, attachments_by_file_no.get(i)) for (i, (desc, ext)) in enumerate(assignment.file_descriptions, 1)] - if answer == answer_with_errors: - answer.form = form_with_errors + if answer.id == sent_answer_id: + answer.forms = mark_forms else: - answer.form = MarkForm( - answer=answer, instance=answer.mark_set.filter(expert=expert).first(), prefix='ans%s' % answer.id) + answer.forms = [] + for criterion in assignment.markcriterion_set.all(): + answer.forms.append(MarkForm( + answer=answer, + criterion=criterion, + instance=answer.mark_set.filter(expert=expert, criterion=criterion).first(), + prefix='mark%s-%s' % (answer.id, criterion.id))) return answers @login_required -def answer_list(request, assignment_id): +def answer_list(request, assignment_id, marked=False): assignment = get_object_or_404(Assignment, id=assignment_id) - if request.user not in assignment.experts.all(): + expert = request.user + if expert not in assignment.experts.all(): return HttpResponseForbidden('Not allowed') - answers = available_answers(assignment, request.user) + if request.POST: + # ugly :/ + answer_id = None + for post_key, value in request.POST.iteritems(): + if post_key.endswith('answer_id'): + answer_id = int(value) + answer = get_object_or_404(Answer, id=answer_id) + + if answer not in assignment.available_answers(expert, marked=marked): + return HttpResponseForbidden('Not allowed') + if answer.assignment.is_active(): + return HttpResponseForbidden('Not allowed') + all_valid, forms = get_mark_forms(answer, request) + if all_valid: + if marked: + return HttpResponseRedirect(reverse('stage2_marked_answers', args=[answer.assignment_id])) + else: + return HttpResponseRedirect(reverse('stage2_answer_list', args=[answer.assignment_id])) + else: + sent_forms = answer_id, forms + else: + sent_forms = None + answers = available_answers(assignment, expert, sent_forms=sent_forms, marked=marked) return render(request, 'stage2/answer_list.html', { 'answers': answers, 'assignment': assignment, - 'field_counts': assignment.field_counts(answers) + 'field_counts': assignment.field_counts(answers) if not marked else None, + 'supervisor': expert in assignment.supervisors.all(), + 'marked': marked }) -@login_required -def marked_answer_list(request, assignment_id): - assignment = get_object_or_404(Assignment, id=assignment_id) - if request.user not in assignment.experts.all(): - return HttpResponseForbidden('Not allowed') - return render(request, 'stage2/answer_list.html', { - 'answers': available_answers(assignment, request.user, marked=True), - 'assignment': assignment, - 'marked': True, - }) +def get_mark_forms(answer, request): + all_valid = True + created_marks = [] + forms = [] + for criterion in answer.assignment.markcriterion_set.all(): + mark, created = Mark.objects.get_or_create( + answer=answer, criterion=criterion, expert=request.user, defaults={'points': 0}) + if created: + created_marks.append(mark) + form = MarkForm( + data=request.POST, answer=answer, criterion=criterion, instance=mark, + prefix='mark%s-%s' % (answer.id, criterion.id)) + if form.is_valid(): + form.save() + else: + all_valid = False + forms.append(form) + if not all_valid: + for mark in created_marks: + mark.delete() + return all_valid, forms @login_required @@ -178,25 +234,6 @@ def expert_download(request, attachment_id): return attachment_download(attachment) -@require_POST -@login_required -def mark_answer(request, answer_id): - answer = get_object_or_404(Answer, id=answer_id) - if request.user not in answer.assignment.experts.all(): - return HttpResponseForbidden('Not allowed') - if answer.assignment.is_active(): - return HttpResponseForbidden('Not allowed') - mark, created = Mark.objects.get_or_create(answer=answer, expert=request.user, defaults={'points': 0}) - form = MarkForm(data=request.POST, answer=answer, instance=mark, prefix='ans%s' % answer.id) - if form.is_valid(): - form.save() - elif created: - mark.delete() - - return HttpResponseRedirect(reverse( - 'stage2_answer_list' if created else 'stage2_marked_answers', args=[answer.assignment_id])) - - @login_required def csv_results(request): import csv -- 2.20.1