diff --git a/core/forms.py b/core/forms.py index c0f6346f53784d8f500a2c397983a07015c22f99..766cf783951d4692af986f8786f9e9c45ded71f6 100644 --- a/core/forms.py +++ b/core/forms.py @@ -21,18 +21,13 @@ class FeedbackForm(ModelForm): full_score = self.instance.of_submission.type.full_score if not cleaned_data.get("score") <= full_score: raise ValidationError( - "Score too high. Maximium score is %(max_score)d", + "Score too high. Maximum score is %(max_score)d", code='over_max_score', params={'max_score': full_score}, ) - if cleaned_data.get("final"): - raise ValidationError( - "Feedback is final and not editable", - code="is_final", - ) - if not cleaned_data.get("text"): + cleaned_data["status"] = Feedback.EDITABLE raise ValidationError( "Feedback should not be empty", code="is_empty", @@ -41,4 +36,4 @@ class FeedbackForm(ModelForm): class Meta: model = Feedback auto_id = False - fields = ('text', 'score') + fields = ('text', 'score', 'status') diff --git a/core/migrations/0009_auto_20170405_0958.py b/core/migrations/0009_auto_20170405_0958.py new file mode 100644 index 0000000000000000000000000000000000000000..dc52c582e78ba5f83ccbdeb6e7d2b756068b01af --- /dev/null +++ b/core/migrations/0009_auto_20170405_0958.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-05 09:58 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_auto_20170403_2313'), + ] + + operations = [ + migrations.RemoveField( + model_name='feedback', + name='empty', + ), + migrations.RemoveField( + model_name='feedback', + name='final', + ), + migrations.AddField( + model_name='feedback', + name='status', + field=models.CharField(choices=[('I', 'inital'), ('F', 'final'), ('R', 'needs review')], default='I', max_length=1), + ), + ] diff --git a/core/migrations/0010_auto_20170405_1406.py b/core/migrations/0010_auto_20170405_1406.py new file mode 100644 index 0000000000000000000000000000000000000000..1d66f625c5e0fb8a7e9cf8fe10e7b9e09b54a39b --- /dev/null +++ b/core/migrations/0010_auto_20170405_1406.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-05 14:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_auto_20170405_0958'), + ] + + operations = [ + migrations.AlterField( + model_name='feedback', + name='origin', + field=models.CharField(choices=[('E', 'was empty'), ('UT', 'passed unittests'), ('CF', 'did not compile'), ('LF', 'could not link'), ('M', 'created by a human. yak!')], default='M', max_length=2, unique=True), + ), + migrations.AlterField( + model_name='feedback', + name='status', + field=models.CharField(choices=[('I', 'initial'), ('A', 'final'), ('R', 'request review'), ('O', 'open')], default='I', max_length=1, unique=True), + ), + ] diff --git a/core/migrations/0011_auto_20170405_1406.py b/core/migrations/0011_auto_20170405_1406.py new file mode 100644 index 0000000000000000000000000000000000000000..8b494be8ec480286709d1c148ffe1b0a558e38ed --- /dev/null +++ b/core/migrations/0011_auto_20170405_1406.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-05 14:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_auto_20170405_1406'), + ] + + operations = [ + migrations.AlterField( + model_name='feedback', + name='origin', + field=models.CharField(choices=[('E', 'was empty'), ('UT', 'passed unittests'), ('CF', 'did not compile'), ('LF', 'could not link'), ('M', 'created by a human. yak!')], default='M', max_length=2), + ), + migrations.AlterField( + model_name='feedback', + name='status', + field=models.CharField(choices=[('I', 'initial'), ('A', 'final'), ('R', 'request review'), ('O', 'open')], default='I', max_length=1), + ), + ] diff --git a/core/migrations/0012_auto_20170405_1905.py b/core/migrations/0012_auto_20170405_1905.py new file mode 100644 index 0000000000000000000000000000000000000000..4ff5ba52b76de8049b601e5769e98f7a6fe85a6f --- /dev/null +++ b/core/migrations/0012_auto_20170405_1905.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-05 19:05 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0011_auto_20170405_1406'), + ] + + operations = [ + migrations.AlterField( + model_name='feedback', + name='status', + field=models.CharField(choices=[('I', 'editable'), ('A', 'accepted'), ('R', 'request review'), ('O', 'request reassignment')], default='I', max_length=1), + ), + ] diff --git a/core/migrations/0013_auto_20170405_1908.py b/core/migrations/0013_auto_20170405_1908.py new file mode 100644 index 0000000000000000000000000000000000000000..1bc6aab3dcd5802ceea44c658f3f981d018d34f1 --- /dev/null +++ b/core/migrations/0013_auto_20170405_1908.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-05 19:08 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0012_auto_20170405_1905'), + ] + + operations = [ + migrations.RemoveField( + model_name='submission', + name='final_feedback', + ), + migrations.AddField( + model_name='submission', + name='accepted_feedback', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Feedback'), + ), + ] diff --git a/core/migrations/0014_auto_20170405_1908.py b/core/migrations/0014_auto_20170405_1908.py new file mode 100644 index 0000000000000000000000000000000000000000..af59dbdef954e2e15a9cf0518d4309f0644120e1 --- /dev/null +++ b/core/migrations/0014_auto_20170405_1908.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-05 19:08 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0013_auto_20170405_1908'), + ] + + operations = [ + migrations.RenameField( + model_name='submission', + old_name='accepted_feedback', + new_name='final_feedback', + ), + ] diff --git a/core/models.py b/core/models.py index c2e2e55c0d34d359b5759176aef46f26c4aed7fe..7904fc842b879515c3f3932e1b3c85aebc326030 100644 --- a/core/models.py +++ b/core/models.py @@ -3,7 +3,7 @@ from string import ascii_lowercase from django.contrib.auth.models import User from django.db import models -from django.db.models import Count +from django.db.models import Count, Q MAX_FEEDBACK_PER_SUBMISSION = 1 SLUG_LENGTH = 16 @@ -126,8 +126,6 @@ class Feedback(models.Model): # Fields text = models.TextField() slug = models.SlugField(editable=False, unique=True, default=random_slug) - final = models.BooleanField(default=False) - empty = models.BooleanField(default=True) score = models.PositiveIntegerField(default=0) of_submission = models.ForeignKey( Submission, @@ -136,27 +134,42 @@ class Feedback(models.Model): of_tutor = models.ForeignKey( User, related_name='corrected_submissions', - limit_choices_to={'groups__name': 'Tutors'} ) of_reviewer = models.ForeignKey( User, related_name='reviewed_submissions', - limit_choices_to={'groups__name': 'Reviewers'}, blank=True, null=True ) - # adding an origin - WAS_EMPTY = 'E' - PASSED_UNIT_TESTS = 'UT' - DID_NOT_COMPILE = 'CF' - COULD_NOT_LINK = 'LF' - MANUAL = 'M' + # what is the current status of our feedback + EDITABLE = 'I' + ACCEPTED = 'A' + NEEDS_REVIEW = 'R' + OPEN = 'O' + STATUS = ( + (EDITABLE, 'editable'), + (ACCEPTED, 'accepted'), + (NEEDS_REVIEW, 'request review'), + (OPEN, 'request reassignment'), + ) + status = models.CharField( + max_length=1, + choices=STATUS, + default=EDITABLE, + ) + + # how was this feedback created + WAS_EMPTY = 'E' + PASSED_UNIT_TESTS = 'UT' + DID_NOT_COMPILE = 'CF' + COULD_NOT_LINK = 'LF' + MANUAL = 'M' ORIGIN = ( - (WAS_EMPTY, 'was empty'), + (WAS_EMPTY, 'was empty'), (PASSED_UNIT_TESTS, 'passed unittests'), - (DID_NOT_COMPILE, 'did not compile'), - (COULD_NOT_LINK, 'could not link'), - (MANUAL, 'created by a human. yak!'), + (DID_NOT_COMPILE, 'did not compile'), + (COULD_NOT_LINK, 'could not link'), + (MANUAL, 'created by a human. yak!'), ) origin = models.CharField( max_length=2, @@ -179,8 +192,8 @@ class Feedback(models.Model): @classmethod def tutor_unfinished_feedback(cls, user): - """Gets only the feedback that is assigned and not final. A tutor - should have only one feedback assigned that is not final + """Gets only the feedback that is assigned and not accepted. A tutor + should have only one feedback assigned that is not accepted Arguments: user {User} -- the tutor who formed the request @@ -189,7 +202,7 @@ class Feedback(models.Model): Feedback -- the feedback or none if no feedback was assigned """ tutor_feedback = cls.objects.filter( - of_tutor=user, empty=True, + Q(of_tutor=user), Q(status=Feedback.EDITABLE), ) return tutor_feedback[0] if tutor_feedback else None @@ -201,33 +214,35 @@ class Feedback(models.Model): [list] -- a QuerySet of tasks that have been assigned to this tutor """ tutor_feedback = cls.objects.filter(of_tutor=user) - assert len(tutor_feedback.objects.filter(empty=True)) <= 1 return tutor_feedback def finalize_feedback(self, user): - """ Used to mark feedback as final (reviewed) + """ Used to mark feedback as accepted (reviewed) This makes it uneditable by the tutor Arguments: user {[type]} -- [description] """ - self.empty = False - self.final = True + assert Feedback.status != Feedback.ACCEPTED + self.status = Feedback.ACCEPTED self.of_reviewer = user self.of_submission.final_feedback = self self.of_submission.save() self.save() def unfinalize_feedback(self): - """ Used to mark feedback as final (reviewed) + """ Used to mark feedback as accepted (reviewed) This makes it uneditable by the tutor - - Arguments: - user {[type]} -- [description] """ - self.final = False + assert Feedback.status == Feedback.ACCEPTED + self.origin = Feedback.MANUAL self.of_reviewer = None self.of_submission.final_feedback = None self.save() + + def reassign_to_tutor(self, user): + self.of_tutor = user + self.status = Feedback.EDITABLE + self.save() diff --git a/core/static/lib/css/custom.css b/core/static/lib/css/custom.css index ddf9f8f22df07614e834a4d5d6f033076c01e6dc..1a88e398267593d21057e9094195709874257c5a 100644 --- a/core/static/lib/css/custom.css +++ b/core/static/lib/css/custom.css @@ -26,7 +26,11 @@ pre { } .editor-pre { - height: 400px; + height: 200px; +} + +.nopadding { + padding: 0 !important; } .nopadding-right { diff --git a/core/templates/core/feedback_badge.html b/core/templates/core/feedback_badge.html new file mode 100644 index 0000000000000000000000000000000000000000..724f0ec9e605bc5b15f402036ebec2c9b649a2b7 --- /dev/null +++ b/core/templates/core/feedback_badge.html @@ -0,0 +1,9 @@ +{% if feedback.status == feedback.EDITABLE %} + <span class="badge badge-info">{{feedback.get_status_display}}</span> +{% elif feedback.status == feedback.NEEDS_REVIEW %} + <span class="badge badge-primary">{{feedback.get_status_display}}</span> +{% elif feedback.status == feedback.ACCEPTED %} + <span class="badge badge-success">{{feedback.get_status_display}}</span> +{% elif feedback.status == feedback.OPEN %} + <span class="badge badge-warning">{{feedback.get_status_display}}</span> +{% endif %} diff --git a/core/templates/core/feedback_form.html b/core/templates/core/feedback_form.html index 265e6b6a463867dc2d373a4df3e0dcbd73a78ca5..68a99f6ada233b8056cb6223fc3dcd946e01599a 100644 --- a/core/templates/core/feedback_form.html +++ b/core/templates/core/feedback_form.html @@ -36,7 +36,7 @@ </a> <div id="collapse5" class="collapse hide" role="tabpanel"> <div class="card-block m-2"> - <div id="solution" class="editor editor-pre">{{feedback.of_submission.type.possible_solution}}</div> + <div id="solution" class="editor editor-code">{{feedback.of_submission.type.possible_solution}}</div> </div> </div> </div> @@ -51,43 +51,64 @@ </div> <div class="col my-2"> - <div class="card"> - <h4 class="card-header">Please provide your feedback</h4> - <div class="card-block"> - <div class="editor editor-code" id="tutor_text" autofocus>{{feedback.text}}</div> + <div class="row-auto mb-2"> + <div class="card"> + <h4 class="card-header">Please provide your feedback</h4> + <div class="card-block"> + <div class="editor editor-code" id="tutor_text" autofocus>{{feedback.text}}</div> + </div> </div> </div> - <form action="{% url 'FeedbackEdit' feedback.slug %}" method="post" id="form1"> - {% csrf_token %} - {% for field in form %} - {% if field.name == "score" %} - <div class="row mt-2"> - <div class="col-6"> - <div class="input-group"> - <span class="input-group-addon" id="sizing-addon1">Score:</span> - <input class="form-control" id="id_score" min="0" max="{{ feedback.of_submission.type.full_score }}" name="score" type="number" value="{{ field.value }}" required> + + <div class="row-auto"> + <form action="{% url 'FeedbackEdit' feedback.slug %}" method="post" id="form1"> + {% csrf_token %} + <div hidden> {{ form.text }} </div> + <div class="form-inline"> + + {# Score field #} + <div class="input-group col-4 nopadding mr-1"> + <span class="input-group-addon">Score:</span> + <input + class="form-control" + id="id_score" + min="0" max="{{ feedback.of_submission.type.full_score }}" + name="score" + type="number" + value="{{ form.score.value }}" + required + {% if feedback.status == feedback.ACCEPTED %}readonly{% endif %}> <span class="input-group-btn"> <button id="assign_full_score" class="btn btn-secondary" type="button"><code>/{{ feedback.of_submission.type.full_score }}</code></button> </span> </div> - </div> - <div class="col-6"> - {% if not feedback.final %} - <button type="submit" form="form1" class="btn btn-success mb-1" name="update" value="Submit">Submit</button> - <button type="submit" form="form1" class="btn btn-success mb-1" name="update" value="Next">Next</button> - <a href="{% url 'FeedbackDelete' feedback.slug %}" class="btn btn-danger mb-1" name="delete" value="Delete">Delete</a> - {% else %} - <button class="btn btn-secondary mb-1 mx-1" value="Submit" disabled>View is read only</button> + {% with form.fields.status as status %} + <div class="form-group mr-1"> + <select class="custom-select" id="id_status" name="status"> + {% for val, name in status.choices %} + <option value="{{val}}" {% if val == feedback.status %}selected{% endif %}> {{name}}</option> + {% endfor %} + </select> + </div> + {% endwith %} + + <button type="submit" form="form1" class="btn btn-secondary mr-1" name="update" value="Submit">Submit</button> + + {% if feedback.status != feedback.ACCEPTED and feedback.origin == feedback.MANUAL%} + <button type="submit" form="form1" class="btn btn-success mr-1" name="update" value="Next">Next</button> {% endif %} - </div> - {% else %} - <div hidden> {{ field }} </div> - {% endif %} - {% endfor %} - </div> - </form> + {% if feedback.origin != feedback.MANUAL %} + <a href="{% url 'FeedbackDelete' feedback.slug %}" class="btn btn-outline-danger" name="delete" value="Delete">Delete auto feedback</a> + {% endif %} + + {% if feedback.status == feedback.ACCEPTED %} + <button class="btn btn-secondary mr-1" value="Submit" disabled>View is read only</button> + {% endif %} + </div> + </form> + </div> {# This is where all the messages pop up #} {% include "core/message_box.html" %} </div> @@ -100,21 +121,21 @@ {% block script_block %} <script> - $(document).ready(function(){ - - $('#collapseAllOpen').click(function(){ - $('.collapse').collapse('show'); - }); - $('#collapseAllClose').click(function(){ - $('.collapse').collapse('hide'); - }); + $('#collapseAllOpen').click(function(){ + $('.collapse').collapse('show'); + }); - $('#assign_full_score').click(function(){ - $('#id_score')[0].value = {{ feedback.of_submission.type.full_score }}; - }) + $('#collapseAllClose').click(function(){ + $('.collapse').collapse('hide'); }); + {% if feedback.status != feedback.ACCEPTED %} + $('#assign_full_score').click(function(){ + $('#id_score')[0].value = {{ feedback.of_submission.type.full_score }}; + }) + {% endif %} + // we need this one for the compiler erros readonly var editor_pre = ace.edit("pre_corrections"); editor_pre.setOptions({ @@ -140,12 +161,12 @@ // we need this one for the tutor var editor_tutor = ace.edit("tutor_text"); var textarea = $('textarea[id="id_text"]'); - textarea.hide(); editor_tutor.focus(); editor_tutor.getSession().on("change", function () { textarea.val(editor_tutor.getSession().getValue()); }); - {% if feedback.final %} + + {% if feedback.status == feedback.ACCEPTED %} editor_tutor.setOptions({ readOnly: true, }) diff --git a/core/templates/core/feedback_list.html b/core/templates/core/feedback_list.html index 8c893ea95c1fc5124169ead48407e21f94b6c8b1..0fe40eb651c2172abb123cadecc15ac449e6c507 100644 --- a/core/templates/core/feedback_list.html +++ b/core/templates/core/feedback_list.html @@ -2,7 +2,7 @@ <a data-toggle="collapse" href="#collapse{{unique}}"> <h5 class="card-header">{{header}}</h5> </a> - <div id="collapse{{unique}}" class="collapse hide" role="tabpanel"> + <div id="collapse{{unique}}" class="collapse {{expanded}}" role="tabpanel"> <div class="card-block"> <table class="table nomargin"> <thead> @@ -19,9 +19,7 @@ {% for feedback in feedback_list %} <tr> <td class="align-middle"> - {% if feedback.final %} - <span class="badge badge-success">Final</span> - {% endif %} + {% include "core/feedback_badge.html" %} </td> <td class="align-middle"> {{ feedback.of_submission.type }} </td> <td class="align-middle"> {{ feedback.of_submission.student }} </td> @@ -31,11 +29,6 @@ {% if not feedback.origin == feedback.WAS_EMPTY %} <a href="{% url 'FeedbackEdit' feedback.slug %}" class="btn btn-outline-primary mb-1" name="edit" value="View">View</a> <a href="{% url 'FeedbackDelete' feedback.slug %}" class="btn btn-outline-danger mb-1" name="delete" value="Delete">Delete</a> - {% if not feedback.final %} - <a class="btn btn-outline-success mb-1" href="{% url 'FeedbackMarkFinal' feedback.slug %}"> Mark final </a> - {% else %} - <a class="btn btn-outline-secondary mb-1" href="{% url 'FeedbackMarkNotFinal' feedback.slug %}"> Undo </a> - {% endif %} {% endif %} </td> </tr> diff --git a/core/templates/core/reviewer_startpage.html b/core/templates/core/reviewer_startpage.html index 7c13fdaf6c8a5ec581a64086526933d5294c223e..05ba813de61700dc75a7443f32a145cc063a5026 100644 --- a/core/templates/core/reviewer_startpage.html +++ b/core/templates/core/reviewer_startpage.html @@ -74,9 +74,8 @@ <tbody> {% for feedback in feedback_list_manual %} <tr> - <td class="align-middle"> {% if feedback.final %} - <span class="badge badge-success">Final</span> - {% endif %} + <td class="align-middle"> + {% include "core/feedback_badge.html" %} </td> <td class="align-middle"> {{ feedback.of_submission.type }} </td> <td class="align-middle"> {{ feedback.of_submission.student }} </td> @@ -85,11 +84,6 @@ <td class="align-middle"> <a href="{% url 'FeedbackEdit' feedback.slug %}" class="btn btn-outline-primary mb-1" name="edit" value="View">View</a> <a href="{% url 'FeedbackDelete' feedback.slug %}" class="btn btn-outline-danger mb-1" name="delete" value="Delete">Delete</a> - {% if not feedback.final %} - <a class="btn btn-outline-success mb-1" href="{% url 'FeedbackMarkFinal' feedback.slug %}"> Mark final </a> - {% else %} - <a class="btn btn-outline-secondary mb-1" href="{% url 'FeedbackMarkNotFinal' feedback.slug %}"> Undo </a> - {% endif %} </td> </tr> {% endfor %} @@ -99,9 +93,9 @@ </div> {# This is card for empty feedback for the sake of completeness #} - {% include "core/feedback_list.html" with header="Did not compile feedback" unique="2" feedback_list=feedback_list_did_not_compile %} - {% include "core/feedback_list.html" with header="Could not link feedback" unique="3" feedback_list=feedback_list_could_not_link %} - {% include "core/feedback_list.html" with header="Empty feedback" unique="1" feedback_list=feedback_list_empty %} + {% include "core/feedback_list.html" with expanded="show" header="Did not compile feedback" unique="2" feedback_list=feedback_list_did_not_compile %} + {% include "core/feedback_list.html" with expanded="show" header="Could not link feedback" unique="3" feedback_list=feedback_list_could_not_link %} + {% include "core/feedback_list.html" with expanded="hide" header="Empty feedback" unique="1" feedback_list=feedback_list_empty %} </div> </div> diff --git a/core/templates/core/tutor_startpage.html b/core/templates/core/tutor_startpage.html index fea2a4f39a9d1afb8baba43ca9688232070af9eb..a3f5b8591c0c577f0a84b595b41cc4a1e1813e1b 100644 --- a/core/templates/core/tutor_startpage.html +++ b/core/templates/core/tutor_startpage.html @@ -10,7 +10,7 @@ <div class="col-5"> {# This is a control panel where new work can be requested #} - <div class="card"> + <div class="card mb-2"> <h4 class="card-header">Overview</h4> <table class="table nomargin"> <thead> @@ -37,6 +37,29 @@ </div> </div> + {# Open feedback will be displayed here #} + {% if feedback_open_list %} + <div class="card mb-2"> + <h4 class="card-header">Open Feedback</h4> + <table class="table nomargin"> + <thead> + <th>Task</th> + <th>Tutor</th> + <th></th> + </thead> + <tbody> + {% for feedback in feedback_open_list %} + <tr> + <td class="align-middle"> {{ feedback.of_submission.type }} </td> + <td class="align-middle"> {{ feedback.of_tutor }} </td> + <td class="align-middle"><a class="btn btn-secondary" href="{% url 'FeedbackEdit' feedback.slug %}"> Assign </a></td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + {% endif %} + {# This is where all the messages pop up #} {% include "core/message_box.html" %} @@ -47,6 +70,7 @@ <table class="table"> <thead> <tr> + <th>Status</th> <th>Submission Type</th> <th>Score</th> <th></th> @@ -55,10 +79,13 @@ <tbody> {% for feedback in feedback_list %} <tr> + <td> + {% include "core/feedback_badge.html" %} + </td> <td> {{ feedback.of_submission.type }} </td> <td> <code> {{ feedback.score }} / {{feedback.of_submission.type.full_score}} </code> </td> <td> - {% if feedback.final %} + {% if feedback.status == feedback.ACCEPTED %} <a class="btn btn-secondary" href="{% url 'FeedbackEdit' feedback.slug %}"> View </a> {% else %} <a class="btn btn-primary" href="{% url 'FeedbackEdit' feedback.slug %}"> Edit </a> diff --git a/core/urls.py b/core/urls.py index 6cb9bd907be7bb2b2073181c19d26deb1f903e72..992524b9c8caaed5d945dd108715942fbc6ed718 100644 --- a/core/urls.py +++ b/core/urls.py @@ -14,10 +14,10 @@ urlpatterns = [ url(r'^feedback/edit/(?P<feedback_slug>\w+)/$', views.FeedbackEdit.as_view(), name='FeedbackEdit'), url(r'^feedback/delete/(?P<feedback_slug>\w+)/$', views.delete_feedback, name='FeedbackDelete'), - url(r'^feedback/markfinal/(?P<feedback_slug>\w+)/$', views.markfinal_feedback, name='FeedbackMarkFinal'), - url(r'^feedback/markfinal/(?P<feedback_slug>\w+)/undo/$', views.markunfinal_feedback, name='FeedbackMarkNotFinal'), url(r'^submission/view/(?P<slug>\w+)/$', views.SubmissionView.as_view(), name='SubmissionView'), + + url(r'^csv/$', views.export_csv, name='export') ] urlpatterns += staticfiles_urlpatterns() diff --git a/core/views/__init__.py b/core/views/__init__.py index 63ae4f402f8e1f3edd689c7d7dffd1860e558dff..e38262c866dfc845ff6e94071fbe71f84ea42125 100644 --- a/core/views/__init__.py +++ b/core/views/__init__.py @@ -3,3 +3,4 @@ from .submission import * from .login import * from .user_startpages import * from .index import * +from .export_csv import * diff --git a/core/views/export_csv.py b/core/views/export_csv.py new file mode 100644 index 0000000000000000000000000000000000000000..b322258eb6672e039925b630e962d3cbc30fc235 --- /dev/null +++ b/core/views/export_csv.py @@ -0,0 +1,28 @@ +import csv +from django.http import HttpResponse + +from core.models import Student, SubmissionType, Submission +from core.custom_annotations import group_required + + +@group_required('Reviewers') +def export_csv(request): + # Create the HttpResponse object with the appropriate CSV header. + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename="grady_results.csv"' + + writer = csv.writer(response) + writer.writerow(['Matrikel', 'Name', 'Summe'] + + [s.name for s in SubmissionType.objects.all().order_by('name')]) + + for student in Student.objects.all(): + submissions = Submission.objects.filter(student=student) + score_list = [s.final_feedback.score if s.final_feedback else 0 for s in submissions.order_by('type__name')] + writer.writerow([ + student.matrikel_no, + student.name, + sum(score_list), + *score_list + ]) + + return response diff --git a/core/views/feedback.py b/core/views/feedback.py index 4aa7230759cc0259fd19cee85e47ec0926ed5de2..b7cff5c7dfd8efc983fc50fc6c07b706d848a62b 100644 --- a/core/views/feedback.py +++ b/core/views/feedback.py @@ -29,31 +29,14 @@ def create_feedback(request, type_slug=None): return HttpResponseRedirect(reverse('FeedbackEdit', args=(feedback.slug,))) -@group_required('Tutors', 'Reviewers') +@group_required('Reviewers') def delete_feedback(request, feedback_slug): """ Hook to ensure object is owned by request.user. """ instance = Feedback.objects.get(slug=feedback_slug) - if instance.of_tutor != request.user and not in_groups(request.user, ('Reviewers', )): - raise Http404 instance.delete() return HttpResponseRedirect(reverse('start')) -@group_required('Reviewers') -def markfinal_feedback(request, feedback_slug): - instance = Feedback.objects.get(slug=feedback_slug) - instance.finalize_feedback(request.user) - return HttpResponseRedirect(reverse('start')) - - -@group_required('Reviewers') -def markunfinal_feedback(request, feedback_slug): - instance = Feedback.objects.get(slug=feedback_slug) - instance.unfinalize_feedback() - instance.origin = Feedback.MANUAL - return HttpResponseRedirect(reverse('start')) - - class FeedbackEdit(UpdateView): """docstring for FeedbackCreate""" @@ -68,17 +51,22 @@ class FeedbackEdit(UpdateView): def get_object(self): instance = Feedback.objects.get(slug=self.kwargs['feedback_slug']) - if instance.of_tutor != self.request.user and not in_groups(self.request.user, ('Reviewers', )): - raise Http404 - return instance + if instance.of_tutor == self.request.user or in_groups(self.request.user, ('Reviewers', )): + return instance + elif instance.status == Feedback.OPEN: + instance.reassign_to_tutor(self.request.user) + return instance + raise Http404 def form_valid(self, form): """ If the form is valid, redirect to the supplied URL. """ if form.is_valid(): - form.instance.empty = False - form.save() + if form.instance.status == Feedback.ACCEPTED: + form.instance.finalize_feedback(self.request.user) + else: + form.instance.unfinalize_feedback() if 'Next' in self.request.POST['update'] and not in_groups(self.request.user, ('Reviewers', )): return HttpResponseRedirect(reverse('CreateFeedbackForType', args=(form.instance.of_submission.type.slug,))) return HttpResponseRedirect(self.get_success_url()) diff --git a/core/views/user_startpages.py b/core/views/user_startpages.py index fb5f19f09d68f6bb2eaa5795e3eb52a04fb0e05a..1e8c7c1324513e6ed317ff2f286e439f00892721 100644 --- a/core/views/user_startpages.py +++ b/core/views/user_startpages.py @@ -1,6 +1,6 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User -from django.db.models import Count, Case, When, IntegerField, Value +from django.db.models import Count, Q from django.http import HttpResponseRedirect from django.shortcuts import render from django.urls import reverse @@ -46,6 +46,7 @@ def tutor_view(request): context = { 'submission_type_list': get_annotated_feedback_count(), 'feedback_list': Feedback.objects.filter(of_tutor=request.user), + 'feedback_open_list': Feedback.objects.filter(Q(status=Feedback.OPEN) & ~Q(of_tutor=request.user)), } return render(request, 'core/tutor_startpage.html', context) diff --git a/populatedb.py b/populatedb.py index decde083143f252faa09daf92cf022c41506de75..e446c0be06b2277bdd4bc2e93a4cbbb2254b5ab8 100644 --- a/populatedb.py +++ b/populatedb.py @@ -5,7 +5,6 @@ import django import getpass import json import argparse -import subprocess from collections import namedtuple django.setup() @@ -101,27 +100,24 @@ def add_auto_feedback(submission, compiler_output): if not compiler_output: return # let the tutor do his job - def deduct_feedback_type(): + def deduct_feedback_type() -> (str, str): if not submission.text: - return Feedback.WAS_EMPTY + return Feedback.WAS_EMPTY, Feedback.ACCEPTED elif compiler_output.endswith('DID NOT COMPILE'): - return Feedback.DID_NOT_COMPILE + return Feedback.DID_NOT_COMPILE, Feedback.NEEDS_REVIEW elif compiler_output.endswith('COULD NOT LINK'): - return Feedback.COULD_NOT_LINK + return Feedback.COULD_NOT_LINK, Feedback.NEEDS_REVIEW auto_correct, _ = User.objects.get_or_create(username='auto_correct') feedback = Feedback() feedback.text = "--- Was generated automatically ---" - feedback.empty = False - feedback.origin = deduct_feedback_type() + feedback.origin, feedback.status = deduct_feedback_type() feedback.of_submission = submission feedback.of_tutor = auto_correct + feedback.save() if feedback.origin == Feedback.WAS_EMPTY: - feedback.final = True - feedback.save() submission.final_feedback = feedback submission.save() - feedback.save() print(f"- Created {feedback.origin} Feedback for Submission {submission}") return feedback