Commit 98a25240 authored by Thom Wiggers's avatar Thom Wiggers 📐
Browse files

Merge branch 'feature/education' into 'master'

Add education app

Weet niet zeker of ik alles heb, maar t lijkt er wel op:
- Course Overview
  - View course
     - Download exam
     - Download summary
     - Submit exam
     - Submit summary
- Book Sale

En natuurlijk het migratiescript.

Ik heb ook wat aanpassingen gemaakt tegenover de huidige implementatie binnen Concrete5. Zo heb ik de course codes als slugs vervangen door id's. Er wordt namelijk gelinkt naar de vakken als hiervan nog examens/samenvattingen van voorkomen bij andere vakken, en zo is het mogelijk dat, omdat ze kunnen worden hergebruikt, de link naar een heel ander vak gaat.

Closes #6  and #62

See merge request !93
parents 4222a5ff 293fc81b
"""
This module registers admin pages for the models
"""
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from utils.translation import TranslatedModelAdmin
from . import models
admin.site.register(models.Category)
@admin.register(models.Course)
class CourseAdmin(TranslatedModelAdmin):
fields = ('name', 'shorthand', 'course_code', 'ec', 'since', 'until',
'period', 'categories', 'old_courses')
@admin.register(models.Exam)
class ExamAdmin(TranslatedModelAdmin):
list_display = ('type', 'course', 'exam_date', 'uploader',
'accepted')
list_filter = ('accepted', 'exam_date', 'type')
actions = ['accept', 'reject']
def accept(self, request, queryset):
queryset.update(accepted=True)
accept.short_description = _("Mark exams as accepted")
def reject(self, request, queryset):
queryset.update(accepted=False)
reject.short_description = _("Mark exams as rejected")
@admin.register(models.Summary)
class SummaryAdmin(TranslatedModelAdmin):
list_display = ('name', 'course', 'uploader', 'accepted')
list_filter = ('accepted',)
actions = ['accept', 'reject']
def accept(self, request, queryset):
queryset.update(accepted=True)
accept.short_description = _("Mark summaries as accepted")
def reject(self, request, queryset):
queryset.update(accepted=False)
reject.short_description = _("Mark summaries as rejected")
from django.apps import AppConfig
class EducationConfig(AppConfig):
name = 'education'
from django.conf import settings
from django.forms import (
ModelForm,
DateField,
SelectDateWidget,
ModelChoiceField,
ChoiceField
)
from .models import Exam, Summary, Course
class AddExamForm(ModelForm):
exam_date = DateField(widget=SelectDateWidget())
course = ModelChoiceField(
queryset=Course.objects.order_by('name_' + settings.LANGUAGE_CODE),
empty_label=None)
type = ChoiceField(choices=Exam.EXAM_TYPES)
class Meta:
model = Exam
fields = ('file', 'course', 'type', 'exam_date')
class AddSummaryForm(ModelForm):
course = ModelChoiceField(
queryset=Course.objects.order_by('name_' + settings.LANGUAGE_CODE),
empty_label=None)
class Meta:
model = Summary
fields = ('name', 'year', 'file', 'course', 'author')
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-10-03 10:12+0200\n"
"PO-Revision-Date: 2016-10-03 10:12+0200\n"
"Last-Translator: Sébastiaan Versteeg <se_bastiaan@outlook.com>\n"
"Language-Team: \n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.8.8\n"
#: admin.py:29
msgid "Mark exams as accepted"
msgstr "Markeer tentamens als geaccepteerd"
#: admin.py:34
msgid "Mark exams as rejected"
msgstr "Markeer tentamens als afgekeurd"
#: admin.py:46
msgid "Mark summaries as accepted"
msgstr "Markeer samenvattingen als geaccepteerd"
#: admin.py:51
msgid "Mark summaries as rejected"
msgstr "Markeer samenvattingen als afgekeurd"
#: models.py:23
msgid "category"
msgstr "categorie"
#: models.py:24 models.py:35
msgid "categories"
msgstr "categorieën"
#: models.py:42
msgid "old courses"
msgstr "oude vakken"
#: models.py:56 templates/education/course.html:72
msgid "EC"
msgstr "EC"
#: models.py:66
msgid "period"
msgstr "periode"
#: models.py:76 models.py:124 models.py:170
msgid "course"
msgstr "vak"
#: models.py:77
msgid "courses"
msgstr "vakken"
#: models.py:82
msgid "Document"
msgstr "Document"
#: models.py:83
msgid "Exam"
msgstr "Tentamen"
#: models.py:84
msgid "Partial Exam"
msgstr "Deeltentamen"
#: models.py:85
msgid "Resit"
msgstr "Hertentamen"
#: models.py:86
msgid "Practice Exam"
msgstr "Oefententamen"
#: models.py:91
msgid "exam type"
msgstr "type tentamen"
#: models.py:96
msgid "exam name"
msgstr "naam tentamen"
#: models.py:102 models.py:154
msgid "uploader"
msgstr "uploader"
#: models.py:110 models.py:174
msgid "accepted"
msgstr "geaccepteerd"
#: models.py:115
msgid "exam date"
msgstr "tentamendatum"
#: models.py:142
msgid "exam"
msgstr "tentamen"
#: models.py:143
msgid "exams"
msgstr "tentamens"
#: models.py:149
msgid "summary name"
msgstr "naam samenvatting"
#: models.py:165
msgid "author"
msgstr "auteur"
#: models.py:190
msgid "summary"
msgstr "samenvatting"
#: models.py:191
msgid "summaries"
msgstr "samenvattingen"
#: templates/education/add_exam.html:4 templates/education/add_exam.html:7
#: templates/education/course.html:78
msgid "Submit Exam"
msgstr "Tentamen Insturen"
#: templates/education/add_exam.html:11
msgid "Exam submitted successfully."
msgstr "Tentamen succesvol ingestuurd."
#: templates/education/add_exam.html:37 templates/education/add_summary.html:37
msgid "submit"
msgstr "insturen"
#: templates/education/add_summary.html:4
#: templates/education/add_summary.html:7 templates/education/course.html:81
msgid "Submit Summary"
msgstr "Samenvatting Insturen"
#: templates/education/add_summary.html:11
msgid "Summary submitted successfully."
msgstr "Samenvatting succesvol ingestuurd."
#: templates/education/books.html:11 templates/education/books.html:14
msgid "Book Sale"
msgstr "Boekverkoop"
#: templates/education/books.html:16
#, python-format
msgid ""
"Thalia's book sale goes through bookstore Roelants. As a member of Thalia "
"you get 10%% discount on your order. So how does the ordering work? Simple: "
"visit the Roelants webstore using <a href=\"https://www.roelants.nl/"
"studenten/products/alle-producten/3543/p.product_price/asc.html?"
"r=sawqllhrjzy4dq3ui8pf\">this link</a>. Then choose the books you need and "
"use the code 'THALIAkorting' to activate the discount."
msgstr ""
"De boekenverkoop van Thalia gaat via boekhandel Roelants. Via deze "
"boekhandel krijg je als Thaliaan 10%% korting op je bestelling. Hoe gaat dat "
"bestellen nou in zijn werk? Simpel: ga naar de Roelants webwinkel via <a "
"href=\"https://www.roelants.nl/studenten/products/alle-producten/3543/p."
"product_price/asc.html?r=sawqllhrjzy4dq3ui8pf\">deze link</a. Vervolgens "
"kies je de boeken uit die jij voor je vakken nodig hebt. Als je klaar bent "
"gebruik je tijdens het afrekenen de code \"THALIAkorting\" om de korting op "
"je bestelling te krijgen."
#: templates/education/books.html:23
msgid ""
"It's also possible to order your books at the physical shops, but the "
"discount is only eligible when using the website. It is useful to order your "
"books at least two weeks before you need them, because they are not always "
"easy to get. In case books are missing from the catalog, or if you discover "
"any other problems with the books, send an email to <a href=\"mailto:"
"onderwijs@thalia.nu\">onderwijs@thalia.nu</a>."
msgstr ""
"Het is ook mogelijk om bij de boekhandel op de campus of in de stad je "
"boeken te bestellen maar alleen bij webbestellingen heb je recht op de "
"lidmaatschaps-korting. Bij het bestellen van je boeken is het handig om "
"minstens 2 weken voor dat je de boeken nodig hebt je bestelling rond te "
"hebben, aangezien de boeken niet altijd even makkelijk te leveren zijn. "
"Mocht er een boek ontbreken of er een ander probleem met de boeken zijn, "
"stuur gerust een e-mail naar onderwijs@thalia.nu."
#: templates/education/course.html:4
msgid "Course"
msgstr "Vak"
#: templates/education/course.html:14
msgid ""
"Thalia does not have any documents for this course, unfortunately. Are you "
"in possession of exams or summaries for this course? Then let us know or add "
"them to the catalog using the submission page!"
msgstr ""
"Er zijn bij Thalia helaas geen documenten bekend over dit vak. Ben jij in "
"het bezit van tentamens of samenvattingen over dit vak? Laat het ons weten!"
#: templates/education/course.html:29
msgid "Exams"
msgstr "Tentamens"
#: templates/education/course.html:29
msgid "Summaries"
msgstr "Samenvattingen"
#: templates/education/course.html:45
#, python-format
msgid ""
"These documents were collected for <a href=\"%(course_url)s\">"
"%(course_name)s</a>, a predecessor of this course."
msgstr ""
"Deze documenten zijn van de cursus <a href=\"%(course_url)s\">"
"%(course_name)s</a>, een voorloper van dit vak."
#: templates/education/course.html:59
#, python-format
msgid ""
"This is the overview for <b>%(name)s</b>. You can find all the exams and "
"summaries that Thalia has here."
msgstr ""
"Dit is de overzichtspagina voor <b>%(name)s</b>. Hier kun je per jaar alle "
"bij Thalia bekende tentamens en samenvattingen terugvinden."
#: templates/education/course.html:64
msgid ""
"Keep in mind that old exams and summaries may not always test the same "
"material as this year."
msgstr ""
"Houd er rekening mee dat oude tentamens en samenvattingen niet altijd "
"representatief zijn voor wat er dit jaar wordt getoetst."
#: templates/education/course.html:71
msgid "Course code"
msgstr "Cursus code"
#: templates/education/course.html:73
msgid "Period"
msgstr "Periode"
#: templates/education/courses.html:4 templates/education/courses.html:7
msgid "Course overview"
msgstr "Vakkenoverzicht"
#: templates/education/courses.html:9
msgid ""
"This overview contains all courses for which Thalia has exams, summaries or "
"other documents. Select a course to find out more.<br/> Can't find a course? "
"Let the <a href=\"mailto:educacie@thalia.nu\">education committee</a> know."
msgstr ""
"Overzicht van alle vakken waar Thalia tentamens, samenvatting of andere "
"documenten voor heeft. Selecteer een vak om er naar te navigeren.<br />Staat "
"er een vak niet tussen? Laat dat dan weten aan de <a href=\"mailto:"
"educacie@thalia.nu\">educatiecommissie</a>."
#: templates/education/courses.html:21
msgid "all courses"
msgstr "alle vakken"
#: views.py:30
msgid "Summary"
msgstr "Samenvatting"
#~ msgid "Mark exams as unaccepted"
#~ msgstr "Mark tentamens als niet geaccepteerd"
#~ msgid "Mark summaries as unaccepted"
#~ msgstr "Mark samenvattingen als niet geaccepteerd"
import json
import os
import requests
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.core.files.base import ContentFile
from django.utils.dateparse import parse_date
from django.utils.translation import activate
from education.models import Summary, Exam, Course, Category
from utils.management.commands import legacylogin
def filefield_from_url(filefield, url):
file = ContentFile(requests.get(url).content)
filefield.save(os.path.basename(url), file)
class Command(legacylogin.Command):
help = 'Scrapes the education data from the old Thalia website'
def handle(self, *args, **options):
activate('en')
input_val = input(
'Do you want to delete all existing objects? (type yes or no) ')
if input_val == 'yes':
Summary.objects.all().delete()
Exam.objects.all().delete()
Course.objects.all().delete()
Category.objects.all().delete()
session = requests.Session()
key = input('Please enter the education API key: ')
url = ('https://thalia.nu/index.php/onderwijs/api?apikey=' + key)
src = session.get(url).text
if 'invalid api key' in src:
raise PermissionDenied('Invalid API key')
data = json.loads(src)
category_map = {}
print('Importing categories')
for key in data['categories']:
name = data['categories'][key]
id = Category()
id.name_nl = name
id.name_en = name
id.save()
category_map[key] = id.pk
print('Importing categories complete')
course_map = {}
print('Importing courses')
for key in data['courses']:
src = data['courses'][key]
course = Course()
course.name_nl = src['name']
course.name_en = src['name']
course.course_code = src['course_code']
course.shorthand_nl = src['course_shorthand']
course.shorthand_en = src['course_shorthand']
course.ec = int(src['e_c_t_s'])
course.period = src['quarter'].replace(' en ', ' & ')
course.since = src['since']
course.until = src['until'] if src['until'] != 0 else None
course.save()
for id in src['categories']:
course.categories.add(Category.objects
.get(pk=category_map[str(id)]))
course_map[key] = course.pk
print('Combining courses with predecessors')
for key in data['courses']:
src = data['courses'][key]
course = Course.objects.get(pk=course_map[key])
try:
for id in src['predecessors']:
if id == 0:
continue
old_course = Course.objects.get(pk=course_map[str(id)])
course.old_courses.add(old_course)
except KeyError:
pass
course.save()
print('Importing courses complete')
print('Importing summaries')
for key in data['summaries']:
src = data['summaries'][key]
summary = Summary()
summary.name = src['name']
summary.author = '' if src['author'] is None else src['author']
summary.year = int(src['year'])
summary.uploader_date = parse_date(src['uploader_date'])
summary.accepted = src['accepted'] == '1'
course_id = str(src['course_id'])
summary.course = Course.objects.get(pk=course_map[course_id])
try:
summary.uploader = User.objects.get(username=src['uploader'])
except User.DoesNotExist:
summary.uploader = User.objects.get(pk=1)
filefield_from_url(summary.file, src['file_url'])
summary.save()
print('Importing summaries complete')
print('Importing exams')
for key in data['exams']:
src = data['exams'][key]
exam = Exam()
exam.name = '' if src['name'] is None else src['name']
exam.accepted = src['accepted'] == '1'
course_id = str(src['course_id'])
exam.course = Course.objects.get(pk=course_map[course_id])
type_map = {
0: 'document',
1: 'exam',
2: 'partial',
3: 'resit',
5: 'practice'
}
exam.type = type_map.get(int(src['type']), 'document')
exam.exam_date = parse_date(src['date'])
exam.uploader_date = parse_date(src['uploader_date'])
try:
exam.uploader = User.objects.get(username=src['uploader'])
except User.DoesNotExist:
exam.uploader = User.objects.get(pk=1)
filefield_from_url(exam.file, src['file_url'])
exam.save()
print('Importing exams complete')
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-09-18 15:35
from __future__ import unicode_literals
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('members', '0007_member_receive_newsletter'),
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name_en', models.CharField(max_length=64, verbose_name='name (EN)')),
('name_nl', models.CharField(max_length=64, verbose_name='name (NL)')),
],
options={
'verbose_name': 'category',
'verbose_name_plural': 'categories',
},
),
migrations.CreateModel(
name='Course',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('course_code', models.CharField(max_length=16)),
('ec', models.IntegerField(verbose_name='EC')),
('since', models.IntegerField()),
('until', models.IntegerField(blank=True)),
('period', models.CharField(max_length=64, verbose_name='period')),
('shorthand_en', models.CharField(max_length=10, verbose_name='shorthand (EN)')),
('shorthand_nl', models.CharField(max_length=10, verbose_name='shorthand (NL)')),
('name_en', models.CharField(max_length=255, verbose_name='name (EN)')),
('name_nl', models.CharField(max_length=255, verbose_name='name (NL)')),
('categories', models.ManyToManyField(blank=True, to='education.Category', verbose_name='categories')),
('old_courses', models.ManyToManyField(blank=True, to='education.Course', verbose_name='old courses')),
],
options={
'verbose_name': 'course',
'verbose_name_plural': 'courses',
},
),
migrations.CreateModel(
name='Exam',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(choices=[('document', 'Document'), ('exam', 'Exam'), ('partial', 'Partial Exam'), ('resit', 'Resit'), ('practice', 'Practice Exam')], max_length=40, verbose_name='exam type')),
('name', models.CharField(max_length=255, verbose_name='exam name', blank=True)),
('uploader_date', models.DateField(default=django.utils.timezone.now)),
('accepted', models.BooleanField(default=False, verbose_name='accepted')),
('exam_date', models.DateField(verbose_name='exam date')),
('file', models.FileField(upload_to='education/files/exams/')),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='education.Course', verbose_name='course')),
('uploader', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='uploader')),
],
options={
'verbose_name': 'exam',
'verbose_name_plural': 'exams',
},
),
migrations.CreateModel(
name='Summary',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='summary name')),
('uploader_date', models.DateField(default=django.utils.timezone.now)),
('year', models.IntegerField()),
('author', models.CharField(max_length=64, verbose_name='author')),
('accepted', models.BooleanField(default=False, verbose_name='accepted')),
('file', models.FileField(upload_to='education/files/summary/')),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='education.Course', verbose_name='course')),
('uploader', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='uploader')),
],
options={
'verbose_name': 'summary',
'verbose_name_plural': 'summaries',
},
),
]