diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py index 0a1832a515a9b716324bfae5134e2c92024de6d1..41d82568a409984d883a048591eb3171deb98e6a 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -2,10 +2,11 @@ # Generated by Django 1.10.6 on 2017-04-05 20:11 from __future__ import unicode_literals -import core.models +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion + +import core.models class Migration(migrations.Migration): diff --git a/core/migrations/0002_auto_20170412_1447.py b/core/migrations/0002_auto_20170412_1447.py index 592dc9d2199eb7dc7250b274b44dd3bbba027ae6..f92b185386910b6e770fa8403e32b8b2502ba24d 100644 --- a/core/migrations/0002_auto_20170412_1447.py +++ b/core/migrations/0002_auto_20170412_1447.py @@ -2,8 +2,8 @@ # Generated by Django 1.10.7 on 2017-04-12 14:47 from __future__ import unicode_literals -from django.db import migrations, models import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/core/migrations/0005_auto_20170413_0124.py b/core/migrations/0005_auto_20170413_0124.py index 571477e152adb23bfe722f5ceeb6c98c2dd9c7ab..b25f249e46b66aab720450b013058e13bc31beda 100644 --- a/core/migrations/0005_auto_20170413_0124.py +++ b/core/migrations/0005_auto_20170413_0124.py @@ -2,8 +2,8 @@ # Generated by Django 1.10.7 on 2017-04-13 01:24 from __future__ import unicode_literals -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/core/migrations/0007_auto_20170522_1827.py b/core/migrations/0007_auto_20170522_1827.py index f69800a37fe4bdb41c7f1d037dc4d2eb2e077b7a..fb6c0578ab3202c63ea9376750c9fff4fc7b54af 100644 --- a/core/migrations/0007_auto_20170522_1827.py +++ b/core/migrations/0007_auto_20170522_1827.py @@ -2,8 +2,8 @@ # Generated by Django 1.10.7 on 2017-05-22 18:27 from __future__ import unicode_literals -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/core/migrations/0010_auto_20170710_1604.py b/core/migrations/0010_auto_20170710_1604.py index 319da912d39b54051376e985c63dc8407adf5c0d..ab702ede9c84a59e0762885ed0aaef9d40d5ae2d 100644 --- a/core/migrations/0010_auto_20170710_1604.py +++ b/core/migrations/0010_auto_20170710_1604.py @@ -2,8 +2,8 @@ # Generated by Django 1.10.7 on 2017-07-10 16:04 from __future__ import unicode_literals -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/core/migrations/0012_auto_20170711_1104.py b/core/migrations/0012_auto_20170711_1104.py index ca27ba4ce3c4c3b614a893878768bc9b02c9765e..9cc1976430b11665b40073f68b976860fd163598 100644 --- a/core/migrations/0012_auto_20170711_1104.py +++ b/core/migrations/0012_auto_20170711_1104.py @@ -2,8 +2,8 @@ # Generated by Django 1.10.7 on 2017-07-11 11:04 from __future__ import unicode_literals -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/core/migrations/0013_auto_20170712_1643.py b/core/migrations/0013_auto_20170712_1643.py new file mode 100644 index 0000000000000000000000000000000000000000..bd526207077997edd6dd8b0d2277dd1537d96f66 --- /dev/null +++ b/core/migrations/0013_auto_20170712_1643.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-07-12 16:43 +from __future__ import unicode_literals + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0012_auto_20170711_1104'), + ] + + operations = [ + migrations.AlterField( + model_name='examtype', + name='module_reference', + field=models.CharField(max_length=50, unique=True), + ), + migrations.AlterField( + model_name='student', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='student', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/core/migrations/0014_auto_20170712_1704.py b/core/migrations/0014_auto_20170712_1704.py new file mode 100644 index 0000000000000000000000000000000000000000..65a9c943d2aa857d13e44a062bacf5b24629bd5b --- /dev/null +++ b/core/migrations/0014_auto_20170712_1704.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-07-12 17:04 +from __future__ import unicode_literals + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0013_auto_20170712_1643'), + ] + + operations = [ + migrations.AlterField( + model_name='feedback', + name='of_tutor', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedback_list', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/core/models.py b/core/models.py index 8f417ff80cd5967f47be54d862c562b9a30d7f5a..6801e33b57afcd821a58b8b0ddaac94956c5a884 100644 --- a/core/models.py +++ b/core/models.py @@ -41,13 +41,15 @@ ########################################################################## -from random import sample, randrange -from string import ascii_lowercase from collections import OrderedDict +from random import randrange, sample +from string import ascii_lowercase from django.contrib.auth.models import User from django.db import models -from django.db.models import Q, F, Sum, Value as V, When, Case, BooleanField +from django.db.models import Value as V +from django.db.models import (BooleanField, Case, Count, F, IntegerField, Q, + Sum, When) from django.db.models.functions import Coalesce SLUG_LENGTH = 16 @@ -61,6 +63,13 @@ def random_matrikel_no(): return str(2e7 + randrange(1e8)) +def get_annotated_tutor_list(): + return User.objects\ + .annotate(Count('corrected_submissions'))\ + .filter(groups__name='Tutors')\ + .order_by('-corrected_submissions__count') + + class ExamType(models.Model): class Meta: @@ -92,6 +101,39 @@ class SubmissionType(models.Model): verbose_name = "SubmissionType" verbose_name_plural = "SubmissionType Set" + @classmethod + def get_annotated_feedback_count(cls): + """ Annotates submission lists with counts + + count both + * number of submission per submission type + * count of received feedback per submission type + * + Alternative with case + Count(Case( + When(submissions__feedback_list__origin=Feedback.MANUAL, + then=Value(1)), output_field=IntegerField()) + ) + + Returns: + annotated queryset + """ + return cls.objects\ + .annotate( # to display only manual + feedback_count=Count( + Case( + When( + Q(submissions__feedback__isnull=False) & + Q(submissions__feedback__status=Feedback.ACCEPTED), + then=V(1)), output_field=IntegerField(), + ) + ) + ).annotate( + submission_count=Count('submissions') + ).annotate( + percentage=(F('feedback_count') * 100 / F('submission_count')) + ).all().order_by('name') + class Student(models.Model): # Fields @@ -102,6 +144,7 @@ class Student(models.Model): unique=True, max_length=8, default=random_matrikel_no) user = models.OneToOneField( User, on_delete=models.CASCADE, + related_name='student', limit_choices_to={'groups__name': 'Students'}, ) @@ -128,15 +171,6 @@ class Student(models.Model): ) ) - def overall_score(self): # TODO purge - return sum(self.score_per_submission().values()) - - def has_passed_exam(self): - return self.overall_score >= self.exam.pass_score - - def has_pass_or_fail_exam(self): - return self.exam.pass_only - def disable(self): self.has_logged_in = True self.save() @@ -263,7 +297,7 @@ class Feedback(models.Model): unique=True, blank=False, null=False) of_tutor = models.ForeignKey( - User, related_name='corrected_submissions',) + User, related_name='feedback_list',) of_reviewer = models.ForeignKey( User, related_name='reviewed_submissions', @@ -320,6 +354,14 @@ class Feedback(models.Model): def get_full_score(self): return self.of_submission.type.full_score + @classmethod + def get_open_feedback(cls, user): + return cls.objects.filter( + Q(status=Feedback.OPEN) & + ~Q(of_tutor=user) # you shall not request your own feedback + ) + + @classmethod def tutor_unfinished_feedback(cls, user): """Gets only the feedback that is assigned and not accepted. A tutor diff --git a/core/templates/core/r/reviewer_base.html b/core/templates/core/r/reviewer_base.html index f2e5e46f9d01d24e2b65e4a28986f7f3d60012f4..c79f575f6e3321ba897604d6c1dd143394a65e58 100644 --- a/core/templates/core/r/reviewer_base.html +++ b/core/templates/core/r/reviewer_base.html @@ -4,8 +4,8 @@ {% block navbar %} <a class="nav-item nav-link" href="{% url 'start' %}">Feedback</a> -<a class="nav-item nav-link" href="{% url 'submission_list' %}">Submissions</a> -<a class="nav-item nav-link" href="{% url 'student_list' %}">Students</a> +<a class="nav-item nav-link" href="{% url 'ReviewerSubmissionListView' %}">Submissions</a> +<a class="nav-item nav-link" href="{% url 'ReviewerStudentListView' %}">Students</a> <div class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="http://example.com" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> Export diff --git a/core/templates/core/r/student_list.html b/core/templates/core/r/student_list.html index 4d4837216ba95aa407d95bea01f1c6afd236de6a..57f433f05720ecc6e29f96ea93772ad86635c508 100644 --- a/core/templates/core/r/student_list.html +++ b/core/templates/core/r/student_list.html @@ -28,9 +28,9 @@ <td>{{student.exam}}</td> {% for sub in student.submissions.all %} <td>{% if sub.feedback %} - <a href="{% url 'SubmissionViewReviewer' sub.slug %}" role="button" class="btn-link btn-sm"><code>{{sub.feedback.score}} / {{sub.type.full_score}}</code></a> + <a href="{% url 'ReviewerSubmissionView' sub.slug %}" role="button" class="btn-link btn-sm"><code>{{sub.feedback.score}} / {{sub.type.full_score}}</code></a> {% else %} - <a href="{% url 'SubmissionViewReviewer' sub.slug %}" role="button" class="btn btn-outline-primary btn-sm">View</a> + <a href="{% url 'ReviewerSubmissionView' sub.slug %}" role="button" class="btn btn-outline-primary btn-sm">View</a> {% endif %} </td> {% endfor %} <td><code>{{student.overall_score}}</code></td> diff --git a/core/templates/core/r/student_submission_list.html b/core/templates/core/r/student_submission_list.html index 59bea6014bdaabec8d182e335d77db81e34ed8be..484bdd8e06323d57ae8256cd09ad8ef8f53fe145 100644 --- a/core/templates/core/r/student_submission_list.html +++ b/core/templates/core/r/student_submission_list.html @@ -21,7 +21,7 @@ <tbody> {% for submission in submission_list %} <tr> - <td class="align-middle fit"> <a href="{% url 'SubmissionViewReviewer' submission.slug %}" class="btn btn-outline-primary mb-1" name="edit" value="View">View submission</a></td> + <td class="align-middle fit"> <a href="{% url 'ReviewerSubmissionView' submission.slug %}" class="btn btn-outline-primary mb-1" name="edit" value="View">View submission</a></td> <td class="align-middle"> {{ submission.type }} </td> <td class="align-middle"> {{ submission.student }} </td> <td class="align-middle fit"> diff --git a/core/templates/core/s/student_startpage.html b/core/templates/core/s/student_startpage.html index 9ee3bb14f84c78314c88b3265fb712efc1e1aa84..cd016dd7f5b03e445b895b4b2149f082118e580c 100644 --- a/core/templates/core/s/student_startpage.html +++ b/core/templates/core/s/student_startpage.html @@ -5,11 +5,10 @@ {% block navbar %} Student Exam View {% endblock navbar %} {% block body_block %} - <div class="row justify-content-center"> <div class="col-6"> <div class="row my-3"> - <h2>Hello {{ student.student }}</h2> + <h2>Submissions of {{ student.name }}</h2> </div> <div class="row my-2"> @@ -21,7 +20,7 @@ <th></th> </thead> <tbody> - {% for submission in submission_list %} + {% for submission in student.submissions.all %} <tr class="align-middle"> <td class="align-middle"> {% if submission.seen_by_student %} @@ -38,7 +37,7 @@ {% endif %} {% endwith %} </td> - <td class="align-middle"><a class="btn btn-primary" href="{% url 'SubmissionViewStudent' submission.slug %}">View</a></td> + <td class="align-middle"><a class="btn btn-primary" href="{% url 'StudentSubmissionView' submission.slug %}">View</a></td> </tr> {% endfor %} </tbody> @@ -46,7 +45,4 @@ </div> </div> </div> - - - {% endblock body_block %} diff --git a/core/templates/core/t/tutor_startpage.html b/core/templates/core/t/tutor_startpage.html index c9c3277ad5f2ca06d345280e6b3e6cc24b5d240a..52836ebf0bb4bb65d215a602108ca300b513f097 100644 --- a/core/templates/core/t/tutor_startpage.html +++ b/core/templates/core/t/tutor_startpage.html @@ -38,7 +38,7 @@ <tbody> <tr> <td class="fit"><strong>Your contribution:</strong></td> - <td colspan="6"><code>{% if feedback_list|length > 0 %} {{feedback_list|length}} {% else %} None. Sad. {% endif %}</code></td> + <td colspan="6"><code>{% if tutor.feedback_list.all|length > 0 %} {{tutor.feedback_list.all|length}} {% else %} None. Sad. {% endif %}</code></td> </tr> </tbody> </table> @@ -88,7 +88,7 @@ </tr> </thead> <tbody> - {% for feedback in feedback_list %} + {% for feedback in tutor.feedback_list.all %} <tr> <td> {% include "core/component/feedback_badge.html" %} diff --git a/core/urls.py b/core/urls.py index 4c36c25c43a46df9a68316a5114d4b5b40ff89e4..867d523a9e2629c36d74dcdda61c6a9514a50d8c 100644 --- a/core/urls.py +++ b/core/urls.py @@ -14,13 +14,13 @@ 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'^r/studentlist/$', views.StudentListView.as_view(), name='student_list'), - url(r'^r/submission/list/$', views.get_submission_list, name='submission_list'), - url(r'^r/submission/view/(?P<slug>\w+)/$', views.SubmissionViewReviewer.as_view(), name='SubmissionViewReviewer'), + url(r'^r/student/list/$', views.ReviewerStudentListView.as_view(), name='ReviewerStudentListView'), + url(r'^r/submission/list/$', views.ReviewerSubmissionListView.as_view(), name='ReviewerSubmissionListView'), + url(r'^r/submission/view/(?P<slug>\w+)/$', views.ReviewerSubmissionView.as_view(), name='ReviewerSubmissionView'), url(r'^r/submission/create-feedback-for/(?P<slug>\w+)/$', views.create_feedback_for_submission, name='create_feedback_for_submission'), url(r'^r/submission/download/(?P<slug>\w+)/$', views.download_submissions, name='download_submissions'), - url(r'^s/submission/view/(?P<slug>\w+)/$', views.SubmissionViewStudent.as_view(), name='SubmissionViewStudent'), + url(r'^s/submission/view/(?P<slug>\w+)/$', views.StudentSubmissionView.as_view(), name='StudentSubmissionView'), url(r'^csv/$', views.export_csv, name='export') ] diff --git a/core/views/__init__.py b/core/views/__init__.py index 9c990fbac6cd13d181bd5e7d190e563dc61067e2..67868d83114fb61ae463b7c2cd5b9bb0127f5bb1 100644 --- a/core/views/__init__.py +++ b/core/views/__init__.py @@ -1,6 +1,9 @@ from .login import * from .feedback import * +from .generics import * + from .submission 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 index 93dbffda982558fb661d244a1fc38e6dacb38ae9..1e1a7fdd12d212085b0f36a0bef223019bdd2106 100644 --- a/core/views/export_csv.py +++ b/core/views/export_csv.py @@ -1,8 +1,9 @@ import csv + from django.http import HttpResponse -from core.models import Student, SubmissionType from core.custom_annotations import group_required +from core.models import Student, SubmissionType @group_required('Reviewers') @@ -15,12 +16,12 @@ def export_csv(request): writer.writerow(['Matrikel', 'Username', 'Name', 'Sum'] + [s.name for s in SubmissionType.objects.all().order_by('name')]) - for student in Student.objects.all(): + for student in Student.get_overall_score_annotated_submission_list(): writer.writerow([ student.matrikel_no, student.user.username, student.name, - student.overall_score(), + student.overall_score, *student.score_per_submission().values() ]) diff --git a/core/views/feedback.py b/core/views/feedback.py index 5c60bc4e254c638feede522bbc7906ff1b613861..db95a26d8457c2286f871350787b1fa5846f4b9b 100644 --- a/core/views/feedback.py +++ b/core/views/feedback.py @@ -1,7 +1,7 @@ from random import choice from django.contrib import messages -from django.http import Http404, HttpResponseRedirect, HttpResponse +from django.http import Http404, HttpResponse, HttpResponseRedirect from django.urls import reverse from django.utils.decorators import method_decorator from django.views.generic.edit import UpdateView diff --git a/core/views/generics.py b/core/views/generics.py new file mode 100644 index 0000000000000000000000000000000000000000..6afd8c675c3dc4707226755f5d773e2852fedf20 --- /dev/null +++ b/core/views/generics.py @@ -0,0 +1,58 @@ +from django.utils.decorators import method_decorator +from django.views.generic import DetailView, ListView, View + +from core.custom_annotations import group_required +from core.models import SubmissionType, get_annotated_tutor_list + + +class StudentView(View): + + @method_decorator(group_required('Students',)) + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) + + +class StudentListView(StudentView, ListView): + pass + + +class StudentDetailView(StudentView, DetailView): + pass + + +class TutorView(View): + + @method_decorator(group_required('Tutors',)) + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) + + +class TutorListView(TutorView, ListView): + pass + + +class TutorDetailView(TutorView, DetailView): + pass + + +class ReviewerView(View): + + @method_decorator(group_required('Reviewers',)) + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) + + +class ReviewerListView(ReviewerView, ListView): + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + return { + 'submission_type_list': SubmissionType.get_annotated_feedback_count(), + 'tutor_list': get_annotated_tutor_list(), + **context, + } + + +class ReviewerDetailView(ReviewerView, DetailView): + pass diff --git a/core/views/login.py b/core/views/login.py index 4ebc8c7620d762bce09e61acd43440443c571bbd..708a1a89a1f8e67add915f8541d86dbb64111e02 100644 --- a/core/views/login.py +++ b/core/views/login.py @@ -1,12 +1,11 @@ +from django.contrib import messages from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required -from django.contrib import messages from django.http import HttpResponseRedirect from django.urls import reverse from core.custom_annotations import in_groups - __all__ = ('user_login', 'user_logout') def is_disabled(user): diff --git a/core/views/submission.py b/core/views/submission.py index fc6a14bb4a935f9b69f218f7f7a0133e0e5cc120..8d7eff78bfe5f9a5572f091d1c2284826a87916d 100644 --- a/core/views/submission.py +++ b/core/views/submission.py @@ -1,23 +1,15 @@ -from django.utils.decorators import method_decorator -from django.views.generic import DetailView, ListView -from django.shortcuts import render -from django.db.models import Sum +from django.views.generic import DetailView +from core.custom_annotations import in_groups +from core.models import Feedback, Student, Submission +from core.views.generics import (ReviewerDetailView, ReviewerListView, + StudentView) -from core.custom_annotations import group_required, in_groups -from core.models import Submission, Feedback, Student -from .user_startpages import get_annotated_feedback_count, get_annotated_tutor_list +class StudentSubmissionView(StudentView, DetailView): - -class SubmissionViewStudent(DetailView): - - template_name = 'core/s/single_submission.html' - model = Submission - - @method_decorator(group_required('Students',)) - def dispatch(self, *args, **kwargs): - return super(SubmissionViewStudent, self).dispatch(*args, **kwargs) + template_name = 'core/s/single_submission.html' + model = Submission def get_object(self): obj = Submission.objects.get(slug=self.kwargs['slug']) @@ -27,49 +19,27 @@ class SubmissionViewStudent(DetailView): return obj -class SubmissionViewReviewer(DetailView): +class ReviewerSubmissionView(ReviewerDetailView): + model = Submission template_name = 'core/r/single_submission.html' - model = Submission - - @method_decorator(group_required('Reviewers',)) - def dispatch(self, *args, **kwargs): - return super(SubmissionViewReviewer, self).dispatch(*args, **kwargs) def get_object(self): return Submission.objects.get(slug=self.kwargs['slug']) -@group_required('Reviewers') -def get_submission_list(request): - context = { - 'submission_list': Submission.objects.all(), - 'submission_type_list': get_annotated_feedback_count(), - 'tutor_list': get_annotated_tutor_list(), - } - return render(request, 'core/r/student_submission_list.html', context) +class ReviewerSubmissionListView(ReviewerListView): + + model = Submission + template_name = 'core/r/student_submission_list.html' + context_object_name = 'submission_list' -class StudentListView(ListView): +class ReviewerStudentListView(ReviewerListView): model = Student template_name = 'core/r/student_list.html' context_object_name = 'student_list' - @method_decorator(group_required('Reviewers',)) - def dispatch(self, *args, **kwargs): - return super().dispatch(*args, **kwargs) - def get_queryset(self): return self.model.get_overall_score_annotated_submission_list() - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - context = { - **context, - 'submission_type_list': get_annotated_feedback_count(), - 'tutor_list': get_annotated_tutor_list(), - } - - return context diff --git a/core/views/user_startpages.py b/core/views/user_startpages.py index f6fd29f16b8398882e791c24200b08170bca4b7b..748c3da10c7a41a96b3fb030469c42a678abc7e6 100644 --- a/core/views/user_startpages.py +++ b/core/views/user_startpages.py @@ -1,87 +1,67 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User -from django.db.models import Count, Q, F, Case, When, IntegerField, Value from django.http import HttpResponseRedirect -from django.shortcuts import render from django.urls import reverse -from core.custom_annotations import group_required, in_groups -from core.models import Feedback, Submission, SubmissionType +from core.custom_annotations import in_groups +from core.models import Feedback, Student, SubmissionType +from core.views.generics import (ReviewerListView, StudentDetailView, + TutorDetailView) @login_required(login_url='/') def user_home(request): if in_groups(request.user, ('Students', )): - return student_view(request) + return StudentStartPage.as_view()(request) elif in_groups(request.user, ('Tutors', )): - return tutor_view(request) + return TutorStartPage.as_view()(request) elif in_groups(request.user, ('Reviewers', )): - return reviewer_view(request) + return ReviewerFeedbackListView.as_view()(request) else: return HttpResponseRedirect(reverse('index')) -def get_annotated_feedback_count(): - """ Annotates submission lists with counts - - count both - * number of submission per submission type - * count of received feedback per submission type - * - Alternative with case - Count(Case( - When(submissions__feedback_list__origin=Feedback.MANUAL, - then=Value(1)), output_field=IntegerField()) - ) - - Returns: - annotated queryset - """ - return SubmissionType.objects\ - .annotate( # to display only manual - feedback_count=Count( - Case( - When(Q(submissions__feedback__isnull=False) & Q(submissions__feedback__status=Feedback.ACCEPTED), - then=Value(1)), output_field=IntegerField(), - ) - ) - ).annotate( - submission_count=Count('submissions') - ).annotate( - percentage=(F('feedback_count') * 100 / F('submission_count')) - ).all().order_by('name') - -def get_annotated_tutor_list(): - return User.objects.annotate(Count('corrected_submissions')).filter(groups__name='Tutors') \ - .order_by('-corrected_submissions__count') - -@group_required('Tutors') -def tutor_view(request): - submission_type = get_annotated_feedback_count() - context = { - 'submission_type_list': submission_type, - '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/t/tutor_startpage.html', context) - - -@group_required('Students') -def student_view(request): - context = { - 'student': request.user, - 'submission_list': Submission.objects.filter(student__user=request.user) - } - return render(request, 'core/s/student_startpage.html', context) - - -@group_required('Reviewers') -def reviewer_view(request): - context = { - 'submission_type_list': get_annotated_feedback_count(), - 'tutor_list': get_annotated_tutor_list(), - 'feedback_list_manual': Feedback.objects.filter(origin=Feedback.MANUAL), - 'feedback_list_empty': Feedback.objects.filter(origin=Feedback.WAS_EMPTY), - 'feedback_list_did_not_compile': Feedback.objects.filter(origin=Feedback.DID_NOT_COMPILE), - 'feedback_list_could_not_link': Feedback.objects.filter(origin=Feedback.COULD_NOT_LINK), - } - return render(request, 'core/r/reviewer_startpage.html', context) + +class TutorStartPage(TutorDetailView): + + model = User + template_name = 'core/t/tutor_startpage.html' + context_object_name = 'tutor' + + def get_object(self): + return self.request.user + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + return { + 'submission_type_list': SubmissionType.get_annotated_feedback_count(), + 'feedback_open_list': Feedback.get_open_feedback(self.get_object()), + **context + } + + +class StudentStartPage(StudentDetailView): + + model = Student + template_name = 'core/s/student_startpage.html' + + def get_object(self): + return self.request.user.student + + +class ReviewerFeedbackListView(ReviewerListView): + """ This is the de facto startpage of the reviewer accounts""" + + model = Feedback + template_name = 'core/r/reviewer_startpage.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + return { + 'feedback_list_manual': self.model.objects.filter(origin=Feedback.MANUAL), + 'feedback_list_empty': self.model.objects.filter(origin=Feedback.WAS_EMPTY), + 'feedback_list_did_not_compile': self.model.objects.filter(origin=Feedback.DID_NOT_COMPILE), + 'feedback_list_could_not_link': self.model.objects.filter(origin=Feedback.COULD_NOT_LINK), + **context + } diff --git a/grady/settings/default.py b/grady/settings/default.py index 646bf784389530c20921834f7fe5dc87756e2e4e..cdd76ebdd3bafa7746c0e9c36d25b131fdaf57fb 100644 --- a/grady/settings/default.py +++ b/grady/settings/default.py @@ -123,6 +123,9 @@ GRAPH_MODELS = { 'group_models': True, } +LOGIN_REDIRECT_URL = '/' +LOGIN_URL = '/' + MESSAGE_TAGS = { messages.DEBUG: 'alert-info', diff --git a/util/importer.py b/util/importer.py index 00d296226fabef031184d43c84c31c93b67325a2..b1598a059e4546e786c2bb4d842c01eb8ea7cebf 100644 --- a/util/importer.py +++ b/util/importer.py @@ -1,16 +1,17 @@ import collections import csv +import json import os import readline import secrets -import json from typing import Callable from django.contrib.auth.models import Group, User import util.convert import util.processing -from core.models import Feedback, Student, Submission, SubmissionType, Test, ExamType +from core.models import (ExamType, Feedback, Student, Submission, + SubmissionType, Test) from util.messages import * from util.processing import EmptyTest diff --git a/util/populatedb.py b/util/populatedb.py deleted file mode 100644 index f5a712b0d12d68a978a2e5b7765b616311d3f8c5..0000000000000000000000000000000000000000 --- a/util/populatedb.py +++ /dev/null @@ -1,297 +0,0 @@ -import argparse -import csv -import json -import os -from collections import namedtuple - -import django -import xkcdpass.xkcd_password as xp -from django.contrib.auth.models import Group, User - -from core.models import Feedback, Student, Submission, SubmissionType - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'grady.settings') - -django.setup() - -INFO = 1 - -HTML_DIR = 'html' -SOLUTION_DIR = 'code/code-lsg' - -wordfile = xp.locate_wordfile() -wordlist = xp.generate_wordlist(wordfile=wordfile, min_length=5, max_length=8) - - - - -if INFO: - info = print -else: - info = lambda _: None - - -def parseme(): - parser = argparse.ArgumentParser() - parser.add_argument( - '--create-users', - help='Will just populate auth_db', - action='store_true') - parser.add_argument( - '--compiler-output-only', - help='Will only add compiler output to existing data', - action='store_true') - parser.add_argument( - 'DATADIR', - help='a folder containing a predefined set of files with information', - default='data') - parser.add_argument( - '-s', '--submissions', - help='A file with submission code and student user names', - default='submissions.json', - metavar='FILE') - parser.add_argument( - '-t', '--tutors', - help='A list of tutor names', - default='tutors', - metavar="FILE") - parser.add_argument( - '-r', '--reviewers', - help='A list of reviewer names', - default='reviewers', - metavar="FILE") - parser.add_argument( - '-st', '--submission_types', - help='some kind of descriptions for all the submission types', - default='submission_types.csv', - metavar="FILE") - - args = parser.parse_args() - - args.tutors = os.path.join(args.DATADIR, args.tutors) - args.reviewers = os.path.join(args.DATADIR, args.reviewers) - args.submissions = os.path.join(args.DATADIR, args.submissions) - args.submission_types = os.path.join(args.DATADIR, args.submission_types) - - return args - - -def add_submission_type(name, - score, - task_description="__task_description: what the student saw", - possible_solution="__possible_solution: a sample solution", - correction_guideline="__possible_solution: a way to correct the task",): - task, created = SubmissionType.objects.get_or_create(name=name) - task.full_score = int(score) - task.task_description = task_description - task.possible_solution = possible_solution - task.correction_guideline = correction_guideline - task.save() - if created: - info(f"- Created Task {task.name}") - else: - info(f"- Task {task.name} was already created") - return task - - -def student_has_all_submissions(student): - return Submission.objects.filter(student=student).count() \ - == SubmissionType.objects.all().count() - - -def add_submission(type, text, student, compiler_output): - if student_has_all_submissions(student): - return None - - sub = Submission() - sub.type = type - sub.text = text - sub.student = student - sub.pre_corrections = compiler_output - sub.save() - add_auto_feedback(sub, compiler_output) - info(f"- Created Submission of Type {sub.type}") - return sub - - -def add_compiler_output_only(type, text, student, compiler_output): - - sub = Submission.objects.get(type=type, student=student) - if not sub: - return - sub.pre_corrections = compiler_output - sub.save() - info(f"- Added compiler output to submission {sub.type}") - return sub - - -def add_auto_feedback(submission, compiler_output): - if submission.text and not compiler_output: - return # let the tutor do his job - - def deduct_feedback_type() -> (str, str): - if not submission.text: - return Feedback.WAS_EMPTY, Feedback.ACCEPTED - elif compiler_output.endswith('DID NOT COMPILE'): - return Feedback.DID_NOT_COMPILE, Feedback.NEEDS_REVIEW - elif compiler_output.endswith('COULD NOT LINK'): - return Feedback.COULD_NOT_LINK, Feedback.NEEDS_REVIEW - return None, None - - auto_correct, _ = User.objects.get_or_create(username='auto_correct') - feedback = Feedback() - feedback.text = "--- Was generated automatically ---" - feedback.origin, feedback.status = deduct_feedback_type() - if feedback.origin is None and feedback.status is None: - return - feedback.of_submission = submission - feedback.of_tutor = auto_correct - feedback.save() - if feedback.origin == Feedback.WAS_EMPTY: - submission.final_feedback = feedback - submission.save() - info(f"- Created {feedback.origin} Feedback for Submission {submission}") - return feedback - - -def add_student(username, name, matrikel_no): - student_group, _ = Group.objects.get_or_create(name='Students') - student, created = Student.objects.get_or_create( - matrikel_no=matrikel_no, - user=add_user(username, student_group) - ) - if created: - student.name = name - student.matrikel_no = matrikel_no - student.save() - return student - - -def add_user(username, group): - user, created = User.objects.get_or_create(username=username) - - if created: - password = xp.generate_xkcdpassword(wordlist, numwords=2) - login_writer.writerow([username, password]) - user.set_password(password) - group.user_set.add(user) - info(f"- Created user {user} and added him to group {group}") - user.save() - else: - info(f"- User {user} of group {group} was already created.") - - return user - - -def add_group(group_name): - group, _ = Group.objects.get_or_create(name=group_name) - info(f"- Created group {group}") - return group - - -def create_superuser(): - try: - username = 'doncamillo' - password = xp.generate_xkcdpassword(wordlist, numwords=2) - login_writer.writerow(username, password) - User.objects.create_superuser( - username=username, password=password, email='mail-gardy@jmx.io') - except Exception as e: - info("- Superuser was already created.") - return - - -class PopulateDatabase: - - """docstring for PopulateDatabase""" - - __slots__ = ( - 'args', - 'type_dict', - 'student_group', - 'tutor_group', - 'reviewer_group', - ) - - def __init__(self, args): - self.args = args - if self.args.create_users: - self.create_groups() - self.create_user_accounts() - else: # dirty - self.create_submission_types() - self.populate_submissions() - - def create_groups(self): - self.student_group = add_group('Students') - self.tutor_group = add_group('Tutors') - self.reviewer_group = add_group('Reviewers') - - def create_user_accounts(self): - with open(self.args.tutors) as tutors: - for tutor in tutors: - add_user(tutor.strip(), self.tutor_group) - - with open(self.args.reviewers) as reviewers: - for reviewer in reviewers: - add_user(reviewer.strip(), self.reviewer_group) - - def create_submission_types(self): - submission_type = namedtuple('submission_type', 'id name score') - with open(args.submission_types) as data: - types = list(submission_type(*line.strip().split(', ')) - for line in data if line) - - self.type_dict = {} - for t in types: - with \ - open(os.path.join(self.args.DATADIR, SOLUTION_DIR, t.id + '-lsg.c' )) as lsg, \ - open(os.path.join(self.args.DATADIR, HTML_DIR, t.id + '.html' )) as desc: - self.type_dict[t.id] = add_submission_type( - f"[{t.id}] {t.name}", - t.score, - desc.read(), - lsg.read(), - ) - - def populate_submissions(self): - with open(self.args.submissions) as data: - stud_data = json.JSONDecoder().decode(data.read()) - - for user, userdata in stud_data.items(): - student = add_student( - user, userdata['name'], userdata['matrikel_no']) - for s, code in userdata['submissions'].items(): - if self.args.compiler_output_only: - add_compiler_output_only( - self.type_dict[s], code, student, - userdata['compiler_output'][s] - ) - else: - add_submission( - self.type_dict[s], code, student, - userdata['compiler_output'][s] - ) - - -# Start execution here! -if __name__ == '__main__': - args = parseme() - print("Starting population script...") - - LOGIN_FILE = 'login_data.csv' - - try: - login_data_f = open(LOGIN_FILE, 'a') - if os.stat(LOGIN_FILE).st_size == 0: - login_writer = csv.writer(login_data_f) - login_writer.writerow(['username', 'password']) - else: - login_writer = csv.writer(login_data_f) - - # start the actual population - create_superuser() - PopulateDatabase(args) - - finally: - login_data_f.close() diff --git a/util/testcases.py b/util/testcases.py index 3e9892fdf4c07a4babf315e1a9437fac7e7c5899..491d3074bb9261c786cc8915ef2f7f950a78e861 100644 --- a/util/testcases.py +++ b/util/testcases.py @@ -3,6 +3,7 @@ import os import random import re from string import ascii_letters, digits + try: import processing except ModuleNotFoundError: