Merged in feature/VBV-326-auftrag-benachrichtigungen-rebased (pull request #157)
Feature/VBV-326 auftrag benachrichtigungen rebased * Implement notifications for assignments # Conflicts: # server/vbv_lernwelt/assignment/services.py * Add cypress test for student assignment submission * Update django-notifications-hq * Rework Approved-by: Daniel Egger
This commit is contained in:
parent
24d57577cc
commit
4b0a881055
|
|
@ -75,6 +75,7 @@ function onNotificationClick(notification: Notification) {
|
|||
<span
|
||||
class="text-left text-sm leading-6 text-black lg:text-base"
|
||||
style="hyphens: none"
|
||||
:data-cy="`notification-target-idx-${index}-verb`"
|
||||
>
|
||||
{{ notification.verb }}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ const onSubmit = async () => {
|
|||
value: 'value',
|
||||
checked: state.confirmInput,
|
||||
}"
|
||||
data-cy="confirm-submit-results"
|
||||
@toggle="state.confirmInput = !state.confirmInput"
|
||||
></ItCheckbox>
|
||||
<div class="w-full border-b border-gray-400">
|
||||
|
|
@ -104,6 +105,7 @@ const onSubmit = async () => {
|
|||
value: 'value',
|
||||
checked: state.confirmPerson,
|
||||
}"
|
||||
data-cy="confirm-submit-person"
|
||||
@toggle="state.confirmPerson = !state.confirmPerson"
|
||||
></ItCheckbox>
|
||||
<div class="flex flex-row items-center pb-6 pl-[49px]">
|
||||
|
|
@ -133,13 +135,17 @@ const onSubmit = async () => {
|
|||
variant="primary"
|
||||
size="large"
|
||||
:disabled="!state.confirmInput || !state.confirmPerson"
|
||||
data-cy="submit-assignment"
|
||||
@click="onSubmit"
|
||||
>
|
||||
<p>{{ $t("assignment.submitAssignment") }}</p>
|
||||
</ItButton>
|
||||
</div>
|
||||
<div v-else class="pt-6">
|
||||
<ItSuccessAlert :text="t('assignment.assignmentSubmitted')"></ItSuccessAlert>
|
||||
<ItSuccessAlert
|
||||
:text="t('assignment.assignmentSubmitted')"
|
||||
data-cy="success-text"
|
||||
></ItSuccessAlert>
|
||||
<p class="pt-6">
|
||||
{{
|
||||
$t("assignment.submissionNotificationDisclaimer", { name: circleExpertName })
|
||||
|
|
|
|||
|
|
@ -86,4 +86,27 @@ describe("assignmentStudent.cy.js", () => {
|
|||
cy.get('[data-cy="nav-progress-step-4"]').click();
|
||||
cy.testLearningContentTitle("Teilaufgabe 4: Deine Empfehlungen");
|
||||
});
|
||||
|
||||
it("can submit assignment", () => {
|
||||
cy.visit(
|
||||
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice?step=7"
|
||||
);
|
||||
cy.get('[data-cy="confirm-submit-results"]').click();
|
||||
cy.get('[data-cy="confirm-submit-person"]').click();
|
||||
cy.get('[data-cy="submit-assignment"]').click();
|
||||
cy.get('[data-cy="success-text"]').should("exist");
|
||||
|
||||
// Check if trainer received notification
|
||||
cy.clearLocalStorage();
|
||||
cy.clearCookies();
|
||||
cy.reload(true);
|
||||
login("test-trainer1@example.com", "test");
|
||||
cy.visit("/notifications");
|
||||
cy.get(`[data-cy=notification-idx-0]`).within(() => {
|
||||
cy.get('[data-cy="unread"]').should("exist");
|
||||
cy.get('[data-cy="notification-target-idx-0-verb"]').contains(
|
||||
"Test Student1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» abgegeben."
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
export const login = (username, password) => {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/api/core/login/',
|
||||
method: "POST",
|
||||
url: "/api/core/login/",
|
||||
body: { username, password },
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const logout = () => {
|
||||
cy.request({
|
||||
method: "POST",
|
||||
url: "/api/core/logout/",
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ django-model-utils==4.2.0
|
|||
# django-notifications-hq
|
||||
django-modelcluster==6.0
|
||||
# via wagtail
|
||||
django-notifications-hq==1.7.0
|
||||
django-notifications-hq==1.8.2
|
||||
# via -r requirements.in
|
||||
django-permissionedforms==0.1
|
||||
# via wagtail
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ django-model-utils==4.2.0
|
|||
# django-notifications-hq
|
||||
django-modelcluster==6.0
|
||||
# via wagtail
|
||||
django-notifications-hq==1.7.0
|
||||
django-notifications-hq==1.8.2
|
||||
# via -r requirements.in
|
||||
django-permissionedforms==0.1
|
||||
# via wagtail
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from vbv_lernwelt.assignment.graphql.types import AssignmentCompletionObjectType
|
|||
from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletionStatus
|
||||
from vbv_lernwelt.assignment.services import update_assignment_completion
|
||||
from vbv_lernwelt.core.models import User
|
||||
from vbv_lernwelt.course.models import CourseSession
|
||||
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
|
||||
from vbv_lernwelt.course.permissions import has_course_access, is_course_session_expert
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
|
@ -55,20 +55,30 @@ class AssignmentCompletionMutation(graphene.Mutation):
|
|||
):
|
||||
raise PermissionDenied()
|
||||
|
||||
course_session = CourseSession.objects.get(id=course_session_id)
|
||||
assignment_data = {
|
||||
"assignment_user": assignment_user,
|
||||
"assignment": assignment,
|
||||
"course_session": CourseSession.objects.get(id=course_session_id),
|
||||
"course_session": course_session,
|
||||
"completion_data": json.loads(completion_data_string),
|
||||
"completion_status": completion_status,
|
||||
}
|
||||
|
||||
if completion_status == AssignmentCompletionStatus.SUBMITTED:
|
||||
# TODO: determine proper way to assign evaluation user
|
||||
experts = CourseSessionUser.objects.filter(
|
||||
course_session=course_session, role="EXPERT"
|
||||
)
|
||||
if not experts:
|
||||
raise PermissionDenied()
|
||||
assignment_data["evaluation_user"] = experts[0].user
|
||||
|
||||
evaluation_data = {}
|
||||
|
||||
if completion_status in [
|
||||
if completion_status in (
|
||||
AssignmentCompletionStatus.EVALUATION_SUBMITTED,
|
||||
AssignmentCompletionStatus.EVALUATION_IN_PROGRESS,
|
||||
]:
|
||||
):
|
||||
if not is_course_session_expert(info.context.user, course_session_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
|
|
|
|||
|
|
@ -295,6 +295,13 @@ class AssignmentCompletion(models.Model):
|
|||
)
|
||||
]
|
||||
|
||||
def get_assignment_evaluation_frontend_url(self):
|
||||
"""
|
||||
Used by the expert to evaluate the assignment
|
||||
Example: /course/überbetriebliche-kurse/cockpit/assignment/371/18
|
||||
"""
|
||||
return f"{self.course_session.course.get_cockpit_url()}/assignment/{self.assignment.id}/{self.assignment_user.id}"
|
||||
|
||||
|
||||
class AssignmentCompletionAuditLog(models.Model):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from copy import deepcopy
|
||||
from gettext import gettext
|
||||
|
||||
from django.utils import timezone
|
||||
from rest_framework import serializers
|
||||
|
|
@ -14,6 +15,7 @@ from vbv_lernwelt.core.models import User
|
|||
from vbv_lernwelt.core.utils import find_first
|
||||
from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession
|
||||
from vbv_lernwelt.course.services import mark_course_completion
|
||||
from vbv_lernwelt.notify.service import NotificationService
|
||||
|
||||
|
||||
def update_assignment_completion(
|
||||
|
|
@ -64,7 +66,6 @@ def update_assignment_completion(
|
|||
)
|
||||
|
||||
if validate_completion_status_change:
|
||||
# TODO: check time?
|
||||
if completion_status == AssignmentCompletionStatus.SUBMITTED:
|
||||
if ac.completion_status in [
|
||||
"SUBMITTED",
|
||||
|
|
@ -125,8 +126,38 @@ def update_assignment_completion(
|
|||
|
||||
if completion_status == AssignmentCompletionStatus.SUBMITTED:
|
||||
ac.submitted_at = timezone.now()
|
||||
if evaluation_user:
|
||||
verb = gettext(
|
||||
"%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» abgegeben."
|
||||
) % {
|
||||
"sender": assignment_user.get_full_name(),
|
||||
"assignment_title": assignment.title,
|
||||
}
|
||||
NotificationService.send_user_interaction_notification(
|
||||
recipient=evaluation_user,
|
||||
verb=verb,
|
||||
sender=ac.assignment_user,
|
||||
course=course_session.course.title,
|
||||
target_url=ac.get_assignment_evaluation_frontend_url(),
|
||||
)
|
||||
elif completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED:
|
||||
ac.evaluation_submitted_at = timezone.now()
|
||||
learning_content_assignment = assignment.learningcontentassignment_set.first()
|
||||
if learning_content_assignment:
|
||||
assignment_frontend_url = learning_content_assignment.get_frontend_url()
|
||||
verb = gettext(
|
||||
"%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» bewertet."
|
||||
) % {
|
||||
"sender": evaluation_user.get_full_name(),
|
||||
"assignment_title": assignment.title,
|
||||
}
|
||||
NotificationService.send_user_interaction_notification(
|
||||
recipient=ac.assignment_user,
|
||||
verb=verb,
|
||||
sender=evaluation_user,
|
||||
course=course_session.course.title,
|
||||
target_url=assignment_frontend_url,
|
||||
)
|
||||
|
||||
ac.completion_status = completion_status.value
|
||||
|
||||
|
|
|
|||
|
|
@ -91,21 +91,32 @@ class NotificationService:
|
|||
emailed = False
|
||||
if cls._should_send_email(notification_type, recipient):
|
||||
emailed = cls._send_email(recipient, verb, target_url)
|
||||
response = notify.send(
|
||||
sender=sender,
|
||||
recipient=recipient,
|
||||
log = logger.bind(
|
||||
verb=verb,
|
||||
notification_type=notification_type,
|
||||
sender=sender.get_full_name(),
|
||||
recipient=recipient.get_full_name(),
|
||||
course=course,
|
||||
target_url=target_url,
|
||||
)
|
||||
# Custom Notification model fields cannot be set using the notify.send() method.
|
||||
# https://github.com/django-notifications/django-notifications/issues/301
|
||||
sent_notification: Notification = response[0][1][0]
|
||||
sent_notification.target_url = target_url
|
||||
sent_notification.notification_type = notification_type
|
||||
sent_notification.course = course
|
||||
sent_notification.target_url = target_url
|
||||
sent_notification.actor_avatar_url = actor_avatar_url
|
||||
sent_notification.emailed = emailed
|
||||
sent_notification.save()
|
||||
try:
|
||||
response = notify.send(
|
||||
sender=sender,
|
||||
recipient=recipient,
|
||||
verb=verb,
|
||||
)
|
||||
sent_notification: Notification = response[0][1][0]
|
||||
sent_notification.target_url = target_url
|
||||
sent_notification.notification_type = notification_type
|
||||
sent_notification.course = course
|
||||
sent_notification.actor_avatar_url = actor_avatar_url
|
||||
sent_notification.emailed = emailed
|
||||
sent_notification.save()
|
||||
except Exception as e:
|
||||
log.bind(exception=e)
|
||||
log.error("Failed to send notification")
|
||||
else:
|
||||
log.info("Notification sent successfully")
|
||||
|
||||
@staticmethod
|
||||
def _should_send_email(
|
||||
|
|
@ -117,8 +128,12 @@ class NotificationService:
|
|||
|
||||
@staticmethod
|
||||
def _send_email(recipient: User, verb: str, target_url: Optional[str]) -> bool:
|
||||
return EmailService.send_email(
|
||||
recipient=recipient,
|
||||
verb=verb,
|
||||
target_url=target_url,
|
||||
)
|
||||
try:
|
||||
return EmailService.send_email(
|
||||
recipient=recipient,
|
||||
verb=verb,
|
||||
target_url=target_url,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send email to {recipient}: {e}")
|
||||
return False
|
||||
|
|
|
|||
Loading…
Reference in New Issue