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:
- 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.
- 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:
- 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).
$survey->setParent($targetCourse)->addCourseLink($targetCourse, $targetSession).
- Persist via
CSurveyRepository::create($survey) (same ResourceRepository::create() →
persist()+flush() used everywhere else in the resource system).
- For each
CSurveyTemplateQuestion (in sort order): create a CSurveyQuestion linked to
the new survey, copying question→surveyQuestion, comment→surveyQuestionComment,
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.vue — BaseTable: 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):
- 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.
- 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.
- 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).
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):
- Migration (§1.3) + entities (§1.1, §1.2) — no UI yet.
SurveyTemplateService::instantiate() (§2), tested via PHPUnit only.
- Admin template CRUD — API (§3.1) + Vue views (§3.2/§3.3) — an admin can now author a
template end-to-end.
- 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.
- 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.
- Teacher-facing "Use template" dropdown (§4) — last, since it's the piece that most
depends on everything above already working.
- 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.
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:
false), visible and editable only by admins, which — whentrue— 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):
"
c_resource_link". No such table/class exists in the current (2.0) codebase — that namematches Chamilo 1.x. The actual 2.0 mechanism is
Chamilo\CoreBundle\Entity\ResourceLink(table
resource_link), created viaAbstractResource::addCourseLink(Course $course, ?Session $session, ...).CSurvey(src/CourseBundle/Entity/CSurvey.php) already extendsAbstractResourceand isscoped to a course/session entirely through a
ResourceLinkon itsResourceNode—there is no
CSurveyRelCourse-style join table to reason about. This plan uses the realmechanism throughout.
ROLE_ADMINonly, not session admins, for both template authoring andthe 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.
CSurvey::$surveyType = 0), notthe meeting-poll (
3) or personality/conditional (1) variants, which have enough extramoving parts (date-slot voting, question branching) to deserve their own follow-up rather
than complicating the first version of templating.
resultsHiddenFromTeacher = false, regardless of anything on the template — the flag is explicitly something anadmin sets per real survey, inside the course, per the request's own description of the
workflow (§5, §6). Templates do not carry this flag.
CSurveyalready has a legacy string column literallynamed
template(set to the constant'template'on every create/copy — a dead leftover,not a real "is this a template" flag) and an
isSharedcolumn. Neither is related to thisfeature. 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.
CSurvey/CSurveyQuestion/CSurveyQuestionOption(share the same question-typevocabulary 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 tothe platform's
AccessUrlinstead of aCourse.ExternalToolis the existing precedentfor a
CourseBundle/bundle-scoped-but-not-course-scopedAbstractResource— see §1.1.1. Data model
1.1 New entities
CSurveyTemplate(src/CourseBundle/Entity/CSurveyTemplate.php, tablec_survey_template)Extends
AbstractResource, implementsResourceInterfaceandChamilo\CoreBundle\Entity\ResourceToRootInterface— the same interfaceChamilo\LtiBundle\Entity\ExternalToolimplements to get itsResourceNodeparented directlyto the current
AccessUrlinstead of to aCourse(ResourceListener::prePersist()alreadyspecial-cases
ResourceToRootInterfacethis way — no listener changes needed).CSurvey'siid-as-PK conventionAccessUrl, nullableTicketProjectCSurvey::$subtitleCSurvey::$introCSurvey::$surveyThanks'0'CSurvey::$anonymousUser(FK)Deliberately excluded versus
CSurvey:availFrom/availTill,invited/answered(meaningless before instantiation),
code(regenerated fresh per instantiation, see §2, toavoid collisions), the nested-tree
lft/rgt/lvl/surveyParentgrouping (templates aren'tgrouped the way live surveys can be),
surveyType(fixed at0, see §0).CSurveyTemplateQuestion(tablec_survey_template_question)CSurveyTemplate(FK, cascade delete)CSurveyQuestion::$surveyQuestionCSurveyQuestion::$surveyQuestionCommentCSurveyQuestion::$type(yesno,multiplechoice,multipleresponse,dropdown,open,comment,score,percentage,multiplechoiceother,selectivedisplay,pagebreak)scoretypeExcluded versus
CSurveyQuestion:sharedQuestionId,surveyGroupPri/Sec1/Sec2(allbelong to the nested-tree "grouped questions across surveys" feature templates don't need),
display(a live-answering concern).CSurveyTemplateQuestionOption(tablec_survey_template_question_option)CSurveyTemplateQuestion(FK, cascade delete)CSurveyQuestionOption::$value1.2
CSurveychangesAdd two columns to the existing
c_surveytable:falseCSurveyTemplate, nullable (FK,ON DELETE SET NULL)1.3 Migration
One new file in
src/CoreBundle/Migrations/Schema/V200/:c_survey_template,c_survey_template_question,c_survey_template_question_option(with the same
resource_node_idFK column patternc_surveyitself uses, since these areAbstractResourceentities too).c_survey: addresults_hidden_from_teacher TINYINT(1) NOT NULL DEFAULT 0and(if kept)
source_template_id INT NULLwith its FK.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(orsrc/CoreBundle/Service/Survey/SurveyTemplateService.phpif the team prefers survey servicescentralized in CoreBundle alongside the existing
SurveyConfigurationProcessoretc. — eitherlocation is consistent with existing precedent, the CourseBundle placement mirrors §1's
choice; pick one, don't split the feature across both without reason).
This mirrors, almost field-for-field, what
SurveyCopyProcessor::copySurvey()/copyQuestions()(src/CoreBundle/State/Survey/SurveyCopyProcessor.php) already does whencopying 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, resetcounters) is identical; only the source type differs (
CSurveyTemplate*instead of anotherCSurvey's rows).Steps:
CSurvey: copytitle,subtitle,intro,surveyThanks,anonymous,shuffle,oneQuestionPerPage,displayQuestionNumber,isMandatory,durationfrom thetemplate; generate a fresh unique
code(reuseSurveyCopyProcessor's helper); setinvited = 0,answered = 0,surveyType = 0,resultsHiddenFromTeacher = false,sourceTemplate = $template(if kept, §1.2).$survey->setParent($targetCourse)->addCourseLink($targetCourse, $targetSession).CSurveyRepository::create($survey)(sameResourceRepository::create()→persist()+flush()used everywhere else in the resource system).CSurveyTemplateQuestion(insortorder): create aCSurveyQuestionlinked tothe new survey, copying
question→surveyQuestion,comment→surveyQuestionComment,type,sort,isRequired,maxValue; for each of itsCSurveyTemplateQuestionOptionrows, create a matchingCSurveyQuestionOption.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)
ApiResource/Survey/SurveyTemplateList.php,State/Survey/SurveyTemplateListProvider.phpApiResource/Survey/SurveyTemplateConfiguration.php,State/Survey/SurveyTemplateConfigurationProvider.php/...Processor.phpApiResource/Survey/SurveyTemplateQuestion.php,State/Survey/SurveyTemplateQuestionProvider.php/...Processor.php— deliberately mirrorsSurveyQuestion/SurveyQuestionProvider/Processorfile-for-fileSurveyTemplateConfigurationProcessor(Deleteoperation)All operations:
security: "is_granted('ROLE_ADMIN')".SurveyTemplateList'sGetCollectionis 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 whocalls it — the fuller
SurveyTemplateConfiguration/SurveyTemplateQuestiondetail resourcesstay
ROLE_ADMIN-only.3.2 Vue views
New directory
assets/vue/views/surveytemplate/, deliberately mirroring the shape ofassets/vue/views/survey/:SurveyTemplateList.vue—BaseTable: title, subtitle, question count, updated date. Rowactions: 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 asSurveyConfigurationView.vue's "Basicinformation" + "Survey behavior" cards, minus availability dates and the fields excluded in
§1.1. "Edit questions" button leads to:
SurveyTemplateQuestions.vue— mirrorsSurveyQuestionsView.vue's question builder(add/reorder/delete question, per-type option editor), operating on
CSurveyTemplateQuestion/Optioninstead 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 inIndexController), so no new Symfonyroute is needed. Add to
assets/vue/router/admin.js'schildren:Add an admin menu entry (mirroring the existing
item-ticket-systemblock) insrc/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"BaseSelectin the header block,v-if="!isEditMode"(create only — matches where theexisting "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):
GET /api/survey-template/list(SurveyTemplateList,{id, title, subtitle}[]) to populate the dropdown — one extra lightweight call, no schemachange to the existing create-configuration response.
GET /api/survey-template/configuration/{id}(
SurveyTemplateConfiguration, admin-only for writes but thisGetoperation isreadable 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.
SurveyConfiguration'sPostoperation includes the chosentemplateId(nullable int) alongside whatever field values are currently in the form(template defaults, teacher-edited, or a mix).
SurveyConfigurationProcessor::createSurvey()(src/CoreBundle/State/Survey/SurveyConfigurationProcessor.php):when
templateIdis present, callSurveyTemplateService::instantiate()(§2) targetingthe current course/session context (resolved the same way this processor already resolves
cid/sidfor a normal create) to get a fully-formedCSurveywith its questionsalready 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
templateIdis 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')]),POSTbody{ templateId, courseId, sessionId? }. Validates:templateIdresolves to an existingCSurveyTemplate(scoped to the currentAccessUrlif set, same nullable-fallback lookup as
TicketProject).courseIdresolves to an existingCourse.sessionIdgiven, it must actually be linked to that course (a genuinesession-course pair) — reject otherwise, don't silently create a survey in an
unrelated combination.
Calls
SurveyTemplateService::instantiate()(§2) with the resolved course/session and thecurrent admin user, and returns a confirmation payload
{ surveyId, surveyTitle, courseTitle, sessionTitle? }— no editing capability from thisresponse, 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 bySurveyTemplateList'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.vuea teacher would use— which already grants
ROLE_ADMINfull access via the course-context security expressionsevery other survey operation already uses (
is_granted('ROLE_ADMIN')is already an implicitallow across Symfony's role hierarchy for any of the
ROLE_CURRENT_COURSE_*-gatedoperations, since admins pass every
is_granted()check in this codebase's existing pattern— confirm this holds for
SurveyConfiguration's current security expression, which today isROLE_CURRENT_COURSE_TEACHER or ROLE_CURRENT_COURSE_SESSION_TEACHERonly, not yetincluding
ROLE_ADMINexplicitly — see §6.2 for the exact expression change needed so anadmin 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,BaseDialogwith: template name shown read-only,
BaseSelectfor course (searchable/autocomplete —reuse whatever component
UsergroupAddCourses.vueuses for its course picker for UIconsistency), a second
BaseSelectfor session (populated once a course is chosen, listingonly 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 aBaseCheckbox"Results hidden from teacher/tutor" in the "Survey behavior" card,
v-if="isAdmin"(a newboolean 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
resultsHiddenFromTeacherandisAdmin($this->security->isGranted('ROLE_ADMIN')) tothe 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()(andcreateSurvey(), in case a non-adminclient 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 acourse. Confirm/update its
security:attribute(
src/CoreBundle/ApiResource/Survey/SurveyConfiguration.php) to explicitly includeor is_granted('ROLE_ADMIN')alongside the existingis_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_ADMINbranch on this specific operation (only onSurveyReportingandSurveyInvitation, which do have one).7. Effect of the flag when
trueTwo 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_buttonsetting 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 checkalongside 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):placed before the existing
isReportingHidden()check (order doesn't change behaviorhere, 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
SurveyReportingGetoperation itself, and all ofexportCsv/exportXlsx/exportByClassXlsx/exportPackageZip— so gating here covers direct URL access and everyexport 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$canReportcomputation:
Also return a distinct
resultsHiddenFromTeacherboolean in the row payload (safe to expose— knowing results are restricted isn't sensitive) so
SurveyListView.vuecan tell whycanReportis false and render accordingly: whencanReportisfalsebecause of thisflag 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:
File:
SurveyReportingView.vue— if a teacher reaches this route directly anyway(bypassing the hidden icon, e.g. via a stale bookmark), the
GETcall 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(routeSurveyInvitations, 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
resultsHiddenFromTeacheris true, reassuring the teacher that response-tracking andreminders still work even though full results don't — a small UX nicety, not a requirement.
8. Security checklist (per CLAUDE.md Rule 13 — OWASP)
SurveyTemplateConfigurationProcessor,SurveyTemplateQuestionProcessor), admin assign action (SurveyTemplateAssignController)CsrfTokenManagerInterfacepattern used across the Survey module already (SurveyCsrfTokenValidationTrait); dedicated token idsurvey_template_admin/survey_template_assignROLE_ADMINonly, verified server-side on every operation — not just hidden in the Vue admin navSurveyConfigurationProcessorresultsHiddenFromTeacheris only ever applied to the entity whenis_granted('ROLE_ADMIN'); a non-admin request that includes the field in its body has it silently dropped, never applied — the Vuev-ifis a convenience, not the actual controlSurveyReportingProvider::assertCanViewReporting()ROLE_CURRENT_COURSE_TEACHERandROLE_CURRENT_COURSE_SESSION_TEACHERalike when the per-survey flag is set (no coach-extend-rights bypass for this specific gate, unlike the platform-wide setting);ROLE_ADMINalways bypassesSurveyTemplateListProviderfilters/sorting (if any search/sort is added to the admin list)SurveyListProvider/TicketListProviderSurveyTemplateAssignControllercourseId/sessionIdcast 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 courseCSurveyTemplate/CSurveyTemplateQuestionfree-text fields (title, subtitle, intro, question text, option labels), same as the equivalent live-survey fields already are{{ }}/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 (nov-html) rather than introducing a new rendering path9. Testing plan
9.1 PHPUnit
SurveyTemplateService::instantiate(): produces aCSurveywith all mirrored fieldscopied correctly, a fresh unique
code,invited/answeredreset to 0,resultsHiddenFromTeacheralwaysfalseregardless of any pre-existing state, correctlyResourceLink-attached to the given course (and, separately, to a given session-coursepair), and all questions/options copied in order with types/values intact.
SurveyTemplateConfigurationProcessor/SurveyTemplateQuestionProcessor: non-admin requestsrejected (403); CSRF-missing/invalid requests rejected on every mutating operation.
SurveyTemplateAssignController: rejects an unrelated(courseId, sessionId)pair; rejectsa non-existent
templateId; happy path creates exactly one survey, resource-linked to theright course/session, with the right questions.
SurveyConfigurationProcessor: a create payload withtemplateIdproduces a survey whosequestions match the template; a create/update payload from a simulated non-admin user that
includes
resultsHiddenFromTeacher: trueis persisted with the field stillfalse(silently ignored, not erroring the whole request); the same payload from a simulated admin
correctly sets it.
SurveyReportingProvider::assertCanViewReporting(): teacher and session-teacher/coach bothdenied (403) when the survey's flag is
true; admin allowed regardless; unaffected when theflag is
false(existing behavior/tests must still pass).SurveyListProvider::normalizeTeacherSurvey():canReportisfalsefor a teacher whenthe survey's flag is
true,truefor an admin viewing the same row.9.2 Behat (
tests/behat/features/survey/, extending the existing survey feature files ifpresent, or a new
survey-templates.featuremirroring 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 ofquestions, 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 acourse, 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 sessioncoach/tutor: as admin, set the flag
trueon 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).
(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
nameattribute per the project rule so these steps can useI fill in "name" with "value"/I select "option" from "name". Each feature file createsand tears down its own template/survey/course-assignment state.
10. Rollout notes / suggested build order
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
SurveyTemplateListreturns 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).
CSurvey's existing rows all getresultsHiddenFromTeacher = falseby the migration's column default; nothing else aboutexisting surveys changes shape.
SurveyTemplateService::instantiate()(§2), tested via PHPUnit only.template end-to-end.
course, and confirm from inside that course that it's a normal, fully-editable survey.
resultsHiddenFromTeacherflag (§6) + its reporting/list gates (§7) — independent oftemplates entirely, could in fact be built and shipped before §3–§5 if sequencing
the higher-risk security-relevant piece first is preferred.
depends on everything above already working.
at the end.
11. Explicitly out of scope / open questions for whoever picks this up
"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).here at all.
scoped to
ROLE_ADMINonly per the request's literal wording (§0); revisit if that's toostrict in practice.
sourceTemplatetraceability field (§1.2) — included as a low-cost nice-to-have, not arequirement; safe to drop without affecting anything else in this plan.
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
iidfor 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.