Skip to content

Commit

Permalink
grade sheet storage and display management
Browse files Browse the repository at this point in the history
  • Loading branch information
urvdp committed Nov 13, 2024
1 parent 455f65d commit ec7cd4a
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 85 deletions.
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ RUN mkdir /state && \
COPY --chown=spz:spz uwsgi.ini uwsgi.ini
COPY --chown=spz:spz src/spz spz

# create files directory (save excel files)
RUN mkdir /home/spz/code/spz/files && \
chown -R spz:spz /home/spz/code/spz/files

# switch to spz user
USER 1000

Expand Down
2 changes: 2 additions & 0 deletions src/spz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ def rlrc_comment():
('/internal/grades/<int:course_id>/edit', admin_views.edit_grade, ['GET', 'POST']),
('/internal/grades/<int:course_id>/edit_view', admin_views.edit_grade_view, ['GET', 'POST']),
('/internal/grades/<int:course_id>/import_grade', admin_views.import_grade, ['GET', 'POST']),
('/internal/delete_sheet/<int:file_id>', admin_views.delete_sheet, ['GET', 'POST']),
('/internal/download_sheet/<int:file_id>', admin_views.download_sheet, ['GET']),
('/internal/teacher/<int:id>/attendance/<int:course_id>', admin_views.attendances, ['GET', 'POST']),
('/internal/teacher/<int:id>/attendance/<int:course_id>/edit/<int:class_id>', admin_views.edit_attendances,
['GET', 'POST']),
Expand Down
84 changes: 77 additions & 7 deletions src/spz/administration/admin_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
Manages the mapping between routes and their activities for the administrators.
"""
import json
import os
import socket
from flask import request, redirect, render_template, url_for, flash, jsonify, make_response
from flask_login import current_user, login_required, login_user, logout_user
from flask_mail import Message
from sphinx.cmd.quickstart import suffix

from spz import app
from spz import models, db, log
Expand Down Expand Up @@ -313,7 +315,7 @@ def edit_teacher(id):
def teacher():
return dict(user=current_user)


@login_required
@templated('internal/administration/grade.html')
def grade(course_id):
course = models.Course.query.get_or_404(course_id)
Expand All @@ -322,7 +324,7 @@ def grade(course_id):

return dict(course=course, exam_date=exam_date)


@login_required
@templated('internal/administration/edit_grade.html')
def edit_grade(course_id):
course = models.Course.query.get_or_404(course_id)
Expand Down Expand Up @@ -365,7 +367,7 @@ def edit_grade(course_id):

return dict(course=course, form=form, exam_date=exam_date)


@login_required
@templated('internal/administration/edit_grade_view.html')
def edit_grade_view(course_id):
course = models.Course.query.get_or_404(course_id)
Expand Down Expand Up @@ -397,22 +399,90 @@ def edit_grade_view(course_id):

return dict(course=course, exam_date=exam_date)

@login_required
@templated('internal/administration/import_grade.html')
def import_grade(course_id):
course = models.Course.query.get_or_404(course_id)
form = forms.ImportGradeForm()
if form.validate_on_submit():
try:
file = form.file.data
# Save the file securely in the specified upload folder
#filepath = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
#file.save(filepath)
flash("File uploaded successfully!", "success")

# ToDo: sanity check for valid grade sheet file

# ToDo: grade import

# first add the db entry to map to the file
suffix = file.filename.split(".")[-1] # only '.xls' or '.xlsx' files pass the form validation
file_increment = len(course.grade_sheets) + 1
course_name = course.full_name.replace(" ", "_").replace("/", "_")
filename = course_name + "_version" + str(file_increment) + "." + suffix

# in case there have been file deletions, a not used filename is chosen
while os.path.exists(os.path.join(app.config['FILE_DIR'], filename)):
file_increment += 1
filename = course_name + "_version" + str(file_increment) + "." + suffix

file_entry = models.GradeSheets(
course_id=course.id,
user_id=current_user.id,
filename=filename
)
db.session.add(file_entry)

# now save the file to the docker file volume
file.save(file_entry.dir)
flash(f"saved under: '{file_entry.dir}'", "info")

db.session.commit()
flash("Notenliste wurde erfolgreich hochgeladen!", "success")
return redirect(url_for('grade', course_id=course.id))
except Exception as e:
db.session.rollback()
flash(_('Noten konnten nicht importiert werden: %(error)s', error=e), 'negative')
return dict(form=form, course=course)

@login_required
def download_sheet(file_id):
file = models.GradeSheets.query.get_or_404(file_id)

if not os.path.exists(file.dir):
flash(_('Die Datei existiert nicht oder wurde entfernt.'), 'negative')
return redirect(url_for('grade', course_id=file.course_id))

try:
with open(file.dir, 'rb') as f:
response = make_response(f.read())
response.headers['Content-Disposition'] = f'attachment; filename={file.filename}'
response.headers['Content-Type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
return response
except IOError as e:
flash(_('Datei konnte nicht heruntergeladen werden: %(error)s', error=str(e)), 'negative')
return redirect(url_for('grade', course_id=file.course_id))

@login_required
@templated('internal/administration/delete_grade_sheet.html')
def delete_sheet(file_id):
file = models.GradeSheets.query.get_or_404(file_id)

if not os.path.exists(file.dir):
flash(_('Die Datei existiert nicht oder wurde entfernt.'), 'negative')
return redirect(url_for('grade', course_id=file.course_id))

if request.method == 'POST':
try:
os.remove(file.dir)
db.session.delete(file)
db.session.commit()
flash(_('Datei erfolgreich gelöscht.'), 'success')
except Exception as e:
db.session.rollback()
flash(_('Datei konnte nicht gelöscht werden: %(error)s', error=e), 'negative')
return redirect(url_for('grade', course_id=file.course_id))

return dict(file=file)



@templated('internal/administration/attendances.html')
def attendances(id, course_id):
Expand Down
29 changes: 19 additions & 10 deletions src/spz/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
Manages the mapping between abstract entities and concrete database models.
"""
import enum
import os
from enum import Enum
from binascii import hexlify
from datetime import datetime, timedelta, timezone
import pytz
from functools import total_ordering
import random
import string
Expand Down Expand Up @@ -1268,7 +1269,7 @@ def __init__(self, state, code_verifier):
self.is_student = False

