Author SHA1 Message Date
thatguygriff c73b10d779 Merge pull request 'Bump version to 1.5.1' (#169) from release/bump-1.5.1 into main
CI / Tests (PHP 8.1) (push) Successful in 47s
CI / Tests (PHP 8.2) (push) Successful in 55s
CI / No Debug Code (push) Successful in 2s
CI / PHPStan (push) Successful in 2m52s
CI / Coding Standards (push) Successful in 2m57s
CI / Tests (PHP 8.3) (push) Successful in 2m47s
CI / Build Plugin Zip (push) Successful in 2m51s
Reviewed-on: #169
2026-07-30 19:48:12 +00:00
Release Bot 7ea8d653ee Bump version to 1.5.1 and open changelog section 2026-07-30 19:46:05 +00:00
thatguygriff 1e4e21e8d3 Merge pull request 'Show a recurring lesson's policy acceptances and intake answers on every week of it' (#168) from fix/167-series-policy-acceptances into main
CI / Tests (PHP 8.2) (push) Successful in 44s
CI / Tests (PHP 8.1) (push) Successful in 54s
CI / No Debug Code (push) Successful in 1s
CI / PHPStan (push) Successful in 2m54s
CI / Coding Standards (push) Successful in 2m58s
CI / Tests (PHP 8.3) (push) Successful in 2m43s
Release / Build and Publish Release (push) Successful in 2m58s
Release / Open next-version bump PR (push) Successful in 4s
CI / Build Plugin Zip (push) Successful in 2m46s
Reviewed-on: #168
2026-07-30 19:41:03 +00:00
thatguygriffandClaude Opus 5 df3462a8b3 Show a series' policy acceptances on every occurrence
CI / Tests (PHP 8.2) (pull_request) Successful in 44s
CI / Tests (PHP 8.1) (pull_request) Successful in 54s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m54s
CI / Coding Standards (pull_request) Successful in 2m59s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m43s
CI / Build Plugin Zip (pull_request) Skipped
A weekly booking reserves a series of lessons, but the student answers
the intake and ticks the policy boxes once — so BookingEndpoint records
both against the anchor lesson alone. The admin detail view looked them
up by whichever lesson id was being viewed, so every occurrence after
the first showed no answers and no acceptances at all.

LessonDetail now takes the Lesson rather than a bare id and resolves the
registration to `series_id ?? id`, so each occurrence reads the anchor's
records. This is the same seam PaymentService already uses to find a
series lesson's payment on the anchor.

Nothing was ever missing from the database, so existing bookings read
correctly with no migration and no schema change.

Closes #167

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-30 16:35:26 -03:00
thatguygriff 748478f2f1 Merge pull request 'Let the account holder edit their own details on the profile page' (#166) from feature/165-editable-own-profile into main
CI / Tests (PHP 8.1) (push) Successful in 46s
CI / Tests (PHP 8.2) (push) Successful in 55s
CI / No Debug Code (push) Successful in 3s
CI / PHPStan (push) Successful in 2m57s
CI / Coding Standards (push) Successful in 3m3s
CI / Tests (PHP 8.3) (push) Successful in 2m48s
CI / Build Plugin Zip (push) Successful in 2m50s
Reviewed-on: #166
2026-07-30 19:29:21 +00:00
7 changed files with 87 additions and 13 deletions
+5
View File
@@ -11,6 +11,8 @@ When a `v*` tag is pushed, `.gitea/workflows/release.yml` publishes the matching
the plugin to the next patch version and adds a fresh section here for it. Record the plugin to the next patch version and adds a fresh section here for it. Record
each change under the current top section as you work. each change under the current top section as you work.
## [1.5.1]
## [1.5.0] ## [1.5.0]
### Added ### Added
@@ -18,6 +20,9 @@ each change under the current top section as you work.
- **You can now edit your own details on the profile page**, not just your students'. The page is called **Your profile**, and until now the one person on it you could not change was yourself: a mistyped name at signup, or a name that had since changed, meant asking the studio to fix it. **Your details** now sits at the top of the page with your name, your birth year, and whether you take lessons yourself. Your email address is shown but not editable — it is also how you sign in, so changing it stays a studio-side job. - **You can now edit your own details on the profile page**, not just your students'. The page is called **Your profile**, and until now the one person on it you could not change was yourself: a mistyped name at signup, or a name that had since changed, meant asking the studio to fix it. **Your details** now sits at the top of the page with your name, your birth year, and whether you take lessons yourself. Your email address is shown but not editable — it is also how you sign in, so changing it stays a studio-side job.
- **"I take lessons myself" can be corrected after signup.** Signup asks whether you are registering just yourself, only on behalf of students, or both, and the answer decides whether you are offered as a student when booking. Choosing wrongly — or taking up lessons later alongside the children you book for — used to leave you asking the studio to change it. Ticking the box makes you bookable again and asks for your birth year like any other student; unticking it takes you back off the list without discarding the birth year you already gave, so ticking it back on costs you nothing. - **"I take lessons myself" can be corrected after signup.** Signup asks whether you are registering just yourself, only on behalf of students, or both, and the answer decides whether you are offered as a student when booking. Choosing wrongly — or taking up lessons later alongside the children you book for — used to leave you asking the studio to change it. Ticking the box makes you bookable again and asks for your birth year like any other student; unticking it takes you back off the list without discarding the birth year you already gave, so ticking it back on costs you nothing.
### Fixed
- **A recurring lesson now shows the policies the student accepted on every week of it, not just the first.** Booking a weekly lesson reserves a series of them, and the student answers the intake questions and agrees to the studio's policies once, for the whole reservation. Opening any week after the first showed no answers and no policies accepted — as though nothing had been agreed to. Nothing was ever missing: the agreement was recorded against the first lesson of the series and every other week was looking for one of its own. Each week of a series now shows the intake answers and the full acceptance record — policy, version, when it was accepted, and from where — captured when the reservation was booked. Existing bookings read correctly straight away; there is nothing to re-collect from anyone.
## [1.4.1] ## [1.4.1]
### Added ### Added
+1 -1
View File
@@ -160,7 +160,7 @@ instructor may only open their own lessons; the studio **Scheduler** may open an
- Model: `Unsupervised\Schedular\Booking\Lesson` - Model: `Unsupervised\Schedular\Booking\Lesson`
- Registration gate: `Unsupervised\Schedular\Registration\RegistrationGate` — validates and records intake answers + booking-scoped policy acceptances; shared with group enrolment - Registration gate: `Unsupervised\Schedular\Registration\RegistrationGate` — validates and records intake answers + booking-scoped policy acceptances; shared with group enrolment
- Admin controller: `Unsupervised\Schedular\Booking\LessonController` - Admin controller: `Unsupervised\Schedular\Booking\LessonController`
- Admin lesson detail presenter: `Unsupervised\Schedular\Booking\LessonDetail` (per-lesson intake answers + policy acceptances), template `templates/admin/lesson-detail.php` - Admin lesson detail presenter: `Unsupervised\Schedular\Booking\LessonDetail` (per-lesson intake answers + policy acceptances), template `templates/admin/lesson-detail.php`. A weekly series is answered for and agreed to once, against the anchor lesson, so the presenter reads `series_id ?? id` — every occurrence shows the same intake and audit trail, not just the first.
- REST endpoint: `Unsupervised\Schedular\Booking\BookingEndpoint` - REST endpoint: `Unsupervised\Schedular\Booking\BookingEndpoint`
- Frontend: `Unsupervised\Schedular\Booking\BookingPage`, `Unsupervised\Schedular\Auth\LoginPage` - Frontend: `Unsupervised\Schedular\Booking\BookingPage`, `Unsupervised\Schedular\Auth\LoginPage`
- Upcoming-lessons panel: rendered client-side into `#us-my-lessons` by `assets/js/booking.js` (`lessonRowHtml`/`renderMyLessons`), mirrored for the editor by `BlockPreview::upcomingLessons()` — keep the two markup shapes in step. - Upcoming-lessons panel: rendered client-side into `#us-my-lessons` by `assets/js/booking.js` (`lessonRowHtml`/`renderMyLessons`), mirrored for the editor by `BlockPreview::upcomingLessons()` — keep the two markup shapes in step.
+2 -2
View File
@@ -75,8 +75,8 @@ class LessonController {
$accepts = []; $accepts = [];
} else { } else {
$row = $this->row( $lesson ); $row = $this->row( $lesson );
$answers = $this->detail->answers( $lessonId ); $answers = $this->detail->answers( $lesson );
$accepts = $this->detail->acceptances( $lessonId ); $accepts = $this->detail->acceptances( $lesson );
} }
include USC_PLUGIN_DIR . 'templates/admin/lesson-detail.php'; include USC_PLUGIN_DIR . 'templates/admin/lesson-detail.php';
+18 -4
View File
@@ -17,6 +17,12 @@ use Unsupervised\Schedular\Registration\QuestionRepository;
* *
* Scoped to a single lesson (the `lesson` registration type), mirroring the * Scoped to a single lesson (the `lesson` registration type), mirroring the
* per-student history in {@see \Unsupervised\Schedular\Auth\StudentHistory}. * per-student history in {@see \Unsupervised\Schedular\Auth\StudentHistory}.
*
* A weekly reservation is answered for and agreed to once, so its answers and
* acceptances hang off the series anchor. Every occurrence therefore reads its
* series' registration, not its own id — otherwise only the first lesson of a
* series showed the intake and the audit trail, and the rest looked as though
* nothing had been accepted.
*/ */
class LessonDetail { class LessonDetail {
@@ -33,7 +39,7 @@ class LessonDetail {
* *
* @return list<array{question: string, answer: string}> * @return list<array{question: string, answer: string}>
*/ */
public function answers( int $lessonId ): array { public function answers( Lesson $lesson ): array {
return array_map( return array_map(
function ( Answer $answer ): array { function ( Answer $answer ): array {
$question = $this->questions->findById( $answer->questionId ); $question = $this->questions->findById( $answer->questionId );
@@ -44,7 +50,7 @@ class LessonDetail {
'answer' => '' === $value ? '—' : $value, 'answer' => '' === $value ? '—' : $value,
]; ];
}, },
$this->answers->findByRegistration( Answer::REG_LESSON, $lessonId ) $this->answers->findByRegistration( Answer::REG_LESSON, $this->registrationId( $lesson ) )
); );
} }
@@ -54,7 +60,7 @@ class LessonDetail {
* *
* @return list<array{policy: string, version: string, accepted_at: string, ip: string}> * @return list<array{policy: string, version: string, accepted_at: string, ip: string}>
*/ */
public function acceptances( int $lessonId ): array { public function acceptances( Lesson $lesson ): array {
return array_map( return array_map(
function ( PolicyAcceptance $acceptance ): array { function ( PolicyAcceptance $acceptance ): array {
$version = $this->versions->findById( $acceptance->policyVersionId ); $version = $this->versions->findById( $acceptance->policyVersionId );
@@ -67,7 +73,15 @@ class LessonDetail {
'ip' => $acceptance->ipAddress ?? '', 'ip' => $acceptance->ipAddress ?? '',
]; ];
}, },
$this->acceptances->findByRegistration( PolicyAcceptance::REG_LESSON, $lessonId ) $this->acceptances->findByRegistration( PolicyAcceptance::REG_LESSON, $this->registrationId( $lesson ) )
); );
} }
/**
* The lesson id the booking's answers and acceptances were recorded against:
* the series anchor for a weekly reservation, the lesson itself otherwise.
*/
private function registrationId( Lesson $lesson ): int {
return $lesson->seriesId ?? (int) $lesson->id;
}
} }
+4 -2
View File
@@ -233,10 +233,12 @@ class LessonControllerTest extends TestCase
$this->bookings->shouldReceive('findById')->once()->with(1)->andReturn($lesson); $this->bookings->shouldReceive('findById')->once()->with(1)->andReturn($lesson);
$this->availability->shouldReceive('findById')->once()->with(10)->andReturn($slot); $this->availability->shouldReceive('findById')->once()->with(10)->andReturn($slot);
$this->offerings->shouldReceive('findById')->once()->with(8)->andReturn($offering); $this->offerings->shouldReceive('findById')->once()->with(8)->andReturn($offering);
$this->detail->shouldReceive('answers')->once()->with(1)->andReturn([ // The lesson itself is handed over, so the presenter can follow a series
// occurrence back to the anchor its answers and acceptances hang off.
$this->detail->shouldReceive('answers')->once()->with($lesson)->andReturn([
['question' => 'Skill level', 'answer' => 'Beginner'], ['question' => 'Skill level', 'answer' => 'Beginner'],
]); ]);
$this->detail->shouldReceive('acceptances')->once()->with(1)->andReturn([ $this->detail->shouldReceive('acceptances')->once()->with($lesson)->andReturn([
['policy' => 'Cancellation', 'version' => 'v2', 'accepted_at' => '2026-07-01 10:00:00', 'ip' => '1.2.3.4'], ['policy' => 'Cancellation', 'version' => 'v2', 'accepted_at' => '2026-07-01 10:00:00', 'ip' => '1.2.3.4'],
]); ]);
+55 -2
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Unsupervised\Schedular\Tests\Unit\Booking; namespace Unsupervised\Schedular\Tests\Unit\Booking;
use Mockery; use Mockery;
use Unsupervised\Schedular\Booking\Lesson;
use Unsupervised\Schedular\Booking\LessonDetail; use Unsupervised\Schedular\Booking\LessonDetail;
use Unsupervised\Schedular\Policy\AcceptanceRepository; use Unsupervised\Schedular\Policy\AcceptanceRepository;
use Unsupervised\Schedular\Policy\Policy; use Unsupervised\Schedular\Policy\Policy;
@@ -60,7 +61,7 @@ class LessonDetailTest extends TestCase
['question' => 'Skill level', 'answer' => 'Beginner'], ['question' => 'Skill level', 'answer' => 'Beginner'],
['question' => '#9', 'answer' => '—'], ['question' => '#9', 'answer' => '—'],
], ],
$this->detail->answers(7) $this->detail->answers($this->lesson(7))
); );
} }
@@ -89,7 +90,59 @@ class LessonDetailTest extends TestCase
'ip' => '1.2.3.4', 'ip' => '1.2.3.4',
], ],
], ],
$this->detail->acceptances(7) $this->detail->acceptances($this->lesson(7))
);
}
public function testSeriesOccurrenceReadsTheAnchorsAnswersAndAcceptances(): void
{
// Occurrence #12 of a weekly reservation anchored on lesson 7: the intake
// and the agreement were recorded once, against the anchor.
$occurrence = $this->lesson(12, seriesId: 7);
$this->answers->shouldReceive('findByRegistration')->once()->with(Answer::REG_LESSON, 7)->andReturn([
new Answer(questionId: 2, registrationType: Answer::REG_LESSON, registrationId: 7, studentId: 5, answerValue: 'Beginner'),
]);
$this->questions->shouldReceive('findById')->with(2)->andReturn(new Question(offeringId: 1, label: 'Skill level', id: 2));
$this->acceptances->shouldReceive('findByRegistration')->once()->with(PolicyAcceptance::REG_LESSON, 7)->andReturn([
new PolicyAcceptance(
policyVersionId: 4,
studentId: 5,
registrationType: PolicyAcceptance::REG_LESSON,
registrationId: 7,
ipAddress: '1.2.3.4',
acceptedAt: '2026-07-01 10:00:00'
),
]);
$this->versions->shouldReceive('findById')->with(4)->andReturn(new PolicyVersion(policyId: 3, versionNumber: 2, id: 4));
$this->policies->shouldReceive('findById')->with(3)->andReturn(new Policy(title: 'Cancellation', slug: 'cancellation', id: 3));
self::assertSame(
[['question' => 'Skill level', 'answer' => 'Beginner']],
$this->detail->answers($occurrence)
);
self::assertSame(
[[
'policy' => 'Cancellation',
'version' => 'v2',
'accepted_at' => '2026-07-01 10:00:00',
'ip' => '1.2.3.4',
]],
$this->detail->acceptances($occurrence)
);
}
private function lesson(int $id, ?int $seriesId = null): Lesson
{
return new Lesson(
slotId: 1,
studentId: 5,
instructorId: 9,
offeringId: 1,
recurrence: null === $seriesId ? Lesson::RECURRENCE_SINGLE : Lesson::RECURRENCE_WEEKLY,
seriesId: $seriesId,
id: $id
); );
} }
} }
+2 -2
View File
@@ -3,7 +3,7 @@
* Plugin Name: Unsupervised Scheduler * Plugin Name: Unsupervised Scheduler
* Plugin URI: https://git.unsupervised.ca/Unsupervised/unsupervised-scheduler * Plugin URI: https://git.unsupervised.ca/Unsupervised/unsupervised-scheduler
* Description: Instructor/student lesson scheduling for WordPress. * Description: Instructor/student lesson scheduling for WordPress.
* Version: 1.5.0 * Version: 1.5.1
* Requires at least: 6.2 * Requires at least: 6.2
* Requires PHP: 8.1 * Requires PHP: 8.1
* Author: Unsupervised * Author: Unsupervised
@@ -21,7 +21,7 @@ if (! defined('ABSPATH')) {
exit; exit;
} }
define('USC_VERSION', '1.5.0'); define('USC_VERSION', '1.5.1');
define('USC_PLUGIN_FILE', __FILE__); define('USC_PLUGIN_FILE', __FILE__);
define('USC_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('USC_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('USC_PLUGIN_URL', plugin_dir_url(__FILE__)); define('USC_PLUGIN_URL', plugin_dir_url(__FILE__));