- {{ $t("Anwesenheitskontrolle Präsenzkurse") }}
+ {{ $t("a.Anwesenheitskontrolle Präsenzkurse") }}
{{
diff --git a/client/src/pages/dashboard/DashboardDueDatesPage.vue b/client/src/pages/dashboard/DashboardDueDatesPage.vue
index 81bfb207..602088bc 100644
--- a/client/src/pages/dashboard/DashboardDueDatesPage.vue
+++ b/client/src/pages/dashboard/DashboardDueDatesPage.vue
@@ -31,7 +31,7 @@ const courses = computed(() => {
return [
{
id: UNFILTERED,
- name: `${t("Lehrgang")}: ${t("a.Alle")}`,
+ name: `${t("a.Lehrgang")}: ${t("a.Alle")}`,
slug: "",
},
..._(dashboardDueDates.value)
diff --git a/client/src/pages/dashboard/DashboardPersonsPage.vue b/client/src/pages/dashboard/DashboardPersonsPage.vue
index 38ecc405..8b7212ce 100644
--- a/client/src/pages/dashboard/DashboardPersonsPage.vue
+++ b/client/src/pages/dashboard/DashboardPersonsPage.vue
@@ -91,7 +91,7 @@ const regions = computed(() => {
return [
{
id: UNFILTERED,
- name: `${t("Region")}: ${t("a.Alle")}`,
+ name: `${t("a.Region")}: ${t("a.Alle")}`,
},
...values,
];
@@ -141,7 +141,7 @@ const generations = computed(() => {
return [
{
id: UNFILTERED,
- name: `${t("Generation")}: ${t("a.Alle")}`,
+ name: `${t("a.Generation")}: ${t("a.Alle")}`,
},
...values,
];
@@ -164,7 +164,7 @@ const roles = computed(() => {
return [
{
id: UNFILTERED,
- name: `${t("Rolle")}: ${t("a.Alle")}`,
+ name: `${t("a.Rolle")}: ${t("a.Alle")}`,
},
...values,
];
@@ -186,7 +186,7 @@ const chosenProfiles = computed(() => {
return [
{
id: UNFILTERED,
- name: `${t("Zulassungsprofil")}: ${t("a.Alle")}`,
+ name: `${t("a.Zulassungsprofil")}: ${t("a.Alle")}`,
},
...values,
];
@@ -210,7 +210,7 @@ const paidYears = computed(() => {
return [
{
id: UNFILTERED,
- name: `${t("Jahr")}: ${t("a.Alle")}`,
+ name: `${t("a.Jahr")}: ${t("a.Alle")}`,
},
...values,
];
diff --git a/cypress/e2e/assignment/assignmentTrainerBearbeiten.cy.js b/cypress/e2e/assignment/assignmentTrainerBearbeiten.cy.js
new file mode 100644
index 00000000..373894c4
--- /dev/null
+++ b/cypress/e2e/assignment/assignmentTrainerBearbeiten.cy.js
@@ -0,0 +1,145 @@
+import { TEST_STUDENT1_USER_ID, TEST_TRAINER1_USER_ID } from "../../consts";
+import { EXPERT_COCKPIT_URL, login } from "../helpers";
+
+describe("assignmentTrainer.cy.js", () => {
+ beforeEach(() => {
+ cy.manageCommand(
+ "cypress_reset --create-assignment-completion --create-assignment-evaluation",
+ );
+ login("test-trainer1@example.com", "test");
+ });
+
+ describe("Can reevaluation assignment", () => {
+ it("can start evaluation and store evaluation results", () => {
+ cy.visit(EXPERT_COCKPIT_URL);
+ cy.get(
+ '[data-cy="show-details-btn-test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice"]',
+ ).click();
+
+ cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
+
+ // on EvaluationSummary page
+ cy.get(
+ '[data-cy="assignment-history"] [data-cy="assignment-history-entry"]',
+ ).should("have.length", 2);
+ cy.get('[data-cy="assignment-history"]')
+ .should("contain", "Ergebnisse abgegeben")
+ .should("contain", "Bewertung freigegeben");
+ cy.get('[data-cy="user-points"]').should("contain", "24");
+ cy.get('[data-cy="total-points"]').should("contain", "100%");
+
+ // reevaluation
+ cy.get('[data-cy="btn-reopen"]').click();
+
+ cy.get('[data-cy="evaluation-task"]').should(
+ "contain",
+ "Beurteilungskriterium 1 / 5",
+ );
+ cy.get('[data-cy="subtask-4"]').click();
+ cy.wait(500);
+ cy.get('[data-cy="next-step"]').click();
+
+ cy.get('[data-cy="evaluation-task"]').should(
+ "contain",
+ "Beurteilungskriterium 2 / 5",
+ );
+ cy.get('[data-cy="next-step"]').click();
+
+ cy.get('[data-cy="evaluation-task"]').should(
+ "contain",
+ "Beurteilungskriterium 3 / 5",
+ );
+ cy.get('[data-cy="subtask-4"]').click();
+ cy.wait(500);
+ cy.get('[data-cy="next-step"]').click();
+
+ cy.get('[data-cy="evaluation-task"]').should(
+ "contain",
+ "Beurteilungskriterium 4 / 5",
+ );
+ cy.get('[data-cy="subtask-2"]').click();
+ cy.wait(500);
+ cy.get('[data-cy="next-step"]').click();
+
+ cy.get('[data-cy="evaluation-task"]').should(
+ "contain",
+ "Beurteilungskriterium 5 / 5",
+ );
+ cy.get('[data-cy="next-step"]').click();
+
+ cy.get('[data-cy="user-points"]').should("contain", "19");
+ cy.get('[data-cy="total-points"]').should("contain", "79%");
+
+ cy.get('[data-cy="reason-text"]').type("nochmal bewertet");
+ cy.get(
+ '[data-cy="assignment-history"] [data-cy="assignment-history-entry"]',
+ ).should("have.length", 3);
+ cy.get('[data-cy="assignment-history"]')
+ .should("contain", "Ergebnisse abgegeben")
+ .should("contain", "Bewertung freigegeben")
+ .should("contain", "Bewertung erneut bearbeitet");
+
+ cy.get('[data-cy="submit-evaluation"]').click();
+
+ // check stored data
+ cy.get('[data-cy="result-section"]').should(
+ "contain",
+ "Deine Bewertung für Test Student1 wurde freigegeben",
+ );
+ cy.reload();
+ cy.get(
+ '[data-cy="assignment-history"] [data-cy="assignment-history-entry"]',
+ ).should("have.length", 4);
+
+ cy.loadAssignmentCompletion(
+ "evaluation_user_id",
+ TEST_TRAINER1_USER_ID,
+ ).then((ac) => {
+ expect(ac.completion_status).to.equal("EVALUATION_SUBMITTED");
+ expect(ac.evaluation_points).to.equal(19);
+ expect(ac.completion_data.expert_evaluation_comment.text).to.equal(
+ "nochmal bewertet",
+ );
+ expect(ac.additional_json_data.submission_history).to.have.length(4);
+ expect(ac.additional_json_data.submission_history[0].status).to.equal(
+ "SUBMITTED",
+ );
+ expect(ac.additional_json_data.submission_history[1].status).to.equal(
+ "EVALUATION_SUBMITTED",
+ );
+ expect(ac.additional_json_data.submission_history[2].status).to.equal(
+ "EVALUATION_IN_PROGRESS",
+ );
+ expect(ac.additional_json_data.submission_history[3].status).to.equal(
+ "EVALUATION_SUBMITTED",
+ );
+
+ // check AssignmentCompletionAuditLog
+ cy.task(
+ "runSql",
+ `select * from assignment_assignmentcompletionauditlog
+ where assignment_slug = 'test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice'
+ and assignment_user_id = '${TEST_STUDENT1_USER_ID}'
+ order by created_at asc;`,
+ ).then((res) => {
+ console.log(res);
+ expect(res.rows).to.have.length(3);
+ expect(res.rows[0].completion_status).to.equal("SUBMITTED");
+
+ expect(res.rows[1].completion_status).to.equal(
+ "EVALUATION_SUBMITTED",
+ );
+ expect(res.rows[1].evaluation_points).to.equal(24);
+
+ expect(res.rows[2].completion_status).to.equal(
+ "EVALUATION_SUBMITTED",
+ );
+ expect(res.rows[2].evaluation_points).to.equal(19);
+ expect(res.rows[2].completion_data.expert_evaluation_comment.text).to.equal(
+ "nochmal bewertet",
+ );
+ });
+ });
+ });
+ });
+});
diff --git a/cypress/e2e/courseSpecificSettings.cy.js b/cypress/e2e/courseSpecificSettings.cy.js
index 91cfbc4c..6d0387ba 100644
--- a/cypress/e2e/courseSpecificSettings.cy.js
+++ b/cypress/e2e/courseSpecificSettings.cy.js
@@ -100,7 +100,7 @@ describe("courseSpecificSettings.cy.js", () => {
"Lernbegleitung"
);
cy.get('[data-cy="navigation-learning-mentor-link"]').click();
- cy.get('[data-cy="lm-my-lms-title"]').contains("Meine Lernbegleiter");
+ cy.get('[data-cy="lm-my-lms-title"]').contains("Meine Lernbegleitung");
cy.get('[data-cy="lm-invite-mentor-button"]').contains(
"Neue Lernbegleitung einladen"
);
diff --git a/cypress/e2e/learningMentor/mentorTasks/praxisauftrag.cy.js b/cypress/e2e/learningMentor/mentorTasks/praxisauftrag.cy.js
index ec6fcb1c..7fed6dfa 100644
--- a/cypress/e2e/learningMentor/mentorTasks/praxisauftrag.cy.js
+++ b/cypress/e2e/learningMentor/mentorTasks/praxisauftrag.cy.js
@@ -100,7 +100,7 @@ describe("praxisauftrag.cy.js", () => {
cy.get('[data-cy="next-step"]').click();
cy.get('[data-cy="submit-evaluation"]').click();
- cy.get('[data-cy="next-step"]').click();
+ cy.get('[data-cy="btn-close"]').click();
cy.visit("/");
diff --git a/cypress/e2e/learningMentor/overview/memberOnly.cy.js b/cypress/e2e/learningMentor/overview/memberOnly.cy.js
index 271fc467..76ce28fb 100644
--- a/cypress/e2e/learningMentor/overview/memberOnly.cy.js
+++ b/cypress/e2e/learningMentor/overview/memberOnly.cy.js
@@ -63,7 +63,7 @@ describe("memberOnly.cy.js", () => {
it("uses term Lernbegleitung in VV-course", () => {
cy.visit(MENTOR_MENTEES_URL_VV);
cy.get(MAIN_NAVIGATION_MENTOR_LINK).should("contain", "Lernbegleitung");
- cy.get(MENTEE_MENTORS_TITLE).should("contain", "Meine Lernbegleiter");
+ cy.get(MENTEE_MENTORS_TITLE).should("contain", "Meine Lernbegleitung");
cy.get(MENTEE_INVITE_MENTOR).should(
"contain",
"Neue Lernbegleitung einladen"
diff --git a/server/config/settings/base.py b/server/config/settings/base.py
index f0a96529..2f79e970 100644
--- a/server/config/settings/base.py
+++ b/server/config/settings/base.py
@@ -59,6 +59,7 @@ DATABASES = {
default="postgres://postgres@localhost:5432/vbv_lernwelt",
)
}
+
DATABASES["default"]["ATOMIC_REQUESTS"] = env.bool(
"DATABASE_ATOMIC_REQUESTS", default=True
)
diff --git a/server/vbv_lernwelt/assignment/admin.py b/server/vbv_lernwelt/assignment/admin.py
index 45713e51..009c3f71 100644
--- a/server/vbv_lernwelt/assignment/admin.py
+++ b/server/vbv_lernwelt/assignment/admin.py
@@ -1,7 +1,11 @@
from django.contrib import admin
from django.db.models import JSONField
-from vbv_lernwelt.assignment.models import AssignmentCompletion
+from vbv_lernwelt.assignment.models import (
+ AssignmentCompletion,
+ AssignmentCompletionAuditLog,
+)
+from vbv_lernwelt.core.admin import LogAdmin
from vbv_lernwelt.core.admin_utils import PrettyJSONWidget
@@ -16,6 +20,7 @@ class AssignmentCompletionAdmin(admin.ModelAdmin):
"assignment",
"get_circle",
"assignment_user",
+ "evaluation_user",
"course_session",
"completion_status",
"evaluation_points",
@@ -27,7 +32,14 @@ class AssignmentCompletionAdmin(admin.ModelAdmin):
"course_session__course",
"course_session",
]
- search_fields = ["assignment_user__email"]
+ search_fields = [
+ "assignment_user__email",
+ "assignment_user__first_name",
+ "assignment_user__last_name",
+ "evaluation_user__email",
+ "evaluation_user__first_name",
+ "evaluation_user__last_name",
+ ]
readonly_fields = [
"assignment_user",
"assignment",
@@ -53,3 +65,40 @@ class AssignmentCompletionAdmin(admin.ModelAdmin):
if change and "evaluation_points_deducted" in form.changed_data:
obj.evaluation_points_deducted_user = request.user
super().save_model(request, obj, form, change)
+
+
+@admin.register(AssignmentCompletionAuditLog)
+class AssignmentCompletionAuditLogAdmin(LogAdmin):
+ date_hierarchy = "created_at"
+ list_display = [
+ "created_at",
+ "assignment",
+ "get_circle",
+ "assignment_user",
+ "evaluation_user",
+ "course_session",
+ "completion_status",
+ "evaluation_points",
+ ]
+ list_filter = [
+ "completion_status",
+ "assignment__assignment_type",
+ "course_session__course",
+ "course_session",
+ ]
+ search_fields = [
+ "assignment_user__email",
+ "assignment_user__first_name",
+ "assignment_user__last_name",
+ "evaluation_user__email",
+ "evaluation_user__first_name",
+ "evaluation_user__last_name",
+ ]
+
+ def get_circle(self, obj):
+ try:
+ return obj.learning_content_page.specific.get_circle().title
+ except Exception:
+ return ""
+
+ get_circle.short_description = "Circle"
diff --git a/server/vbv_lernwelt/assignment/graphql/types.py b/server/vbv_lernwelt/assignment/graphql/types.py
index 536dc73a..c6181ee1 100644
--- a/server/vbv_lernwelt/assignment/graphql/types.py
+++ b/server/vbv_lernwelt/assignment/graphql/types.py
@@ -16,6 +16,7 @@ class AssignmentCompletionObjectType(DjangoObjectType):
completion_data = GenericScalar()
task_completion_data = GenericScalar()
learning_content_page_id = graphene.ID(source="learning_content_page_id")
+ additional_json_data = GenericScalar()
# rounded to sensible representation
evaluation_points = graphene.Float()
diff --git a/server/vbv_lernwelt/assignment/management/commands/assignment_create_initial_submission_history.py b/server/vbv_lernwelt/assignment/management/commands/assignment_create_initial_submission_history.py
new file mode 100644
index 00000000..5a3b7892
--- /dev/null
+++ b/server/vbv_lernwelt/assignment/management/commands/assignment_create_initial_submission_history.py
@@ -0,0 +1,71 @@
+import djclick as click
+import structlog
+
+from vbv_lernwelt.assignment.models import AssignmentCompletion
+
+logger = structlog.get_logger(__name__)
+
+
+def create_initial_submission_history(apps=None, schema_editor=None):
+ if apps is None:
+ # pylint: disable=import-outside-toplevel
+ from vbv_lernwelt.assignment.models import AssignmentCompletion
+ else:
+ AssignmentCompletion = apps.get_model("assignment", "AssignmentCompletion")
+
+ for ac in AssignmentCompletion.objects.filter(
+ assignment__assignment_type__in=["PRAXIS_ASSIGNMENT", "CASEWORK"]
+ ):
+ num_entries = ac_create_initial_submission_history(ac)
+ # print(f"Created initial submission history for {ac} {num_entries}")
+
+
+def ac_create_initial_submission_history(ac: AssignmentCompletion):
+ submission_history = ac.additional_json_data.get("submission_history", [])
+
+ num_entries = 0
+
+ if len(submission_history) > 0:
+ return num_entries
+
+ if ac.submitted_at:
+ entry = {
+ "timestamp": ac.submitted_at.isoformat(),
+ "status": "SUBMITTED",
+ "translation_key": "a.Ergebnisse abgegeben",
+ "user_id": str(ac.assignment_user.id),
+ "user_email": ac.assignment_user.email,
+ "user_display_name": (
+ f"{ac.assignment_user.first_name} {ac.assignment_user.last_name}"
+ ),
+ }
+ submission_history.append(entry)
+ num_entries += 1
+
+ if ac.evaluation_submitted_at:
+ translation_key = "a.Bewertung freigegeben"
+ if ac.assignment.assignment_type == "PRAXIS_ASSIGNMENT":
+ translation_key = "Feedback freigegeben"
+
+ entry = {
+ "timestamp": ac.evaluation_submitted_at.isoformat(),
+ "status": "EVALUATION_SUBMITTED",
+ "translation_key": translation_key,
+ "user_id": str(ac.evaluation_user.id),
+ "user_email": ac.evaluation_user.email,
+ "user_display_name": (
+ f"{ac.evaluation_user.first_name} {ac.evaluation_user.last_name}"
+ ),
+ }
+ submission_history.append(entry)
+ num_entries += 1
+
+ ac.additional_json_data["submission_history"] = submission_history
+ ac.save()
+
+ return num_entries
+
+
+@click.command()
+def command():
+ create_initial_submission_history()
diff --git a/server/vbv_lernwelt/assignment/migrations/0015_assignmentcompletionauditlog_evaluation_points_deducted_and_more.py b/server/vbv_lernwelt/assignment/migrations/0015_assignmentcompletionauditlog_evaluation_points_deducted_and_more.py
new file mode 100644
index 00000000..7aaff5c4
--- /dev/null
+++ b/server/vbv_lernwelt/assignment/migrations/0015_assignmentcompletionauditlog_evaluation_points_deducted_and_more.py
@@ -0,0 +1,43 @@
+# Generated by Django 4.2.13 on 2024-09-16 12:00
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("assignment", "0014_evaluation_points_deducted"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="assignmentcompletionauditlog",
+ name="evaluation_points_deducted",
+ field=models.FloatField(default=0.0, verbose_name="Punkteabzug"),
+ ),
+ migrations.AddField(
+ model_name="assignmentcompletionauditlog",
+ name="evaluation_points_deducted_reason",
+ field=models.TextField(
+ blank=True, default="", verbose_name="Punkteabzug Begründung"
+ ),
+ ),
+ migrations.AddField(
+ model_name="assignmentcompletionauditlog",
+ name="evaluation_points_deducted_user",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="+",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ migrations.AddField(
+ model_name="assignmentcompletionauditlog",
+ name="evaluation_points_deducted_user_email",
+ field=models.CharField(blank=True, default="", max_length=255),
+ ),
+ ]
diff --git a/server/vbv_lernwelt/assignment/migrations/0016_script_submission_history.py b/server/vbv_lernwelt/assignment/migrations/0016_script_submission_history.py
new file mode 100644
index 00000000..129e9b84
--- /dev/null
+++ b/server/vbv_lernwelt/assignment/migrations/0016_script_submission_history.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.13 on 2024-09-17 11:37
+
+from django.db import migrations
+
+from vbv_lernwelt.assignment.management.commands.assignment_create_initial_submission_history import (
+ create_initial_submission_history,
+)
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ (
+ "assignment",
+ "0015_assignmentcompletionauditlog_evaluation_points_deducted_and_more",
+ ),
+ ]
+
+ operations = [migrations.RunPython(create_initial_submission_history)]
diff --git a/server/vbv_lernwelt/assignment/models.py b/server/vbv_lernwelt/assignment/models.py
index f60cb268..2c21a5cb 100644
--- a/server/vbv_lernwelt/assignment/models.py
+++ b/server/vbv_lernwelt/assignment/models.py
@@ -485,6 +485,23 @@ class AssignmentCompletionAuditLog(models.Model):
evaluation_max_points = models.FloatField(null=True, blank=True)
evaluation_passed = models.BooleanField(null=True, blank=True)
+ evaluation_points_deducted = models.FloatField(
+ default=0.0, verbose_name="Punkteabzug"
+ )
+ evaluation_points_deducted_reason = models.TextField(
+ default="", blank=True, verbose_name="Punkteabzug Begründung"
+ )
+ evaluation_points_deducted_user = models.ForeignKey(
+ User,
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ related_name="+",
+ )
+ evaluation_points_deducted_user_email = models.CharField(
+ max_length=255, blank=True, default=""
+ )
+
def recalculate_assignment_passed(ac: AssignmentCompletion):
if ac.evaluation_points_final is not None and ac.evaluation_max_points is not None:
diff --git a/server/vbv_lernwelt/assignment/services.py b/server/vbv_lernwelt/assignment/services.py
index 90d6bb1e..3251e359 100644
--- a/server/vbv_lernwelt/assignment/services.py
+++ b/server/vbv_lernwelt/assignment/services.py
@@ -142,6 +142,13 @@ def update_assignment_completion(
}
)
+ evaluation_reopened = False
+ if (
+ completion_status == AssignmentCompletionStatus.EVALUATION_IN_PROGRESS
+ and ac.completion_status == "EVALUATION_SUBMITTED"
+ ):
+ evaluation_reopened = True
+
if evaluation_max_points is None:
ac.evaluation_max_points = assignment.get_max_points()
else:
@@ -229,9 +236,16 @@ def update_assignment_completion(
evaluation_points=evaluation_points,
evaluation_max_points=ac.evaluation_max_points,
evaluation_passed=ac.evaluation_passed,
+ evaluation_points_deducted=ac.evaluation_points_deducted,
+ evaluation_points_deducted_reason=ac.evaluation_points_deducted_reason,
+ evaluation_points_deducted_user=ac.evaluation_points_deducted_user,
)
if evaluation_user:
acl.evaluation_user_email = evaluation_user.email
+ if ac.evaluation_points_deducted_user:
+ acl.evaluation_points_deducted_user_email = (
+ ac.evaluation_points_deducted_user.email
+ )
# copy over the question data, so that we don't lose the context
subtasks = assignment.get_input_tasks()
@@ -241,23 +255,66 @@ def update_assignment_completion(
acl.completion_data[key].update(task_data)
acl.save()
- if completion_status in [
+ if (
+ completion_status
+ in [
AssignmentCompletionStatus.EVALUATION_SUBMITTED,
- AssignmentCompletionStatus.EVALUATION_IN_PROGRESS,
AssignmentCompletionStatus.SUBMITTED,
- ]:
- learning_content = (
- learning_content_page
- if learning_content_page
- else assignment.find_attached_learning_content()
+ ]
+ or evaluation_reopened
+ ) and assignment.assignment_type != AssignmentType.EDONIQ_TEST.value:
+ # make history entry
+ submission_history = ac.additional_json_data.get("submission_history", [])
+ entry = {
+ "timestamp": timezone.now().isoformat(),
+ "status": completion_status.value,
+ }
+
+ if completion_status == AssignmentCompletionStatus.SUBMITTED:
+ entry["translation_key"] = "a.Ergebnisse abgegeben"
+ entry["user_id"] = str(assignment_user.id)
+ entry["user_email"] = assignment_user.email
+ entry["user_display_name"] = (
+ f"{assignment_user.first_name} {assignment_user.last_name}"
+ )
+ else:
+ entry["user_id"] = str(evaluation_user.id)
+ entry["user_email"] = evaluation_user.email
+ entry["user_display_name"] = (
+ f"{evaluation_user.first_name} {evaluation_user.last_name}"
+ )
+ if completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED:
+ if assignment.assignment_type == AssignmentType.PRAXIS_ASSIGNMENT.value:
+ entry["translation_key"] = "a.Feedback freigegeben"
+ else:
+ entry["translation_key"] = "a.Bewertung freigegeben"
+ elif completion_status == AssignmentCompletionStatus.EVALUATION_IN_PROGRESS:
+ if assignment.assignment_type == AssignmentType.PRAXIS_ASSIGNMENT.value:
+ entry["translation_key"] = "a.Feedback erneut bearbeitet"
+ else:
+ entry["translation_key"] = "a.Bewertung erneut bearbeitet"
+
+ submission_history.append(entry)
+ ac.additional_json_data["submission_history"] = submission_history
+ ac.save()
+
+ if completion_status in [
+ AssignmentCompletionStatus.EVALUATION_SUBMITTED,
+ AssignmentCompletionStatus.EVALUATION_IN_PROGRESS,
+ AssignmentCompletionStatus.SUBMITTED,
+ ]:
+ learning_content = (
+ learning_content_page
+ if learning_content_page
+ else assignment.find_attached_learning_content()
+ )
+ if learning_content:
+ mark_course_completion(
+ user=assignment_user,
+ page=learning_content,
+ course_session=course_session,
+ completion_status=CourseCompletionStatus.SUCCESS.value,
)
- if learning_content:
- mark_course_completion(
- user=assignment_user,
- page=learning_content,
- course_session=course_session,
- completion_status=CourseCompletionStatus.SUCCESS.value,
- )
return ac, created
@@ -267,7 +324,8 @@ def _remove_unknown_entries(assignment, completion_data):
Removes all entries from completion_data which are not known to the assignment
"""
input_task_ids = [task["id"] for task in assignment.get_input_tasks()]
+ keys = set(input_task_ids) | {"expert_evaluation_comment"}
filtered_completion_data = {
- key: value for key, value in completion_data.items() if key in input_task_ids
+ key: value for key, value in completion_data.items() if key in keys
}
return filtered_completion_data
diff --git a/server/vbv_lernwelt/assignment/tests/test_services.py b/server/vbv_lernwelt/assignment/tests/test_services.py
index 14a7d016..7ec9c155 100644
--- a/server/vbv_lernwelt/assignment/tests/test_services.py
+++ b/server/vbv_lernwelt/assignment/tests/test_services.py
@@ -190,6 +190,13 @@ class UpdateAssignmentCompletionTestCase(TestCase):
"test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice",
)
+ # will create submission_history entry
+ submission_history = ac.additional_json_data.get("submission_history", [])
+ self.assertEqual(len(submission_history), 1)
+ entry = submission_history[0]
+ self.assertEqual(entry["status"], "SUBMITTED")
+ self.assertEqual(entry["user_email"], "student")
+
# AssignmentCompletionAuditLog entry will remain event after deletion of foreign keys
ac.delete()
self.user.delete()
@@ -512,6 +519,13 @@ class UpdateAssignmentCompletionTestCase(TestCase):
user_input["user_data"], {"text": "Ich würde nichts weiteres empfehlen."}
)
+ # will create submission_history entry
+ submission_history = ac.additional_json_data.get("submission_history", [])
+ self.assertEqual(len(submission_history), 1)
+ entry = submission_history[0]
+ self.assertEqual(entry["status"], "EVALUATION_SUBMITTED")
+ self.assertEqual(entry["user_email"], "admin")
+
# will create AssignmentCompletionAuditLog entry
acl = AssignmentCompletionAuditLog.objects.get(
assignment_user=self.user,
@@ -553,3 +567,156 @@ class UpdateAssignmentCompletionTestCase(TestCase):
)
self.assertIsNone(acl.assignment_user)
self.assertIsNone(acl.assignment)
+
+ def test_can_reopen_evaluated_submission(self):
+ subtasks = self.assignment.filter_user_subtasks(
+ subtask_types=["user_text_input"]
+ )
+ user_text_input = find_first(
+ subtasks,
+ pred=lambda x: (value := x.get("value"))
+ and value.get("text", "").startswith(
+ "Gibt es zusätzliche Deckungen, die du der Person empfehlen würdest?"
+ ),
+ )
+
+ ac = AssignmentCompletion.objects.create(
+ assignment_user=self.user,
+ assignment=self.assignment,
+ course_session=self.course_session,
+ completion_status=AssignmentCompletionStatus.SUBMITTED.value,
+ completion_data={
+ user_text_input["id"]: {
+ "user_data": {"text": "Ich würde nichts weiteres empfehlen."}
+ },
+ },
+ )
+
+ evaluation_task = self.assignment.get_evaluation_tasks()[0]
+
+ update_assignment_completion(
+ assignment_user=self.user,
+ assignment=self.assignment,
+ course_session=self.course_session,
+ completion_data={
+ evaluation_task["id"]: {
+ "expert_data": {"points": 2, "text": "Gut gemacht!"}
+ },
+ },
+ completion_status=AssignmentCompletionStatus.EVALUATION_IN_PROGRESS,
+ evaluation_user=self.trainer,
+ )
+
+ update_assignment_completion(
+ assignment_user=self.user,
+ assignment=self.assignment,
+ course_session=self.course_session,
+ completion_data={},
+ completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED,
+ evaluation_user=self.trainer,
+ evaluation_points=16,
+ )
+
+ ac = AssignmentCompletion.objects.get(
+ assignment_user=self.user,
+ assignment=self.assignment,
+ course_session=self.course_session,
+ )
+
+ self.assertEqual(ac.completion_status, "EVALUATION_SUBMITTED")
+ self.assertEqual(ac.evaluation_points, 16)
+ self.assertEqual(ac.evaluation_max_points, 24)
+ self.assertTrue(ac.evaluation_passed)
+ trainer_input = ac.completion_data[evaluation_task["id"]]
+ self.assertDictEqual(
+ trainer_input["expert_data"], {"points": 2, "text": "Gut gemacht!"}
+ )
+ user_input = ac.completion_data[user_text_input["id"]]
+ self.assertDictEqual(
+ user_input["user_data"], {"text": "Ich würde nichts weiteres empfehlen."}
+ )
+
+ update_assignment_completion(
+ assignment_user=self.user,
+ assignment=self.assignment,
+ course_session=self.course_session,
+ completion_data={},
+ completion_status=AssignmentCompletionStatus.EVALUATION_IN_PROGRESS,
+ evaluation_user=self.trainer,
+ )
+
+ ac = AssignmentCompletion.objects.get(
+ assignment_user=self.user,
+ assignment=self.assignment,
+ course_session=self.course_session,
+ )
+ self.assertEqual(ac.completion_status, "EVALUATION_IN_PROGRESS")
+
+ update_assignment_completion(
+ assignment_user=self.user,
+ assignment=self.assignment,
+ course_session=self.course_session,
+ completion_data={
+ evaluation_task["id"]: {
+ "expert_data": {
+ "points": 2,
+ "text": "Gut gemacht. Ich musste es noch einmal anschauen.",
+ }
+ },
+ },
+ completion_status=AssignmentCompletionStatus.EVALUATION_IN_PROGRESS,
+ evaluation_user=self.trainer,
+ )
+
+ # will create submission_history entry
+ submission_history = ac.additional_json_data.get("submission_history", [])
+ self.assertEqual(len(submission_history), 2)
+ entry = submission_history[1]
+ self.assertEqual(entry["status"], "EVALUATION_IN_PROGRESS")
+ self.assertEqual(entry["user_email"], "admin")
+
+ update_assignment_completion(
+ assignment_user=self.user,
+ assignment=self.assignment,
+ course_session=self.course_session,
+ completion_data={},
+ completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED,
+ evaluation_user=self.trainer,
+ evaluation_points=16,
+ )
+
+ ac = AssignmentCompletion.objects.get(
+ assignment_user=self.user,
+ assignment=self.assignment,
+ course_session=self.course_session,
+ )
+
+ # will create submission_history entry
+ submission_history = ac.additional_json_data.get("submission_history", [])
+ self.assertEqual(len(submission_history), 3)
+ entry = submission_history[2]
+ self.assertEqual(entry["status"], "EVALUATION_SUBMITTED")
+ self.assertEqual(entry["user_email"], "admin")
+
+ self.assertEqual(ac.completion_status, "EVALUATION_SUBMITTED")
+ self.assertEqual(ac.evaluation_points, 16)
+ self.assertEqual(ac.evaluation_max_points, 24)
+ self.assertTrue(ac.evaluation_passed)
+ trainer_input = ac.completion_data[evaluation_task["id"]]
+ self.assertDictEqual(
+ trainer_input["expert_data"],
+ {"points": 2, "text": "Gut gemacht. Ich musste es noch einmal anschauen."},
+ )
+ user_input = ac.completion_data[user_text_input["id"]]
+ self.assertDictEqual(
+ user_input["user_data"], {"text": "Ich würde nichts weiteres empfehlen."}
+ )
+
+ # it will have created another AssignmentCompletionAuditLog entry
+ acl_qs = AssignmentCompletionAuditLog.objects.filter(
+ assignment_user=self.user,
+ assignment=self.assignment,
+ course_session=self.course_session,
+ completion_status="EVALUATION_SUBMITTED",
+ )
+ self.assertEqual(acl_qs.count(), 2)
diff --git a/server/vbv_lernwelt/core/management/commands/cypress_reset.py b/server/vbv_lernwelt/core/management/commands/cypress_reset.py
index 37ae54af..507ff23c 100644
--- a/server/vbv_lernwelt/core/management/commands/cypress_reset.py
+++ b/server/vbv_lernwelt/core/management/commands/cypress_reset.py
@@ -6,7 +6,11 @@ from django.contrib.auth.hashers import make_password
from django.db import connection
from django.utils import timezone
-from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion
+from vbv_lernwelt.assignment.models import (
+ Assignment,
+ AssignmentCompletion,
+ AssignmentCompletionAuditLog,
+)
from vbv_lernwelt.competence.models import PerformanceCriteria
from vbv_lernwelt.core.constants import (
TEST_COURSE_SESSION_BERN_ID,
@@ -183,6 +187,7 @@ def command(
CourseCompletion.objects.all().delete()
Notification.objects.all().delete()
AssignmentCompletion.objects.all().delete()
+ AssignmentCompletionAuditLog.objects.all().delete()
FeedbackResponse.objects.all().delete()
CourseSessionAttendanceCourse.objects.all().update(attendance_user_list=[])
diff --git a/server/vbv_lernwelt/course/graphql/types.py b/server/vbv_lernwelt/course/graphql/types.py
index 2c243ccd..89bc4b47 100644
--- a/server/vbv_lernwelt/course/graphql/types.py
+++ b/server/vbv_lernwelt/course/graphql/types.py
@@ -291,8 +291,8 @@ class CourseSessionObjectType(DjangoObjectType):
_add_course_session_user(rel.participant)
if self.course.configuration.is_uk:
- # happy path, members and experts
if me_csu:
+ # VBV-708: Teilnehmer und Trainer haben Zugriff auf alle Teilnehmer im Ük
for course_session_user in CourseSessionUser.objects.filter(
course_session_id=self.id
).distinct():
@@ -300,6 +300,45 @@ class CourseSessionObjectType(DjangoObjectType):
user.id for user in course_session_users_resolved
]:
_add_course_session_user(course_session_user)
+ elif CourseSessionGroup.objects.filter(
+ supervisor=info.context.user, course_session=self
+ ).exists():
+ # VBV-708: Supervisor (Regionenleiter) has access to all users and circles
+ for course_session_user in CourseSessionUser.objects.filter(
+ course_session_id=self.id
+ ).distinct():
+ if course_session_user.id not in [
+ user.id for user in course_session_users_resolved
+ ]:
+ _add_course_session_user(course_session_user)
+
+ circles = (
+ self.course.get_learning_path()
+ .get_descendants()
+ .live()
+ .specific()
+ .exact_type(Circle)
+ )
+ user = info.context.user
+ course_session_users_resolved.append(
+ CourseSessionUserObjectsType(
+ id=f"{user.id}-{self.id}-as-ephemeral-supervisor", # noqa
+ user_id=user.id, # noqa
+ first_name=user.first_name, # noqa
+ last_name=user.last_name, # noqa
+ email=user.email, # noqa
+ avatar_url=user.avatar_url, # noqa
+ role=CourseSessionUser.Role.EXPERT, # noqa
+ circles=[ # noqa
+ CourseSessionUserExpertCircleType( # noqa
+ id=circle.id, # noqa
+ title=circle.title, # noqa
+ slug=circle.slug, # noqa
+ )
+ for circle in circles
+ ],
+ )
+ )
else:
# VBV-708: user has only "AgentParticipantRole" and is not in the list of users
for rel in AgentParticipantRelation.objects.filter(
@@ -310,67 +349,4 @@ class CourseSessionObjectType(DjangoObjectType):
]:
_add_course_session_user(rel.participant)
- # workaround for supervisor
- # add supervisor to the list of users (as expert)
- course_session_id = self.id # noqa
- user = info.context.user # noqa
-
- if CourseSessionGroup.objects.filter(
- course_session=course_session_id, supervisor=user
- ).exists():
- if course_session := CourseSession.objects.filter(
- id=course_session_id
- ).first():
- circles = (
- course_session.course.get_learning_path()
- .get_descendants()
- .live()
- .specific()
- .exact_type(Circle)
- )
-
- course_session_users_resolved.append(
- CourseSessionUserObjectsType(
- id=f"{user.id}-as-ephemeral-supervisor", # noqa
- user_id=user.id, # noqa
- first_name=user.first_name, # noqa
- last_name=user.last_name, # noqa
- email=user.email, # noqa
- avatar_url=user.avatar_url, # noqa
- role=CourseSessionUser.Role.EXPERT, # noqa
- circles=[ # noqa
- CourseSessionUserExpertCircleType( # noqa
- id=circle.id, # noqa
- title=circle.title, # noqa
- slug=circle.slug, # noqa
- )
- for circle in circles
- ],
- )
- )
-
return course_session_users_resolved
-
- def _add_course_session_user(
- self, course_session_user, course_session_users_resolved
- ):
- course_session_users_resolved.append(
- CourseSessionUserObjectsType(
- id=course_session_user.id, # noqa
- user_id=course_session_user.user.id, # noqa
- first_name=course_session_user.user.first_name, # noqa
- last_name=course_session_user.user.last_name, # noqa
- email=course_session_user.user.email, # noqa
- avatar_url=course_session_user.user.avatar_url, # noqa
- role=course_session_user.role, # noqa
- circles=[ # noqa
- CourseSessionUserExpertCircleType( # noqa
- id=circle.id, # noqa
- title=circle.title, # noqa
- slug=circle.slug, # noqa
- )
- for circle in course_session_user.expert.all() # noqa
- ],
- optional_attendance=course_session_user.optional_attendance, # noqa
- )
- )