class GradeSheets(db.Model):
"""Database model for the xls/xlsx grade sheets
"""Database model for the xls/xlsx grade sheet mapping
:param id: unique ID
:param course_id: course ID
Expand All @@ -1277,24 +1278,32 @@ class GradeSheets(db.Model):
:param upload_at: timestamp of the upload in GMT
"""

__tablename__ = 'gradesheets'
__tablename__ = 'grade_sheets'

id = db.Column(db.Integer, primary_key=True)
course_id = db.Column(db.Integer, db.ForeignKey('course.id'))
dir = db.Column(db.String(100), nullable=False)
filename = db.Column(db.String(40), nullable=False)
upload_at = db.Column(db.DateTime(), default=datetime.now(timezone.utc).replace(tzinfo=None))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
filename = db.Column(db.String(60), nullable=False)
upload_at = db.Column(db.DateTime(), default=lambda: datetime.now(timezone.utc).replace(tzinfo=None))


def __init__(self, course_id, dir, filename):
def __init__(self, course_id, user_id, filename):
self.course_id = course_id
self.dir = dir
self.user_id = user_id
self.filename = filename

def __repr__(self):
return '<GradeSheet %r>' % self.filename

@property
def path(self):
return "{0}{1}".format(self.dir, self.filename)
def dir(self):
return os.path.join(app.config['FILE_DIR'], self.filename)

def get_user(self):
return User.query.get(self.user_id)

@property
def uploat_at_utc(self):
target_timezone = pytz.timezone("Europe/Berlin")
return self.upload_at.astimezone(target_timezone).strftime("%d.%m.%Y %H:%M")

28 changes: 28 additions & 0 deletions src/spz/templates/internal/administration/delete_grade_sheet.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{% extends 'internal/internal.html' %}
{% from 'formhelpers.html' import csrf_field, render_submit %}

{% block caption %}
Datei Löschen: {{ file.filename }}
{% endblock caption %}


{% block internal_body %}
<div class="row">
<h2>{{ file.filename }} Endgültig Löschen</h2>
<div class="ui message">
<p>Soll die Datei <strong>{{ file.filename }} </strong> wirklich gelöscht werden?
<br> Das kann nicht rückgängig gemacht werden.</p>
</div>
<form method="post" class="ui form">
{{ csrf_field() }}

<div class="ui two fluid buttons">
<button type="submit" class="ui red button">Löschen</button>

<div class="or"></div>

<a href="{{ url_for('grade', course_id=file.course_id) }}" class="ui button">Abbrechen</a>
</div>
</form>
</div>
{% endblock internal_body %}
Loading

0 comments on commit ec7cd4a

Please sign in to comment.