Commit 293fc81b authored by Jan's avatar Jan Committed by Sébastiaan Versteeg

Add education app

Squashed commits:
[dd0f830] Use itertools.chain instead of +=
[9b56e66] Use settings.AUTH_USER_MODEL instead of Member as uploader
[84246e5] Use generator comprehension
[1271094] Rename summary_list to year_list
[02699ea] Preselect course when clicking 'add' in the course view
[07892ad] Don't use implicit uploader_id, use uploader directly
[49fe1ee] Change string concat to str format
[76d7358] Update translations
[ef0eb57] Use list comprehensions instead of map()
[51d7d03] unaccept = reject
[02d945a] Do not change translation util just for the migration script, use activate(lang) instead
[eea1516] Add localisation
[6ea4809] Add sitemap for education
[6aec215] Add actions to admin
[30c7864] Add forms to add exams and summaries
[495ebe2] Change submission url
[6521e3d] Add books page, basic forms and perfect download pages
[14fcca7] Make some small HTML changes
[009207f] Finish course view
[3a32dab] Change urls, translate texts and use absolute urls
[cc96cec] Rename stylesheet and use compressor, change text-align center
[56f77b4] Make sure that migrations really work
[3a18e56] Fix URL in and remove API key from education migration script
[25ae23c] Fix PEP8
[4e04316] Fix PEP8
[3078693] Add education migration script
[4e337e8] Correct course model translations, admin, ManyToMany relation and verbose_names
[0bbdd37] - Make categories and courses multilingual
- Define urls like the other apps
- Squash migrations
- Make texts translatable
[b3cecfb] Make texts translatable
[7bf745c] Code in pep8 format
[a434966] Summarries and exams downloadable
[394a109] Categories and old courses
[42a1c11] only show exams and summaries when they are accepted
[b562b6a] Basic page for course
[125ebb5] First version course overview
[55c0d06] Dynamic default dates should not be called
[5215f51] New migrations for education package
[f6ff3bf] added summaries in education model
[708bdf7] initial commit for education package
parent 4222a5ff
"""
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')
This diff was suppressed by a .gitattributes entry.
# 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',
},
),
]
from django.conf import settings
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from utils.snippets import datetime_to_lectureyear
from utils.translation import MultilingualField, ModelTranslateMeta
class Category(models.Model, metaclass=ModelTranslateMeta):
name = MultilingualField(
models.CharField,
max_length=64,
)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('education:category', args=[str(self.pk)])
class Meta: