chore: learning mentor course → course session

This commit is contained in:
Livio Bieri 2024-03-19 15:59:23 +01:00
parent b48d8c0fc3
commit e374ad98de
21 changed files with 166 additions and 115 deletions

View File

@ -3,6 +3,7 @@ import { useCurrentCourseSession } from "@/composables";
import ItModal from "@/components/ui/ItModal.vue";
import { computed, ref } from "vue";
import { useCSRFFetch } from "@/fetchHelpers";
import { useUserStore } from "@/stores/user";
const courseSession = useCurrentCourseSession();
@ -33,6 +34,12 @@ const hasMentors = computed(() => {
});
const validEmail = computed(() => {
const isSelfInvitation = Boolean(inviteeEmail.value === useUserStore().email);
if (isSelfInvitation) {
return false;
}
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(inviteeEmail.value);
});
@ -81,7 +88,7 @@ const inviteMentor = async () => {
<div
v-for="invitation in invitations"
:key="invitation.id"
class="flex flex-col justify-between gap-4 border-b py-2 md:flex-row md:gap-16"
class="flex flex-col justify-between gap-4 border-b py-2 last:border-b-0 md:flex-row md:gap-16"
>
<div class="flex flex-col md:flex-grow md:flex-row">
<div class="flex items-center space-x-2">

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useCSRFFetch } from "@/fetchHelpers";
import { getCockpitUrl } from "@/utils/utils";
import { getLearningMentorUrl } from "@/utils/utils";
const props = defineProps<{
courseId: string;
@ -56,8 +56,8 @@ const { data, error } = useCSRFFetch(
</template>
</i18next>
<div class="mt-4">
<a class="underline" :href="getCockpitUrl(data.course_slug)">
{{ $t("a.Cockpit anschauen") }}
<a class="underline" :href="getLearningMentorUrl(data.course_slug)">
{{ $t("a.Übersicht anschauen") }}
</a>
</div>
</template>

View File

@ -118,9 +118,6 @@ urlpatterns = [
# course
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),
# path(r"api/course/sessions/<signed_int:course_session_id>/users/",
# get_course_session_users,
# name="get_course_session_users"),
path(r"api/course/page/<slug_or_id>/", course_page_api_view,
name="course_page_api_view"),
path(r"api/course/completion/mark/", mark_course_completion_view,

View File

@ -344,24 +344,24 @@ def command(
attendance_course.save()
if create_learning_mentor:
cs_bern = CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID)
uk_mentor = LearningMentor.objects.create(
course=Course.objects.get(id=COURSE_TEST_ID),
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
course_session=cs_bern,
)
uk_mentor.participants.add(
CourseSessionUser.objects.get(
user__id=TEST_STUDENT1_USER_ID,
course_session=CourseSession.objects.get(
id=TEST_COURSE_SESSION_BERN_ID
),
course_session=cs_bern,
)
)
vv_course = Course.objects.get(id=COURSE_VERSICHERUNGSVERMITTLERIN_ID)
vv_course_session = CourseSession.objects.get(course=vv_course)
vv_mentor = LearningMentor.objects.create(
course=vv_course,
mentor=User.objects.get(id=TEST_MENTOR1_USER_ID),
course_session=vv_course_session,
)
vv_mentor.participants.add(
@ -371,7 +371,7 @@ def command(
)
vv_mentor.participants.add(
CourseSessionUser.objec.get(
CourseSessionUser.objects.get(
user__id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID,
course_session=vv_course_session,
)
@ -379,7 +379,7 @@ def command(
vv_student_and_mentor = LearningMentor.objects.create(
mentor=User.objects.get(id=TEST_STUDENT2_VV_AND_VV_MENTOR_USER_ID),
course=vv_course,
course_session=vv_course_session,
)
vv_student_and_mentor.participants.add(

View File

@ -100,9 +100,11 @@ def create_course_session(
def add_learning_mentor(
course: Course, mentor: User, mentee: CourseSessionUser
course_session: CourseSession, mentor: User, mentee: CourseSessionUser
) -> LearningMentor:
learning_mentor = LearningMentor.objects.create(course=course, mentor=mentor)
learning_mentor = LearningMentor.objects.create(
course_session=course_session, mentor=mentor
)
learning_mentor.participants.add(mentee)
return learning_mentor

View File

@ -35,7 +35,7 @@ class CourseCompletionApiTestCase(APITestCase):
self.assertEqual(len(response.json()), 0)
def test_api_courseSession_withCourseSessionUser(self):
csu = CourseSessionUser.objects.create(
CourseSessionUser.objects.create(
course_session=self.course_session,
user=self.user,
)

View File

@ -155,8 +155,8 @@ def get_course_sessions(request):
# enrich with mentor course sessions
mentor_course_sessions = CourseSession.objects.filter(
course__in=LearningMentor.objects.filter(mentor=request.user).values_list(
"course", flat=True
id__in=LearningMentor.objects.filter(mentor=request.user).values_list(
"course_session", flat=True
)
).prefetch_related("course")

View File

@ -192,14 +192,14 @@ def get_learning_mentor_dashboards(
user: User, exclude_course_ids: Set[int]
) -> Tuple[List[Dict[str, str]], Set[int]]:
learning_mentor = LearningMentor.objects.filter(mentor=user).exclude(
course_id__in=exclude_course_ids
course_session__course__id__in=exclude_course_ids
)
dashboards = []
course_ids = set()
for mentor in learning_mentor:
course = mentor.course
course = mentor.course_session.course
course_ids.add(course.id)
dashboards.append(
{

View File

@ -268,7 +268,7 @@ class DashboardTestCase(GraphQLTestCase):
role=CourseSessionUser.Role.MEMBER,
)
add_learning_mentor(course=course_1, mentor=mentor, mentee=csu)
add_learning_mentor(course_session=cs_1, mentor=mentor, mentee=csu)
self.client.force_login(mentor)
@ -314,7 +314,7 @@ class DashboardTestCase(GraphQLTestCase):
role=CourseSessionUser.Role.MEMBER,
)
add_learning_mentor(course=course, mentor=mentor_and_member, mentee=mentee)
add_learning_mentor(course_session=cs, mentor=mentor_and_member, mentee=mentee)
# WHEN
self.client.force_login(mentor_and_member)

View File

@ -22,7 +22,9 @@ def has_course_access(user, course_id):
).exists():
return True
if LearningMentor.objects.filter(course_id=course_id, mentor=user).exists():
if LearningMentor.objects.filter(
course_session__course_id=course_id, mentor=user
).exists():
return True
return CourseSessionUser.objects.filter(
@ -71,7 +73,7 @@ def is_learning_mentor(mentor: User, course_session_id: int):
return False
return LearningMentor.objects.filter(
mentor=mentor, course_id=course_session.course_id
mentor=mentor, course_session=course_session
).exists()
@ -86,7 +88,7 @@ def is_learning_mentor_for_user(
return False
return LearningMentor.objects.filter(
course_id=csu.course_session.course_id, mentor=mentor, participants=csu
course_session_id=course_session_id, mentor=mentor, participants=csu
).exists()
@ -211,7 +213,9 @@ def has_role_in_course(user: User, course: Course) -> bool:
).exists():
return True
if LearningMentor.objects.filter(course=course, mentor=user).exists():
if LearningMentor.objects.filter(
course_session__course=course, mentor=user
).exists():
return True
if CourseSessionGroup.objects.filter(course=course, supervisor=user).exists():

View File

@ -21,10 +21,7 @@ class ActionTestCase(TestCase):
def test_course_session_permissions(self):
# GIVEN
lm = create_user("mentor")
LearningMentor.objects.create(
mentor=lm,
course=self.course,
)
LearningMentor.objects.create(mentor=lm, course_session=self.course_session)
participant = create_user("participant")
add_course_session_user(

View File

@ -50,7 +50,7 @@ class RoleTestCase(TestCase):
# GIVEN
LearningMentor.objects.create(
mentor=self.user,
course=self.course,
course_session=self.course_session,
)
# WHEN
@ -75,7 +75,7 @@ class RoleTestCase(TestCase):
learning_mentor = LearningMentor.objects.create(
mentor=mentor,
course=course,
course_session=course_session,
)
learning_mentor.participants.add(member_csu)

View File

@ -10,7 +10,7 @@ class LearningMentorAdmin(admin.ModelAdmin):
participant_count.short_description = "Participants"
list_display = ["mentor", "course", "participant_count"]
list_display = ["mentor", "course_session", "participant_count"]
search_fields = ["mentor__email"]

View File

@ -0,0 +1,57 @@
# Generated by Django 3.2.20 on 2024-03-19 09:58
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
def reverse_migrate_course_session_to_course(apps, schema_editor):
LearningMentor = apps.get_model("learning_mentor", "LearningMentor")
for lm in LearningMentor.objects.all():
lm.course = lm.course_session.course
lm.save()
def migrate_course_to_course_session(apps, schema_editor):
LearningMentor = apps.get_model("learning_mentor", "LearningMentor")
CourseSession = apps.get_model("course", "CourseSession")
for lm in LearningMentor.objects.all():
# first is fine for VV there is only one course session per course
course_session = CourseSession.objects.filter(course=lm.course).first()
lm.course_session = course_session
lm.save()
class Migration(migrations.Migration):
dependencies = [
("course", "0007_auto_20240226_1553"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("learning_mentor", "0005_alter_learningmentor_mentor"),
]
operations = [
migrations.AddField(
model_name="learningmentor",
name="course_session",
field=models.ForeignKey(
# this is a dummy value, it will be replaced by the migration
default=-1,
on_delete=django.db.models.deletion.CASCADE,
to="course.coursesession",
),
preserve_default=False,
),
migrations.AlterUniqueTogether(
name="learningmentor",
unique_together={("mentor", "course_session")},
),
migrations.RunPython(
code=migrate_course_to_course_session,
reverse_code=reverse_migrate_course_session_to_course,
),
migrations.RemoveField(
model_name="learningmentor",
name="course",
),
]

View File

@ -9,7 +9,7 @@ from vbv_lernwelt.course.models import CourseSessionUser
class LearningMentor(models.Model):
mentor = models.ForeignKey(User, on_delete=models.CASCADE)
course = models.ForeignKey("course.Course", on_delete=models.CASCADE)
course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE)
participants = models.ManyToManyField(
CourseSessionUser,
@ -18,7 +18,7 @@ class LearningMentor(models.Model):
)
class Meta:
unique_together = [["mentor", "course"]]
unique_together = [["mentor", "course_session"]]
verbose_name = "Lernbegleiter"
verbose_name_plural = "Lernbegleiter"

View File

@ -10,7 +10,9 @@ def validate_student(sender, instance, action, reverse, model, pk_set, **kwargs)
if action == "pre_add":
participants = model.objects.filter(pk__in=pk_set)
for participant in participants:
if participant.course_session.course != instance.course:
if participant.course_session != instance.course_session:
raise ValidationError(
"Participant (CourseSessionUser) does not match the course for this mentor."
)
if participant.user == instance.mentor:
raise ValidationError("You cannot mentor yourself.")

View File

@ -35,6 +35,34 @@ class LearningMentorInvitationTest(APITestCase):
# THEN
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@patch("vbv_lernwelt.learning_mentor.views.send_email")
def test_create_denies_self_invitation(self, mock_send_mail) -> None:
# GIVEN
self.client.force_login(self.participant)
add_course_session_user(
self.course_session,
self.participant,
role=CourseSessionUser.Role.MEMBER,
)
invite_url = reverse(
"create_invitation", kwargs={"course_session_id": self.course_session.id}
)
# WHEN
email = self.participant.email
response = self.client.post(invite_url, data={"email": email})
# THEN
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data,
{
"message": "You cannot invite yourself",
},
)
@patch("vbv_lernwelt.learning_mentor.views.send_email")
def test_create_invitation(self, mock_send_mail) -> None:
# GIVEN
@ -164,49 +192,11 @@ class LearningMentorInvitationTest(APITestCase):
},
)
def test_accept_invitation_role_collision(self) -> None:
# GIVEN
participant_cs_user = add_course_session_user(
self.course_session,
self.participant,
role=CourseSessionUser.Role.MEMBER,
)
invitee = create_user("invitee")
invitation = MentorInvitation.objects.create(
participant=participant_cs_user, email=invitee.email
)
# Make invitee a trainer
add_course_session_user(
self.course_session,
invitee,
role=CourseSessionUser.Role.EXPERT,
)
self.client.force_login(invitee)
accept_url = reverse(
"accept_invitation", kwargs={"course_session_id": self.course_session.id}
)
# WHEN
response = self.client.post(accept_url, data={"invitation_id": invitation.id})
# THEN
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data,
{
"message": "User already has a role in this course",
"code": "existingRole",
},
)
def test_accept_invitation(self) -> None:
# GIVEN
participant_cs_user = add_course_session_user(
self.course_session,
self.participant,
course_session=self.course_session,
user=self.participant,
role=CourseSessionUser.Role.MEMBER,
)
@ -229,7 +219,9 @@ class LearningMentorInvitationTest(APITestCase):
self.assertFalse(MentorInvitation.objects.filter(id=invitation.id).exists())
self.assertTrue(
LearningMentor.objects.filter(
mentor=invitee, course=self.course, participants=participant_cs_user
mentor=invitee,
course_session=self.course_session,
participants=participant_cs_user,
).exists()
)

View File

@ -77,7 +77,7 @@ class LearningMentorAPITest(APITestCase):
# GIVEN
self.client.force_login(self.mentor)
LearningMentor.objects.create(
mentor=self.mentor, course=self.course_session.course
mentor=self.mentor, course_session=self.course_session
)
# WHEN
@ -93,8 +93,7 @@ class LearningMentorAPITest(APITestCase):
participants = [self.participant_1, self.participant_2, self.participant_3]
self.client.force_login(self.mentor)
mentor = LearningMentor.objects.create(
mentor=self.mentor,
course=self.course_session.course,
mentor=self.mentor, course_session=self.course_session
)
mentor.participants.set(participants)
@ -120,8 +119,7 @@ class LearningMentorAPITest(APITestCase):
self.client.force_login(self.mentor)
mentor = LearningMentor.objects.create(
mentor=self.mentor,
course=self.course_session.course,
mentor=self.mentor, course_session=self.course_session
)
mentor.participants.set(participants)
@ -204,7 +202,7 @@ class LearningMentorAPITest(APITestCase):
mentor = LearningMentor.objects.create(
mentor=self.mentor,
course=self.course_session.course,
course_session=self.course_session,
)
participants = [self.participant_1, self.participant_2, self.participant_3]
@ -265,8 +263,7 @@ class LearningMentorAPITest(APITestCase):
)
learning_mentor = LearningMentor.objects.create(
mentor=self.mentor,
course=self.course_session.course,
mentor=self.mentor, course_session=self.course_session
)
learning_mentor.participants.add(participant_cs_user)
@ -300,8 +297,7 @@ class LearningMentorAPITest(APITestCase):
)
learning_mentor = LearningMentor.objects.create(
mentor=self.mentor,
course=self.course_session.course,
mentor=self.mentor, course_session=self.course_session
)
learning_mentor.participants.add(participant_cs_user)
@ -326,11 +322,19 @@ class LearningMentorAPITest(APITestCase):
def test_mentor_multiple_courses(self) -> None:
# GIVEN
course_a, _ = create_course("Course A")
course_session_a = create_course_session(course=course_a, title="Test A")
course_b, _ = create_course("Course B")
course_session_b = create_course_session(course=course_b, title="Test B")
# WHEN
LearningMentor.objects.create(mentor=self.mentor, course=course_a)
LearningMentor.objects.create(mentor=self.mentor, course=course_b)
LearningMentor.objects.create(
mentor=self.mentor, course_session=course_session_a
)
LearningMentor.objects.create(
mentor=self.mentor, course_session=course_session_b
)
# THEN
self.assertEqual(LearningMentor.objects.count(), 2)

View File

@ -8,7 +8,7 @@ from rest_framework.response import Response
from vbv_lernwelt.core.serializers import UserSerializer
from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
from vbv_lernwelt.iam.permissions import has_role_in_course, is_course_session_member
from vbv_lernwelt.iam.permissions import is_course_session_member
from vbv_lernwelt.learning_mentor.content.praxis_assignment import (
get_praxis_assignments,
)
@ -31,7 +31,7 @@ def mentor_summary(request, course_session_id: int):
course_session = CourseSession.objects.get(id=course_session_id)
mentor = get_object_or_404(
LearningMentor, mentor=request.user, course=course_session.course
LearningMentor, mentor=request.user, course_session=course_session
)
participants = mentor.participants.filter(course_session=course_session)
@ -129,8 +129,13 @@ def create_invitation(request, course_session_id: int):
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
invitation = serializer.save()
if serializer.validated_data.get("email") == user.email:
return Response(
data={"message": "You cannot invite yourself"},
status=status.HTTP_400_BAD_REQUEST,
)
invitation = serializer.save()
target_url = f"/lernbegleitung/{course_session_id}/invitation/{invitation.id}"
send_email(
@ -158,7 +163,7 @@ def list_user_mentors(request, course_session_id: int):
)
mentors = LearningMentor.objects.filter(
course=course_session.course, participants=course_session_user
course_session=course_session, participants=course_session_user
)
return Response(MentorSerializer(mentors, many=True).data)
@ -193,25 +198,9 @@ def accept_invitation(request, course_session_id: int):
status=status.HTTP_400_BAD_REQUEST,
)
if LearningMentor.objects.filter(
mentor=request.user, course=course_session.course
).exists():
mentor = LearningMentor.objects.get(
mentor=request.user, course=course_session.course
)
else:
if has_role_in_course(request.user, course_session.course):
return Response(
data={
"message": "User already has a role in this course",
"code": "existingRole",
},
status=status.HTTP_400_BAD_REQUEST,
)
mentor = LearningMentor.objects.create(
mentor=request.user, course=course_session.course
)
mentor, _ = LearningMentor.objects.get_or_create(
mentor=request.user, course_session=course_session
)
mentor.participants.add(invitation.participant)
invitation.delete()

View File

@ -53,7 +53,7 @@ class SelfEvaluationFeedbackAPI(APITestCase):
learning_mentor = LearningMentor.objects.create(
mentor=self.mentor,
course=self.course_session.course,
course_session=self.course_session,
)
learning_mentor.participants.add(member_csu)

View File

@ -39,7 +39,7 @@ def start_self_evaluation_feedback(request, learning_unit_id):
feedback_provider_user = get_object_or_404(User, id=feedback_provider_user_id)
if not LearningMentor.objects.filter(
course=learning_unit.get_course(),
course_session__course=learning_unit.get_course(),
mentor=feedback_provider_user,
participants__user=request.user,
).exists():