Beurteilungskriterium {{ taskIndex + 1 }} /
{{ props.assignment.evaluation_tasks.length }}
@@ -91,6 +91,7 @@ const evaluateAssignmentCompletionDebounced = useDebounceFn(
v-for="(subTask, index) in task.value.sub_tasks"
:key="index"
class="mb-4 flex items-center last:mb-0"
+ :data-cy="`subtask-${subTask.points}`"
>
diff --git a/client/src/pages/cockpit/assignmentsPage/AssignmentDetails.vue b/client/src/pages/cockpit/assignmentsPage/AssignmentDetails.vue
index 17923ca7..17aa4319 100644
--- a/client/src/pages/cockpit/assignmentsPage/AssignmentDetails.vue
+++ b/client/src/pages/cockpit/assignmentsPage/AssignmentDetails.vue
@@ -85,6 +85,7 @@ const assignmentDetail = computed(() =>
:key="csu.user_id + csu.session_title"
:name="`${csu.first_name} ${csu.last_name}`"
:avatar-url="csu.avatar_url"
+ :data-cy="csu.last_name"
>
@@ -130,6 +131,7 @@ const assignmentDetail = computed(() =>
v-if="submissionStatusForUser(csu.user_id)?.progressStatus === 'success'"
:to="`/course/${props.courseSession.course.slug}/cockpit/assignment/${assignment.assignmentId}/${csu.user_id}`"
class="w-full text-right underline"
+ data-cy="show-results"
>
Ergebnisse anzeigen
diff --git a/cypress.config.js b/cypress.config.js
index 619c85bf..f77ddffd 100644
--- a/cypress.config.js
+++ b/cypress.config.js
@@ -8,7 +8,7 @@ module.exports = defineConfig({
viewportWidth: 1280,
viewportHeight: 720,
retries: {
- runMode: 1,
+ runMode: 2,
openMode: 0,
},
reporter: "junit",
diff --git a/cypress/consts.js b/cypress/consts.js
new file mode 100644
index 00000000..303130ce
--- /dev/null
+++ b/cypress/consts.js
@@ -0,0 +1,8 @@
+// ids for cypress test data
+export const ADMIN_USER_ID = -1;
+export const TEST_TRAINER1_USER_ID = -11;
+export const TEST_STUDENT1_USER_ID = -21;
+export const TEST_STUDENT2_USER_ID = -22;
+
+export const TEST_COURSE_SESSION_BERN_ID = -1;
+export const TEST_COURSE_SESSION_ZURICH_ID = -2;
diff --git a/cypress/e2e/assignments.cy.js b/cypress/e2e/assignment/assignmentStudent.cy.js
similarity index 98%
rename from cypress/e2e/assignments.cy.js
rename to cypress/e2e/assignment/assignmentStudent.cy.js
index 7ab491c9..cbe656f5 100644
--- a/cypress/e2e/assignments.cy.js
+++ b/cypress/e2e/assignment/assignmentStudent.cy.js
@@ -1,4 +1,4 @@
-import { login } from "./helpers";
+import { login } from "../helpers";
describe("student test", () => {
beforeEach(() => {
diff --git a/cypress/e2e/assignment/assignmentTrainer.cy.js b/cypress/e2e/assignment/assignmentTrainer.cy.js
new file mode 100644
index 00000000..e9f58f09
--- /dev/null
+++ b/cypress/e2e/assignment/assignmentTrainer.cy.js
@@ -0,0 +1,87 @@
+import { TEST_STUDENT1_USER_ID } from "../../consts";
+import { login } from "../helpers";
+
+describe("trainer test", () => {
+ beforeEach(() => {
+ cy.manageCommand("cypress_reset --create-completion");
+ login("test-trainer1@example.com", "test");
+ });
+
+ it("can open cockpit assignment page and open user assignment", () => {
+ cy.visit("/course/test-lehrgang/cockpit/assignment");
+
+ cy.get('[data-cy="Student1"]').should("contain", "Ergebnisse abgegeben");
+ cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
+
+ cy.get('[data-cy="student-submission"]').should("contain", "Ergebnisse");
+ cy.get('[data-cy="student-submission"]').should("contain", "Student1");
+ });
+
+ it("can start evaluation and store evaluation results", () => {
+ cy.visit("/course/test-lehrgang/cockpit/assignment");
+
+ cy.get('[data-cy="Student1"]').find('[data-cy="show-results"]').click();
+
+ cy.get('[data-cy="start-evaluation"]').click();
+ cy.get('[data-cy="evaluation-task"]').should(
+ "contain",
+ "Beurteilungskriterium 1 / 5"
+ );
+
+ // without text input the button should be disabled
+ cy.get('[data-cy="next-step"]').should("be.disabled");
+
+ // with text you can continue
+ cy.get('[data-cy="subtask-4"]').click();
+ cy.get('[data-cy="reason-text"]').type("Gut gemacht!");
+ // wait for debounce
+ 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="subtask-2"]').click();
+ cy.get('[data-cy="reason-text"]').type("Nicht so gut");
+ cy.wait(500);
+
+ // revisit step 1 will show stored data
+ cy.get('[data-cy="previous-step"]').click();
+ cy.get('[data-cy="reason-text"]')
+ .find("textarea")
+ .should("have.value", "Gut gemacht!");
+
+ // even after reload
+ cy.reload();
+ cy.get('[data-cy="reason-text"]')
+ .find("textarea")
+ .should("have.value", "Gut gemacht!");
+
+ // it can access step directly via url
+ cy.url().then((url) => {
+ const step2Url = url.replace("step=1", "step=2");
+ console.log(step2Url);
+ cy.visit(step2Url);
+ });
+
+ cy.get('[data-cy="reason-text"]')
+ .find("textarea")
+ .should("have.value", "Nicht so gut");
+
+ // load AssignmentCompletion from DB and check
+ cy.loadAssignmentCompletion(
+ "assignment_user_id",
+ TEST_STUDENT1_USER_ID
+ ).then((ac) => {
+ expect(ac.completion_status).to.equal("evaluation_in_progress");
+ expect(JSON.stringify(ac.completion_data)).to.include("Nicht so gut");
+ expect(Cypress._.values(ac.completion_data)).to.deep.include({
+ expert_data: { points: 2, text: "Nicht so gut" },
+ });
+ expect(Cypress._.values(ac.completion_data)).to.deep.include({
+ expert_data: { points: 4, text: "Gut gemacht!" },
+ });
+ });
+ });
+});
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 988073ee..d884523d 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -52,55 +52,77 @@
const _ = Cypress._;
-Cypress.Commands.add('manageCommand', (command, preCommand = '') => {
+Cypress.Commands.add("manageCommand", (command, preCommand = "") => {
const execCommand = `${preCommand} python server/manage.py ${command} --settings=config.settings.test_cypress`;
console.log(execCommand);
- return cy.exec(execCommand, { failOnNonZeroExit: false }).then(result => {
- if(result.code) {
- throw new Error(`Execution of "${command}" failed
+ return cy
+ .exec(
+ // hack to add my asdf python instance to the path
+ // so I can run the test directly from within IntelliJ
+ `PATH=/Users/daniel/workspace/vbv_lernwelt/.direnv/python-3.10.6/bin:$PATH && ${execCommand}`,
+ {
+ failOnNonZeroExit: false,
+ }
+ )
+ .then((result) => {
+ if (result.code) {
+ throw new Error(`Execution of "${command}" failed
Exit code: ${result.code}
Stdout:\n${result.stdout}
Stderr:\n${result.stderr}`);
- }
- });
+ }
+ });
});
-Cypress.Commands.add('manageShellCommand', (command) => {
+Cypress.Commands.add("manageShellCommand", (command) => {
return cy.manageCommand(`shell -c '${command}'`);
});
function loadObjectJson(key, value, djangoModelPath, serializerModelPath) {
- const djangoModel = _.last(djangoModelPath.split('.'));
- const djangoModelImportPath = _.initial(djangoModelPath.split('.')).join('.');
- const serializerModel = _.last(serializerModelPath.split('.'));
- const serializerModelImportPath = _.initial(serializerModelPath.split('.')).join('.');
+ const djangoModel = _.last(djangoModelPath.split("."));
+ const djangoModelImportPath = _.initial(djangoModelPath.split(".")).join(".");
+ const serializerModel = _.last(serializerModelPath.split("."));
+ const serializerModelImportPath = _.initial(
+ serializerModelPath.split(".")
+ ).join(".");
let filterPart = `${key}=${value}`;
- if(_.isArray(key)) {
- filterPart = _.zip(key, value).map(([k, v]) => {
- return `${k}=${v}`;
- }).join(',');
+ if (_.isArray(key)) {
+ filterPart = _.zip(key, value)
+ .map(([k, v]) => {
+ return `${k}=${v}`;
+ })
+ .join(",");
}
const command = `from ${djangoModelImportPath} import ${djangoModel};
- from ${serializerModelImportPath} import ${serializerModel};
- from myservice.apps.core.serializers import create_json_from_objects;
- object = ${djangoModel}.objects.filter(${filterPart}).first();
- print(create_json_from_objects(object, ${serializerModel}, many=False));
- exit()`.replace(/(?:\r\n|\r|\n)/g, '');
- return cy.manageShellCommand(command).then(result => {
+ from ${serializerModelImportPath} import ${serializerModel};
+ from vbv_lernwelt.core.serializers import create_json_from_objects;
+ object = ${djangoModel}.objects.filter(${filterPart}).first();
+ print(create_json_from_objects(object, ${serializerModel}, many=False));
+ exit();
+ `.replace(/(?:\r\n|\r|\n)/g, "");
+ return cy.manageShellCommand(command).then((result) => {
const objectJson = JSON.parse(result.stdout);
console.log(objectJson);
return objectJson;
});
-
}
-Cypress.Commands.add('makeSelfEvaluation', (answers) => {
+Cypress.Commands.add("loadAssignmentCompletion", (key, value) => {
+ return loadObjectJson(
+ key,
+ value,
+ "vbv_lernwelt.assignment.models.AssignmentCompletion",
+ "vbv_lernwelt.assignment.serializers.AssignmentCompletionSerializer"
+ );
+});
+
+Cypress.Commands.add("makeSelfEvaluation", (answers) => {
for (let i = 0; i < answers.length; i++) {
const answer = answers[i];
if (answer) {
- cy.get('[data-cy="success"]').click();
+ cy.get('[data-cy="success"]').click();
} else {
cy.get('[data-cy="fail"]').click();
}
@@ -112,7 +134,6 @@ Cypress.Commands.add('makeSelfEvaluation', (answers) => {
}
});
-
// Cypress.Commands.add('loadApiClientRequestResponseLog', (key, value) => {
// return loadObjectJson(
// key,
diff --git a/server/config/urls.py b/server/config/urls.py
index 3ea80d15..a6f9b74e 100644
--- a/server/config/urls.py
+++ b/server/config/urls.py
@@ -4,7 +4,8 @@ from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth.decorators import user_passes_test
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
-from django.urls import include, path, re_path
+from django.urls import include, path, re_path, register_converter
+from django.urls.converters import IntConverter
from django.views import defaults as default_views
from grapple import urls as grapple_urls
from ratelimit.exceptions import Ratelimited
@@ -50,6 +51,20 @@ from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls
+class SignedIntConverter(IntConverter):
+ regex = r"-?\d+"
+
+ def to_python(self, value):
+ return int(value)
+
+ def to_url(self, value):
+ return str(value)
+
+
+# Register the converter
+register_converter(SignedIntConverter, "signed_int")
+
+
def raise_example_error(request):
"""
raise error to check if it gets logged
@@ -88,15 +103,17 @@ urlpatterns = [
# course
path(r"api/course/sessions/", get_course_sessions, name="get_course_sessions"),
- path(r"api/course/sessions//users/", get_course_session_users,
+ path(r"api/course/sessions//users/",
+ get_course_session_users,
name="get_course_session_users"),
path(r"api/course/page//", course_page_api_view,
name="course_page_api_view"),
path(r"api/course/completion/mark/", mark_course_completion_view,
name="mark_course_completion"),
- path(r"api/course/completion//", request_course_completion,
+ path(r"api/course/completion//",
+ request_course_completion,
name="request_course_completion"),
- path(r"api/course/completion///",
+ path(r"api/course/completion///",
request_course_completion_for_user,
name="request_course_completion_for_user"),
@@ -105,15 +122,17 @@ urlpatterns = [
name="upsert_user_assignment_completion"),
path(r"api/assignment/evaluate/", evaluate_assignment_completion,
name="evaluate_assignment_completion"),
- path(r"api/assignment///",
+ path(r"api/assignment///",
request_assignment_completion,
name="request_assignment_completion"),
- path(r"api/assignment///status/",
- request_assignment_completion_status,
- name="request_assignment_completion_status"),
- path(r"api/assignment////",
- request_assignment_completion_for_user,
- name="request_assignment_completion_for_user"),
+ path(
+ r"api/assignment///status/",
+ request_assignment_completion_status,
+ name="request_assignment_completion_status"),
+ path(
+ r"api/assignment////",
+ request_assignment_completion_for_user,
+ name="request_assignment_completion_for_user"),
# documents
path(r'api/core/document/start/', document_upload_start,
diff --git a/server/vbv_lernwelt/core/constants.py b/server/vbv_lernwelt/core/constants.py
index dfd65020..ff1cfc9f 100644
--- a/server/vbv_lernwelt/core/constants.py
+++ b/server/vbv_lernwelt/core/constants.py
@@ -3,3 +3,12 @@ DEFAULT_RICH_TEXT_FEATURES = [
"bold",
"italic",
]
+
+# ids for cypress test data
+ADMIN_USER_ID = -1
+TEST_TRAINER1_USER_ID = -11
+TEST_STUDENT1_USER_ID = -21
+TEST_STUDENT2_USER_ID = -22
+
+TEST_COURSE_SESSION_BERN_ID = -1
+TEST_COURSE_SESSION_ZURICH_ID = -2
diff --git a/server/vbv_lernwelt/core/create_default_users.py b/server/vbv_lernwelt/core/create_default_users.py
index 172b269e..21d6ea5b 100644
--- a/server/vbv_lernwelt/core/create_default_users.py
+++ b/server/vbv_lernwelt/core/create_default_users.py
@@ -1,6 +1,12 @@
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import Group
+from vbv_lernwelt.core.constants import (
+ ADMIN_USER_ID,
+ TEST_STUDENT1_USER_ID,
+ TEST_STUDENT2_USER_ID,
+ TEST_TRAINER1_USER_ID,
+)
from vbv_lernwelt.core.models import User
default_users = [
@@ -70,12 +76,14 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
avatar_url="",
password=default_password,
language="de",
+ id=None,
):
student_user, created = _get_or_create_user(
user_model=user_model,
username=email,
password=password,
language=language,
+ id=id,
)
student_user.first_name = first_name
student_user.last_name = last_name
@@ -83,9 +91,9 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
student_user.groups.add(student_group)
student_user.save()
- def _create_admin_user(email, first_name, last_name, avatar_url=""):
+ def _create_admin_user(email, first_name, last_name, avatar_url="", id=None):
admin_user, created = _get_or_create_user(
- user_model=user_model, username=email, password=default_password
+ user_model=user_model, username=email, password=default_password, id=id
)
admin_user.is_superuser = True
admin_user.is_staff = True
@@ -107,6 +115,7 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
first_name="Peter",
last_name="Adminson",
avatar_url="/static/avatars/avatar_iterativ.png",
+ id=ADMIN_USER_ID,
)
for user_data in default_users:
@@ -232,19 +241,25 @@ def create_default_users(user_model=User, group_model=Group, default_password=No
# users for cypress tests
_create_student_user(
+ id=TEST_TRAINER1_USER_ID,
email="test-trainer1@example.com",
first_name="Test",
last_name="Trainer1",
+ avatar_url="/static/avatars/uk1.patrizia.huggel.jpg",
)
_create_student_user(
+ id=TEST_STUDENT1_USER_ID,
email="test-student1@example.com",
first_name="Test",
last_name="Student1",
+ avatar_url="/static/avatars/uk1.michael.meier.jpg",
)
_create_student_user(
+ id=TEST_STUDENT2_USER_ID,
email="test-student2@example.com",
first_name="Test",
last_name="Student2",
+ avatar_url="/static/avatars/uk1.lina.egger.jpg",
)
@@ -252,6 +267,7 @@ def _get_or_create_user(user_model, *args, **kwargs):
username = kwargs.get("username", None)
password = kwargs.get("password", None)
language = kwargs.get("language", "de")
+ id = kwargs.get("id", None)
created = False
user = user_model.objects.filter(username=username).first()
@@ -262,6 +278,7 @@ def _get_or_create_user(user_model, *args, **kwargs):
password=make_password(password),
email=username,
language=language,
+ id=id,
)
created = True
return user, created
diff --git a/server/vbv_lernwelt/core/management/commands/cypress_reset.py b/server/vbv_lernwelt/core/management/commands/cypress_reset.py
index 844beae7..59cf44c6 100644
--- a/server/vbv_lernwelt/core/management/commands/cypress_reset.py
+++ b/server/vbv_lernwelt/core/management/commands/cypress_reset.py
@@ -1,15 +1,37 @@
import djclick as click
-from vbv_lernwelt.assignment.models import AssignmentCompletion
+from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion
+from vbv_lernwelt.core.constants import (
+ TEST_COURSE_SESSION_BERN_ID,
+ TEST_STUDENT1_USER_ID,
+)
from vbv_lernwelt.core.models import User
-from vbv_lernwelt.course.models import CourseCompletion
+from vbv_lernwelt.course.creators.test_course import (
+ create_test_assignment_submitted_data,
+)
+from vbv_lernwelt.course.models import CourseCompletion, CourseSession
from vbv_lernwelt.notify.models import Notification
@click.command()
-def command():
+@click.option(
+ "--create-completion/--no-create-completion",
+ default=False,
+ help="will create completion data for some users",
+)
+def command(create_completion):
print("cypress reset data")
CourseCompletion.objects.all().delete()
Notification.objects.all().delete()
AssignmentCompletion.objects.all().delete()
User.objects.all().update(language="de")
+
+ if create_completion:
+ print("create completion data for test course")
+ create_test_assignment_submitted_data(
+ assignment=Assignment.objects.get(
+ slug="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice"
+ ),
+ course_session=CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID),
+ user=User.objects.get(id=TEST_STUDENT1_USER_ID),
+ )
diff --git a/server/vbv_lernwelt/core/serializers.py b/server/vbv_lernwelt/core/serializers.py
index a927d748..f801321c 100644
--- a/server/vbv_lernwelt/core/serializers.py
+++ b/server/vbv_lernwelt/core/serializers.py
@@ -1,9 +1,15 @@
from rest_framework import serializers
+from rest_framework.renderers import JSONRenderer
from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseSessionUser
+def create_json_from_objects(objects, serializer_class, many=True) -> str:
+ serializer = serializer_class(objects, many=many)
+ return JSONRenderer().render(serializer.data).decode("utf-8")
+
+
class UserSerializer(serializers.ModelSerializer):
course_session_experts = serializers.SerializerMethodField()
diff --git a/server/vbv_lernwelt/course/creators/test_course.py b/server/vbv_lernwelt/course/creators/test_course.py
index 380f0ee2..91e12f72 100644
--- a/server/vbv_lernwelt/course/creators/test_course.py
+++ b/server/vbv_lernwelt/course/creators/test_course.py
@@ -7,12 +7,17 @@ from wagtail.models import Site
from vbv_lernwelt.assignment.creators.create_assignments import create_test_assignment
from vbv_lernwelt.assignment.models import Assignment
+from vbv_lernwelt.assignment.services import update_assignment_completion
from vbv_lernwelt.competence.factories import (
CompetencePageFactory,
CompetenceProfilePageFactory,
PerformanceCriteriaFactory,
)
from vbv_lernwelt.competence.models import CompetencePage
+from vbv_lernwelt.core.constants import (
+ TEST_COURSE_SESSION_BERN_ID,
+ TEST_COURSE_SESSION_ZURICH_ID,
+)
from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.tests.helpers import create_locales_for_wagtail
from vbv_lernwelt.course.consts import COURSE_TEST_ID
@@ -65,11 +70,13 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
# course sessions
cs_bern = CourseSession.objects.create(
course_id=COURSE_TEST_ID,
- title="Bern 2022 a",
+ title="Test Bern 2022 a",
+ id=TEST_COURSE_SESSION_BERN_ID,
)
cs_zurich = CourseSession.objects.create(
course_id=COURSE_TEST_ID,
- title="Zürich 2022 a",
+ title="Test Zürich 2022 a",
+ id=TEST_COURSE_SESSION_ZURICH_ID,
)
trainer1 = User.objects.get(email="test-trainer1@example.com")
@@ -100,6 +107,30 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False):
return course
+def create_test_assignment_submitted_data(assignment, course_session, user):
+ if assignment and course_session and user:
+ subtasks = assignment.filter_user_subtasks(subtask_types=["user_text_input"])
+ for index, subtask in enumerate(subtasks):
+ user_text = f"Lorem ipsum dolor sit amet... {index}"
+
+ update_assignment_completion(
+ assignment_user=user,
+ assignment=assignment,
+ course_session=course_session,
+ completion_data={
+ subtask["id"]: {
+ "user_data": {"text": user_text},
+ }
+ },
+ )
+ update_assignment_completion(
+ assignment_user=user,
+ assignment=assignment,
+ course_session=course_session,
+ completion_status="submitted",
+ )
+
+
def create_test_course_with_categories(apps=None, schema_editor=None):
if apps is not None:
Course = apps.get_model("course", "Course")