Add studio-defined account-registration questions
CI / Tests (PHP 8.2) (pull_request) Successful in 44s
CI / Tests (PHP 8.1) (pull_request) Successful in 45s
CI / No Debug Code (pull_request) Successful in 2s
CI / Coding Standards (pull_request) Successful in 2m46s
CI / PHPStan (pull_request) Successful in 2m58s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m41s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.2) (pull_request) Successful in 44s
CI / Tests (PHP 8.1) (pull_request) Successful in 45s
CI / No Debug Code (pull_request) Successful in 2s
CI / Coding Standards (pull_request) Successful in 2m46s
CI / PHPStan (pull_request) Successful in 2m58s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m41s
CI / Build Plugin Zip (pull_request) Skipped
Studio admins can now define registration questions that every new student answers as a required second step during signup, with each student's answers shown under a "Registration Information" section in the admin. Extends the existing Registration domain: us_questions gains a scope column (offering | account) and a nullable offering_id, and account answers reuse us_question_answers with registration_type = 'account'. Authoring reuses the Offerings -> Questions page via an "Account signup" scope (studio-admin only). The registration form becomes two steps (progressive enhancement via assets/js/register.js; works without JS); required answers are validated before the account is created and apply to all signup paths (invite, group link, self-approval). StudentHistory::registrationInfo() powers the admin section. Bumps the plugin version to 1.1.0 so dbDelta runs the schema migration. Closes #90 Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
@@ -23,8 +23,13 @@ class QuestionController {
|
||||
$userId = get_current_user_id();
|
||||
$manageAll = current_user_can( RoleManager::CAP_MANAGE_INSTRUCTORS );
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only offering selector.
|
||||
$offeringId = absint( Val::int( $_GET['offering_id'] ?? 0 ) );
|
||||
// The selector posts either an offering id or the sentinel `account`.
|
||||
// Account-signup questions are studio-wide, so only studio admins manage them.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only selector.
|
||||
$selection = sanitize_text_field( Val::string( wp_unslash( $_GET['offering_id'] ?? '' ) ) );
|
||||
$accountScope = $manageAll && Question::SCOPE_ACCOUNT === $selection;
|
||||
|
||||
$offeringId = $accountScope ? 0 : absint( Val::int( $selection ) );
|
||||
$offeringList = $manageAll ? $this->offerings->findAll() : $this->offerings->findAll( $userId );
|
||||
$selectedOffering = $offeringId > 0 ? $this->offerings->findById( $offeringId ) : null;
|
||||
|
||||
@@ -33,7 +38,13 @@ class QuestionController {
|
||||
}
|
||||
|
||||
$questions = null;
|
||||
if ( null !== $selectedOffering ) {
|
||||
if ( $accountScope ) {
|
||||
if ( isset( $_POST['usc_action'] ) && check_admin_referer( 'usc_question_action' ) ) {
|
||||
$this->handleFormAction( null );
|
||||
}
|
||||
|
||||
$questions = $this->questions->findByScope( Question::SCOPE_ACCOUNT );
|
||||
} elseif ( null !== $selectedOffering ) {
|
||||
if ( isset( $_POST['usc_action'] ) && check_admin_referer( 'usc_question_action' ) ) {
|
||||
$this->handleFormAction( $selectedOffering );
|
||||
}
|
||||
@@ -44,20 +55,24 @@ class QuestionController {
|
||||
include USC_PLUGIN_DIR . 'templates/admin/questions.php';
|
||||
}
|
||||
|
||||
private function handleFormAction( Offering $offering ): void {
|
||||
/**
|
||||
* Handle an add/delete action for the given context: an offering, or account
|
||||
* scope when $offering is null.
|
||||
*/
|
||||
private function handleFormAction( ?Offering $offering ): void {
|
||||
// Nonce is verified by the caller (renderPage) before this method runs.
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||
$action = sanitize_key( Val::string( wp_unslash( $_POST['usc_action'] ?? '' ) ) );
|
||||
|
||||
if ( 'add' === $action ) {
|
||||
$this->addQuestion( (int) $offering->id );
|
||||
$this->addQuestion( $offering );
|
||||
}
|
||||
|
||||
if ( 'delete' === $action ) {
|
||||
$questionId = absint( Val::int( $_POST['question_id'] ?? 0 ) );
|
||||
if ( $questionId > 0 ) {
|
||||
$question = $this->questions->findById( $questionId );
|
||||
if ( $question && $question->offeringId === (int) $offering->id ) {
|
||||
if ( $question && $this->belongsToContext( $question, $offering ) ) {
|
||||
$this->questions->delete( $questionId );
|
||||
}
|
||||
}
|
||||
@@ -65,7 +80,7 @@ class QuestionController {
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
private function addQuestion( int $offeringId ): void {
|
||||
private function addQuestion( ?Offering $offering ): void {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||
$label = sanitize_text_field( Val::string( wp_unslash( $_POST['label'] ?? '' ) ) );
|
||||
$fieldType = sanitize_key( Val::string( wp_unslash( $_POST['field_type'] ?? Question::FIELD_TEXT ) ) );
|
||||
@@ -76,17 +91,30 @@ class QuestionController {
|
||||
|
||||
$this->questions->insert(
|
||||
new Question(
|
||||
offeringId: $offeringId,
|
||||
offeringId: null === $offering ? null : (int) $offering->id,
|
||||
label: $label,
|
||||
fieldType: $fieldType,
|
||||
options: $this->parseOptions( sanitize_textarea_field( Val::string( wp_unslash( $_POST['options'] ?? '' ) ) ) ),
|
||||
isRequired: isset( $_POST['is_required'] ),
|
||||
sortOrder: absint( Val::int( $_POST['sort_order'] ?? 0 ) ),
|
||||
scope: null === $offering ? Question::SCOPE_ACCOUNT : Question::SCOPE_OFFERING,
|
||||
)
|
||||
);
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a question belongs to the current editing context — the given
|
||||
* offering, or account scope when $offering is null.
|
||||
*/
|
||||
private function belongsToContext( Question $question, ?Offering $offering ): bool {
|
||||
if ( null === $offering ) {
|
||||
return Question::SCOPE_ACCOUNT === $question->scope;
|
||||
}
|
||||
|
||||
return $question->offeringId === (int) $offering->id;
|
||||
}
|
||||
|
||||
private function canManageOffering( Offering $offering, int $userId, bool $manageAll ): bool {
|
||||
return $manageAll || $offering->instructorId === $userId;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user