Skip to content

Commit

Permalink
Add helper to generate export tokens for campus portal integration (#23)
Browse files Browse the repository at this point in the history
* Add helper to generate export tokens for campus portal integration

* add view for campus export, integrate javascript method to copy link

* setup CampusExportForm for link generation for each course level

* enhance layout, integrate javascript function to copy link on click

* integrate logic for campus grade export

* replace dict response with jsonify to create valid json response

* dont submit students that did not pass the course to campus

* validate parameter formats for campus grade export

---------

Co-authored-by: jan <[email protected]>
Co-authored-by: Jan Fenker <[email protected]>
  • Loading branch information
3 people authored Jun 22, 2024
1 parent 3db16e2 commit 07eeaa4
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 42 deletions.
9 changes: 6 additions & 3 deletions src/spz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ class CustomFlask(Flask):
"""
jinja_options = dict(Flask.jinja_options, trim_blocks=True, lstrip_blocks=True, auto_reload=False)


app = CustomFlask(__name__, instance_relative_config=True)

# Configuration loading
Expand Down Expand Up @@ -201,9 +200,13 @@ def rlrc_comment():
('/internal/teacher', admin_views.teacher, ['GET', 'POST']),
('/internal/grades/<int:course_id>', admin_views.grade, ['GET', 'POST']),
('/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>/edit_view', admin_views.edit_grade_view, ['GET', 'POST']),
('/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'])
('/internal/teacher/<int:id>/attendance/<int:course_id>/edit/<int:class_id>', admin_views.edit_attendances, ['GET', 'POST']),

('/api/campus_portal/export/<string:export_token>', views.campus_portal_grades, ['GET']),
('/internal/campus_portal/export', views.campus_export_language, ['GET', 'POST']),
('/internal/campus_portal/export/<int:id>', views.campus_export_course, ['GET', 'POST']),

]

Expand Down
19 changes: 19 additions & 0 deletions src/spz/campusportal/export_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from datetime import datetime, timedelta
from spz import app, tasks

import jwt

def generate_export_token_for_courses(courses):
return jwt.encode({'courses': courses, 'exp': datetime.utcnow() + timedelta(hours=1)},
key=app.config['SECRET_KEY'], algorithm="HS256")

def get_courses_from_export_token(export_token):
try:
courses = jwt.decode(export_token, key=app.config['SECRET_KEY'], algorithms="HS256")['courses']

if isinstance(courses, list):
return courses

return False
except Exception as e:
return False
23 changes: 23 additions & 0 deletions src/spz/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,28 @@ class GradeForm(FlaskForm):
class AttendanceForm(FlaskForm):
attendance = HiddenField("attendance_id")



class CampusExportForm(FlaskForm):
"""
Represents the form for exporting the grades of the applicants to the Campus System.
"""

courses = SelectField(
'Kurse',
coerce=str
)

def __init__(self, grouped_by_level, *args, **kwargs):
super(CampusExportForm, self).__init__(*args, **kwargs)
self.courses.choices = cached.grouped_by_level_to_choicelist(grouped_by_level)

def get_courses(self):
return self.courses.data

def update_course(self, grouped_by_level):
self.courses.choices = cached.grouped_by_level_to_choicelist(grouped_by_level)

class ResetLanguagePWs(FlaskForm):
"""Represents the form for send pws to all teachers of a language."""

Expand All @@ -1080,3 +1102,4 @@ def __init__(self, language, *args, **kwargs):

def get_send_mail(self):
return self.send_mail.data

17 changes: 6 additions & 11 deletions src/spz/forms/cached.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,9 @@ def all_courses_to_choicelist():
]


"""
@cache.cached(key_prefix='all_languages')
def all_languages_to_choicelist():
languages = models.Language.query \
.order_by(models.Language.name)
return [
(language.id, '{0}'.format(language.name))
for language in languages
]
"""
@cache.cached(key_prefix='courses_grouped_by_level')
def grouped_by_level_to_choicelist(grouped_courses: dict):
choices = []
for level, courses in grouped_courses.items():
choices.append((courses[0].level, '{0}'.format(courses[0].name)))
return choices
30 changes: 20 additions & 10 deletions src/spz/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,16 @@ def __lt__(self, other):
def full_name(self):
return '{} {}'.format(self.first_name, self.last_name)

@property
def tag_is_digit(self):
if self.tag is None:
return False
try:
int(self.tag)
return True
except ValueError:
return False

@property
def sanitized_grade(self):
if self.grade is None:
Expand All @@ -260,16 +270,16 @@ def full_grade(self):
if self.grade is None:
return "-"
conversion_table = [
(98, 1),
(95, 1.3),
(90, 1.7),
(85, 2),
(79, 2.3),
(73, 2.7),
(68, 3),
(62, 3.3),
(56, 3.7),
(50, 4)
(98, "1"),
(95, "1,3"),
(90, "1,7"),
(85, "2"),
(79, "2,3"),
(73, "2,7"),
(68, "3"),
(62, "3,3"),
(56, "3,7"),
(50, "4")
]

for percentage, grade in conversion_table:
Expand Down
72 changes: 57 additions & 15 deletions src/spz/templates/baselayout.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,47 @@
{% block head %}
{% endblock head %}

{% block style %}

<style>
.inline-flex-container {
{% block style %}
.inline-flex-container {
display: inline-flex;
align-items: center; /* Vertical alignment */
justify-content: center; /* Horizontal alignment */
}

.mb-3 {
}
.ui.message p {
word-wrap: break-word;
word-break: break-all;
overflow-wrap: break-word;
white-space: normal;
}
#notification {
display: none;
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: #4caf50;
color: white;
padding: 15px;
border-radius: 5px;
z-index: 1000;
}
.mb-3 {
margin-bottom: 1em; /* Adjust the value as needed */
}
}

.mt-3 {
margin-top: 1em; /* Adjust the value as needed */
}
.my-3 {
margin-top: 1em; /* Adjust the value as needed */
margin-bottom: 1em; /* Adjust the value as needed */
display: block;
}
.mt-3 {
margin-top: 1em; /* Adjust the value as needed */
}
.my-3 {
margin-top: 1em; /* Adjust the value as needed */
margin-bottom: 1em; /* Adjust the value as needed */
display: block;
}
{% endblock style %}
</style>
{% endblock style %}

</head>
<body>
<div class="print-warning">
Expand Down Expand Up @@ -118,8 +137,31 @@ <h1 class="ui header">
{% assets "all_js" %}
<script src="{{ ASSET_URL }}"></script>
{% endassets %}

<script>
// copies test of the hiddenText input field to the clipboard
function copyToClipboard() {
var copyText = document.getElementById("hiddenText").value;
navigator.clipboard.writeText(copyText).then(function () {
showNotification('Link in Zwischenablage kopiert!');
}, function (err) {
console.error('Async: Link konnte nicht kopiert werden: ', err);
});
}
// notification of successful copy-to-clipboard process
function showNotification(message) {
var notification = document.getElementById('notification');
notification.textContent = message;
notification.style.display = 'block';
setTimeout(function() {
notification.style.display = 'none';
}, 2250); // Show notification for 2.25 seconds
}
</script>
{% endblock scripts %}



{{ rlrc_comment() }}
</body>
</html>
37 changes: 37 additions & 0 deletions src/spz/templates/internal/campusportal/campus_export.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{% extends 'internal/internal.html' %}
{% from 'formhelpers.html' import td_sorted, csrf_field, render_option, render_submit %}

{% block caption %}
Noten-Export für Campus Portal
{% endblock caption %}


{% block internal_body %}
<div class="row">
<form method="post" class="ui form">
{{ csrf_field() }}
<h3 class="ui dividing header">Auswahl</h3>
{{ render_option(form.courses, required=True, help='Kurslevel für Export auswählen') }}


{{ render_submit(submit='Export-Link Generieren') }}

</form>
</div>
{% if link|length > 0 %}
<div class="row">
<h3 class="ui dividing header">Export-Link</h3>
<p>Der Export-Link für das Campus Portal wurde generiert. Dieser Link kann in das Campus Portal eingefügt
werden, um die Noten der Kurse mit dem gewählten Level zu importieren.</p>
<div id="notification">Link wurde in die Zwischenablage kopiert.</div>
<div>
<input type="hidden" value="{{ link }}" id="hiddenText">
<div class="ui button" onclick="copyToClipboard()">Link Kopieren</div>
</div>
<div class="ui message">
<div class="header">Export-Link</div>
<p>{{ link }}</p>
</div>
</div>
{% endif %}
{% endblock internal_body %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{% extends 'internal/internal.html' %}
{% from 'formhelpers.html' import td_sorted %}

{% block caption %}
Noten-Export für Campus Portal
{% endblock caption %}


{% block internal_body %}
<div class="row">
<table class="ui selectable sortable compact small striped table">
<thead>
<tr>
<th>Sprache</th>
</tr>
</thead>
<tbody>
{% for l in language %}
<tr>
<td><a
href="{{ url_for('campus_export_course', id=l.id) }}"><strong>{{ l.name }}</strong></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock internal_body %}
3 changes: 2 additions & 1 deletion src/spz/templates/internal/internal.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
<a class="item" href="{{ url_for('outstanding') }}"><i class="warning sign icon"></i> Offene Beträge</a>
<a class="item" href="{{ url_for('statistics') }}"><i class="pie chart icon"></i> Statistiken</a>
<a class="item" href="{{ url_for('duplicates') }}"><i class="random icon"></i> Doppelgänger</a>
<a class="item" href="{{ url_for('administration_teacher') }}"><i class="users icon"></i> Dozentenverwaltung</a>
<a class="item" href="{{ url_for('administration_teacher') }}"><i class="users icon"></i>Dozentenverwaltung</a>
<a class="item" href="{{ url_for('campus_export_language') }}"><i class="cloud upload icon"></i>Campus Portal Export</a>
{% if current_user.is_superuser %}
<a class="item" href="{{ url_for('preterm') }}"><i class="star icon"></i> Prioritär&shy;anmeldungen</a>
<a class="item" href="{{ url_for('unique') }}"><i class="trash icon"></i> Wartelisten Bereinigen</a>
Expand Down
Loading

0 comments on commit 07eeaa4

Please sign in to comment.