Skip to content

Admin-authored Survey Templates and restricted results access #8760

Description

@ywarnier

Admin-authored Survey Templates + Per-survey "Results hidden from teacher/tutor" flag

Note: this description still needs a review.

Surveys can now be dissociated from courses (they don't require a c_id to work), which is great because if there is one thing that teachers/trainers would love to have, it's a template for a "training satisfaction" or "hot take" survey that they can directly use in their course.
Equally, portal managers would like to be able to send (anonymous) surveys to users that have followed courses, including a way to ask whether they were satisfied with the teacher/trainer, and this, sometimes, can be tricky to deal with.

Implementation plan for two related additions to Chamilo's Survey tool:

  1. Admin-authored, portal-wide survey templates (not attached to any course/session), usable by teachers via a new "Use template" dropdown when creating a survey, and instantiable by admins directly into a chosen course or session-course from the admin panel.
  2. A new per-survey boolean, "Results hidden from teacher/tutor" (default false), visible and editable only by admins, which — when true — blocks teachers/coaches from viewing that survey's results while still letting them see the response count and manage reminders. There already exists a "visible_results" field but this is probably used for some other purpose. Make sure of that before implementing something completely different.

Note: additionally also define a visibility to the survey template, that will allow admins to decide whether it is visible as a template.

This document describes what to build, not how to write the code.


0. Assumptions, corrections, and scope calls made while planning this

Stated explicitly per project convention (state assumptions, surface conflicts rather than
silently picking one):

  • Terminology correction: the request refers to attaching a survey to a course via
    "c_resource_link". No such table/class exists in the current (2.0) codebase — that name
    matches Chamilo 1.x. The actual 2.0 mechanism is Chamilo\CoreBundle\Entity\ResourceLink
    (table resource_link), created via AbstractResource::addCourseLink(Course $course, ?Session $session, ...).
    CSurvey (src/CourseBundle/Entity/CSurvey.php) already extends AbstractResource and is
    scoped to a course/session entirely through a ResourceLink on its ResourceNode
    there is no CSurveyRelCourse-style join table to reason about. This plan uses the real
    mechanism throughout.
  • "Admin" means ROLE_ADMIN only, not session admins, for both template authoring and
    the results-hiding flag — the request says "only admins," and this is the strictest
    reading. If delegated session-admin access to either capability turns out to be wanted,
    that's a small, additive change to the security expressions below, not a redesign.
  • v1 templates support the "normal" survey type only (CSurvey::$surveyType = 0), not
    the meeting-poll (3) or personality/conditional (1) variants, which have enough extra
    moving parts (date-slot voting, question branching) to deserve their own follow-up rather
    than complicating the first version of templating.
  • A survey instantiated from a template always starts with resultsHiddenFromTeacher = false, regardless of anything on the template — the flag is explicitly something an
    admin sets per real survey, inside the course, per the request's own description of the
    workflow (§5, §6). Templates do not carry this flag.
  • Naming collision to be aware of: CSurvey already has a legacy string column literally
    named template (set to the constant 'template' on every create/copy — a dead leftover,
    not a real "is this a template" flag) and an isShared column. Neither is related to this
    feature. Do not repurpose them; the new template concept is a separate entity (§1), to
    avoid tangling new logic into an already-legacy, poorly-named column.
  • Bundle placement: the new template entities are placed in CourseBundle alongside
    CSurvey/CSurveyQuestion/CSurveyQuestionOption (share the same question-type
    vocabulary and will be maintained by whoever maintains the survey tool), even though, like
    Chamilo\LtiBundle\Entity\ExternalTool, they are not course-scoped and are parented to
    the platform's AccessUrl instead of a Course. ExternalTool is the existing precedent
    for a CourseBundle/bundle-scoped-but-not-course-scoped AbstractResource — see §1.1.

1. Data model

1.1 New entities

CSurveyTemplate (src/CourseBundle/Entity/CSurveyTemplate.php, table c_survey_template)

Extends AbstractResource, implements ResourceInterface and
Chamilo\CoreBundle\Entity\ResourceToRootInterface — the same interface
Chamilo\LtiBundle\Entity\ExternalTool implements to get its ResourceNode parented directly
to the current AccessUrl instead of to a Course (ResourceListener::prePersist() already
special-cases ResourceToRootInterface this way — no listener changes needed).

Field Type Notes
iid int PK, matches CSurvey's iid-as-PK convention
accessUrl AccessUrl, nullable portal scoping for multi-URL installs, same nullable-fallback convention as TicketProject
title string(255)
subtitle text, nullable mirrors CSurvey::$subtitle
intro text, nullable mirrors CSurvey::$intro
surveyThanks text, nullable mirrors CSurvey::$surveyThanks
anonymous string, default '0' mirrors CSurvey::$anonymous
shuffle bool
oneQuestionPerPage bool
displayQuestionNumber bool
isMandatory bool
duration int, nullable
createdBy User (FK)
createdAt / updatedAt datetime

Deliberately excluded versus CSurvey: availFrom/availTill, invited/answered
(meaningless before instantiation), code (regenerated fresh per instantiation, see §2, to
avoid collisions), the nested-tree lft/rgt/lvl/surveyParent grouping (templates aren't
grouped the way live surveys can be), surveyType (fixed at 0, see §0).

CSurveyTemplateQuestion (table c_survey_template_question)

Field Type Notes
iid int PK
template CSurveyTemplate (FK, cascade delete)
question text mirrors CSurveyQuestion::$surveyQuestion
comment text, nullable mirrors CSurveyQuestion::$surveyQuestionComment
type string same vocabulary as CSurveyQuestion::$type (yesno, multiplechoice, multipleresponse, dropdown, open, comment, score, percentage, multiplechoiceother, selectivedisplay, pagebreak)
sort int
isRequired bool
maxValue int, nullable relevant for score type

Excluded versus CSurveyQuestion: sharedQuestionId, surveyGroupPri/Sec1/Sec2 (all
belong to the nested-tree "grouped questions across surveys" feature templates don't need),
display (a live-answering concern).

CSurveyTemplateQuestionOption (table c_survey_template_question_option)

Field Type Notes
iid int PK
templateQuestion CSurveyTemplateQuestion (FK, cascade delete)
optionText string(255)
sort int
value string, nullable mirrors CSurveyQuestionOption::$value

1.2 CSurvey changes

Add two columns to the existing c_survey table:

Field Type Notes
resultsHiddenFromTeacher bool, default false the new flag (§6)
sourceTemplate CSurveyTemplate, nullable (FK, ON DELETE SET NULL) optional addition beyond the literal request — a one-column, low-cost way for admins to later see "which surveys came from which template." Drop this if the implementer wants to keep the change smaller; nothing else in this plan depends on it.

1.3 Migration

One new file in src/CoreBundle/Migrations/Schema/V200/:

  • Create c_survey_template, c_survey_template_question, c_survey_template_question_option
    (with the same resource_node_id FK column pattern c_survey itself uses, since these are
    AbstractResource entities too).
  • Alter c_survey: add results_hidden_from_teacher TINYINT(1) NOT NULL DEFAULT 0 and
    (if kept) source_template_id INT NULL with its FK.
  • No settings/fixture change is needed for this feature — nothing here is behind an on/off
    toggle; unlike the earlier Support Bot feature, nothing in this request asks for one, so
    none is introduced.

2. Shared instantiation logic

New service: src/CourseBundle/Service/SurveyTemplateService.php (or
src/CoreBundle/Service/Survey/SurveyTemplateService.php if the team prefers survey services
centralized in CoreBundle alongside the existing SurveyConfigurationProcessor etc. — either
location is consistent with existing precedent, the CourseBundle placement mirrors §1's
choice; pick one, don't split the feature across both without reason).

instantiate(CSurveyTemplate $template, Course $targetCourse, ?Session $targetSession, User $actingUser): CSurvey

This mirrors, almost field-for-field, what SurveyCopyProcessor::copySurvey() /
copyQuestions() (src/CoreBundle/State/Survey/SurveyCopyProcessor.php) already does when
copying a live survey from one course to another — reuse that file's code-generation and
question/option-copy helpers rather than re-implementing them
, since the shape of the work
(new unique code, copy question rows in order, copy option rows per question, reset
counters) is identical; only the source type differs (CSurveyTemplate* instead of another
CSurvey's rows).

Steps:

  1. Build a new CSurvey: copy title, subtitle, intro, surveyThanks, anonymous,
    shuffle, oneQuestionPerPage, displayQuestionNumber, isMandatory, duration from the
    template; generate a fresh unique code (reuse SurveyCopyProcessor's helper); set
    invited = 0, answered = 0, surveyType = 0, resultsHiddenFromTeacher = false,
    sourceTemplate = $template (if kept, §1.2).
  2. $survey->setParent($targetCourse)->addCourseLink($targetCourse, $targetSession).
  3. Persist via CSurveyRepository::create($survey) (same ResourceRepository::create()
    persist()+flush() used everywhere else in the resource system).
  4. For each CSurveyTemplateQuestion (in sort order): create a CSurveyQuestion linked to
    the new survey, copying questionsurveyQuestion, commentsurveyQuestionComment,
    type, sort, isRequired, maxValue; for each of its
    CSurveyTemplateQuestionOption rows, create a matching CSurveyQuestionOption.

This one service is the single place both call sites below invoke — no duplicated
copy-logic between the teacher-facing and admin-facing paths.


3. Admin authoring UI for templates

3.1 API surface (mirrors the Survey module's own List/Provider/Processor split)

Concern Pattern New files
List templates (dropdown source + admin list) ApiResource + Provider ApiResource/Survey/SurveyTemplateList.php, State/Survey/SurveyTemplateListProvider.php
Get/create/update one template's fields ApiResource + Provider/Processor ApiResource/Survey/SurveyTemplateConfiguration.php, State/Survey/SurveyTemplateConfigurationProvider.php / ...Processor.php
Manage one template's questions/options ApiResource + Provider/Processor ApiResource/Survey/SurveyTemplateQuestion.php, State/Survey/SurveyTemplateQuestionProvider.php / ...Processor.php — deliberately mirrors SurveyQuestion/SurveyQuestionProvider/Processor file-for-file
Delete a template folded into SurveyTemplateConfigurationProcessor (Delete operation)

All operations: security: "is_granted('ROLE_ADMIN')". SurveyTemplateList's GetCollection
is the one exception with broader read access (see §4 — teachers need to read the list for
the dropdown): security: "is_granted('ROLE_ADMIN') or is_granted('ROLE_CURRENT_COURSE_TEACHER') or is_granted('ROLE_CURRENT_COURSE_SESSION_TEACHER')",
returning only {id, title, subtitle} per template (no admin-only detail) regardless of who
calls it — the fuller SurveyTemplateConfiguration/SurveyTemplateQuestion detail resources
stay ROLE_ADMIN-only.

3.2 Vue views

New directory assets/vue/views/surveytemplate/, deliberately mirroring the shape of
assets/vue/views/survey/:

  • SurveyTemplateList.vueBaseTable: title, subtitle, question count, updated date. Row
    actions: edit (secondary-text/pencil), edit questions, delete (danger-text/delete,
    behind useConfirmation), "Assign to course" (see §5). Header action: "+ Create"
    (success).
  • SurveyTemplateEdit.vue — same field set/layout as SurveyConfigurationView.vue's "Basic
    information" + "Survey behavior" cards, minus availability dates and the fields excluded in
    §1.1. "Edit questions" button leads to:
  • SurveyTemplateQuestions.vue — mirrors SurveyQuestionsView.vue's question builder
    (add/reorder/delete question, per-type option editor), operating on
    CSurveyTemplateQuestion/Option instead of the live equivalents.

3.3 Routing

Same simplification as prior admin-only features in this codebase: nest under the existing
/admin/{vueRouting} catch-all (already registered in IndexController), so no new Symfony
route is needed. Add to assets/vue/router/admin.js's children:

{
  name: "SurveyTemplateList",
  path: "survey-templates",
  meta: { requiresAdmin: true, showBreadcrumb: true, breadcrumb: "Survey templates" },
  component: () => import("../views/surveytemplate/SurveyTemplateList.vue"),
},
{
  name: "SurveyTemplateCreate",
  path: "survey-templates/create",
  meta: { requiresAdmin: true, showBreadcrumb: true, breadcrumb: "Create template" },
  component: () => import("../views/surveytemplate/SurveyTemplateEdit.vue"),
},
{
  name: "SurveyTemplateEdit",
  path: "survey-templates/:id(\\d+)/edit",
  meta: { requiresAdmin: true, showBreadcrumb: true, breadcrumb: "Edit template" },
  component: () => import("../views/surveytemplate/SurveyTemplateEdit.vue"),
},
{
  name: "SurveyTemplateQuestions",
  path: "survey-templates/:id(\\d+)/questions",
  meta: { requiresAdmin: true, showBreadcrumb: true, breadcrumb: "Template questions" },
  component: () => import("../views/surveytemplate/SurveyTemplateQuestions.vue"),
},

Add an admin menu entry (mirroring the existing item-ticket-system block) in
src/CoreBundle/Controller/Admin/IndexBlocksController.php, pointing at
/admin/survey-templates.


4. Teacher-facing: "Use template" dropdown

File: assets/vue/views/survey/SurveyConfigurationView.vue — add a "Use template"
BaseSelect in the header block, v-if="!isEditMode" (create only — matches where the
existing "Back to survey list"/"Edit questions" buttons already sit).

Behavior, chosen to avoid the client having to submit full question payloads (source of truth
for template questions stays server-side, see below):

  1. On mount (create mode), fetch GET /api/survey-template/list (SurveyTemplateList,
    {id, title, subtitle}[]) to populate the dropdown — one extra lightweight call, no schema
    change to the existing create-configuration response.
  2. When the teacher picks a template, fetch GET /api/survey-template/configuration/{id}
    (SurveyTemplateConfiguration, admin-only for writes but this Get operation is
    readable by any teacher — same broadened-read pattern as §3.1's list) and use its fields to
    pre-fill the local form model only (title, subtitle, intro, thanks message, anonymous/
    shuffle/one-question-per-page/display-question-number/mandatory/duration). The teacher can
    still edit any of these before saving, same as any other create-form default.
  3. On save, the create payload to SurveyConfiguration's Post operation includes the chosen
    templateId (nullable int) alongside whatever field values are currently in the form
    (template defaults, teacher-edited, or a mix).
  4. SurveyConfigurationProcessor::createSurvey() (src/CoreBundle/State/Survey/SurveyConfigurationProcessor.php):
    when templateId is present, call SurveyTemplateService::instantiate() (§2) targeting
    the current course/session context (resolved the same way this processor already resolves
    cid/sid for a normal create) to get a fully-formed CSurvey with its questions
    already copied
    , then apply the submitted form fields on top (title/dates/etc., in case
    the teacher edited them after picking the template) before the final persist. When
    templateId is absent, behavior is exactly what it is today (blank survey, no questions).

Net effect for the teacher: picking a template and saving produces a survey that already has
all of the template's questions built — they land on the survey list (or straight into
"Edit questions" if that's this view's existing post-save redirect) with nothing further
required, but nothing stops them from still editing/adding/removing questions afterward.


5. Admin: instantiate a template directly into a course or session-course

New endpoint: src/CoreBundle/Controller/Api/SurveyTemplateAssignController.php
(#[AsController], #[Route('/api/survey-template/assign')], #[IsGranted('ROLE_ADMIN')]),
POST body { templateId, courseId, sessionId? }. Validates:

  • templateId resolves to an existing CSurveyTemplate (scoped to the current AccessUrl
    if set, same nullable-fallback lookup as TicketProject).
  • courseId resolves to an existing Course.
  • if sessionId given, it must actually be linked to that course (a genuine
    session-course pair) — reject otherwise, don't silently create a survey in an
    unrelated combination.

Calls SurveyTemplateService::instantiate() (§2) with the resolved course/session and the
current admin user, and returns a confirmation payload
{ surveyId, surveyTitle, courseTitle, sessionTitle? }no editing capability from this
response
, per the request's explicit constraint (§0, next paragraph).

CSRF: same convention as everything else in this project —
SurveyTemplateAssignController::CSRF_TOKEN_ID = 'survey_template_assign', token returned by
SurveyTemplateList's payload and validated on this POST.

5.1 The "admin cannot edit, only assign" constraint

This is enforced simply by not building an edit UI for course-attached surveys in the admin
panel at all
— the admin panel only ever exposes template CRUD (§3) and this one
assign-action (§5), never a view of an individual course-survey's configuration/questions.
To edit a survey once it's in a course (including to set §6's flag), an admin has to
navigate into that course and use the same SurveyConfigurationView.vue a teacher would use
— which already grants ROLE_ADMIN full access via the course-context security expressions
every other survey operation already uses (is_granted('ROLE_ADMIN') is already an implicit
allow across Symfony's role hierarchy for any of the ROLE_CURRENT_COURSE_*-gated
operations, since admins pass every is_granted() check in this codebase's existing pattern
— confirm this holds for SurveyConfiguration's current security expression, which today is
ROLE_CURRENT_COURSE_TEACHER or ROLE_CURRENT_COURSE_SESSION_TEACHER only, not yet
including ROLE_ADMIN explicitly — see §6.2 for the exact expression change needed so an
admin who navigates into the course can actually open the edit form at all).

5.2 UI

New view or a dialog on SurveyTemplateList.vue (a "Assign to course" row action, BaseDialog
with: template name shown read-only, BaseSelect for course (searchable/autocomplete —
reuse whatever component UsergroupAddCourses.vue uses for its course picker for UI
consistency), a second BaseSelect for session (populated once a course is chosen, listing
only sessions that course actually belongs to; optional — leaving it blank targets the base
course), and a "Create survey" button. On success, show the confirmation payload's info (new
survey title + where it was created) — no link into an admin-side editor, since none exists
(§5.1); optionally a plain informational note "To edit this survey, open it from inside the
course."


6. "Results hidden from teacher/tutor" flag

6.1 Field visibility/editability — admin-only, inside the course edit form

File: assets/vue/views/survey/SurveyConfigurationView.vue — add a BaseCheckbox
"Results hidden from teacher/tutor" in the "Survey behavior" card, v-if="isAdmin" (a new
boolean the component needs from the backend, analogous to how it already knows isEditMode/
canEdit).

Backend, read side: SurveyConfigurationProvider (src/CoreBundle/State/Survey/SurveyConfigurationProvider.php)
adds resultsHiddenFromTeacher and isAdmin ($this->security->isGranted('ROLE_ADMIN')) to
the DTO it builds for both create-defaults and edit-mode responses.

Backend, write side — this is the actual access-control point, not the v-if:
SurveyConfigurationProcessor::updateSurvey() (and createSurvey(), in case a non-admin
client crafts a raw request including the field) must only apply
$survey->setResultsHiddenFromTeacher(...) when $this->security->isGranted('ROLE_ADMIN')
— silently ignore the field otherwise (do not error the whole request over it; a teacher's
payload simply won't include the checkbox, so silently dropping an unexpected value if
someone crafts one by hand is the correct behavior, not a hard failure). This is a
mass-assignment defense: hiding the field in Vue is not sufficient on its own (§9).

6.2 Security expression fix needed for admins to reach the edit form at all

Per §5.1, admins need to be able to open SurveyConfiguration's edit operation from inside a
course. Confirm/update its security: attribute
(src/CoreBundle/ApiResource/Survey/SurveyConfiguration.php) to explicitly include
or is_granted('ROLE_ADMIN') alongside the existing
is_granted('ROLE_CURRENT_COURSE_TEACHER') or is_granted('ROLE_CURRENT_COURSE_SESSION_TEACHER')
if it doesn't already implicitly resolve true for admins — verify against the actual current
attribute rather than assume, since this plan's research pass did not confirm an explicit
ROLE_ADMIN branch on this specific operation (only on SurveyReporting and
SurveyInvitation, which do have one).


7. Effect of the flag when true

Two existing, already-identified insertion points in the current codebase (no new gating
mechanism/Voter needed — consistent with how the existing platform-wide
survey.hide_survey_reporting_button setting already gates the same two places):

7.1 Server-side gate (the actual security boundary)

File: SurveyReportingProvider::assertCanViewReporting()
(src/CoreBundle/State/Survey/SurveyReportingProvider.php). Add the per-survey check
alongside the existing platform-wide one, and make sure it also blocks the coach/tutor
bypass that setting currently allows (the request says "teacher/tutor", so both must be
blocked when this flag is set — unlike the platform-wide setting, which can be relaxed for
coaches via survey.extend_rights_for_coach_on_survey):

if ($survey->isResultsHiddenFromTeacher() && !$this->security->isGranted('ROLE_ADMIN')) {
    throw new AccessDeniedHttpException(
        "This survey's results are only accessible to authorized roles."
    );
}

placed before the existing isReportingHidden() check (order doesn't change behavior
here, but keeping the per-survey, always-strict check separate from the
platform-wide-with-coach-exception check keeps the two concerns from being conflated in one
conditional). This single function already guards every reporting entry point — the
SurveyReporting Get operation itself, and all of exportCsv/exportXlsx/
exportByClassXlsx/exportPackageZip — so gating here covers direct URL access and every
export format in one place, not just the main Vue page.

7.2 UI: hide the tracking icon, show the message

File: SurveyListProvider::normalizeTeacherSurvey()
(src/CoreBundle/State/Survey/SurveyListProvider.php). Extend the existing $canReport
computation:

$canReport = !$hideReportingButton
    && !$isUnsupportedPersonality
    && (!$survey->isResultsHiddenFromTeacher() || $this->security->isGranted('ROLE_ADMIN'));

Also return a distinct resultsHiddenFromTeacher boolean in the row payload (safe to expose
— knowing results are restricted isn't sensitive) so SurveyListView.vue can tell why
canReport is false and render accordingly: when canReport is false because of this
flag specifically
(not the platform-wide setting), show a muted/disabled icon in the slot
the reporting icon normally occupies, with a tooltip/inline text reading exactly:

This survey's results are only accessible to authorized roles.

File: SurveyReportingView.vue — if a teacher reaches this route directly anyway
(bypassing the hidden icon, e.g. via a stale bookmark), the GET call now 403s per §7.1;
render the same message as a full-page notice instead of the results table, rather than a
generic error screen.

7.3 What stays available to teachers/tutors regardless of the flag

SurveyInvitationsView.vue (route SurveyInvitations, already showing Invited/Answered/
Pending counts and reminder controls, separate from the reporting page — see research: this
page already exists and is exactly the "counts + reminders only" surface the request asks
for) is unaffected — no gating changes needed there, since it never exposed
per-question results in the first place. Optionally add a small info banner on this page when
resultsHiddenFromTeacher is true, reassuring the teacher that response-tracking and
reminders still work even though full results don't — a small UX nicety, not a requirement.


8. Security checklist (per CLAUDE.md Rule 13 — OWASP)

Concern Where it applies Mitigation
CSRF Template CRUD/questions (SurveyTemplateConfigurationProcessor, SurveyTemplateQuestionProcessor), admin assign action (SurveyTemplateAssignController) Same CsrfTokenManagerInterface pattern used across the Survey module already (SurveyCsrfTokenValidationTrait); dedicated token id survey_template_admin / survey_template_assign
Broken access control All template-mutation and assign endpoints ROLE_ADMIN only, verified server-side on every operation — not just hidden in the Vue admin nav
Broken access control (flag) SurveyConfigurationProcessor resultsHiddenFromTeacher is only ever applied to the entity when is_granted('ROLE_ADMIN'); a non-admin request that includes the field in its body has it silently dropped, never applied — the Vue v-if is a convenience, not the actual control
Broken access control (reporting) SurveyReportingProvider::assertCanViewReporting() Blocks ROLE_CURRENT_COURSE_TEACHER and ROLE_CURRENT_COURSE_SESSION_TEACHER alike when the per-survey flag is set (no coach-extend-rights bypass for this specific gate, unlike the platform-wide setting); ROLE_ADMIN always bypasses
SQL injection SurveyTemplateListProvider filters/sorting (if any search/sort is added to the admin list) Bound parameters throughout; sort field allowlist map, same pattern as SurveyListProvider/TicketListProvider
Mass parameter manipulation SurveyTemplateAssignController courseId/sessionId cast with (int), and the (course, session) pair is validated to actually belong together before instantiation — an admin (or a crafted request) cannot attach a survey to a session that doesn't belong to the given course
XSS CSurveyTemplate/CSurveyTemplateQuestion free-text fields (title, subtitle, intro, question text, option labels), same as the equivalent live-survey fields already are No behavior change needed — Vue's {{ }}/bound-value rendering already auto-escapes these on the live survey views this feature reuses; verify the new template views follow the same binding style (no v-html) rather than introducing a new rendering path

9. Testing plan

9.1 PHPUnit

  • SurveyTemplateService::instantiate(): produces a CSurvey with all mirrored fields
    copied correctly, a fresh unique code, invited/answered reset to 0,
    resultsHiddenFromTeacher always false regardless of any pre-existing state, correctly
    ResourceLink-attached to the given course (and, separately, to a given session-course
    pair), and all questions/options copied in order with types/values intact.
  • SurveyTemplateConfigurationProcessor/SurveyTemplateQuestionProcessor: non-admin requests
    rejected (403); CSRF-missing/invalid requests rejected on every mutating operation.
  • SurveyTemplateAssignController: rejects an unrelated (courseId, sessionId) pair; rejects
    a non-existent templateId; happy path creates exactly one survey, resource-linked to the
    right course/session, with the right questions.
  • SurveyConfigurationProcessor: a create payload with templateId produces a survey whose
    questions match the template; a create/update payload from a simulated non-admin user that
    includes resultsHiddenFromTeacher: true is persisted with the field still false
    (silently ignored, not erroring the whole request); the same payload from a simulated admin
    correctly sets it.
  • SurveyReportingProvider::assertCanViewReporting(): teacher and session-teacher/coach both
    denied (403) when the survey's flag is true; admin allowed regardless; unaffected when the
    flag is false (existing behavior/tests must still pass).
  • SurveyListProvider::normalizeTeacherSurvey(): canReport is false for a teacher when
    the survey's flag is true, true for an admin viewing the same row.

9.2 Behat (tests/behat/features/survey/, extending the existing survey feature files if

present, or a new survey-templates.feature mirroring the view directory structure)

Per the project's mandatory Behat rule (every new feature/interface, all CRUD interactions,
once per role that has access, explicit deny-scenario for roles that shouldn't):

  • manage-survey-templates.feature (admin only): create a template with a couple of
    questions, edit it, delete it. Include a scenario that a non-admin (teacher, student)
    cannot reach /admin/survey-templates (denied/redirected).
  • use-survey-template.feature, run as a teacher: open the "create survey" form inside a
    course, pick a template from "Use template," save, and verify the resulting survey has the
    template's questions already present.
  • assign-survey-template.feature (admin only): assign a template to a base course,
    verify the survey appears there; separately assign to a session-course, verify it appears
    scoped to that session; verify there is no admin-panel path to edit that survey's content
    (only re-assign/create-another); log in to the course as that admin and verify the survey
    can be edited from there, including seeing and changing the new flag.
  • hide-survey-results.feature, run once as a teacher and once as a session
    coach/tutor
    : as admin, set the flag true on a survey; log in as teacher/tutor and verify
    — no reporting/tracking icon in the survey list, the exact message when navigating directly
    to the reporting route, the invitations/counts + reminders page still works normally. Then,
    separately, verify a student creating/editing a survey never sees the flag at all (it's
    not present in their form, since students can't create surveys in the first place — confirm
    this is still gated the same way it already is today, unrelated to this feature).
  • A scenario confirming a teacher's survey edit form never renders or accepts the flag
    (field absent from the form; a raw submitted value, if crafted, has no effect — ties back
    to the PHPUnit coverage above, but Behat should confirm the field is simply not in the
    rendered form for this role).

Every new form control needs a name attribute per the project rule so these steps can use
I fill in "name" with "value" / I select "option" from "name". Each feature file creates
and tears down its own template/survey/course-assignment state.


10. Rollout notes / suggested build order

  • No settings/feature flag gates this — it ships enabled, matching how the existing Survey
    tool itself isn't behind a toggle. Nothing is changed for installs that never create a
    template: the "Use template" dropdown is simply empty/hidden if SurveyTemplateList
    returns zero rows (recommend hiding the dropdown entirely rather than showing an empty
    select — a currently-templateless portal shouldn't show new, confusing UI for nothing).
  • No backward-compatibility concerns: CSurvey's existing rows all get
    resultsHiddenFromTeacher = false by the migration's column default; nothing else about
    existing surveys changes shape.
  • Suggested build order (each step independently testable before the next):
    1. Migration (§1.3) + entities (§1.1, §1.2) — no UI yet.
    2. SurveyTemplateService::instantiate() (§2), tested via PHPUnit only.
    3. Admin template CRUD — API (§3.1) + Vue views (§3.2/§3.3) — an admin can now author a
      template end-to-end.
    4. Admin "assign to course" (§5) — an admin can now put a template-based survey into a real
      course, and confirm from inside that course that it's a normal, fully-editable survey.
    5. The resultsHiddenFromTeacher flag (§6) + its reporting/list gates (§7) — independent of
      templates entirely, could in fact be built and shipped before §3–§5 if sequencing
      the higher-risk security-relevant piece first is preferred.
    6. Teacher-facing "Use template" dropdown (§4) — last, since it's the piece that most
      depends on everything above already working.
    7. Behat coverage for all of the above, incrementally alongside each step rather than only
      at the end.

11. Explicitly out of scope / open questions for whoever picks this up

  • Batch-assign a template to many courses/sessions in one admin action (the request says
    "a given" course, singular — §5's UI can be used repeatedly; a dual-list bulk picker like
    UsergroupAddCourses.vue's is a natural v2 if this turns out to be a common admin workflow).
  • Templates for meeting-poll / personality survey types (§0) — deferred, not designed
    here at all.
  • Session-admin access to template authoring or the results-hiding flag — currently
    scoped to ROLE_ADMIN only per the request's literal wording (§0); revisit if that's too
    strict in practice.
  • sourceTemplate traceability field (§1.2) — included as a low-cost nice-to-have, not a
    requirement; safe to drop without affecting anything else in this plan.
  • Whether templates themselves should ever be editable after being used in a way that
    offers to "update all surveys created from this template" — not requested, and would be a
    meaningfully bigger feature (propagating an edit across N already-independent, already
    possibly-answered live surveys); this plan treats instantiation as a one-time copy with no
    ongoing link back to the template's content (only, optionally, its iid for audit).

12. Create multi-lingual demo "training satisfaction" survey template that ships with Chamilo

Create the fixtures to have one default, typical "training satisfaction" survey and make sure it's available to (and can be modified by) the admins when installing/upgrading Chamilo. It should be available to teachers/trainers as a template for their courses.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions