Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
d31029b
Scenario: Create coursework assignment with double marking
opitz Nov 19, 2025
3327655
Scenario: Add markers
opitz Nov 19, 2025
3974e1e
Scenario: Allocate markers
opitz Nov 20, 2025
6a82714
Scenario: Check anonymity
opitz Nov 20, 2025
ffdf716
cleanup
opitz Nov 20, 2025
0dd0220
using new behat step 'And there is a double-blind marking coursework'
opitz Nov 20, 2025
c11656d
Scenario: Add extension to a student
opitz Nov 20, 2025
e7897f4
Scenario: Student can submit a PDF file
opitz Nov 21, 2025
6a1c2b2
No 'formative or summative' setting
opitz Nov 23, 2025
148714e
addressed GHA issues
opitz Nov 23, 2025
0782934
added Behat steps to create a submission and/or finalise it for a giv…
opitz Nov 24, 2025
288c92f
Scenario: Manager can submit on behalf of students.
opitz Nov 25, 2025
c66f97f
added behat step i_assign_user_as_role_for_student_in_coursework()
opitz Nov 26, 2025
d08a62f
added Behat step i_click_add_mark_for_marker_in_row()
opitz Nov 26, 2025
0247374
added test PDF files, addressed Code Checker issues
opitz Nov 26, 2025
b03b3a4
Scenario: Mark the assignments
opitz Nov 26, 2025
fab04ba
addressed more Code Checker issues
opitz Nov 26, 2025
cf63821
defining courseworkmarker role capabilities
opitz Dec 8, 2025
bd33232
'percentage distance' => 'Percentage distance'
opitz Dec 9, 2025
4d14921
defining correct roles for courseworkexamoffice, courseworkmarker and…
opitz Dec 10, 2025
a2a022c
Scenario: Moderate the assessment
opitz Dec 11, 2025
91e9eb6
Scenario: Check moderation
opitz Dec 12, 2025
3174877
Scenario: Release the grades
opitz Dec 12, 2025
06a00b9
Scenario: Student 1 sees the released grades
opitz Dec 12, 2025
524d41b
Scenario: Student 2 sees disagreed released grades
opitz Dec 12, 2025
a1e2a68
Scenario: Student 3 does not see unmoderated released grades
opitz Dec 12, 2025
2a99682
Scenario: Check moderation form
opitz Dec 12, 2025
38b634b
removed test case where student should not see unmoderated feedback
opitz Dec 15, 2025
6a66d64
addressed code review issues by copilot
opitz Dec 15, 2025
dfb23f8
addressed code review issues by copilot2
opitz Dec 15, 2025
49d70ad
refactored assigning role capabilities in Behat tests
opitz Dec 17, 2025
ffc0ad8
removed behat generator file for now, addressed copilot issues
opitz Dec 17, 2025
b1ab327
addressed (some) code checker issues
opitz Dec 18, 2025
25d3aaa
refactored behat steps to use $this->coursework
opitz Jan 7, 2026
eed0f21
using dynamic dates for deadline
opitz Jan 8, 2026
e0e5343
updated full name format error message
opitz Jan 8, 2026
54ded6b
using ternary operator to determine extended deadline
opitz Jan 8, 2026
8595632
Refactored Exception to coding_exception
opitz Jan 8, 2026
4acd88d
refactored there_is_a_double_blind_marking_coursework() and there_is_…
opitz Jan 8, 2026
a681920
refactored i_assign_user_as_role_for_student_in_coursework() into i_a…
opitz Jan 8, 2026
e0e2e62
removed i_click_add_mark_for_marker_in_row()
opitz Jan 8, 2026
c25a0ef
added student_has_a_finalised_submission()
opitz Jan 8, 2026
f0825cb
refactored i_follow_in_row()
opitz Jan 8, 2026
f2c1d5e
refactored i_should_see_text_in_row()
opitz Jan 8, 2026
ce761c5
Refactored role 'courseworkmarker' to 'courseworkdbm'
opitz Jan 8, 2026
3d3b579
using core_role_set_assign_allowed()
opitz Jan 8, 2026
a073372
removed i_should_see_mark_in_row()
opitz Jan 8, 2026
40e3f11
using named_student_has_a_submission()
opitz Jan 8, 2026
456b451
removed should_see_submit_button()
opitz Jan 8, 2026
59691be
removed Scenario: Add markers
opitz Jan 8, 2026
2f058c5
removed unused user teacher1
opitz Jan 8, 2026
cdfad18
removed custom field
opitz Jan 8, 2026
59b589c
removed unneccessary logins,
opitz Jan 8, 2026
9f5fb79
added $coursework->assessorallocationstrategy = "none"
opitz Jan 9, 2026
8a10025
revoking refactoring i_should_see_text_in_row() for now
opitz Jan 9, 2026
ca332dc
check moderator can edit before marks have been released
opitz Jan 9, 2026
133eb65
using ExpectationException
opitz Jan 12, 2026
e59de35
addressed copilot code review issues
opitz Jan 12, 2026
6c8d0c6
addressed copilot code review issues 2
opitz Jan 12, 2026
4c33925
reintroduced checking int for datestring to avoid error
opitz Jan 12, 2026
97c69a7
addressed copilot code review issues 3
opitz Jan 13, 2026
42d2c24
refactored Scenario: Check moderation
opitz Jan 16, 2026
137a15d
addressed copilot code review issues 4
opitz Jan 16, 2026
af894d8
addressed copilot code review issues 5
opitz Jan 19, 2026
7eea472
addressed copilot code review issues 6
opitz Jan 19, 2026
d3c874f
addressed copilot code review issues 7
opitz Jan 19, 2026
cdcff4d
refactored into two files: double_marking_blind.feature and moderatio…
opitz Jan 20, 2026
b4137bc
updated message
opitz Jan 20, 2026
e41fff9
removed allow_role_to_assign_role() as no longer needed
opitz Jan 20, 2026
a8ab47c
replaced i_should_see_text_in_row() with core step and removed it
opitz Jan 20, 2026
0610dbe
replaced i_follow_in_row() with core step and removed it
opitz Jan 20, 2026
eab83a0
addressed copilot code review issues 8
opitz Jan 21, 2026
3fd615e
using unique feature
opitz Jan 22, 2026
b4958ee
removed trailing spaces
opitz Jan 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 322 additions & 6 deletions tests/behat/behat_mod_coursework.php
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,13 @@ public function the_coursework_start_date_is_disabled() {
$this->coursework->update_attribute('startdate', 0);
}

