diff --git a/website/education/templates/education/add_exam.html b/website/education/templates/education/add_exam.html index 3c306473ee0833bd61d43ffb67777a2b5aaf81b9..51c1e557e47a4ff0041955728b75a52244253460 100644 --- a/website/education/templates/education/add_exam.html +++ b/website/education/templates/education/add_exam.html @@ -12,9 +12,10 @@ {% trans "Submit Exam" %} - {% if saved %} - {% trans "Exam submitted successfully." as success_text %} - {% alert 'success' success_text dismissable=True %} + {% if messages %} + {% for message in messages %} + {% alert message.tags message dismissable=True %} + {% endfor %} {% endif %}
- {% if saved %} - {% trans "Summary submitted successfully." as success_text %} - {% alert 'success' success_text dismissable=True %} + {% if messages %} + {% for message in messages %} + {% alert message.tags message dismissable=True %} + {% endfor %} {% endif %} [0-9]*)/', include([ - url(r'^$', views.course, name="course"), - url(r'^upload-exam/$', views.submit_exam, name="submit-exam"), - url(r'^upload-summary/$', views.submit_summary, name="submit-summary"), + path('education/', include([ + path('books/', BookInfoView.as_view(), name="books"), + path('courses/', include([ + path('', CourseIndexView.as_view(), name="courses"), + path('/', include([ + path('', CourseDetailView.as_view(), name="course"), + path('exam/upload/', ExamCreateView.as_view(), + name="submit-exam"), + path('summary/upload/', SummaryCreateView.as_view(), + name="submit-summary"), + ])), + path('exam//', ExamDetailView.as_view, name="exam"), + path('summary/(/', SummaryDetailView.as_view(), + name="summary"), + path('exam/upload/', ExamCreateView.as_view(), + name="submit-exam"), + path('summary/upload/', SummaryCreateView.as_view(), + name="submit-summary"), ])), + path('student-participation/', + StudentParticipantView.as_view(), name="student-participation"), + path('', RedirectView.as_view( + pattern_name='education:courses', permanent=True), name="index"), ])), - url(r'^exams/(?P[0-9]*)/$', views.exam, name="exam"), - url(r'^summaries/(?P[0-9]*)/$', views.summary, name="summary"), - url(r'^upload-exam/$', views.submit_exam, name="submit-exam"), - url(r'^upload-summary/$', views.submit_summary, name="submit-summary"), - url('^student-participation/$', TemplateView.as_view( - template_name='education/student_participation.html'), - name="student-participation"), - url(r'^$', - RedirectView.as_view(pattern_name='education:courses', - permanent=True), name="index"), ] diff --git a/website/education/views.py b/website/education/views.py index 4908454d7c4464195ba9e72aae54316a2f088026..e5a69c72fa94bb407aafc10e8d3734a25a770b08 100644 --- a/website/education/views.py +++ b/website/education/views.py @@ -4,9 +4,12 @@ from datetime import datetime, date from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied -from django.shortcuts import get_object_or_404, render +from django.http import HttpResponseRedirect, HttpResponse +from django.urls import reverse_lazy from django.utils import timezone +from django.utils.decorators import method_decorator from django.utils.translation import ugettext_lazy as _, get_language +from django.views.generic import ListView, DetailView, CreateView, TemplateView from sendfile import sendfile from members.decorators import membership_required @@ -14,198 +17,196 @@ from .forms import AddExamForm, AddSummaryForm from .models import Category, Course, Exam, Summary -def courses(request): +class CourseIndexView(ListView): """ Renders an overview of the courses - - :param request: the request object - :return: HttpResponse 200 containing the HTML as body - """ - categories = Category.objects.all() - courses = [ - { - 'course_code': x.course_code, - 'name': x.name, - 'categories': x.categories.all(), - 'document_count': sum([x.summary_set.filter(accepted=True).count(), - x.exam_set.filter(accepted=True).count()] + - [c.summary_set.filter(accepted=True).count() - + c.exam_set.filter(accepted=True).count() - for c in - x.old_courses.all()]), - 'url': x.get_absolute_url() - } for x in - Course.objects.order_by(f'name_{ get_language() }').filter( - until=None) - ] - - return render(request, 'education/courses.html', - {'courses': courses, 'categories': categories}) - - -def course(request, id): + """ + queryset = Course.objects.filter(until=None) + template_name = 'education/courses.html' + + def get_ordering(self) -> str: + return f'name_{get_language()}' + + def get_context_data(self, **kwargs) -> dict: + context = super().get_context_data(**kwargs) + context.update({ + 'courses': ({ + 'course_code': x.course_code, + 'name': x.name, + 'categories': x.categories.all(), + 'document_count': sum( + [x.summary_set.filter(accepted=True).count(), + x.exam_set.filter(accepted=True).count()] + + [c.summary_set.filter(accepted=True).count() + + c.exam_set.filter(accepted=True).count() + for c in x.old_courses.all()]), + 'url': x.get_absolute_url() + } for x in context['object_list']), + 'categories': Category.objects.all(), + }) + return context + + +class CourseDetailView(DetailView): """ Renders the detail page of one specific course - - :param request: the request object - :param id: the primary key of the selected course - :return: HttpResponse 200 containing the HTML as body - """ - obj = get_object_or_404(Course, pk=id) - courses = list(obj.old_courses.all()) - courses.append(obj) - items = {} - for course in courses: - for summary in course.summary_set.filter(accepted=True): - if summary.year not in items: - items[summary.year] = {'summaries': [], 'exams': [], - 'legacy': course if course.pk != obj.pk - else None} - items[summary.year]['summaries'].append({ - "year": summary.year, - "name": f'{ _("Summary") } { summary.name }', - "language": summary.language, - "id": summary.id - }) - for exam in course.exam_set.filter(accepted=True): - if exam.year not in items: - items[exam.year] = {'summaries': [], 'exams': [], - 'legacy': course if course.pk != obj.pk - else None} - items[exam.year]['exams'].append({ - "type": "exam", - "year": exam.year, "name": - f"{ exam.get_type_display() } { exam.name }", - "language": exam.language, - "id": exam.id - }) - - return render(request, 'education/course.html', - {'course': obj, 'items': sorted(items.items(), - key=lambda x: x[0])}) - - -@login_required -@membership_required -def exam(request, id): + """ + model = Course + context_object_name = 'course' + template_name = 'education/course.html' + + def get_context_data(self, **kwargs) -> dict: + context = super().get_context_data(**kwargs) + obj = context['course'] + courses = list(obj.old_courses.all()) + courses.append(obj) + items = {} + for course in courses: + for summary in course.summary_set.filter(accepted=True): + if summary.year not in items: + items[summary.year] = { + 'summaries': [], + 'exams': [], + 'legacy': course if course.pk != obj.pk else None + } + items[summary.year]['summaries'].append({ + "year": summary.year, + "name": f'{_("Summary")} {summary.name}', + "language": summary.language, + "id": summary.id + }) + for exam in course.exam_set.filter(accepted=True): + if exam.year not in items: + items[exam.year] = { + 'summaries': [], + 'exams': [], + 'legacy': course if course.pk != obj.pk else None + } + items[exam.year]['exams'].append({ + "type": "exam", + "year": exam.year, "name": + f"{exam.get_type_display()} {exam.name}", + "language": exam.language, + "id": exam.id + }) + context.update({ + 'items': sorted(items.items(), key=lambda x: x[0]) + }) + return context + + +@method_decorator(login_required, 'dispatch') +@method_decorator(membership_required, 'dispatch') +class ExamDetailView(DetailView): """ Fetches and outputs the specified exam - - :param request: the request object - :param id: the id of the exam - :return: 302 if not authenticated else 200 with the file as body """ - exam = get_object_or_404(Exam, id=int(id)) + model = Exam - exam.download_count += 1 - exam.save() + def get(self, request, *args, **kwargs) -> HttpResponse: + response = super().get(request, *args, **kwargs) + exam = response.context_data['object'] + exam.download_count += 1 + exam.save() - ext = os.path.splitext(exam.file.path)[1] - filename = f'{ exam.course.name }-exam{ exam.year }{ ext }' - return sendfile(request, exam.file.path, - attachment=True, attachment_filename=filename) + ext = os.path.splitext(exam.file.path)[1] + filename = f'{exam.course.name}-exam{exam.year}{ext}' + return sendfile(request, exam.file.path, + attachment=True, attachment_filename=filename) -@login_required -@membership_required -def summary(request, id): +@method_decorator(login_required, 'dispatch') +@method_decorator(membership_required, 'dispatch') +class SummaryDetailView(DetailView): """ Fetches and outputs the specified summary - - :param request: the request object - :param id: the id of the summary - :return: 302 if not authenticated else 200 with the file as body """ - obj = get_object_or_404(Summary, id=int(id)) + model = Summary - obj.download_count += 1 - obj.save() + def get(self, request, *args, **kwargs) -> HttpResponse: + response = super().get(request, *args, **kwargs) + obj = response.context_data['object'] + obj.download_count += 1 + obj.save() - ext = os.path.splitext(obj.file.path)[1] - filename = f'{ obj.course.name }-summary{ obj.year }{ ext }' - return sendfile(request, obj.file.path, - attachment=True, attachment_filename=filename) + ext = os.path.splitext(obj.file.path)[1] + filename = f'{obj.course.name}-summary{obj.year}{ext}' + return sendfile(request, obj.file.path, + attachment=True, attachment_filename=filename) -@login_required -def submit_exam(request, id=None): +@method_decorator(login_required, 'dispatch') +@method_decorator(membership_required, 'dispatch') +class ExamCreateView(CreateView): """ Renders the form to submit a new exam - - :param request: the request object - :param id: the course id (optional) - :return: 302 if not authenticated else 200 with the form HTML as body """ - saved = False - - if request.POST: - form = AddExamForm(request.POST, request.FILES) - if form.is_valid(): - saved = True - obj = form.save(commit=False) - obj.uploader = request.member - obj.uploader_date = datetime.now() - obj.save() - - form = AddExamForm() - else: - obj = Exam() - obj.exam_date = date.today() - if id is not None: - obj.course = Course.objects.get(id=id) - form = AddExamForm(instance=obj) - - return render(request, 'education/add_exam.html', - {'form': form, 'saved': saved}) - - -@login_required -def submit_summary(request, id=None): + model = Exam + form_class = AddExamForm + template_name = 'education/add_exam.html' + success_url = reverse_lazy('education:submit-exam') + success_message = _('Exam submitted successfully.') + + def get_initial(self) -> dict: + initial = super().get_initial() + initial['exam_date'] = date.today() + initial['course'] = self.kwargs.get('pk', None) + return initial + + def form_valid(self, form) -> HttpResponse: + self.object = form.save(commit=False) + self.object.uploader = self.request.member + self.object.uploader_date = datetime.now() + self.object.save() + return HttpResponseRedirect(self.get_success_url()) + + +@method_decorator(login_required, 'dispatch') +@method_decorator(membership_required, 'dispatch') +class SummaryCreateView(CreateView): """ Renders the form to submit a new summary - - :param request: the request object - :param id: the course id (optional) - :return: 302 if not authenticated else 200 with the form HTML as body """ - saved = False - - if request.POST: - form = AddSummaryForm(request.POST, request.FILES) - if form.is_valid(): - saved = True - obj = form.save(commit=False) - obj.uploader = request.member - obj.uploader_date = datetime.now() - obj.save() - - obj = Summary() - obj.author = request.member.get_full_name() - form = AddSummaryForm(instance=obj) - else: - obj = Summary() - if id is not None: - obj.course = Course.objects.get(id=id) - obj.author = request.member.get_full_name() - form = AddSummaryForm(instance=obj) - - return render(request, 'education/add_summary.html', - {'form': form, 'saved': saved}) - - -@login_required -def books(request): + model = Summary + form_class = AddSummaryForm + template_name = 'education/add_summary.html' + success_url = reverse_lazy('education:submit-summary') + success_message = _('Summary submitted successfully.') + + def get_initial(self): + initial = super().get_initial() + initial['author'] = self.request.member.get_full_name() + initial['course'] = self.kwargs.get('pk', None) + return initial + + def form_valid(self, form) -> HttpResponse: + self.object = form.save(commit=False) + self.object.uploader = self.request.member + self.object.uploader_date = datetime.now() + self.object.save() + return HttpResponseRedirect(self.get_success_url()) + + +@method_decorator(login_required, 'dispatch') +class BookInfoView(TemplateView): """ Renders a page with information about book sale Only available to members and to-be members + """ + template_name = 'education/books.html' + + def dispatch(self, request, *args, **kwargs) -> HttpResponse: + if ( + request.member.has_active_membership() or + (request.member.earliest_membership and + request.member.earliest_membership.since > timezone.now().date()) + ): + return super().dispatch(request, *args, **kwargs) + raise PermissionDenied - :param request: the request object - :return: 403 if no active membership else 200 with the page HTML as body + +class StudentParticipantView(TemplateView): + """ + Renders a page with information about student information """ - if (request.member and request.member.is_authenticated and - (request.member.current_membership or - (request.member.earliest_membership and - request.member.earliest_membership.since > timezone.now().date()) - )): - return render(request, 'education/books.html') - raise PermissionDenied + template_name = 'education/student_participation.html' diff --git a/website/thaliawebsite/urls.py b/website/thaliawebsite/urls.py index 858a6945d1bc6a940c9956b0655afc402b441dd4..7e638796c555f9c501adfaaa07cf4c46a46ca54a 100644 --- a/website/thaliawebsite/urls.py +++ b/website/thaliawebsite/urls.py @@ -109,7 +109,6 @@ urlpatterns = [ # pylint: disable=invalid-name url(r'^', include('pushnotifications.api.urls')), ])), ])), - url(r'^education/', include('education.urls')), url(r'^announcements/', include('announcements.urls')), url(r'^pushnotifications/', include('pushnotifications.urls')), # Default login helpers @@ -131,6 +130,7 @@ urlpatterns = [ # pylint: disable=invalid-name url(r'^media/private/(?P.*)$', private_media, name='private-media'), url('', include('members.urls')), url('', include('payments.urls')), + url('', include('education.urls')), url('', include('activemembers.urls')), url('', include('documents.urls')), ] + static(settings.MEDIA_URL + 'public/',