Compare commits
6
Commits
v1.2.1
...
17487cde46
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17487cde46 | ||
|
|
13d6b3e14e
|
||
|
|
d866aa7295 | ||
|
|
c4acdb7ca4
|
||
|
|
30928addf8 | ||
|
|
27793fa0aa |
@@ -11,6 +11,12 @@ 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
|
||||
each change under the current top section as you work.
|
||||
|
||||
## [1.2.2]
|
||||
|
||||
### Added
|
||||
- The **Group Classes** block can now be pinned to a single class, under **Classes shown → Class** in the block sidebar (shortcode: `[us_group_classes offering="…"]`). Pick a class and the block shows only that one, so it can be embedded on a page that describes the class. In this mode the class's own description is left out to avoid repeating the page copy — the card shows the schedule, instructor, price, enrolment deadline and the enrol/withdraw controls. Leaving it on **All classes** keeps the full browsable catalog with descriptions.
|
||||
- The **Student Registration** block can now send students onward to a page of your choosing once they finish registering. Its **After email confirmation** panel is now **After registration**: the page you pick there is where the link shown to a newly registered student points — the "Sign in to your account" link after they confirm their email, and a "Continue to your account" link for an invited student, who is signed in immediately. A new **Redirect automatically** option takes them straight there instead of showing the link. Registration errors are never skipped — a failed sign-up and an expired confirmation link still show their message on the page, as does the "check your email to confirm your address" step. The redirect needs a page to be chosen; with none set, students see the link (or, for invited students, just the confirmation) as before.
|
||||
|
||||
## [1.2.1]
|
||||
|
||||
### Fixed
|
||||
|
||||
+10
-3
@@ -151,18 +151,25 @@
|
||||
shortcode: 'us_student_register',
|
||||
attributes: {
|
||||
loginPageId: { type: 'number', default: 0 },
|
||||
autoRedirect: { type: 'boolean', default: false },
|
||||
inviteOnlyMessage: { type: 'string', default: '' },
|
||||
},
|
||||
inspector: (attributes, setAttributes) => [
|
||||
el(
|
||||
PanelBody,
|
||||
{ title: __('After email confirmation', 'unsupervised-schedular'), key: 'confirmation' },
|
||||
{ title: __('After registration', 'unsupervised-schedular'), key: 'confirmation' },
|
||||
el(PageSelect, {
|
||||
label: __('Sign-in page', 'unsupervised-schedular'),
|
||||
help: __('Where the sign-in link shown after a student confirms their email address sends them.', 'unsupervised-schedular'),
|
||||
help: __('Where students are sent once registration finishes — after they confirm their email address, or straight away for an invited student.', 'unsupervised-schedular'),
|
||||
defaultLabel: __('WordPress login screen', 'unsupervised-schedular'),
|
||||
value: attributes.loginPageId,
|
||||
onChange: (loginPageId) => setAttributes({ loginPageId }),
|
||||
}),
|
||||
el(ToggleControl, {
|
||||
label: __('Redirect automatically', 'unsupervised-schedular'),
|
||||
help: __('Send students straight to that page instead of showing the link. Requires a page to be chosen; errors and the "check your email" step are never skipped.', 'unsupervised-schedular'),
|
||||
checked: !!attributes.autoRedirect,
|
||||
onChange: (autoRedirect) => setAttributes({ autoRedirect }),
|
||||
})
|
||||
),
|
||||
el(
|
||||
@@ -192,7 +199,7 @@
|
||||
{ title: __('Classes shown', 'unsupervised-schedular') },
|
||||
el(GroupClassSelect, {
|
||||
label: __('Class', 'unsupervised-schedular'),
|
||||
help: __('Show only one group class, for embedding on a page dedicated to it.', 'unsupervised-schedular'),
|
||||
help: __('Show only one group class, for embedding on a page dedicated to it. That class’s description is left out — the card shows just the schedule, price and enrolment controls.', 'unsupervised-schedular'),
|
||||
value: attributes.offeringId,
|
||||
onChange: (offeringId) => setAttributes({ offeringId }),
|
||||
})
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
|
||||
// When the shortcode/block pins a single offering, only that class is
|
||||
// shown, so the page can be embedded alongside a full class description.
|
||||
// The class's own description is then omitted from the card — the page it
|
||||
// sits on already describes the class — leaving the schedule, price and
|
||||
// enrolment controls.
|
||||
const singleOfferingId = Number(app.dataset.offering || 0);
|
||||
|
||||
function apiFetch(path, options = {}) {
|
||||
@@ -148,7 +151,7 @@
|
||||
${whenLabel(o) ? `<p class="us-class-when">${escHtml(whenLabel(o))}</p>` : ''}
|
||||
${o.instructor_name ? `<p class="us-class-instructor">With ${escHtml(o.instructor_name)}</p>` : ''}
|
||||
${o.schedule_note ? `<p>${escHtml(o.schedule_note)}</p>` : ''}
|
||||
${o.description ? `<p>${escHtml(o.description)}</p>` : ''}
|
||||
${!singleOfferingId && o.description ? `<p>${escHtml(o.description)}</p>` : ''}
|
||||
<p>${escHtml(Number(o.price).toFixed(2))} ${escHtml(o.currency)}</p>
|
||||
${!enrolledMap.has(Number(o.id)) && isEnrollmentOpen(o) && enrolmentDeadline(o)
|
||||
? `<p class="us-enrol-deadline">Enrol by ${escHtml(formatDate(enrolmentDeadline(o)))}</p>`
|
||||
|
||||
@@ -128,6 +128,13 @@ recorded in `us_policy_acceptances` with `registration_type = account` and
|
||||
- `[us_student_register]` — the registration page. In `invite` mode: shows the form for a valid pending invite, else an "by invitation only" message. In `self_approval` mode: shows the form to anyone (editable email), and renders confirmation-result notices from `?us_confirmed=1|expired`.
|
||||
- The invitation-only message is customisable: block attribute `inviteOnlyMessage` (set under the block's **Invitation-only notice** panel) / shortcode attribute `invite_only_message`. Blank falls back to the default wording (`RegistrationPage::inviteOnlyMessage()`).
|
||||
|
||||
## Where Students Go Next
|
||||
The block's **After registration** panel picks the page a student continues to once
|
||||
registration finishes, and whether they get there by hand or automatically.
|
||||
|
||||
- **Sign-in page** (`loginPageId` / `login_page_id`) — the target of the "Sign in to your account" link shown after email confirmation (`?us_confirmed=1|ready`, falling back to the WordPress login screen) and of the "Continue to your account" link an invited student sees on the spot (`?us_registered=invite`; no link at all with no page chosen, since an already-signed-in student has no use for the login screen).
|
||||
- **Redirect automatically** (`autoRedirect`, block only) — sends the student to that page instead of showing the link, via `BlockRegistrar::maybeAutoRedirect()` on `template_redirect`. It fires only on those two finished states (`RegistrationPage::isRegistrationComplete()`), so the "check your email" step, a validation error, and an `expired` confirmation link are always shown rather than redirected past. With no page chosen nothing happens — there is deliberately no login-screen fallback for the redirect. See `editor-blocks.md`.
|
||||
|
||||
## Token Redirect
|
||||
A `template_redirect` handler (`RegistrationPage::maybeRedirectToRegistrationPage()`)
|
||||
sends any front-end request carrying a `us_invite` token to the configured
|
||||
|
||||
@@ -29,8 +29,9 @@ Four blocks have sidebar (inspector) options:
|
||||
| `us-scheduler/booking` | `autoRedirect` (boolean) | `false` | Send logged-out visitors straight to the login page instead of showing the link. |
|
||||
| `us-scheduler/student-login` | `bookingPageId` (number) | `0` | Page the "View available lessons" link points to for logged-in visitors, and the post-login redirect target. `0` = the current page. |
|
||||
| `us-scheduler/student-login` | `autoRedirect` (boolean) | `false` | Send logged-in visitors straight to the booking page instead of showing the link. Does nothing until a booking page is chosen. |
|
||||
| `us-scheduler/student-register` | `loginPageId` (number) | `0` | Page the "Sign in to your account" link points to after a student confirms their email. `0` = the WordPress login screen. Shortcode equivalent: `[us_student_register login_page_id="…"]`. |
|
||||
| `us-scheduler/group-classes` | `offeringId` (number) | `0` | Restrict the page to a single group class, for embedding on a page dedicated to that class. `0` = browse all classes. Shortcode equivalent: `[us_group_classes offering="…"]`. |
|
||||
| `us-scheduler/student-register` | `loginPageId` (number) | `0` | Page students continue to once registration finishes — the "Sign in to your account" link after they confirm their email, and the "Continue to your account" link an invited student gets on the spot. `0` = the WordPress login screen for the confirmation link, and no link at all for the (already signed-in) invited student. Shortcode equivalent: `[us_student_register login_page_id="…"]`. |
|
||||
| `us-scheduler/student-register` | `autoRedirect` (boolean) | `false` | Send students straight to that page instead of showing the link. Does nothing until a page is chosen — there is no login-screen fallback here. |
|
||||
| `us-scheduler/group-classes` | `offeringId` (number) | `0` | Restrict the page to a single group class, for embedding on a page dedicated to that class. The class description is then omitted — only the schedule, instructor, price and enrolment controls are shown, so the surrounding page's own copy is not repeated. `0` = browse all classes, descriptions included. Shortcode equivalent: `[us_group_classes offering="…"]`. |
|
||||
|
||||
The page selects list all published pages; if a chosen page is later deleted,
|
||||
the blocks fall back to their defaults. The group-classes block's class
|
||||
@@ -48,6 +49,15 @@ queried singular post's content for the block (including inside nested
|
||||
blocks), and redirects when the block opts in. A block whose target is its
|
||||
own page is ignored to avoid a redirect loop.
|
||||
|
||||
The registration block's auto-redirect additionally only fires on a
|
||||
**finished** registration — `RegistrationPage::isRegistrationComplete()`: an
|
||||
invited student who is now logged in (`?us_registered=invite`), or a
|
||||
self-signup back from the emailed confirmation link (`?us_confirmed=ready|1`).
|
||||
The intermediate "check your email" step and every failure (a validation
|
||||
error, `?us_confirmed=expired`) stay on the page so the student reads the
|
||||
message. That check runs before the content is parsed, so an ordinary page
|
||||
view does not pay for the extra block scan.
|
||||
|
||||
## How it works
|
||||
|
||||
- **`BlockRegistrar`** (`src/BlockRegistrar.php`) hooks `init` and registers
|
||||
@@ -78,7 +88,9 @@ placeholder content:
|
||||
- **Booking** — `#us-booking-app` with sample `.us-day` / `.us-slot` rows and
|
||||
disabled Book buttons.
|
||||
- **Group classes** — `#us-group-app` with a sample `.us-class` card and a
|
||||
disabled Enrol button.
|
||||
disabled Enrol button. When `offeringId` pins a single class the preview
|
||||
drops the sample description, matching what the live page renders in that
|
||||
mode.
|
||||
- **Login** — the real `templates/frontend/login-page.php` template (it has
|
||||
no request-state dependencies).
|
||||
- **Registration** — a disabled sample of the `.us-register-form` fields.
|
||||
|
||||
@@ -176,7 +176,7 @@ class becomes enrollable for them — they choose whether to enrol.
|
||||
- Model: `Unsupervised\Schedular\GroupClass\Enrollment`
|
||||
- Admin controller: `Unsupervised\Schedular\GroupClass\GroupClassController` — `renderPage` (studio admin per-class summary, `view_all_lessons`) and `renderInstructorPage` (instructor summary + `?class_id` roster detail, `view_own_lessons`)
|
||||
- REST endpoint: `Unsupervised\Schedular\GroupClass\EnrollmentEndpoint`
|
||||
- Frontend: `Unsupervised\Schedular\GroupClass\GroupClassPage` (`[us_group_classes]` shortcode; `offering="…"` restricts it to a single class for embedding on a dedicated page — the block equivalent is the `offeringId` attribute)
|
||||
- Frontend: `Unsupervised\Schedular\GroupClass\GroupClassPage` (`[us_group_classes]` shortcode; `offering="…"` restricts it to a single class for embedding on a dedicated page — the block equivalent is the `offeringId` attribute). In single-class mode `assets/js/group-classes.js` leaves the class description out of the card, since the page it is embedded on already describes the class; the schedule, instructor, schedule note, price and enrolment controls are still shown.
|
||||
- Reuses `Registration\RegistrationGate` (intake answers + booking-scoped policy acceptance, type `enrollment`)
|
||||
|
||||
> **Payment:** a priced enrolment creates a payment via `Payment\PaymentService`
|
||||
|
||||
@@ -66,9 +66,20 @@ class RegistrationPage {
|
||||
|
||||
if ( is_user_logged_in() ) {
|
||||
if ( self::RESULT_INVITE === $registered ) {
|
||||
// An invited student is done the moment they land here logged in,
|
||||
// so this is where their "continue" link belongs. The sign-in-page
|
||||
// fallback is deliberately not used: pointing someone who is
|
||||
// already signed in at the login screen helps nobody.
|
||||
$continue = $this->continueUrl( $this->successPageId( $atts ) );
|
||||
$link = null === $continue
|
||||
? ''
|
||||
: '<p><a href="' . esc_url( $continue ) . '">'
|
||||
. esc_html__( 'Continue to your account', 'unsupervised-schedular' )
|
||||
. '</a></p>';
|
||||
|
||||
return '<div class="us-register-form"><p class="us-success">'
|
||||
. esc_html__( 'Your account has been created and you are now logged in.', 'unsupervised-schedular' )
|
||||
. '</p></div>';
|
||||
. '</p>' . $link . '</div>';
|
||||
}
|
||||
|
||||
return '<p>' . esc_html__( 'You already have an account and are logged in.', 'unsupervised-schedular' ) . '</p>';
|
||||
@@ -98,7 +109,7 @@ class RegistrationPage {
|
||||
$confirmResult = sanitize_key( Val::string( wp_unslash( $_GET['us_confirmed'] ?? '' ) ) );
|
||||
|
||||
// Where the post-confirmation prompt sends students to sign in.
|
||||
$loginUrl = $this->loginUrl( Val::int( $atts['loginPageId'] ?? $atts['login_page_id'] ?? 0 ) );
|
||||
$loginUrl = $this->loginUrl( $this->successPageId( $atts ) );
|
||||
|
||||
$policyForms = $this->signupPolicies();
|
||||
$accountQuestions = $this->questions->findByScope( Question::SCOPE_ACCOUNT, activeOnly: true );
|
||||
@@ -319,21 +330,68 @@ class RegistrationPage {
|
||||
return $autoApprove ? self::RESULT_CONFIRM_GROUP : self::RESULT_CONFIRM;
|
||||
}
|
||||
|
||||
/**
|
||||
* The page id chosen for the post-registration destination, from either the
|
||||
* block (`loginPageId`) or shortcode (`login_page_id`) attribute.
|
||||
*
|
||||
* @param array<int|string, mixed> $atts
|
||||
*/
|
||||
private function successPageId( array $atts ): int {
|
||||
return Val::int( $atts['loginPageId'] ?? $atts['login_page_id'] ?? 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* URL the post-confirmation sign-in link points to: the chosen login page
|
||||
* when one is configured (and still exists), otherwise the WordPress login
|
||||
* screen.
|
||||
*/
|
||||
private function loginUrl( int $loginPageId ): string {
|
||||
if ( $loginPageId > 0 ) {
|
||||
$url = get_permalink( $loginPageId );
|
||||
return $this->continueUrl( $loginPageId ) ?? wp_login_url();
|
||||
}
|
||||
|
||||
if ( is_string( $url ) ) {
|
||||
return $url;
|
||||
}
|
||||
/**
|
||||
* The chosen post-registration page's URL, or null when none is configured
|
||||
* (or it has since been deleted). Unlike {@see loginUrl()} this has no
|
||||
* WordPress-login-screen fallback, so callers that need a page the student
|
||||
* was actually sent to — the invited-student link and the block's
|
||||
* auto-redirect — can tell "not configured" from "configured".
|
||||
*/
|
||||
public function continueUrl( int $pageId ): ?string {
|
||||
if ( $pageId <= 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return wp_login_url();
|
||||
$url = get_permalink( $pageId );
|
||||
|
||||
return is_string( $url ) ? $url : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this request is a *finished* registration — the states the
|
||||
* block's auto-redirect may act on:
|
||||
*
|
||||
* - an invited student who just signed up and is now logged in, and
|
||||
* - a self-signup returning from the emailed confirmation link, whether
|
||||
* their account is ready (`ready`) or awaiting studio approval (`1`).
|
||||
*
|
||||
* Deliberately excluded: the intermediate "check your email" step (the
|
||||
* student would never see the instruction) and every failure — a validation
|
||||
* error or an expired confirmation link (`expired`) — so the message always
|
||||
* gets shown. The `us_confirmed` values are set by
|
||||
* {@see EmailConfirmationHandler::maybeConfirm()}.
|
||||
*/
|
||||
public function isRegistrationComplete(): bool {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display flag; the submit that set it was nonce-checked.
|
||||
$registered = sanitize_key( Val::string( wp_unslash( $_GET['us_registered'] ?? '' ) ) );
|
||||
|
||||
if ( self::RESULT_INVITE === $registered ) {
|
||||
return is_user_logged_in();
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only display flag set by EmailConfirmationHandler's redirect.
|
||||
$confirmed = sanitize_key( Val::string( wp_unslash( $_GET['us_confirmed'] ?? '' ) ) );
|
||||
|
||||
return in_array( $confirmed, [ '1', 'ready' ], true );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+20
-4
@@ -58,13 +58,29 @@ class BlockPreview {
|
||||
);
|
||||
}
|
||||
|
||||
public static function groupClasses(): string {
|
||||
/**
|
||||
* Sample group-class card.
|
||||
*
|
||||
* @param bool $singleClass Whether the block is pinned to one class, in
|
||||
* which case the live page omits the class
|
||||
* description and the preview does too.
|
||||
*/
|
||||
public static function groupClasses( bool $singleClass = false ): string {
|
||||
$note = $singleClass
|
||||
? __( 'Editor preview — the published page shows the chosen class with its live schedule and enrolment status.', 'unsupervised-schedular' )
|
||||
: __( 'Editor preview — students see live group classes on the published page.', 'unsupervised-schedular' );
|
||||
|
||||
$description = $singleClass
|
||||
? ''
|
||||
: '<p>' . esc_html__( 'A sample class shown so the page can be styled.', 'unsupervised-schedular' ) . '</p>';
|
||||
|
||||
return sprintf(
|
||||
'<div id="us-group-app">%s<div id="us-group-list"><div class="us-class"><h3>%s</h3><p>%s</p><p>%s</p><p>25.00 CAD</p><button type="button" class="us-enrol-btn" disabled>%s</button></div></div></div>',
|
||||
self::note( __( 'Editor preview — students see live group classes on the published page.', 'unsupervised-schedular' ) ),
|
||||
'<div id="us-group-app">%s<div id="us-group-list"><div class="us-class"><h3>%s</h3><p class="us-class-when">%s</p>%s<p>25.00 CAD</p><p class="us-enrol-deadline">%s</p><button type="button" class="us-enrol-btn" disabled>%s</button></div></div></div>',
|
||||
self::note( $note ),
|
||||
esc_html__( 'Beginner Group Class', 'unsupervised-schedular' ),
|
||||
esc_html__( 'Saturdays 10:00 AM–11:00 AM', 'unsupervised-schedular' ),
|
||||
esc_html__( 'A sample class shown so the page can be styled.', 'unsupervised-schedular' ),
|
||||
$description,
|
||||
esc_html__( 'Enrol by Sep 6, 2026', 'unsupervised-schedular' ),
|
||||
esc_html__( 'Enrol', 'unsupervised-schedular' )
|
||||
);
|
||||
}
|
||||
|
||||
+54
-5
@@ -109,6 +109,7 @@ class BlockRegistrar {
|
||||
'type' => 'number',
|
||||
'default' => 0,
|
||||
],
|
||||
'autoRedirect' => $redirectToggle,
|
||||
'inviteOnlyMessage' => [
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
@@ -160,16 +161,21 @@ class BlockRegistrar {
|
||||
* @param array<string, mixed> $attributes Block attributes.
|
||||
*/
|
||||
public function renderGroupClasses( array $attributes = [] ): string {
|
||||
return $this->isEditorPreview() ? BlockPreview::groupClasses() : $this->groupClassPage->render( $attributes );
|
||||
if ( ! $this->isEditorPreview() ) {
|
||||
return $this->groupClassPage->render( $attributes );
|
||||
}
|
||||
|
||||
return BlockPreview::groupClasses( Val::int( $attributes['offeringId'] ?? 0 ) > 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side auto-redirect for blocks that opt in via their autoRedirect
|
||||
* attribute: logged-out visitors on a page containing the booking block
|
||||
* are sent to its login page, and logged-in visitors on a page containing
|
||||
* the student-login block are sent to its booking page. Hooked on
|
||||
* `template_redirect` because block rendering happens after output has
|
||||
* started, too late to send a Location header.
|
||||
* are sent to its login page, logged-in visitors on a page containing the
|
||||
* student-login block are sent to its booking page, and a student who has
|
||||
* just finished registering is sent to the register block's chosen page.
|
||||
* Hooked on `template_redirect` because block rendering happens after
|
||||
* output has started, too late to send a Location header.
|
||||
*/
|
||||
public function maybeAutoRedirect(): void {
|
||||
if ( is_admin() || ! is_singular() ) {
|
||||
@@ -181,6 +187,10 @@ class BlockRegistrar {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $this->maybeRedirectAfterRegistration( $post ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_user_logged_in() ) {
|
||||
$attrs = $this->firstBlockAttrs( $post->post_content, 'us-scheduler/student-login' );
|
||||
if ( null === $attrs || ! Val::bool( $attrs['autoRedirect'] ?? false ) ) {
|
||||
@@ -213,6 +223,45 @@ class BlockRegistrar {
|
||||
$this->redirect( $this->bookingPage->loginUrl( $loginPageId ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a student whose registration has just completed to the register
|
||||
* block's chosen page, when the block opts in. Only the finished states
|
||||
* qualify (see {@see RegistrationPage::isRegistrationComplete()}): a
|
||||
* failure or the "check your email" step stays put so its message is read.
|
||||
* Unlike the other blocks there is no login-screen fallback — with no page
|
||||
* chosen there is nowhere to send them, so the link is shown instead.
|
||||
*
|
||||
* Returns whether the redirect was issued (it only ever returns in tests;
|
||||
* {@see redirect()} exits in production).
|
||||
*/
|
||||
private function maybeRedirectAfterRegistration( \WP_Post $post ): bool {
|
||||
// Checked before parsing the content because it is a couple of query
|
||||
// args, whereas every front-end request would otherwise pay for a
|
||||
// third block scan.
|
||||
if ( ! $this->registrationPage->isRegistrationComplete() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$attrs = $this->firstBlockAttrs( $post->post_content, 'us-scheduler/student-register' );
|
||||
if ( null === $attrs || ! Val::bool( $attrs['autoRedirect'] ?? false ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pageId = Val::int( $attrs['loginPageId'] ?? 0 );
|
||||
if ( $pageId === $post->ID ) {
|
||||
return false; // Redirecting the page to itself would loop.
|
||||
}
|
||||
|
||||
$url = $this->registrationPage->continueUrl( $pageId );
|
||||
if ( null === $url ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->redirect( $url );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attributes of the first occurrence of the named block in the content,
|
||||
* searching inner blocks so blocks nested inside groups or columns are
|
||||
|
||||
@@ -14,7 +14,7 @@ if (! defined('ABSPATH')) {
|
||||
* @var bool $canRegister
|
||||
* @var string $inviteOnlyMessage Text shown when registration is closed and no valid invite is present.
|
||||
* @var bool $open Whether open (self-approval) registration is enabled.
|
||||
* @var string $successType '' | 'invite' (created + logged in) | 'confirm' (check email) | 'confirm_group' (check email; auto-approved on confirm).
|
||||
* @var string $successType '' | 'confirm' (check email) | 'confirm_group' (check email; auto-approved on confirm). The invited-student success is rendered by RegistrationPage::render() itself, which returns before this template for logged-in visitors.
|
||||
* @var string $confirmResult '' | '1' (email confirmed, awaiting approval) | 'ready' (confirmed + auto-approved) | 'expired'.
|
||||
* @var string $loginUrl Where the post-confirmation sign-in link points.
|
||||
* @var string $error
|
||||
@@ -57,9 +57,7 @@ $renderQuestionField = static function (Question $question): void {
|
||||
};
|
||||
?>
|
||||
<div class="us-register-form">
|
||||
<?php if ($successType === 'invite') : ?>
|
||||
<p class="us-success"><?php esc_html_e('Your account has been created and you are now logged in.', 'unsupervised-schedular'); ?></p>
|
||||
<?php elseif ($successType === 'confirm') : ?>
|
||||
<?php if ($successType === 'confirm') : ?>
|
||||
<p class="us-success"><?php esc_html_e('Your account has been created. Check your email for a link to confirm your address — once you do, a studio admin will review and approve your account.', 'unsupervised-schedular'); ?></p>
|
||||
<?php elseif ($successType === 'confirm_group') : ?>
|
||||
<p class="us-success"><?php esc_html_e('Your account has been created. Check your email for a link to confirm your address — once you do, your account is ready to use.', 'unsupervised-schedular'); ?></p>
|
||||
|
||||
@@ -484,6 +484,54 @@ class RegistrationPageTest extends TestCase
|
||||
|
||||
self::assertStringContainsString('us-success', $html);
|
||||
self::assertStringContainsString('now logged in', $html);
|
||||
// No page chosen: the sign-in-screen fallback is useless to someone who
|
||||
// is already signed in, so no link is offered at all.
|
||||
self::assertStringNotContainsString('<a href', $html);
|
||||
}
|
||||
|
||||
public function testInviteSuccessLinksToTheChosenPage(): void
|
||||
{
|
||||
$_GET = [ 'us_registered' => 'invite' ];
|
||||
Functions\when('is_user_logged_in')->justReturn(true);
|
||||
Functions\when('sanitize_key')->alias(static fn ($v) => strtolower((string) $v));
|
||||
Functions\expect('get_permalink')->once()->with(4)->andReturn('http://home.test/welcome/');
|
||||
|
||||
$html = $this->ctx['page']->render([ 'loginPageId' => 4 ]);
|
||||
|
||||
self::assertStringContainsString('now logged in', $html);
|
||||
self::assertStringContainsString('href="http://home.test/welcome/"', $html);
|
||||
}
|
||||
|
||||
public function testContinueUrlIsNullWithoutAResolvablePage(): void
|
||||
{
|
||||
Functions\when('get_permalink')->justReturn(false);
|
||||
|
||||
self::assertNull($this->ctx['page']->continueUrl(0));
|
||||
self::assertNull($this->ctx['page']->continueUrl(4));
|
||||
}
|
||||
|
||||
public function testIsRegistrationCompleteOnlyForFinishedStates(): void
|
||||
{
|
||||
Functions\when('sanitize_key')->alias(static fn ($v) => strtolower((string) $v));
|
||||
|
||||
// [ query args, logged in, finished ]
|
||||
$cases = [
|
||||
'invited student, now logged in' => [['us_registered' => 'invite'], true, true],
|
||||
'invited student, not logged in' => [['us_registered' => 'invite'], false, false],
|
||||
'email confirmed, ready' => [['us_confirmed' => 'ready'], false, true],
|
||||
'email confirmed, pending review' => [['us_confirmed' => '1'], false, true],
|
||||
'confirmation link expired' => [['us_confirmed' => 'expired'], false, false],
|
||||
'awaiting email confirmation' => [['us_registered' => 'confirm'], false, false],
|
||||
'group signup awaiting confirm' => [['us_registered' => 'confirm_group'], false, false],
|
||||
'plain page view' => [[], false, false],
|
||||
];
|
||||
|
||||
foreach ($cases as $label => [$get, $loggedIn, $expected]) {
|
||||
$_GET = $get;
|
||||
Functions\when('is_user_logged_in')->justReturn($loggedIn);
|
||||
|
||||
self::assertSame($expected, $this->ctx['page']->isRegistrationComplete(), $label);
|
||||
}
|
||||
}
|
||||
|
||||
public function testInviteOnlyMessageCanBeCustomised(): void
|
||||
|
||||
@@ -29,6 +29,17 @@ class BlockPreviewTest extends TestCase
|
||||
self::assertStringContainsString('class="us-class"', $html);
|
||||
self::assertStringContainsString('class="us-enrol-btn" disabled', $html);
|
||||
self::assertStringContainsString('us-editor-note', $html);
|
||||
self::assertStringContainsString('A sample class shown so the page can be styled.', $html);
|
||||
}
|
||||
|
||||
public function testSingleClassGroupPreviewDropsTheDescriptionButKeepsScheduleAndEnrolment(): void
|
||||
{
|
||||
$html = BlockPreview::groupClasses(true);
|
||||
|
||||
self::assertStringNotContainsString('A sample class shown so the page can be styled.', $html);
|
||||
self::assertStringContainsString('class="us-class-when"', $html);
|
||||
self::assertStringContainsString('class="us-enrol-deadline"', $html);
|
||||
self::assertStringContainsString('class="us-enrol-btn" disabled', $html);
|
||||
}
|
||||
|
||||
public function testLoginPreviewIncludesTheRealLoginTemplate(): void
|
||||
|
||||
@@ -52,6 +52,12 @@ class BlockRegistrarTest extends TestCase
|
||||
$this->registrationPage = Mockery::mock(RegistrationPage::class);
|
||||
$this->groupClassPage = Mockery::mock(GroupClassPage::class);
|
||||
|
||||
// Most requests are not a just-finished registration; the tests that
|
||||
// exercise that path override this.
|
||||
$this->registrationPage->shouldReceive('isRegistrationComplete')
|
||||
->andReturn(false)
|
||||
->byDefault();
|
||||
|
||||
$this->registrar = new TestableBlockRegistrar(
|
||||
$this->bookingPage,
|
||||
$this->loginPage,
|
||||
@@ -126,7 +132,7 @@ class BlockRegistrarTest extends TestCase
|
||||
array_keys($registered['us-scheduler/student-login']['attributes'])
|
||||
);
|
||||
self::assertSame(
|
||||
['loginPageId', 'inviteOnlyMessage'],
|
||||
['loginPageId', 'autoRedirect', 'inviteOnlyMessage'],
|
||||
array_keys($registered['us-scheduler/student-register']['attributes'])
|
||||
);
|
||||
self::assertSame(
|
||||
@@ -179,6 +185,19 @@ class BlockRegistrarTest extends TestCase
|
||||
self::assertStringContainsString('us-group-app', $this->registrar->renderGroupClasses());
|
||||
}
|
||||
|
||||
public function testEditorPreviewOfAPinnedGroupClassOmitsTheDescription(): void
|
||||
{
|
||||
$this->registrar->preview = true;
|
||||
$this->groupClassPage->shouldNotReceive('render');
|
||||
|
||||
$all = $this->registrar->renderGroupClasses();
|
||||
$single = $this->registrar->renderGroupClasses(['offeringId' => 12]);
|
||||
|
||||
self::assertStringContainsString('A sample class shown so the page can be styled.', $all);
|
||||
self::assertStringNotContainsString('A sample class shown so the page can be styled.', $single);
|
||||
self::assertStringContainsString('us-enrol-deadline', $single);
|
||||
}
|
||||
|
||||
public function testIsEditorPreviewIsFalseOutsideRestRequests(): void
|
||||
{
|
||||
// REST_REQUEST is undefined in the test process, so the real
|
||||
@@ -384,6 +403,145 @@ class BlockRegistrarTest extends TestCase
|
||||
self::assertSame([], $this->registrar->redirects);
|
||||
}
|
||||
|
||||
public function testAutoRedirectSendsAFinishedRegistrationToTheChosenPage(): void
|
||||
{
|
||||
$this->stubSingularRequest(
|
||||
30,
|
||||
[
|
||||
[
|
||||
'blockName' => 'us-scheduler/student-register',
|
||||
'attrs' => ['autoRedirect' => true, 'loginPageId' => 4],
|
||||
'innerBlocks' => [],
|
||||
],
|
||||
],
|
||||
false
|
||||
);
|
||||
|
||||
$this->registrationPage->shouldReceive('isRegistrationComplete')->andReturn(true);
|
||||
$this->registrationPage->shouldReceive('continueUrl')
|
||||
->once()->with(4)->andReturn('https://example.com/welcome/');
|
||||
|
||||
$this->registrar->maybeAutoRedirect();
|
||||
|
||||
self::assertSame(['https://example.com/welcome/'], $this->registrar->redirects);
|
||||
}
|
||||
|
||||
public function testAutoRedirectSendsAJustLoggedInInvitedStudentToTheChosenPage(): void
|
||||
{
|
||||
// The invited-student branch completes logged in, so the logged-in
|
||||
// student-login branch must not get first claim on the request.
|
||||
$this->stubSingularRequest(
|
||||
30,
|
||||
[
|
||||
[
|
||||
'blockName' => 'us-scheduler/student-register',
|
||||
'attrs' => ['autoRedirect' => true, 'loginPageId' => 4],
|
||||
'innerBlocks' => [],
|
||||
],
|
||||
],
|
||||
true
|
||||
);
|
||||
|
||||
$this->registrationPage->shouldReceive('isRegistrationComplete')->andReturn(true);
|
||||
$this->registrationPage->shouldReceive('continueUrl')
|
||||
->once()->with(4)->andReturn('https://example.com/welcome/');
|
||||
|
||||
$this->registrar->maybeAutoRedirect();
|
||||
|
||||
self::assertSame(['https://example.com/welcome/'], $this->registrar->redirects);
|
||||
}
|
||||
|
||||
public function testNoRedirectWhenTheRegistrationIsNotFinished(): void
|
||||
{
|
||||
// e.g. the "check your email" step, or a validation error — the
|
||||
// message has to be read, so the block never redirects past it.
|
||||
$this->stubSingularRequest(
|
||||
30,
|
||||
[
|
||||
[
|
||||
'blockName' => 'us-scheduler/student-register',
|
||||
'attrs' => ['autoRedirect' => true, 'loginPageId' => 4],
|
||||
'innerBlocks' => [],
|
||||
],
|
||||
],
|
||||
false
|
||||
);
|
||||
|
||||
$this->registrationPage->shouldReceive('continueUrl')->never();
|
||||
|
||||
$this->registrar->maybeAutoRedirect();
|
||||
|
||||
self::assertSame([], $this->registrar->redirects);
|
||||
}
|
||||
|
||||
public function testNoRedirectWhenTheRegisterBlockDoesNotOptIn(): void
|
||||
{
|
||||
$this->stubSingularRequest(
|
||||
30,
|
||||
[
|
||||
[
|
||||
'blockName' => 'us-scheduler/student-register',
|
||||
'attrs' => ['loginPageId' => 4],
|
||||
'innerBlocks' => [],
|
||||
],
|
||||
],
|
||||
false
|
||||
);
|
||||
|
||||
$this->registrationPage->shouldReceive('isRegistrationComplete')->andReturn(true);
|
||||
$this->registrationPage->shouldReceive('continueUrl')->never();
|
||||
|
||||
$this->registrar->maybeAutoRedirect();
|
||||
|
||||
self::assertSame([], $this->registrar->redirects);
|
||||
}
|
||||
|
||||
public function testNoRedirectWhenTheRegisterBlockHasNoPageChosen(): void
|
||||
{
|
||||
$this->stubSingularRequest(
|
||||
30,
|
||||
[
|
||||
[
|
||||
'blockName' => 'us-scheduler/student-register',
|
||||
'attrs' => ['autoRedirect' => true],
|
||||
'innerBlocks' => [],
|
||||
],
|
||||
],
|
||||
false
|
||||
);
|
||||
|
||||
$this->registrationPage->shouldReceive('isRegistrationComplete')->andReturn(true);
|
||||
// No page chosen: there is no login-screen fallback to redirect to,
|
||||
// so the student keeps the on-page confirmation instead.
|
||||
$this->registrationPage->shouldReceive('continueUrl')->once()->with(0)->andReturnNull();
|
||||
|
||||
$this->registrar->maybeAutoRedirect();
|
||||
|
||||
self::assertSame([], $this->registrar->redirects);
|
||||
}
|
||||
|
||||
public function testNoRedirectWhenTheRegisterBlockPointsAtItsOwnPage(): void
|
||||
{
|
||||
$this->stubSingularRequest(
|
||||
4,
|
||||
[
|
||||
[
|
||||
'blockName' => 'us-scheduler/student-register',
|
||||
'attrs' => ['autoRedirect' => true, 'loginPageId' => 4],
|
||||
'innerBlocks' => [],
|
||||
],
|
||||
],
|
||||
false
|
||||
);
|
||||
|
||||
$this->registrationPage->shouldReceive('isRegistrationComplete')->andReturn(true);
|
||||
$this->registrationPage->shouldReceive('continueUrl')->never();
|
||||
|
||||
$this->registrar->maybeAutoRedirect();
|
||||
|
||||
self::assertSame([], $this->registrar->redirects);
|
||||
}
|
||||
|
||||
public function testNoRedirectOutsideSingularFrontEndRequests(): void
|
||||
{
|
||||
Functions\when('is_admin')->justReturn(false);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: Unsupervised Scheduler
|
||||
* Plugin URI: https://git.unsupervised.ca/Unsupervised/unsupervised-scheduler
|
||||
* Description: Instructor/student lesson scheduling for WordPress.
|
||||
* Version: 1.2.1
|
||||
* Version: 1.2.2
|
||||
* Requires at least: 6.2
|
||||
* Requires PHP: 8.1
|
||||
* Author: Unsupervised
|
||||
@@ -21,7 +21,7 @@ if (! defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define('USC_VERSION', '1.2.1');
|
||||
define('USC_VERSION', '1.2.2');
|
||||
define('USC_PLUGIN_FILE', __FILE__);
|
||||
define('USC_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||
define('USC_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
Reference in New Issue
Block a user