/**
* @Given /^the coursework start date is now$/
*/
public function the_coursework_start_date_is_now() {
$this->coursework->update_attribute('startdate', time());
}

/**
* @Given /^the coursework start date is in the future$/
*/
Expand Down Expand Up @@ -2651,15 +2658,28 @@ public function i_have_a_submission() {

/**
* Named student has a submission.
* @Given /^the student called "([\w]+)" has a( finalised)? submission*$/
* @Given /^the student called "(?P<name>(?:[^"]|\\")*)" has a( finalised)? submission*$/
*/
public function named_student_has_a_submission(string $firstname, bool $finalised = false) {
public function named_student_has_a_submission(string $fullname, bool $finalised = false) {
global $DB;
$generator = testing_util::get_data_generator()->get_plugin_generator('mod_coursework');
$userid = $DB->get_field_sql(
"SELECT id FROM {user} WHERE firstname = ? AND lastname LIKE 'student%'",
[$firstname]
);

// Check if the name consists of first- and last name.
$nameparts = explode(' ', $fullname, 2);
$firstname = $nameparts[0];
$lastname = $nameparts[1] ?? '';

if (empty($lastname)) {
$userid = $DB->get_field_sql(
"SELECT id FROM {user} WHERE firstname = ? AND lastname LIKE 'student%'",
[$firstname]
);
} else {
$userid = $DB->get_field_sql(
"SELECT id FROM {user} WHERE firstname = ? AND lastname = ?",
[$firstname, $lastname]
);
Comment on lines +2701 to +2704
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe:

            $userid = $DB->get_field('user', 'id', ['firstname' => $firsname, 'lastname' => $lastname]);

}
if ($userid) {
$submission = new stdClass();
$submission->allocatableid = $userid;
Expand Down Expand Up @@ -3360,6 +3380,25 @@ public function i_should_see_submitted_date($date) {
}
}

/**
* For matching a late submitted date ignoring the time part
*
* Example: I should see late submitted date 4 July 2025
*
* @Given /^I should see late submitted date "(?P<date>(?:[^"]|\\")*)"$/
*/
public function i_should_see_late_submitted_date($date) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just use a normal I should see step instead - add a data attribute if needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually a clone of your function i_should_see_submitted_date() but with the added string component "Late" to make the match happen. So what is wrong with my approach that is right with yours?

$page = $this->getsession()->getpage();
$match = $page->find('xpath', "//li[starts-with(normalize-space(string()), 'Submitted Late $date')]");

if (!$match) {
throw new ExpectationException(
"Should have seen expected submitted late date $date, but it was not there",
$this->getsession()
);
}
}

/**
* @Given /^sample marking includes student for stage (\d)$/
*/
Expand Down Expand Up @@ -3621,4 +3660,281 @@ public function i_set_the_field_to_replacing_line_breaks($field, $value) {
$value = str_replace('\n', chr(10), $value);
$this->execute([behat_forms::class, 'i_set_the_field_to'], [$field, $value]);
}

/**
* Sets an extension deadline for a student in a coursework.
*
* Example: And the coursework extension for "Student 1" in "Coursework 1" is "1 January 2027 08:00"
* Example: And the coursework extension for "Student 1" in "Coursework 1" is "## + 1 month ##"
*
* @Given /^the coursework extension for "(?P<fullname_string>(?:[^"]|\\")*)" in "(?P<cwname>(?:[^"]|\\")*)" is "(?P<datestr>(?:[^"]|\\")*)"$/
*/
public function set_extension_for_user($fullname, $cwname, $datestr) {
global $DB;

// Check date string.
if (is_int($datestr) || (is_string($datestr) && ctype_digit($datestr))) {
$extendeddeadline = (int)$datestr;
} else {
$extendeddeadline = strtotime($datestr);
if ($extendeddeadline === false) {
throw new \InvalidArgumentException('Invalid user extension date string: ' . $datestr);
}
}

// Find the coursework by name.
$cw = $DB->get_record('coursework', ['name' => $cwname], '*', MUST_EXIST);

$user = $this->get_user_from_fullname($fullname);

// See if an extension already exists.
$existing = $DB->get_record('coursework_extensions', [
'courseworkid' => $cw->id,
'allocatableid' => $user->id,
'allocatabletype' => 'user',
]);

$record = new stdClass();
$record->courseworkid = $cw->id;
$record->allocatableid = $user->id;
$record->allocatabletype = 'user';
$record->extended_deadline = $extendeddeadline;
$record->createdbyid = 2; // Admin ID.

if ($existing) {
$record->id = $existing->id;
$DB->update_record('coursework_extensions', $record);
} else {
$DB->insert_record('coursework_extensions', $record);
}
}

/**
* @Given /^the following markers are allocated:$/
*
* @param TableNode $allocations Students and their markers.
*/
public function the_following_markers_are_allocated(TableNode $allocations) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my rebuild of the allocation table - with the refactoring done there this is now two steps of standard behat.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I can follow...
Do you have a code example, please?

global $DB;

$datahash = $allocations->getHash();

foreach ($datahash as $allocate) {
$stages = ['assessor_1'];
if (isset($allocate['moderator'])) {
$stages[] = 'moderator';
} else if (isset($allocate['assessor_2'])) {
$stages[] = 'assessor_2';
}

$student = $this->get_user_from_fullname($allocate['student']);
foreach ($stages as $stage) {
$marker = $this->get_user_from_fullname($allocate[$stage]);

$record = $DB->get_record('coursework_allocation_pairs', [
'courseworkid' => $this->coursework->id,
'allocatableid' => $student->id,
'allocatableuser' => $student->id,
'stageidentifier' => $stage,
'allocatabletype' => 'user',
]);
if ($record) {
$record->assessorid = $marker->id;
$record->ismanual = 1;
$DB->update_record('coursework_allocation_pairs', $record);
} else {
$record = new stdClass();
$record->courseworkid = $this->coursework->id;
$record->allocatableid = $student->id;
$record->allocatableuser = $student->id;
$record->assessorid = $marker->id;
$record->stageidentifier = $stage;
$record->allocatabletype = 'user';
$record->ismanual = 1;
$DB->insert_record('coursework_allocation_pairs', $record);
}
}
}
allocation::remove_cache($this->coursework->id);
}

/**
* Return a user record from a given full name ("firstname lastname")
*
* @param string $fullname
* @return stdClass The user record
* @throws coding_exception When the full name is invalid or the user cannot be found
*/
private function get_user_from_fullname(string $fullname) {
global $DB;

// Check if the name consists of first- and last name.
$nameparts = explode(' ', $fullname, 2);
$firstname = $nameparts[0];
$lastname = $nameparts[1] ?? '';

if (empty($lastname)) {
throw new coding_exception("Full name '{$fullname}' must contain at least one space between first and last name.");
}

// Find user by full name (firstname + lastname).
$user = $DB->get_record('user', [
'firstname' => $firstname,
'lastname' => $lastname,
]);

if (!$user) {
throw new coding_exception("Could not find user with name '{$fullname}'.");
}
return $user;
}

/**
* Inserts a grade directly into coursework_feedbacks table.
*
* @When /^the submission from "(?P<studentfullname>[^"]*)" is marked by "(?P<markerfullname>[^"]*)" with:$/
*/
public function mark_coursework_submission_directly(
string $studentfullname,
string $markerfullname,
TableNode $table
) {
global $DB;

$student = $this->get_user_from_fullname($studentfullname);
$marker = $this->get_user_from_fullname($markerfullname);

// Resolve submission for this student.
$submission = $DB->get_record('coursework_submissions', [
'courseworkid' => $this->coursework->id,
'allocatableid' => $student->id,
]);
if (!$submission) {
throw new ExpectationException("Submission for '$studentfullname' not found", $this->getSession());
}

// Resolve marker allocation.
$allocation = $DB->get_record('coursework_allocation_pairs', [
'courseworkid' => $this->coursework->id,
'assessorid' => $marker->id,
'allocatableid' => $student->id,
]);
if (!$allocation) {
throw new ExpectationException("Marker '$markerfullname' for '$studentfullname' not found", $this->getSession());
}

// Extract the provided table values.
$data = $table->getRowsHash();

$mark = isset($data['Mark']) ? floatval($data['Mark']) : null;
$comment = $data['Comment'] ?? '';
$finalised = $data['Finalised'] ?? '';

if ($mark === null) {
throw new ExpectationException("Missing 'Mark' value in table", $this->getSession());
}

// Check if there is already a feedback record.
$existing = $DB->get_record('coursework_feedbacks', [
'submissionid' => $submission->id,
'assessorid' => $marker->id,
'stageidentifier' => $allocation->stageidentifier,
]);

// Insert/update feedback record.
$feedback = new stdClass();
$feedback->submissionid = $submission->id;
$feedback->assessorid = $marker->id;
$feedback->stageidentifier = $allocation->stageidentifier;
$feedback->grade = $mark;
$feedback->feedbackcomment = $comment;
$feedback->lasteditedbyuser = $marker->id;
$feedback->finalised = $finalised;
$feedback->timecreated = time();
$feedback->timemodified = time();

if ($existing) {
$feedback->id = $existing->id;
$DB->update_record('coursework_feedbacks', $feedback);
} else {
$DB->insert_record('coursework_feedbacks', $feedback);
}
}

/**
* Inserts a moderation directly into coursework_mod_agreements table.
*
* @When /^the submission from "(?P<studentfullname>[^"]*)" is moderated by "(?P<moderatorfullname>[^"]*)" with:$/
*/
public function moderate_submission_directly(
string $studentfullname,
string $moderatorfullname,
TableNode $table
) {
global $DB;

$student = $this->get_user_from_fullname($studentfullname);
$moderator = $this->get_user_from_fullname($moderatorfullname);

// Resolve submission for this student.
$submission = $DB->get_record('coursework_submissions', [
'courseworkid' => $this->coursework->id,
'allocatableid' => $student->id,
]);
if (!$submission) {
throw new ExpectationException("Submission for '$studentfullname' not found", $this->getSession());
}

// Resolve moderator allocation.
$allocation = $DB->get_record('coursework_allocation_pairs', [
'courseworkid' => $this->coursework->id,
'assessorid' => $moderator->id,
'allocatableid' => $student->id,
'stageidentifier' => 'moderator',
]);
if (!$allocation) {
throw new ExpectationException("Moderator '$moderatorfullname' for '$studentfullname' not found", $this->getSession());
}

// Resolve feedback.
$params = ['submissionid' => $submission->id];
$sql = "SELECT *
FROM {coursework_feedbacks}
WHERE submissionid = :submissionid
AND stageidentifier LIKE 'assessor_%'";
$feedback = $DB->get_record_sql($sql, $params);

if (!$feedback) {
throw new ExpectationException("Feedback for '$studentfullname' not found", $this->getSession());
}

// Extract the provided table values.
$data = $table->getRowsHash();
$agreementtext = $data['Agreement'] ?? '';
$comment = $data['Comment'] ?? '';

// Check if there is already an agreement record.
$existing = $DB->get_record('coursework_mod_agreements', [
'feedbackid' => $feedback->id,
'moderatorid' => $moderator->id,
]);

// Insert/update agreement record.
$agreement = new stdClass();
$agreement->feedbackid = $feedback->id;
$agreement->moderatorid = $moderator->id;
$agreement->agreement = $agreementtext;
$agreement->modcomment = $comment;
$agreement->modcommentformat = FORMAT_HTML;
$agreement->lasteditedby = $moderator->id;
$agreement->timecreated = time();
$agreement->timemodified = time();

if ($existing) {
$agreement->id = $existing->id;
$DB->update_record('coursework_mod_agreements', $agreement);
} else {
$DB->insert_record('coursework_mod_agreements', $agreement);
}
}
}
Loading