diff --git a/server/vbv_lernwelt/core/admin.py b/server/vbv_lernwelt/core/admin.py index 4fdb1a9b..f2a65d3b 100644 --- a/server/vbv_lernwelt/core/admin.py +++ b/server/vbv_lernwelt/core/admin.py @@ -2,9 +2,29 @@ from django.contrib import admin from django.contrib.auth import admin as auth_admin, get_user_model from django.utils.translation import gettext_lazy as _ +from vbv_lernwelt.core.models import JobLog +from vbv_lernwelt.core.utils import pretty_print_json + User = get_user_model() +class LogAdmin(admin.ModelAdmin): + def has_add_permission(self, request): + return False + + def has_delete_permission(self, request, obj=None): + return False + + def get_readonly_fields(self, request, obj=None): + # set all fields read only + return [field.name for field in self.opts.local_fields] + [ + field.name for field in self.opts.local_many_to_many + ] + + def pretty_print_json(self, json_string): + return pretty_print_json(json_string) + + @admin.register(User) class UserAdmin(auth_admin.UserAdmin): fieldsets = ( @@ -34,3 +54,27 @@ class UserAdmin(auth_admin.UserAdmin): "sso_id", ] search_fields = ["first_name", "last_name", "email", "username", "sso_id"] + + +@admin.register(JobLog) +class JobLogAdmin(LogAdmin): + date_hierarchy = "started" + list_display = ( + "job_name", + "started", + "ended", + "runtime", + "success", + ) + list_filter = ["job_name", "success"] + + def json_data_pretty(self, instance): + return self.pretty_print_json(instance.json_data) + + def get_readonly_fields(self, request, obj=None): + return super().get_readonly_fields(request, obj) + ["json_data_pretty"] + + def runtime(self, obj): + if obj.ended: + return (obj.ended - obj.started).seconds // 60 + return None diff --git a/server/vbv_lernwelt/core/base.py b/server/vbv_lernwelt/core/base.py new file mode 100644 index 00000000..3eaa12a8 --- /dev/null +++ b/server/vbv_lernwelt/core/base.py @@ -0,0 +1,38 @@ +import sys +import traceback + +import structlog +from django.core.management.base import BaseCommand +from django.utils import timezone + +from vbv_lernwelt.core.models import JobLog + +logger = structlog.get_logger(__name__) + + +# pylint: disable=abstract-method +class LoggedCommand(BaseCommand): + def execute(self, *args, **options): + name = getattr(self, "name", "") + if not name: + name = sys.argv[1] + + logger.info("start LoggedCommand", job_name=name, label="job_log") + + self.job_log = JobLog.objects.create(job_name=name) + try: + super(LoggedCommand, self).execute(*args, **options) + self.job_log.refresh_from_db() + self.job_log.ended = timezone.now() + self.job_log.success = True + self.job_log.save() + logger.info( + "LoggedCommand successfully ended", job_name=name, label="job_log" + ) + except Exception as e: + self.job_log.refresh_from_db() + self.job_log.error_message = str(e) + self.job_log.stack_trace = traceback.format_exc() + self.job_log.save() + logger.error("LoggedCommand failed", job_name=name, label="job_log") + raise e diff --git a/server/vbv_lernwelt/core/migrations/0002_joblog.py b/server/vbv_lernwelt/core/migrations/0002_joblog.py new file mode 100644 index 00000000..f36059f0 --- /dev/null +++ b/server/vbv_lernwelt/core/migrations/0002_joblog.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.20 on 2023-08-25 15:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="JobLog", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("started", models.DateTimeField(auto_now_add=True)), + ("ended", models.DateTimeField(blank=True, null=True)), + ("job_name", models.CharField(max_length=255)), + ("success", models.BooleanField(default=False)), + ("error_message", models.TextField(blank=True, default="")), + ("stack_trace", models.TextField(blank=True, default="")), + ("json_data", models.JSONField(default=dict)), + ], + ), + ] diff --git a/server/vbv_lernwelt/core/models.py b/server/vbv_lernwelt/core/models.py index 8f8bcb75..524a2133 100644 --- a/server/vbv_lernwelt/core/models.py +++ b/server/vbv_lernwelt/core/models.py @@ -41,3 +41,16 @@ class SecurityRequestResponseLog(models.Model): response_status_code = models.CharField(max_length=255, blank=True, default="") additional_json_data = JSONField(default=dict, blank=True) + + +class JobLog(models.Model): + started = models.DateTimeField(auto_now_add=True) + ended = models.DateTimeField(blank=True, null=True) + job_name = models.CharField(max_length=255) + success = models.BooleanField(default=False) + error_message = models.TextField(blank=True, default="") + stack_trace = models.TextField(blank=True, default="") + json_data = models.JSONField(default=dict) + + def __str__(self): + return "{job_name} {started:%H:%M %d.%m.%Y}".format(**self.__dict__) diff --git a/server/vbv_lernwelt/core/utils.py b/server/vbv_lernwelt/core/utils.py index 1f4e16fc..75b3e517 100644 --- a/server/vbv_lernwelt/core/utils.py +++ b/server/vbv_lernwelt/core/utils.py @@ -38,3 +38,16 @@ def replace_whitespace(text, replacement=" "): def get_django_content_type(obj): return obj._meta.app_label + "." + type(obj).__name__ + + +def pretty_print_json(json_string): + try: + parsed = json_string + if isinstance(json_string, str): + parsed = json.loads(json_string) + return mark_safe( + "
{}".format(json.dumps(parsed, indent=4, sort_keys=True))
+ )
+ # pylint: disable=broad-except
+ except Exception:
+ return json_string
diff --git a/server/vbv_lernwelt/course_session/migrations/0005_auto_20230825_1723.py b/server/vbv_lernwelt/course_session/migrations/0005_auto_20230825_1723.py
new file mode 100644
index 00000000..5840c77a
--- /dev/null
+++ b/server/vbv_lernwelt/course_session/migrations/0005_auto_20230825_1723.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.20 on 2023-08-25 15:23
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("course_session", "0004_coursesessionedoniqtest"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="coursesessionassignment",
+ options={"ordering": ["course_session", "submission_deadline__start"]},
+ ),
+ migrations.AlterModelOptions(
+ name="coursesessionattendancecourse",
+ options={"ordering": ["course_session", "due_date__start"]},
+ ),
+ migrations.AlterModelOptions(
+ name="coursesessionedoniqtest",
+ options={"ordering": ["course_session", "deadline__start"]},
+ ),
+ ]
diff --git a/server/vbv_lernwelt/notify/management/commands/send_attendance_course_reminders.py b/server/vbv_lernwelt/notify/management/commands/send_attendance_course_reminders.py
index 2cd8ae45..19c81b8a 100644
--- a/server/vbv_lernwelt/notify/management/commands/send_attendance_course_reminders.py
+++ b/server/vbv_lernwelt/notify/management/commands/send_attendance_course_reminders.py
@@ -1,10 +1,10 @@
from datetime import timedelta
-import djclick as click
import structlog
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
+from vbv_lernwelt.core.base import LoggedCommand
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
@@ -55,6 +55,8 @@ def attendance_course_reminder_notification_job():
logger.info("No attendance courses found")
-@click.command()
-def command():
- attendance_course_reminder_notification_job()
+class Command(LoggedCommand):
+ help = "Sends attendance course reminder notifications to participants"
+
+ def handle(self, *args, **options):
+ attendance_course_reminder_notification_job()