├── phpstan-baseline.neon ├── .prettierignore ├── www ├── admin │ └── styles │ │ ├── inquisition-details.css │ │ └── inquisition-question-edit.css └── javascript │ ├── inquisition-checkbox-entry-list.js │ └── inquisition-radio-entry-list.js ├── sql ├── fixtures │ ├── InquisitionQuestionImageSet.sql │ ├── InquisitionQuestionOptionImageSet.sql │ ├── InquisitionQuestionImageDimension.sql │ └── InquisitionQuestionOptionImageDimension.sql ├── tables │ ├── Inquisition.sql │ ├── InquisitionQuestionHint.sql │ ├── InquisitionQuestionGroup.sql │ ├── InquisitionQuestion.sql │ ├── InquisitionQuestionOption.sql │ ├── InquisitionResponse.sql │ ├── InquisitionQuestionImageBinding.sql │ ├── InquisitionQuestionDependency.sql │ ├── InquisitionQuestionOptionImageBinding.sql │ ├── InquisitionInquisitionQuestionBinding.sql │ ├── InquisitionResponseUsedHintBinding.sql │ └── InquisitionResponseValue.sql └── views │ ├── InquisitionResponseTotalQuestionsView.sql │ ├── VisibleInquisitionQuestionView.sql │ └── InquisitionResponseGradeView.sql ├── phpstan.dist.neon ├── prettier.config.js ├── .gitignore ├── Inquisition ├── dataobjects │ ├── InquisitionQuestionImage.php │ ├── InquisitionQuestionOptionImage.php │ ├── InquisitionQuestionImageWrapper.php │ ├── InquisitionQuestionOptionWrapper.php │ ├── InquisitionQuestionWrapper.php │ ├── InquisitionResponseWrapper.php │ ├── InquisitionInquisitionWrapper.php │ ├── InquisitionQuestionOptionImageWrapper.php │ ├── InquisitionQuestionHintWrapper.php │ ├── InquisitionResponseValueWrapper.php │ ├── InquisitionQuestionGroup.php │ ├── InquisitionResponseUsedHintBindingWrapper.php │ ├── InquisitionInquisitionQuestionBindingWrapper.php │ ├── InquisitionQuestionHint.php │ ├── InquisitionResponseUsedHintBinding.php │ ├── InquisitionResponseValue.php │ ├── InquisitionQuestionOption.php │ ├── InquisitionInquisitionQuestionBinding.php │ ├── InquisitionQuestion.php │ ├── InquisitionResponse.php │ └── InquisitionInquisition.php ├── exceptions │ └── InquisitionImportException.php ├── InquisitionQuestionOptionCellRenderer.php ├── admin │ ├── components │ │ ├── Question │ │ │ ├── correct-option.xml │ │ │ ├── import.xml │ │ │ ├── hint-edit.xml │ │ │ ├── edit.xml │ │ │ ├── add.xml │ │ │ ├── index.xml │ │ │ ├── ImageDelete.php │ │ │ ├── Import.php │ │ │ ├── Index.php │ │ │ ├── Order.php │ │ │ ├── ImageUpload.php │ │ │ ├── Edit.php │ │ │ ├── HintOrder.php │ │ │ ├── CorrectOption.php │ │ │ └── ImageOrder.php │ │ ├── Option │ │ │ ├── edit.xml │ │ │ ├── details.xml │ │ │ ├── ImageDelete.php │ │ │ ├── ImageUpload.php │ │ │ ├── Order.php │ │ │ ├── ImageOrder.php │ │ │ └── Delete.php │ │ └── Inquisition │ │ │ ├── image-upload.xml │ │ │ ├── image-delete.xml │ │ │ ├── index.xml │ │ │ ├── edit.xml │ │ │ ├── Edit.php │ │ │ ├── Index.php │ │ │ ├── ImageUpload.php │ │ │ ├── details.xml │ │ │ ├── Delete.php │ │ │ └── ImageDelete.php │ └── InquisitionCorrectOptionRadioButton.php ├── views │ ├── InquisitionQuestionView.php │ ├── InquisitionTextQuestionView.php │ ├── InquisitionFlydownQuestionView.php │ ├── InquisitionRadioListQuestionView.php │ ├── InquisitionCheckboxListQuestionView.php │ ├── InquisitionRadioEntryQuestionView.php │ └── InquisitionCheckboxEntryQuestionView.php ├── InquisitionImporter.php ├── InquisitionFileParser.php ├── InquisitionRadioEntryList.php ├── Inquisition.php ├── InquisitionCheckboxEntryList.php └── InquisitionQuestionImporter.php ├── .editorconfig ├── package.json ├── pnpm-lock.yaml ├── README.md ├── .github ├── pull_request_template.md └── workflows │ └── pull-requests.yml ├── Jenkinsfile ├── dependencies └── inquisition.yaml ├── composer.json └── .php-cs-fixer.php /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /pnpm-lock.yaml 3 | /.pnpm-store 4 | -------------------------------------------------------------------------------- /www/admin/styles/inquisition-details.css: -------------------------------------------------------------------------------- 1 | #question_view li.correct { 2 | color: #558b2f; 3 | } 4 | -------------------------------------------------------------------------------- /sql/fixtures/InquisitionQuestionImageSet.sql: -------------------------------------------------------------------------------- 1 | insert into ImageSet ( 2 | shortname, 3 | use_cdn, 4 | obfuscate_filename 5 | ) values ( 6 | 'inquisition-question', 7 | true, 8 | false 9 | ); 10 | -------------------------------------------------------------------------------- /sql/tables/Inquisition.sql: -------------------------------------------------------------------------------- 1 | create table Inquisition ( 2 | id serial, 3 | title varchar(255), 4 | createdate timestamp not null, 5 | enabled boolean not null default true, 6 | primary key (id) 7 | ); 8 | 9 | -------------------------------------------------------------------------------- /sql/fixtures/InquisitionQuestionOptionImageSet.sql: -------------------------------------------------------------------------------- 1 | insert into ImageSet ( 2 | shortname, 3 | use_cdn, 4 | obfuscate_filename 5 | ) values ( 6 | 'inquisition-question-option', 7 | true, 8 | false 9 | ); 10 | -------------------------------------------------------------------------------- /phpstan.dist.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | phpVersion: 80200 6 | level: 1 7 | paths: 8 | - Inquisition 9 | editorUrl: '%%file%%:%%line%%' 10 | editorUrlTitle: '%%file%%:%%line%%' 11 | -------------------------------------------------------------------------------- /sql/tables/InquisitionQuestionHint.sql: -------------------------------------------------------------------------------- 1 | create table InquisitionQuestionHint ( 2 | id serial, 3 | question integer not null references InquisitionQuestion(id) on delete cascade, 4 | bodytext text, 5 | displayorder integer not null default 0, 6 | primary key(id) 7 | ); -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://prettier.io/docs/en/configuration.html 3 | * @type {import("prettier").Config} 4 | */ 5 | const config = { 6 | singleQuote: true, 7 | tabWidth: 2, 8 | trailingComma: 'none' 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /sql/tables/InquisitionQuestionGroup.sql: -------------------------------------------------------------------------------- 1 | create table InquisitionQuestionGroup ( 2 | id serial, 3 | title varchar(255), 4 | bodytext text, 5 | primary key (id) 6 | ); 7 | 8 | alter table InquisitionQuestion add question_group integer references InquisitionQuestionGroup(id) on delete set null; 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | vendor/ 5 | node_modules/ 6 | 7 | # misc 8 | .DS_Store 9 | .env 10 | .idea/ 11 | .php-cs-fixer.cache 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | *.swp 16 | 17 | # dependency lock file 18 | composer.lock 19 | yarn.lock 20 | 21 | # overrides for local tooling 22 | /phpstan.neon 23 | -------------------------------------------------------------------------------- /sql/tables/InquisitionQuestion.sql: -------------------------------------------------------------------------------- 1 | create table InquisitionQuestion ( 2 | id serial, 3 | bodytext text, 4 | question_type integer not null, 5 | displayorder integer not null default 0, 6 | required boolean not null default true, 7 | enabled boolean not null default true, 8 | primary key (id) 9 | ); 10 | 11 | alter table InquisitionQuestion add correct_option integer references InquisitionQuestionOption(id) on delete set null; 12 | 13 | -------------------------------------------------------------------------------- /sql/tables/InquisitionQuestionOption.sql: -------------------------------------------------------------------------------- 1 | create table InquisitionQuestionOption ( 2 | id serial, 3 | question integer not null references InquisitionQuestion(id) on delete cascade, 4 | title varchar(255), 5 | displayorder integer not null default 0, 6 | include_text boolean not null default false, 7 | primary key(id) 8 | ); 9 | 10 | create index InquisitionQuestionOption_question_index on 11 | InquisitionQuestionOption(question); 12 | -------------------------------------------------------------------------------- /sql/tables/InquisitionResponse.sql: -------------------------------------------------------------------------------- 1 | create table InquisitionResponse ( 2 | id serial, 3 | inquisition integer not null references Inquisition(id) on delete cascade, 4 | createdate timestamp not null, 5 | complete_date timestamp, 6 | grade decimal(5, 2), 7 | primary key (id) 8 | ); 9 | 10 | create index InquisitionResponse_inquisition_index on InquisitionResponse(inquisition); 11 | create index InquisitionResponse_account_index on InquisitionResponse(account); 12 | -------------------------------------------------------------------------------- /www/admin/styles/inquisition-question-edit.css: -------------------------------------------------------------------------------- 1 | #question_option_table_view .title .swat-textarea { 2 | width: 97%; 3 | padding: 1px 2px; 4 | resize: vertical; 5 | } 6 | 7 | #question_option_table_view .title { 8 | width: 100%; 9 | } 10 | 11 | #question_option_table_view thead th, 12 | #question_option_table_view td.correct-option, 13 | #question_option_table_view td.checkbox-column { 14 | text-align: center; 15 | } 16 | 17 | #bodytext { 18 | width: 97%; 19 | } 20 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionQuestionImage.php: -------------------------------------------------------------------------------- 1 | image_set_shortname = 'inquisition-question'; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sql/views/InquisitionResponseTotalQuestionsView.sql: -------------------------------------------------------------------------------- 1 | create or replace view InquisitionResponseTotalQuestionsView as 2 | select InquisitionResponse.id as response, count(1) as total_questions 3 | from InquisitionResponse 4 | inner join Inquisition 5 | on InquisitionResponse.inquisition = Inquisition.id 6 | left outer join InquisitionInquisitionQuestionBinding 7 | on InquisitionInquisitionQuestionBinding.inquisition = Inquisition.id 8 | group by InquisitionResponse.id; 9 | -------------------------------------------------------------------------------- /sql/views/VisibleInquisitionQuestionView.sql: -------------------------------------------------------------------------------- 1 | create or replace view VisibleInquisitionQuestionView as 2 | select id as question from InquisitionQuestion 3 | where InquisitionQuestion.enabled = true and ( 4 | -- Question is always visible if it InquisitionQuestion::TYPE_TEXT (4), 5 | -- or if it has related options 6 | InquisitionQuestion.question_type = 4 or InquisitionQuestion.id in ( 7 | select InquisitionQuestionOption.question from InquisitionQuestionOption 8 | ) 9 | ); 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.php] 16 | indent_size = 4 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | 21 | [Jenkinsfile] 22 | indent_size = 4 23 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionQuestionOptionImage.php: -------------------------------------------------------------------------------- 1 | image_set_shortname = 'inquisition-question-option'; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@silverorange/inquisition", 3 | "private": true, 4 | "engines": { 5 | "node": "^22.14" 6 | }, 7 | "type": "module", 8 | "scripts": { 9 | "prettier": "prettier --check .", 10 | "prettier:write": "prettier --write ." 11 | }, 12 | "devDependencies": { 13 | "prettier": "^3.5.3" 14 | }, 15 | "packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228" 16 | } 17 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | prettier: 12 | specifier: ^3.5.3 13 | version: 3.6.2 14 | 15 | packages: 16 | 17 | prettier@3.6.2: 18 | resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} 19 | engines: {node: '>=14'} 20 | hasBin: true 21 | 22 | snapshots: 23 | 24 | prettier@3.6.2: {} 25 | -------------------------------------------------------------------------------- /sql/tables/InquisitionQuestionImageBinding.sql: -------------------------------------------------------------------------------- 1 | create table InquisitionQuestionImageBinding ( 2 | question integer not null references InquisitionQuestion(id) on delete cascade, 3 | image integer not null references Image(id) on delete cascade, 4 | displayorder integer not null default 0, 5 | primary key (question, image) 6 | ); 7 | 8 | CREATE INDEX InquisitionQuestionImageBinding_question_index ON InquisitionQuestionImageBinding(question); 9 | CREATE INDEX InquisitionQuestionImageBinding_image_index ON InquisitionQuestionImageBinding(image); 10 | -------------------------------------------------------------------------------- /sql/tables/InquisitionQuestionDependency.sql: -------------------------------------------------------------------------------- 1 | create table InquisitionQuestionDependency 2 | ( 3 | question_binding integer not null references InquisitionInquisitionQuestionBinding(id) on delete cascade, 4 | dependent_question_binding integer not null references InquisitionInquisitionQuestionBinding(id) on delete cascade, 5 | 6 | option integer not null references InquisitionQuestionOption(id) on delete cascade 7 | ); 8 | 9 | create index InquisitionQuestionDependency_dependent_question_binding_index on 10 | InquisitionQuestionDependency(dependent_question_binding); 11 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionQuestionImageWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = 18 | SwatDBClassMap::get(InquisitionQuestionImage::class); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionQuestionOptionWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = 16 | SwatDBClassMap::get(InquisitionQuestionOption::class); 17 | 18 | $this->index_field = 'id'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionQuestionWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = SwatDBClassMap::get(InquisitionQuestion::class); 18 | $this->index_field = 'id'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionResponseWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = SwatDBClassMap::get(InquisitionResponse::class); 18 | $this->index_field = 'id'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sql/tables/InquisitionQuestionOptionImageBinding.sql: -------------------------------------------------------------------------------- 1 | create table InquisitionQuestionOptionImageBinding ( 2 | question_option integer not null references InquisitionQuestionOption(id) on delete cascade, 3 | image integer not null references Image(id) on delete cascade, 4 | displayorder integer not null default 0, 5 | primary key (question_option, image) 6 | ); 7 | 8 | CREATE INDEX InquisitionQuestionOptionImageBinding_question_option_index ON InquisitionQuestionOptionImageBinding(question_option); 9 | CREATE INDEX InquisitionQuestionOptionImageBinding_image_index ON InquisitionQuestionOptionImageBinding(image); 10 | -------------------------------------------------------------------------------- /sql/tables/InquisitionInquisitionQuestionBinding.sql: -------------------------------------------------------------------------------- 1 | create table InquisitionInquisitionQuestionBinding ( 2 | id serial, 3 | inquisition integer not null references Inquisition(id) on delete cascade, 4 | question integer not null references InquisitionQuestion(id) on delete cascade, 5 | displayorder integer not null default 0, 6 | 7 | primary key (id) 8 | ); 9 | 10 | create index InquisitionInquisitionQuestionBinding_inquisition_index on 11 | InquisitionInquisitionQuestionBinding(inquisition); 12 | 13 | create index InquisitionInquisitionQuestionBinding_question_index on 14 | InquisitionInquisitionQuestionBinding(question); 15 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionInquisitionWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = 18 | SwatDBClassMap::get(InquisitionInquisition::class); 19 | 20 | $this->index_field = 'id'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionQuestionOptionImageWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = 18 | SwatDBClassMap::get(InquisitionQuestionOptionImage::class); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionQuestionHintWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = 18 | SwatDBClassMap::get(InquisitionQuestionHint::class); 19 | 20 | $this->index_field = 'id'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionResponseValueWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = 18 | SwatDBClassMap::get(InquisitionResponseValue::class); 19 | 20 | $this->index_field = 'id'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionQuestionGroup.php: -------------------------------------------------------------------------------- 1 | table = 'InquisitionQuestionGroup'; 29 | $this->id_field = 'integer:id'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionResponseUsedHintBindingWrapper.php: -------------------------------------------------------------------------------- 1 | index_field = 'question_hint'; 18 | $this->row_wrapper_class = 19 | SwatDBClassMap::get(InquisitionResponseUsedHintBinding::class); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionInquisitionQuestionBindingWrapper.php: -------------------------------------------------------------------------------- 1 | row_wrapper_class = 18 | SwatDBClassMap::get(InquisitionInquisitionQuestionBinding::class); 19 | 20 | $this->index_field = 'id'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Inquisition/exceptions/InquisitionImportException.php: -------------------------------------------------------------------------------- 1 | file_parser = $file_parser; 21 | } 22 | 23 | public function getFileParser() 24 | { 25 | return $this->file_parser; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Inquisition/InquisitionQuestionOptionCellRenderer.php: -------------------------------------------------------------------------------- 1 | visible) { 17 | return; 18 | } 19 | 20 | $span = new SwatHtmlTag('span'); 21 | $span->class = 'inquisition-question-option'; 22 | 23 | if ($this->correct) { 24 | $span->class .= ' inquisition-question-option-correct'; 25 | } 26 | 27 | $span->open(); 28 | parent::render(); 29 | $span->close(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Question/correct-option.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Correct Option 7 | 8 | 9 | Options 10 | 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Option/edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Option 7 | 8 | 9 | Text 10 | 11 | 255 12 | true 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sql/tables/InquisitionResponseUsedHintBinding.sql: -------------------------------------------------------------------------------- 1 | create table InquisitionResponseUsedHintBinding ( 2 | response integer not null references InquisitionResponse(id) on delete cascade, 3 | question_hint integer not null references InquisitionQuestionHint(id) on delete cascade, 4 | question_binding integer not null references InquisitionInquisitionQuestionBinding(id) on delete cascade, 5 | createdate timestamp not null default LOCALTIMESTAMP, 6 | primary key (response, question_hint, question_binding) 7 | ); 8 | 9 | CREATE INDEX InquisitionResponseUsedHintBinding_response_index ON InquisitionResponseUsedHintBinding(response); 10 | CREATE INDEX InquisitionResponseUsedHintBinding_question_hint_index ON InquisitionResponseUsedHintBinding(question_hint); 11 | CREATE INDEX InquisitionResponseUsedHintBinding_response_question_binding_index 12 | ON InquisitionResponseUsedHintBinding(response, question_binding); 13 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionQuestionHint.php: -------------------------------------------------------------------------------- 1 | table = 'InquisitionQuestionHint'; 31 | $this->id_field = 'integer:id'; 32 | 33 | $this->registerInternalProperty( 34 | 'question', 35 | SwatDBClassMap::get(InquisitionQuestion::class) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sql/tables/InquisitionResponseValue.sql: -------------------------------------------------------------------------------- 1 | create table InquisitionResponseValue ( 2 | id serial, 3 | response integer not null references InquisitionResponse(id) on delete cascade, 4 | question_option integer null references InquisitionQuestionOption(id) on delete cascade, 5 | question_binding integer not null references InquisitionInquisitionQuestionBinding(id) on delete cascade, 6 | numeric_value integer, 7 | text_value text, 8 | primary key (id) 9 | ); 10 | 11 | create index InquisitionResponseValue_response_index on 12 | InquisitionResponseValue(response); 13 | 14 | create index InquisitionResponseValue_question_option_index on 15 | InquisitionResponseValue(question_option); 16 | 17 | create index InquisitionResponseValue_question_binding_index on 18 | InquisitionResponseValue(question_binding); 19 | 20 | create index InquisitionResponseValue_response_question_binding_index on 21 | InquisitionResponseValue(response, question_binding); 22 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Question/import.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Import Questions 7 | 8 | 9 | Questions File 10 | 11 | true 12 | text/csv 13 | text/plain 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inquisition 2 | 3 | A package for creating, managing, and running online quizzes. 4 | 5 | Inquisition is responsible for the following basic object types and related 6 | tables: 7 | 8 | - Inquisition (quizzes) 9 | - InquisitionQuestion 10 | - InquisitionInquisitionQuestionBinding 11 | - InquisitionQuestionOption 12 | - InquisitionResponse 13 | - InquisitionResponseValue 14 | 15 | Additional objects are provided for extended features: 16 | 17 | - InquisitionQuestionImage 18 | - InquisitionQuestionOptionImage 19 | - InquisitionQuestionGroup 20 | - InquisitionQuestionHint 21 | - InquisitionResponseUsedHintBinding 22 | 23 | It provides pages for displaying these objects and admin tools for managing 24 | them. 25 | 26 | There is also a CSV importer and exporter for question management. 27 | 28 | ## Installation 29 | 30 | Make sure the silverorange composer repository is added to the `composer.json` 31 | for the project and then run: 32 | 33 | ```sh 34 | composer require silverorange/inquisition 35 | ``` 36 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Inquisition/image-upload.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Image 9 | 10 | true 11 | image/jpeg 12 | image/png 13 | image/tiff 14 | 15 | 16 | 17 | 18 | Upload 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /sql/views/InquisitionResponseGradeView.sql: -------------------------------------------------------------------------------- 1 | create or replace view InquisitionResponseGradeView as 2 | select InquisitionResponse.inquisition, 3 | InquisitionResponse.account, 4 | sum( 5 | case when InquisitionQuestion.correct_option = 6 | InquisitionResponseValue.question_option then 1 7 | else 0 8 | end 9 | )::float / 10 | count(InquisitionResponseValue.id)::float as grade 11 | from InquisitionResponse 12 | inner join InquisitionResponseValue on 13 | InquisitionResponseValue.response = InquisitionResponse.id 14 | inner join InquisitionInquisitionQuestionBinding on 15 | InquisitionInquisitionQuestionBinding.id = 16 | InquisitionResponseValue.question_binding 17 | inner join InquisitionQuestion on 18 | InquisitionQuestion.id = 19 | InquisitionInquisitionQuestionBinding.question 20 | where InquisitionResponse.complete_date is not null 21 | and InquisitionResponse.reset_date is null 22 | and InquisitionQuestion.correct_option is not null 23 | group by InquisitionResponse.inquisition, InquisitionResponse.account; 24 | 25 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Add a description of new changes, the reason for new changes, and how the new 4 | changes work here. 5 | 6 | # Testing Instructions (optional) 7 | 8 | Add step-by-step instructions for testing the PR, if necessary. 9 | 10 | 1. Check out this PR 11 | 2. … 12 | 13 | # Developer Checklist 14 | 15 | Before requesting review for this PR, make sure the following tasks are 16 | complete: 17 | 18 | - [ ] I added a link to the relevant Shortcut story, if applicable 19 | - [ ] I added testing instructions, if any 20 | - [ ] I made sure existing CI checks pass 21 | - [ ] I checked that all requirements of the ticket are fulfilled 22 | 23 | # Reviewer Checklist 24 | 25 | Before merging this PR, make sure the following tasks are complete: 26 | 27 | - [ ] I made sure there are no active labels that block merge 28 | - [ ] I followed the testing instructions 29 | - [ ] I made sure the CI checks pass 30 | - [ ] I reviewed the file changes on GitHub 31 | - [ ] I checked that all requirements of the ticket (if any) are fulfilled 32 | -------------------------------------------------------------------------------- /Inquisition/views/InquisitionQuestionView.php: -------------------------------------------------------------------------------- 1 | question_binding = $question_binding; 26 | $this->db = $db; 27 | } 28 | 29 | abstract public function getWidget(?InquisitionResponseValue $value = null); 30 | 31 | public function getResponseValue() 32 | { 33 | $value = SwatDBClassMap::new(InquisitionResponseValue::class); 34 | $value->question_binding = $this->question_binding->id; 35 | 36 | return $value; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Question/hint-edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hint 7 | 8 | 9 | Bodytext 10 | 11 | true 12 | 5 13 | 14 | 15 | 16 | 17 | Done 18 | 19 | 20 | Create and Add Another 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | stages { 4 | stage('Install Composer Dependencies') { 5 | steps { 6 | sh 'rm -rf composer.lock vendor/' 7 | sh 'composer install' 8 | } 9 | } 10 | 11 | stage('Install NPM Dependencies') { 12 | environment { 13 | PNPM_CACHE_FOLDER = "${env.WORKSPACE}/pnpm-cache/${env.BUILD_NUMBER}" 14 | } 15 | steps { 16 | sh 'n -d exec engine corepack enable pnpm' 17 | sh 'n -d exec engine pnpm install' 18 | } 19 | } 20 | 21 | 22 | stage('Check PHP Coding Style') { 23 | steps { 24 | sh 'composer run phpcs:ci' 25 | } 26 | } 27 | 28 | stage('Check PHP Static Analysis') { 29 | steps { 30 | sh 'composer run phpstan:ci' 31 | } 32 | } 33 | 34 | stage('Check Formating') { 35 | steps { 36 | sh 'n -d exec engine pnpm prettier' 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Inquisition/image-delete.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | image 12 | thumb 13 | ../ 14 | 15 | 16 | 17 | 18 | 19 | 20 | apply 21 | 22 | 23 | cancel 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Inquisition/views/InquisitionTextQuestionView.php: -------------------------------------------------------------------------------- 1 | question_binding; 16 | $question = $this->question_binding->question; 17 | 18 | if ($this->textarea === null) { 19 | $id = sprintf('question%s_%s', $binding->id, $question->id); 20 | 21 | $this->textarea = new SwatTextarea($id); 22 | $this->textarea->required = $question->required; 23 | } 24 | 25 | if ($value !== null) { 26 | $this->textarea->value = intval( 27 | $value->getInternalValue('question_option') 28 | ); 29 | } 30 | 31 | return $this->textarea; 32 | } 33 | 34 | public function getResponseValue() 35 | { 36 | $value = parent::getResponseValue(); 37 | $value->text_value = $this->textarea->value; 38 | 39 | return $value; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionResponseUsedHintBinding.php: -------------------------------------------------------------------------------- 1 | table = 'InquisitionResponseUsedHintBinding'; 23 | 24 | $this->registerDateProperty('createdate'); 25 | 26 | $this->registerInternalProperty( 27 | 'response', 28 | SwatDBClassMap::get(InquisitionResponse::class) 29 | ); 30 | 31 | $this->registerInternalProperty( 32 | 'question_hint', 33 | SwatDBClassMap::get(InquisitionQuestionHint::class) 34 | ); 35 | 36 | $this->registerInternalProperty( 37 | 'question_binding', 38 | SwatDBClassMap::get(InquisitionInquisitionQuestionBinding::class) 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Inquisition/index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Inquisitions 7 | 8 | 9 | New Inquisition 10 | Inquisition/Edit 11 | add 12 | 13 | 14 | 15 | 16 | Title 17 | 18 | title 19 | Inquisition/Details?id=%s 20 | id 21 | 22 | 23 | 24 | 25 | question_count 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Inquisition/edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Inquisition 7 | 8 | 9 | Title 10 | 11 | 255 12 | 13 | 14 | 15 | Show on Site? 16 | 21 | false 22 | 23 | true 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /dependencies/inquisition.yaml: -------------------------------------------------------------------------------- 1 | ## 2 | ## Static Web-resource dependencies for the Inquisition package 3 | ## 4 | ## Copyright (c) 2010 silverorange 5 | ## 6 | ## This library is free software; you can redistribute it and/or modify 7 | ## it under the terms of the GNU Lesser General Public License as 8 | ## published by the Free Software Foundation; either version 2.1 of the 9 | ## License, or (at your option) any later version. 10 | ## 11 | ## This library is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | ## Lesser General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU Lesser General Public 17 | ## License along with this library; if not, write to the Free Software 18 | ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | ## 20 | Inquisition: 21 | Depends: 22 | # Package Dependencies 23 | - Swat 24 | 25 | Provides: 26 | # JavaScript resources 27 | packages/inquisition/javascript/inquisition-radio-list-with-text-question-view.js: 28 | 29 | # Style-sheet resources 30 | packages/inquisition/admin/styles/inquisition-details.css: 31 | packages/inquisition/admin/styles/inquisition-question-edit.css: 32 | # vim: set expandtab tabstop=2 shiftwidth=2 softtabstop=2: 33 | -------------------------------------------------------------------------------- /Inquisition/views/InquisitionFlydownQuestionView.php: -------------------------------------------------------------------------------- 1 | question_binding; 16 | $question = $this->question_binding->question; 17 | 18 | if ($this->flydown === null) { 19 | $id = sprintf('question%s_%s', $binding->id, $question->id); 20 | 21 | $this->flydown = new SwatFlydown($id); 22 | $this->flydown->required = $question->required; 23 | 24 | foreach ($question->options as $option) { 25 | $this->flydown->addOption($option->id, $option->title); 26 | } 27 | } 28 | 29 | if ($value !== null) { 30 | $this->flydown->value = intval( 31 | $value->getInternalValue('question_option') 32 | ); 33 | } 34 | 35 | return $this->flydown; 36 | } 37 | 38 | public function getResponseValue() 39 | { 40 | $value = parent::getResponseValue(); 41 | $value->question_option = $this->flydown->value; 42 | 43 | return $value; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Question/edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Question 7 | 8 | 9 | Question 10 | 11 | true 12 | 5 13 | 14 | 15 | 16 | Show on Site? 17 | Hide a question from the site when there are responses to the question that should remain available for reporting. 18 | false 19 | 20 | true 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Inquisition/admin/InquisitionCorrectOptionRadioButton.php: -------------------------------------------------------------------------------- 1 | getForm()->getHiddenField($this->id . '_submitted') === null) { 16 | return; 17 | } 18 | 19 | $data = &$this->getForm()->getFormData(); 20 | $this->value = (array_key_exists('correct_option', $data) 21 | && $data['correct_option'] == $this->id); 22 | } 23 | 24 | public function display() 25 | { 26 | SwatInputControl::display(); 27 | 28 | $this->getForm()->addHiddenField($this->id . '_submitted', 1); 29 | 30 | $input_tag = new SwatHtmlTag('input'); 31 | $input_tag->type = 'radio'; 32 | $input_tag->class = $this->getCSSClassString(); 33 | $input_tag->name = 'correct_option'; 34 | $input_tag->id = $this->id; 35 | $input_tag->value = $this->id; 36 | $input_tag->accesskey = $this->access_key; 37 | 38 | if ($this->value) { 39 | $input_tag->checked = 'checked'; 40 | } 41 | 42 | $input_tag->display(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionResponseValue.php: -------------------------------------------------------------------------------- 1 | table = 'InquisitionResponseValue'; 33 | $this->id_field = 'integer:id'; 34 | 35 | $this->registerInternalProperty( 36 | 'response', 37 | SwatDBClassMap::get(InquisitionResponse::class) 38 | ); 39 | 40 | $this->registerInternalProperty( 41 | 'question_option', 42 | SwatDBClassMap::get(InquisitionQuestionOption::class) 43 | ); 44 | 45 | $this->registerInternalProperty( 46 | 'question_binding', 47 | SwatDBClassMap::get(InquisitionInquisitionQuestionBinding::class) 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Inquisition/views/InquisitionRadioListQuestionView.php: -------------------------------------------------------------------------------- 1 | question_binding; 19 | $question = $this->question_binding->question; 20 | 21 | if ($this->radio_list === null) { 22 | $id = sprintf('question%s_%s', $binding->id, $question->id); 23 | 24 | $this->radio_list = new SwatRadioList($id); 25 | $this->radio_list->required = $question->required; 26 | 27 | foreach ($question->options as $option) { 28 | $this->radio_list->addOption($option->id, $option->title); 29 | } 30 | } 31 | 32 | if ($value !== null) { 33 | $this->radio_list->value = intval( 34 | $value->getInternalValue('question_option') 35 | ); 36 | } 37 | 38 | return $this->radio_list; 39 | } 40 | 41 | public function getResponseValue() 42 | { 43 | $value = parent::getResponseValue(); 44 | $value->question_option = $this->radio_list->value; 45 | 46 | return $value; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /www/javascript/inquisition-checkbox-entry-list.js: -------------------------------------------------------------------------------- 1 | function InquisitionCheckboxEntryList(id) { 2 | this.id = id; 3 | this.entries = []; 4 | this.checkboxs_by_entry = {}; 5 | YAHOO.util.Event.onDOMReady(this.init, this, true); 6 | } 7 | 8 | InquisitionCheckboxEntryList.prototype.init = function () { 9 | this.list = document.getElementById(this.id); 10 | this.checkboxes = YAHOO.util.Dom.getElementsBy( 11 | function (el) { 12 | return el.type == 'checkbox'; 13 | }, 14 | 'input', 15 | this.list 16 | ); 17 | 18 | var id_parts, entry_id, entry; 19 | for (var i = 0; i < this.checkboxes.length; i++) { 20 | id_parts = this.checkboxes[i].id.split('_'); 21 | entry_id = id_parts[0] + '_' + id_parts[1] + '_entry_' + id_parts[2]; 22 | entry = document.getElementById(entry_id); 23 | if (entry) { 24 | this.entries.push(entry); 25 | this.checkboxs_by_entry[entry.id] = this.checkboxes[i]; 26 | } 27 | } 28 | 29 | YAHOO.util.Event.on(this.checkboxes, 'click', this.updateEntries, this, true); 30 | 31 | this.updateEntries(); 32 | }; 33 | 34 | InquisitionCheckboxEntryList.prototype.updateEntries = function () { 35 | var checkbox, entry; 36 | for (var i = 0; i < this.entries.length; i++) { 37 | entry = this.entries[i]; 38 | checkbox = this.checkboxs_by_entry[entry.id]; 39 | if (checkbox.checked) { 40 | YAHOO.util.Dom.removeClass(entry, 'swat-insensitive'); 41 | entry.disabled = false; 42 | } else { 43 | YAHOO.util.Dom.addClass(entry, 'swat-insensitive'); 44 | entry.disabled = true; 45 | } 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /www/javascript/inquisition-radio-entry-list.js: -------------------------------------------------------------------------------- 1 | function InquisitionRadioEntryList(id) { 2 | this.id = id; 3 | this.entries = []; 4 | this.radios_by_entry = {}; 5 | YAHOO.util.Event.onDOMReady(this.init, this, true); 6 | } 7 | 8 | InquisitionRadioEntryList.prototype.init = function () { 9 | this.list = document.getElementById(this.id); 10 | this.radio_buttons = YAHOO.util.Dom.getElementsBy( 11 | function (el) { 12 | return el.type == 'radio'; 13 | }, 14 | 'input', 15 | this.list 16 | ); 17 | 18 | var id_parts, entry_id, entry; 19 | for (var i = 0; i < this.radio_buttons.length; i++) { 20 | id_parts = this.radio_buttons[i].id.split('_'); 21 | entry_id = id_parts[0] + '_' + id_parts[1] + '_entry_' + id_parts[2]; 22 | entry = document.getElementById(entry_id); 23 | if (entry) { 24 | this.entries.push(entry); 25 | this.radios_by_entry[entry.id] = this.radio_buttons[i]; 26 | } 27 | } 28 | 29 | YAHOO.util.Event.on( 30 | this.radio_buttons, 31 | 'click', 32 | this.updateEntries, 33 | this, 34 | true 35 | ); 36 | 37 | this.updateEntries(); 38 | }; 39 | 40 | InquisitionRadioEntryList.prototype.updateEntries = function () { 41 | var radio, entry; 42 | for (var i = 0; i < this.entries.length; i++) { 43 | entry = this.entries[i]; 44 | radio = this.radios_by_entry[entry.id]; 45 | if (radio.checked) { 46 | YAHOO.util.Dom.removeClass(entry, 'swat-insensitive'); 47 | entry.disabled = false; 48 | } else { 49 | YAHOO.util.Dom.addClass(entry, 'swat-insensitive'); 50 | entry.disabled = true; 51 | } 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /Inquisition/views/InquisitionCheckboxListQuestionView.php: -------------------------------------------------------------------------------- 1 | question_binding; 16 | $question = $this->question_binding->question; 17 | 18 | if ($this->checkbox_list === null) { 19 | $id = sprintf('question%s_%s', $binding->id, $question->id); 20 | 21 | $this->checkbox_list = new InquisitionCheckboxEntryList($id); 22 | $this->checkbox_list->required = $question->required; 23 | $this->checkbox_list->show_check_all = false; 24 | 25 | foreach ($question->options as $option) { 26 | $this->checkbox_list->addOption($option->id, $option->title); 27 | } 28 | } 29 | 30 | if ($value !== null) { 31 | $this->checkbox_list->value = intval( 32 | $value->getInternalValue('question_option') 33 | ); 34 | } 35 | 36 | return $this->checkbox_list; 37 | } 38 | 39 | public function getResponseValue() 40 | { 41 | $base_value = parent::getResponseValue(); 42 | $value = []; 43 | 44 | foreach ($this->checkbox_list->values as $list_value) { 45 | $response_value = clone $base_value; 46 | $response_value->question_option = $list_value; 47 | $value[] = $response_value; 48 | } 49 | 50 | return $value; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Inquisition/views/InquisitionRadioEntryQuestionView.php: -------------------------------------------------------------------------------- 1 | question_binding; 16 | $question = $this->question_binding->question; 17 | 18 | if (!$this->radio_table instanceof InquisitionRadioEntryList) { 19 | $id = sprintf('question%s_%s', $binding->id, $question->id); 20 | 21 | $this->radio_table = new InquisitionRadioEntryList($id); 22 | $this->radio_table->required = $question->required; 23 | 24 | foreach ($question->options as $option) { 25 | $this->radio_table->addOption($option->id, $option->title); 26 | if ($option->include_text) { 27 | $this->radio_table->setEntryOption($option->id); 28 | } 29 | } 30 | } 31 | 32 | if ($value instanceof InquisitionResponseValue) { 33 | $this->radio_table->value = intval( 34 | $value->getInternalValue('question_option') 35 | ); 36 | } 37 | 38 | return $this->radio_table; 39 | } 40 | 41 | public function getResponseValue() 42 | { 43 | $value = parent::getResponseValue(); 44 | $value->question_option = $this->radio_table->value; 45 | $value->text_value = $this->radio_table->getEntryValue( 46 | $this->radio_table->value 47 | ); 48 | 49 | return $value; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "silverorange/inquisition", 3 | "description": "A quiz and/or survey framework.", 4 | "type": "library", 5 | "keywords": [ 6 | "questionnaire", 7 | "survey", 8 | "quiz" 9 | ], 10 | "homepage": "https://github.com/silverorange/inquisition", 11 | "license": "LGPL-2.1", 12 | "authors": [ 13 | { 14 | "name": "Charles Waddell", 15 | "email": "charles@silverorange.com" 16 | }, 17 | { 18 | "name": "Isaac Grant", 19 | "email": "isaac@silverorange.com" 20 | }, 21 | { 22 | "name": "Michael Gauthier", 23 | "email": "mike@silverorange.com" 24 | }, 25 | { 26 | "name": "Nick Burka", 27 | "email": "nick@silverorange.com" 28 | } 29 | ], 30 | "repositories": [ 31 | { 32 | "type": "composer", 33 | "url": "https://composer.silverorange.com", 34 | "only": [ 35 | "silverorange/*" 36 | ] 37 | } 38 | ], 39 | "require": { 40 | "php": ">=8.2", 41 | "ext-mbstring": "*", 42 | "silverorange/admin": "^7.0.3", 43 | "silverorange/mdb2": "^3.1.2", 44 | "silverorange/site": "^15.3.2", 45 | "silverorange/swat": "^7.9.2" 46 | }, 47 | "require-dev": { 48 | "friendsofphp/php-cs-fixer": "3.64.0", 49 | "phpstan/phpstan": "^1.12" 50 | }, 51 | "scripts": { 52 | "phpcs": "./vendor/bin/php-cs-fixer check -v", 53 | "phpcs:ci": "./vendor/bin/php-cs-fixer check --config=.php-cs-fixer.php --no-interaction --show-progress=none --diff --using-cache=no -vvv", 54 | "phpcs:write": "./vendor/bin/php-cs-fixer fix -v", 55 | "phpstan": "./vendor/bin/phpstan analyze", 56 | "phpstan:ci": "./vendor/bin/phpstan analyze -vvv --no-progress --memory-limit 2G", 57 | "phpstan:baseline": "./vendor/bin/phpstan analyze --generate-baseline" 58 | }, 59 | "autoload": { 60 | "classmap": [ 61 | "Inquisition/" 62 | ] 63 | }, 64 | "config": { 65 | "sort-packages": true 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.github/workflows/pull-requests.yml: -------------------------------------------------------------------------------- 1 | name: Pull Requests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | runner: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Setup Tailscale 14 | uses: tailscale/github-action@v3 15 | with: 16 | oauth-client-id: ${{ secrets.SILVERORANGE_TAILSCALE_OAUTH_CLIENT_ID }} 17 | oauth-secret: ${{ secrets.SILVERORANGE_TAILSCALE_OAUTH_SECRET }} 18 | tags: tag:silverorange-gh-actions 19 | version: latest 20 | 21 | - name: Check out repository code 22 | uses: actions/checkout@v4 23 | 24 | # Can maybe be replaced with pnpm/action-setup@v4 in the future 25 | - name: Setup pnpm/corepack 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version-file: 'package.json' 29 | - run: npm i -g --force corepack && corepack enable 30 | 31 | - name: Setup Node 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version-file: 'package.json' 35 | cache: 'pnpm' 36 | 37 | - name: Install dependencies 38 | run: pnpm install --frozen-lockfile 39 | 40 | - name: Setup PHP with tools 41 | uses: silverorange/actions-setup-php@v2 42 | with: 43 | php-version: '8.2' 44 | extensions: gd, imagick 45 | 46 | - name: Get composer cache directory 47 | id: composer-cache 48 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 49 | 50 | - name: Cache dependencies 51 | uses: actions/cache@v4 52 | with: 53 | path: ${{ steps.composer-cache.outputs.dir }} 54 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 55 | restore-keys: ${{ runner.os }}-composer- 56 | 57 | - name: Install PHP dependencies 58 | run: 'composer install' 59 | 60 | - name: Run tests 61 | timeout-minutes: 5 62 | run: | 63 | pnpm prettier 64 | composer run phpcs:ci 65 | composer run phpstan:ci 66 | -------------------------------------------------------------------------------- /Inquisition/views/InquisitionCheckboxEntryQuestionView.php: -------------------------------------------------------------------------------- 1 | question_binding; 16 | $question = $this->question_binding->question; 17 | 18 | if (!$this->checkbox_list instanceof InquisitionCheckboxEntryList) { 19 | $id = sprintf('question%s_%s', $binding->id, $question->id); 20 | 21 | $this->checkbox_list = new InquisitionCheckboxEntryList($id); 22 | $this->checkbox_list->required = $question->required; 23 | $this->checkbox_list->show_check_all = false; 24 | 25 | foreach ($question->options as $option) { 26 | $this->checkbox_list->addOption($option->id, $option->title); 27 | if ($option->include_text) { 28 | $this->checkbox_list->setEntryOption($option->id); 29 | } 30 | } 31 | } 32 | 33 | if ($value instanceof InquisitionResponseValue) { 34 | $this->checkbox_list->value = intval( 35 | $value->getInternalValue('question_option') 36 | ); 37 | } 38 | 39 | return $this->checkbox_list; 40 | } 41 | 42 | public function getResponseValue() 43 | { 44 | $base_value = parent::getResponseValue(); 45 | $value = []; 46 | 47 | foreach ($this->checkbox_list->values as $list_value) { 48 | $response_value = clone $base_value; 49 | $response_value->question_option = $list_value; 50 | $response_value->text_value = $this->checkbox_list->getEntryValue( 51 | $list_value 52 | ); 53 | $value[] = $response_value; 54 | } 55 | 56 | return $value; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Inquisition/InquisitionImporter.php: -------------------------------------------------------------------------------- 1 | app = $app; 17 | } 18 | 19 | // inquisition 20 | 21 | public function importInquisition( 22 | InquisitionInquisition $inquisition, 23 | InquisitionFileParser $file 24 | ) { 25 | $this->importInquisitionProperties($inquisition, $file); 26 | $this->importQuestions($inquisition, $file); 27 | } 28 | 29 | protected function importInquisitionProperties( 30 | InquisitionInquisition $inquisition, 31 | InquisitionFileParser $file 32 | ) {} 33 | 34 | // questions 35 | 36 | protected function importQuestions( 37 | InquisitionInquisition $inquisition, 38 | InquisitionFileParser $file 39 | ) { 40 | $importer = $this->getQuestionImporter(); 41 | $questions = $importer->importQuestions($file); 42 | 43 | $binding_class = SwatDBClassMap::get( 44 | InquisitionInquisitionQuestionBinding::class 45 | ); 46 | 47 | foreach ($questions as $question) { 48 | $binding = new $binding_class(); 49 | $binding->setDatabase($this->app->db); 50 | 51 | $binding->question = $question; 52 | $binding->inquisition = $inquisition; 53 | 54 | $previous_binding = $inquisition->question_bindings->getLast(); 55 | 56 | if ($previous_binding instanceof $binding_class) { 57 | $binding->displayorder = $previous_binding->displayorder + 1; 58 | } else { 59 | $binding->displayorder = 1; 60 | } 61 | 62 | $inquisition->question_bindings->add($binding); 63 | } 64 | } 65 | 66 | protected function getQuestionImporter() 67 | { 68 | return new InquisitionQuestionImporter($this->app); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in(__DIR__); 9 | 10 | return (new Config()) 11 | ->setParallelConfig(ParallelConfigFactory::detect(null, null, 2**18-1)) 12 | ->setRules([ 13 | '@PhpCsFixer' => true, 14 | '@PHP82Migration' => true, 15 | 'indentation_type' => true, 16 | 17 | // Overrides for (opinionated) @PhpCsFixer and @Symfony rules: 18 | 19 | // Align "=>" in multi-line array definitions, unless a blank line exists between elements 20 | 'binary_operator_spaces' => ['operators' => ['=>' => 'align_single_space_minimal']], 21 | 22 | // Subset of statements that should be proceeded with blank line 23 | 'blank_line_before_statement' => ['statements' => ['case', 'continue', 'declare', 'default', 'return', 'throw', 'try', 'yield', 'yield_from']], 24 | 25 | // Enforce space around concatenation operator 26 | 'concat_space' => ['spacing' => 'one'], 27 | 28 | // Use {} for empty loop bodies 29 | 'empty_loop_body' => ['style' => 'braces'], 30 | 31 | // Don't change any increment/decrement styles 32 | 'increment_style' => false, 33 | 34 | // Forbid multi-line whitespace before the closing semicolon 35 | 'multiline_whitespace_before_semicolons' => ['strategy' => 'no_multi_line'], 36 | 37 | // Clean up PHPDocs, but leave @inheritDoc entries alone 38 | 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true, 'remove_inheritdoc' => false], 39 | 40 | // Ensure that traits are listed first in classes 41 | // (it would be nice to enforce more, but we'll start simple) 42 | 'ordered_class_elements' => ['order' => ['use_trait']], 43 | 44 | // Ensure that param and return types are sorted consistently, with null at end 45 | 'phpdoc_types_order' => ['sort_algorithm' => 'alpha', 'null_adjustment' => 'always_last'], 46 | 47 | // Yoda style is too weird 48 | 'yoda_style' => false, 49 | ]) 50 | ->setIndent(' ') 51 | ->setLineEnding("\n") 52 | ->setFinder($finder); 53 | -------------------------------------------------------------------------------- /sql/fixtures/InquisitionQuestionImageDimension.sql: -------------------------------------------------------------------------------- 1 | insert into ImageDimension ( 2 | image_set, 3 | default_type, 4 | shortname, 5 | title, 6 | max_width, 7 | max_height, 8 | crop, 9 | dpi, 10 | quality, 11 | strip, 12 | interlace, 13 | resize_filter, 14 | upscale 15 | ) values ( 16 | (select id from ImageSet where shortname = 'inquisition-question'), 17 | (select id from ImageType where mime_type = 'image/jpeg'), 18 | 'original', 19 | 'Original', 20 | null, 21 | null, 22 | false, 23 | 72, 24 | 80, 25 | true, 26 | false, 27 | null, 28 | false 29 | ); 30 | 31 | insert into ImageDimension ( 32 | image_set, 33 | default_type, 34 | shortname, 35 | title, 36 | max_width, 37 | max_height, 38 | crop, 39 | dpi, 40 | quality, 41 | strip, 42 | interlace, 43 | resize_filter, 44 | upscale 45 | ) values ( 46 | (select id from ImageSet where shortname = 'inquisition-question'), 47 | (select id from ImageType where mime_type = 'image/jpeg'), 48 | 'thumb', 49 | 'Thumbnail', 50 | 100, 51 | 100, 52 | true, 53 | 72, 54 | 80, 55 | true, 56 | false, 57 | null, 58 | false 59 | ); 60 | 61 | insert into ImageDimension ( 62 | image_set, 63 | default_type, 64 | shortname, 65 | title, 66 | max_width, 67 | max_height, 68 | crop, 69 | dpi, 70 | quality, 71 | strip, 72 | interlace, 73 | resize_filter, 74 | upscale 75 | ) values ( 76 | (select id from ImageSet where shortname = 'inquisition-question'), 77 | (select id from ImageType where mime_type = 'image/jpeg'), 78 | 'small', 79 | 'Small', 80 | 500, 81 | 500, 82 | true, 83 | 72, 84 | 80, 85 | true, 86 | false, 87 | null, 88 | false 89 | ); 90 | 91 | insert into ImageDimension ( 92 | image_set, 93 | default_type, 94 | shortname, 95 | title, 96 | max_width, 97 | max_height, 98 | crop, 99 | dpi, 100 | quality, 101 | strip, 102 | interlace, 103 | resize_filter, 104 | upscale 105 | ) values ( 106 | (select id from ImageSet where shortname = 'inquisition-question'), 107 | (select id from ImageType where mime_type = 'image/jpeg'), 108 | 'large', 109 | 'Large', 110 | 1000, 111 | 1000, 112 | true, 113 | 72, 114 | 80, 115 | true, 116 | false, 117 | null, 118 | false 119 | ); 120 | -------------------------------------------------------------------------------- /sql/fixtures/InquisitionQuestionOptionImageDimension.sql: -------------------------------------------------------------------------------- 1 | insert into ImageDimension ( 2 | image_set, 3 | default_type, 4 | shortname, 5 | title, 6 | max_width, 7 | max_height, 8 | crop, 9 | dpi, 10 | quality, 11 | strip, 12 | interlace, 13 | resize_filter, 14 | upscale 15 | ) values ( 16 | (select id from ImageSet where shortname = 'inquisition-question-option'), 17 | (select id from ImageType where mime_type = 'image/jpeg'), 18 | 'original', 19 | 'Original', 20 | null, 21 | null, 22 | false, 23 | 72, 24 | 80, 25 | true, 26 | false, 27 | null, 28 | false 29 | ); 30 | 31 | insert into ImageDimension ( 32 | image_set, 33 | default_type, 34 | shortname, 35 | title, 36 | max_width, 37 | max_height, 38 | crop, 39 | dpi, 40 | quality, 41 | strip, 42 | interlace, 43 | resize_filter, 44 | upscale 45 | ) values ( 46 | (select id from ImageSet where shortname = 'inquisition-question-option'), 47 | (select id from ImageType where mime_type = 'image/jpeg'), 48 | 'thumb', 49 | 'Thumbnail', 50 | 100, 51 | 100, 52 | true, 53 | 72, 54 | 80, 55 | true, 56 | false, 57 | null, 58 | false 59 | ); 60 | 61 | insert into ImageDimension ( 62 | image_set, 63 | default_type, 64 | shortname, 65 | title, 66 | max_width, 67 | max_height, 68 | crop, 69 | dpi, 70 | quality, 71 | strip, 72 | interlace, 73 | resize_filter, 74 | upscale 75 | ) values ( 76 | (select id from ImageSet where shortname = 'inquisition-question-option'), 77 | (select id from ImageType where mime_type = 'image/jpeg'), 78 | 'small', 79 | 'Small', 80 | 500, 81 | 500, 82 | true, 83 | 72, 84 | 80, 85 | true, 86 | false, 87 | null, 88 | false 89 | ); 90 | 91 | insert into ImageDimension ( 92 | image_set, 93 | default_type, 94 | shortname, 95 | title, 96 | max_width, 97 | max_height, 98 | crop, 99 | dpi, 100 | quality, 101 | strip, 102 | interlace, 103 | resize_filter, 104 | upscale 105 | ) values ( 106 | (select id from ImageSet where shortname = 'inquisition-question-option'), 107 | (select id from ImageType where mime_type = 'image/jpeg'), 108 | 'large', 109 | 'Large', 110 | 1000, 111 | 1000, 112 | true, 113 | 72, 114 | 80, 115 | true, 116 | false, 117 | null, 118 | false 119 | ); 120 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Question/add.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Question 7 | 8 | 9 | Question 10 | 11 | true 12 | 5 13 | 14 | 15 | 16 | Options 17 | 18 | 19 | 20 | 21 | 2 22 | false 23 | 255 24 | 25 | 26 | 27 | 28 | Correct Option 29 | 30 | 31 | 32 | 33 | 34 | Remove 35 | 36 | 37 | 38 | 4 39 | add another option 40 | 41 | 42 | 43 | 44 | 45 | Done 46 | 47 | 48 | Create and Add Another 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Question/index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Search Questions 7 | 8 | 9 | New Queston 10 | Question/Edit 11 | add 12 | 13 | 14 | 15 | 16 | Keywords 17 | 18 | 19 | 20 | 21 | Search 22 | 23 | 24 | 25 | 26 | 27 | Questions 28 | false 29 | 30 | 31 | 32 | 33 | 34 | id 35 | 36 | 37 | 38 | Question 39 | 40 | bodytext 41 | Question/Details?id=%s 42 | id 43 | 44 | 45 | 46 | 47 | 48 | delete… 49 | 50 | 51 | 52 | 53 | Question 54 | 50 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Question/ImageDelete.php: -------------------------------------------------------------------------------- 1 | question = SwatDBClassMap::new(InquisitionQuestion::class); 21 | $this->question->setDatabase($this->app->db); 22 | 23 | if ($id == '') { 24 | throw new AdminNotFoundException( 25 | 'Question id not provided.' 26 | ); 27 | } 28 | 29 | if (!$this->question->load($id)) { 30 | throw new AdminNotFoundException( 31 | sprintf( 32 | 'Question with id ‘%s’ not found.', 33 | $id 34 | ) 35 | ); 36 | } 37 | 38 | parent::setId($id); 39 | } 40 | 41 | protected function getImageWrapper() 42 | { 43 | return SwatDBClassMap::get(InquisitionQuestionImageWrapper::class); 44 | } 45 | 46 | // build phase 47 | 48 | protected function buildNavBar() 49 | { 50 | AdminDBDelete::buildNavBar(); 51 | 52 | $this->navbar->popEntry(); 53 | 54 | if ($this->inquisition instanceof InquisitionInquisition) { 55 | $this->navbar->createEntry( 56 | $this->inquisition->title, 57 | sprintf( 58 | 'Inquisition/Details?id=%s', 59 | $this->inquisition->id 60 | ) 61 | ); 62 | } 63 | 64 | $this->navbar->createEntry( 65 | $this->getQuestionTitle(), 66 | sprintf( 67 | 'Question/Details?id=%s%s', 68 | $this->question->id, 69 | $this->getLinkSuffix() 70 | ) 71 | ); 72 | 73 | $this->navbar->createEntry( 74 | ngettext( 75 | 'Delete Image', 76 | 'Delete Images', 77 | count($this->images) 78 | ) 79 | ); 80 | } 81 | 82 | protected function getQuestionTitle() 83 | { 84 | // TODO: Update this with some version of getPosition(). 85 | return Inquisition::_('Question'); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionQuestionOption.php: -------------------------------------------------------------------------------- 1 | table = 'InquisitionQuestionOption'; 39 | $this->id_field = 'integer:id'; 40 | 41 | $this->registerInternalProperty( 42 | 'question', 43 | SwatDBClassMap::get(InquisitionQuestion::class) 44 | ); 45 | } 46 | 47 | // loader methods 48 | 49 | protected function loadValues() 50 | { 51 | $sql = sprintf( 52 | 'select * from InquisitionResponseValue 53 | where question_option = %s order by id', 54 | $this->db->quote($this->id, 'integer') 55 | ); 56 | 57 | return SwatDB::query( 58 | $this->db, 59 | $sql, 60 | SwatDBClassMap::get(InquisitionResponseValueWrapper::class) 61 | ); 62 | } 63 | 64 | protected function loadImages() 65 | { 66 | $sql = sprintf( 67 | 'select * from Image 68 | inner join InquisitionQuestionOptionImageBinding 69 | on InquisitionQuestionOptionImageBinding.image = Image.id 70 | where InquisitionQuestionOptionImageBinding.question_option = %s 71 | order by InquisitionQuestionOptionImageBinding.displayorder, 72 | InquisitionQuestionOptionImageBinding.image', 73 | $this->db->quote($this->id, 'integer') 74 | ); 75 | 76 | return SwatDB::query( 77 | $this->db, 78 | $sql, 79 | SwatDBClassMap::get(InquisitionQuestionOptionImageWrapper::class) 80 | ); 81 | } 82 | 83 | protected function loadPosition() 84 | { 85 | $sql = sprintf( 86 | 'select position from ( 87 | select id, rank() over ( 88 | partition by question order by displayorder, id 89 | ) as position from InquisitionQuestionOption 90 | ) as temp where id = %s', 91 | $this->id 92 | ); 93 | 94 | return SwatDB::queryOne($this->db, $sql); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Option/details.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Edit 9 | Option/Edit?id=%s 10 | edit 11 | 12 | 13 | 14 | 15 | 16 | 17 | Bodytext 18 | 19 | title 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | Images 29 | 30 | 31 | 32 | Add Image 33 | Option/ImageUpload?id=%s 34 | add 35 | 36 | 37 | Change Image Order 38 | Option/ImageOrder?id=%s 39 | change-order 40 | 41 | 42 | 43 | 44 | 45 | 46 | image 47 | width 48 | height 49 | preview_image 50 | preview_width 51 | preview_height 52 | false 53 | 54 | 55 | 56 | id 57 | 58 | 59 | 60 | 61 | 62 | delete… 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /Inquisition/InquisitionFileParser.php: -------------------------------------------------------------------------------- 1 | file = $filename; 23 | } else { 24 | $this->file = new SplFileObject($filename, 'r'); 25 | } 26 | 27 | $this->file->setFlags(SplFileObject::READ_CSV); 28 | 29 | $this->defuseBOM(); 30 | } 31 | 32 | public function key(): mixed 33 | { 34 | return $this->file->key(); 35 | } 36 | 37 | public function current(): mixed 38 | { 39 | return $this->file->current(); 40 | } 41 | 42 | public function next(): void 43 | { 44 | if (!$this->file->eof()) { 45 | // count newlines in csv columns 46 | $this->line += mb_substr_count(implode('', $this->current()), "\n"); 47 | 48 | // count next line 49 | $this->line++; 50 | 51 | $this->file->next(); 52 | 53 | // Need to call current to parse next line, otherwise the eof() 54 | // call will not be valid. 55 | $current = $this->current(); 56 | 57 | // skip blank lines 58 | while (!$this->file->eof()) { 59 | if (array_pop($current) === null) { 60 | $this->line++; 61 | $this->file->next(); 62 | $current = $this->current(); 63 | } else { 64 | break; 65 | } 66 | } 67 | } 68 | } 69 | 70 | public function rewind(): void 71 | { 72 | $this->file->rewind(); 73 | $this->line = 1; 74 | } 75 | 76 | public function valid(): bool 77 | { 78 | return $this->file->valid(); 79 | } 80 | 81 | public function eof() 82 | { 83 | return $this->file->eof(); 84 | } 85 | 86 | public function line() 87 | { 88 | return $this->line; 89 | } 90 | 91 | public function row() 92 | { 93 | return $this->file->key() + 1; 94 | } 95 | 96 | /** 97 | * Seeks ahead of the byte-order-mark if it exists in the file. 98 | * 99 | * If there is a BOM at the start of the current line, then move the file 100 | * pointer past the BOM. This will cause subsequent calls to 101 | * $file->current() to skip the BOM. 102 | */ 103 | protected function defuseBOM() 104 | { 105 | $bom = "\xef\xbb\xbf"; 106 | $data = $this->file->current(); 107 | $encoding = '8bit'; 108 | 109 | if (mb_strpos($data[0], $bom, 0, $encoding) === 0) { 110 | $this->file->fseek(mb_strlen($bom, $encoding)); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Question/Import.php: -------------------------------------------------------------------------------- 1 | ui->getWidget('questions_file'); 33 | if ($questions_file->isUploaded()) { 34 | $this->importInquisition($questions_file->getTempFileName()); 35 | } 36 | } 37 | 38 | protected function importInquisition($filename) 39 | { 40 | $inquisition = $this->getObject(); 41 | $initial_questions_count = count($inquisition->question_bindings); 42 | 43 | try { 44 | $importer = $this->getImporter(); 45 | $importer->importInquisition( 46 | $inquisition, 47 | $this->getFileParser($filename) 48 | ); 49 | } catch (InquisitionImportException $e) { 50 | $this->ui->getWidget('questions_file')->addMessage( 51 | new SwatMessage($e->getMessage()) 52 | ); 53 | } 54 | 55 | $final_question_count = count($inquisition->question_bindings); 56 | $this->imported_question_count = $final_question_count - 57 | $initial_questions_count; 58 | } 59 | 60 | protected function getSavedMessage() 61 | { 62 | $locale = SwatI18NLocale::get(); 63 | 64 | return new SwatMessage( 65 | sprintf( 66 | Inquisition::ngettext( 67 | 'One question has been imported.', 68 | '%s questions have been imported.', 69 | $this->imported_question_count 70 | ), 71 | $locale->formatNumber($this->imported_question_count) 72 | ) 73 | ); 74 | } 75 | 76 | protected function getFileParser($filename) 77 | { 78 | return new InquisitionFileParser($filename); 79 | } 80 | 81 | protected function getImporter() 82 | { 83 | return new InquisitionImporter($this->app); 84 | } 85 | 86 | // build phase 87 | 88 | protected function buildFrame() 89 | { 90 | parent::buildFrame(); 91 | 92 | $this->ui->getWidget('edit_frame')->title = 93 | Inquisition::_('Import Questions'); 94 | } 95 | 96 | protected function buildNavBar() 97 | { 98 | parent::buildNavBar(); 99 | 100 | $this->navbar->createEntry(Inquisition::_('Import Questions')); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Inquisition/Edit.php: -------------------------------------------------------------------------------- 1 | ui->loadFromXML($this->getUiXml()); 22 | $this->initInquisition(); 23 | } 24 | 25 | protected function initInquisition() 26 | { 27 | $this->inquisition = SwatDBClassMap::new(InquisitionInquisition::class); 28 | $this->inquisition->setDatabase($this->app->db); 29 | 30 | if ($this->id != '') { 31 | if (!$this->inquisition->load($this->id)) { 32 | throw new AdminNotFoundException( 33 | sprintf('Inquisition with id ‘%s’ not found.', $this->id) 34 | ); 35 | } 36 | } 37 | } 38 | 39 | protected function getUiXml() 40 | { 41 | return __DIR__ . '/edit.xml'; 42 | } 43 | 44 | // process phase 45 | 46 | protected function saveDBData(): void 47 | { 48 | $this->updateInquisition(); 49 | 50 | if ($this->inquisition->isModified()) { 51 | $this->inquisition->save(); 52 | $this->app->messages->add($this->getSavedMessage()); 53 | } 54 | } 55 | 56 | protected function updateInquisition() 57 | { 58 | $values = $this->ui->getValues( 59 | [ 60 | 'title', 61 | ] 62 | ); 63 | 64 | $this->inquisition->title = $values['title']; 65 | 66 | if ($this->ui->hasWidget('enabled')) { 67 | $this->inquisition->enabled = 68 | $this->ui->getWidget('enabled')->value; 69 | } 70 | 71 | if ($this->inquisition->id === null) { 72 | $now = new SwatDate(); 73 | $now->toUTC(); 74 | $this->inquisition->createdate = $now; 75 | } 76 | } 77 | 78 | protected function getSavedMessage() 79 | { 80 | return new SwatMessage(Inquisition::_('Inquisition has been saved.')); 81 | } 82 | 83 | protected function relocate() 84 | { 85 | $this->app->relocate( 86 | sprintf( 87 | 'Inquisition/Details?id=%s', 88 | $this->inquisition->id 89 | ) 90 | ); 91 | } 92 | 93 | // build phase 94 | 95 | protected function loadDBData() 96 | { 97 | $this->ui->setValues($this->inquisition->getAttributes()); 98 | } 99 | 100 | protected function buildNavBar() 101 | { 102 | parent::buildNavBar(); 103 | 104 | $last = $this->navbar->popEntry(); 105 | 106 | if ($this->id != '') { 107 | $this->navbar->createEntry( 108 | $this->inquisition->title, 109 | sprintf( 110 | 'Inquisition/Details?id=%s', 111 | $this->inquisition->id 112 | ) 113 | ); 114 | } 115 | 116 | $this->navbar->addEntry($last); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Inquisition/Index.php: -------------------------------------------------------------------------------- 1 | ui->loadFromXML($this->getUiXml()); 18 | 19 | $view = $this->ui->getWidget('inquisition_view'); 20 | $view->setDefaultOrderbyColumn( 21 | $view->getColumn('title'), 22 | SwatTableViewOrderableColumn::ORDER_BY_DIR_ASCENDING 23 | ); 24 | } 25 | 26 | protected function getUiXml() 27 | { 28 | return __DIR__ . '/index.xml'; 29 | } 30 | 31 | // build phase 32 | 33 | protected function getTableModel(SwatView $view): ?SwatTableModel 34 | { 35 | switch ($view->id) { 36 | case 'inquisition_view': 37 | return $this->getInquisitionTableModel($view); 38 | } 39 | 40 | return null; 41 | } 42 | 43 | protected function getInquisitionTableModel(SwatView $view) 44 | { 45 | $sql = sprintf( 46 | 'select * from Inquisition 47 | order by %s', 48 | $this->getOrderByClause($view, 'title asc') 49 | ); 50 | 51 | $inquisitions = SwatDB::query( 52 | $this->app->db, 53 | $sql, 54 | SwatDBClassMap::get(InquisitionInquisitionWrapper::class) 55 | ); 56 | 57 | // efficiently load the question bindings 58 | $inquisitions->loadAllSubRecordsets( 59 | 'question_bindings', 60 | SwatDBClassMap::get(InquisitionInquisitionQuestionBindingWrapper::class), 61 | 'InquisitionInquisitionQuestionBinding', 62 | 'inquisition', 63 | '', 64 | 'inquisition, displayorder, question' 65 | ); 66 | 67 | $locale = SwatI18NLocale::get(); 68 | $store = new SwatTableStore(); 69 | 70 | foreach ($inquisitions as $inquisition) { 71 | $ds = new SwatDetailsStore($inquisition); 72 | $question_count = count($inquisition->question_bindings); 73 | $visible_question_count = count( 74 | $inquisition->visible_question_bindings 75 | ); 76 | 77 | if ($visible_question_count != $question_count) { 78 | $ds->question_count = sprintf( 79 | Inquisition::ngettext( 80 | '%s question, %s shown on site', 81 | '%s questions, %s shown on site', 82 | $question_count 83 | ), 84 | $locale->formatNumber($question_count), 85 | $locale->formatNumber($visible_question_count) 86 | ); 87 | } else { 88 | $ds->question_count = sprintf( 89 | Inquisition::ngettext( 90 | '%s question', 91 | '%s questions', 92 | $question_count 93 | ), 94 | $locale->formatNumber($question_count) 95 | ); 96 | } 97 | 98 | $store->add($ds); 99 | } 100 | 101 | return $store; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Inquisition/InquisitionRadioEntryList.php: -------------------------------------------------------------------------------- 1 | addJavaScript( 25 | 'packages/inquisition/javascript/inquisition-radio-entry-list.js' 26 | ); 27 | 28 | $yui = new SwatYUI(['dom', 'event']); 29 | $this->html_head_entry_set->addEntrySet($yui->getHtmlHeadEntrySet()); 30 | 31 | $this->classes[] = 'inquisition-radio-entry-list'; 32 | } 33 | 34 | /** 35 | * Processes this radio list. 36 | */ 37 | public function process() 38 | { 39 | parent::process(); 40 | 41 | if ($this->hasEntry($this->value) 42 | && $this->getEntryValue($this->value) === null) { 43 | $message = Inquisition::_( 44 | 'The selected option requires a value to be entered.' 45 | ); 46 | 47 | $this->addMessage(new SwatMessage($message, 'error')); 48 | } 49 | } 50 | 51 | public function getEntryValue($option_value) 52 | { 53 | $value = null; 54 | 55 | if ($this->hasEntry($option_value)) { 56 | $value = $this->getCompositeWidget('entry_' . $option_value)->value; 57 | } 58 | 59 | return $value; 60 | } 61 | 62 | public function setEntryValue($option_value, $text) 63 | { 64 | if ($this->hasEntry($option_value)) { 65 | $this->getCompositeWidget('entry_' . $option_value)->value = $text; 66 | } 67 | } 68 | 69 | public function setEntryOption($value) 70 | { 71 | $this->entry_option_values[] = $value; 72 | } 73 | 74 | public function hasEntry($value) 75 | { 76 | return in_array($value, $this->entry_option_values); 77 | } 78 | 79 | public function display() 80 | { 81 | parent::display(); 82 | Swat::displayInlineJavaScript($this->getInlineJavaScript()); 83 | } 84 | 85 | protected function displayOptionLabel(SwatOption $option, $index) 86 | { 87 | parent::displayOptionLabel($option, $index); 88 | 89 | if ($this->hasEntry($option->value)) { 90 | echo ''; 91 | $this->getCompositeWidget('entry_' . $option->value)->display(); 92 | echo ''; 93 | } 94 | } 95 | 96 | protected function createCompositeWidgets() 97 | { 98 | parent::createCompositeWidgets(); 99 | 100 | foreach ($this->entry_option_values as $value) { 101 | $entry = new SwatEntry($this->id . '_entry_' . $value); 102 | $entry->maxlength = 255; 103 | $this->addCompositeWidget($entry, 'entry_' . $value); 104 | } 105 | } 106 | 107 | protected function getInlineJavaScript() 108 | { 109 | return sprintf( 110 | 'var %s_obj = new InquisitionRadioEntryList(%s);', 111 | $this->id, 112 | SwatString::quoteJavaScriptString($this->id) 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Option/ImageDelete.php: -------------------------------------------------------------------------------- 1 | option = SwatDBClassMap::new(InquisitionQuestionOption::class); 21 | $this->option->setDatabase($this->app->db); 22 | 23 | if ($id == '') { 24 | throw new AdminNotFoundException( 25 | 'Option id not provided.' 26 | ); 27 | } 28 | 29 | if (!$this->option->load($id)) { 30 | throw new AdminNotFoundException( 31 | sprintf( 32 | 'Option with id ‘%s’ not found.', 33 | $id 34 | ) 35 | ); 36 | } 37 | 38 | parent::setId($id); 39 | } 40 | 41 | protected function getImageWrapper() 42 | { 43 | return SwatDBClassMap::get(InquisitionQuestionOptionImageWrapper::class); 44 | } 45 | 46 | // build phase 47 | 48 | protected function buildNavBar() 49 | { 50 | AdminDBDelete::buildNavBar(); 51 | 52 | $this->navbar->popEntry(); 53 | 54 | if ($this->inquisition instanceof InquisitionInquisition) { 55 | $this->navbar->createEntry( 56 | $this->inquisition->title, 57 | sprintf( 58 | 'Inquisition/Details?id=%s', 59 | $this->inquisition->id 60 | ) 61 | ); 62 | } 63 | 64 | $this->navbar->createEntry( 65 | $this->getQuestionTitle(), 66 | sprintf( 67 | 'Question/Details?id=%s%s', 68 | $this->option->question->id, 69 | $this->getLinkSuffix() 70 | ) 71 | ); 72 | 73 | if ($this->option instanceof InquisitionQuestionOption) { 74 | $this->navbar->createEntry( 75 | $this->getOptionTitle(), 76 | sprintf( 77 | 'Option/Details?id=%s%s', 78 | $this->option->id, 79 | $this->getLinkSuffix() 80 | ) 81 | ); 82 | } 83 | 84 | $this->navbar->createEntry( 85 | Inquisition::ngettext( 86 | 'Delete Image', 87 | 'Delete Images', 88 | count($this->images) 89 | ) 90 | ); 91 | } 92 | 93 | protected function getQuestionTitle() 94 | { 95 | // TODO: Update this with some version of getPosition(). 96 | return Inquisition::_('Question'); 97 | } 98 | 99 | protected function getOptionTitle() 100 | { 101 | return sprintf( 102 | Inquisition::_('Option %s'), 103 | $this->option->position 104 | ); 105 | } 106 | 107 | protected function getLinkSuffix() 108 | { 109 | $suffix = null; 110 | if ($this->inquisition instanceof InquisitionInquisition) { 111 | $suffix = sprintf( 112 | '&inquisition=%s', 113 | $this->inquisition->id 114 | ); 115 | } 116 | 117 | return $suffix; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Question/Index.php: -------------------------------------------------------------------------------- 1 | ui->loadFromXML($this->getUiXml()); 18 | 19 | $view = $this->ui->getWidget('index_view'); 20 | $view->setDefaultOrderbyColumn( 21 | $view->getColumn('bodytext'), 22 | SwatTableViewOrderableColumn::ORDER_BY_DIR_ASCENDING 23 | ); 24 | } 25 | 26 | protected function getUiXml() 27 | { 28 | return __DIR__ . '/index.xml'; 29 | } 30 | 31 | // process phase 32 | 33 | protected function processActions(SwatView $view, SwatActions $actions) 34 | { 35 | switch ($actions->selected->id) { 36 | case 'delete': 37 | $this->app->replacePage('Question/Delete'); 38 | $this->app->getPage()->setItems($view->getSelection()); 39 | break; 40 | } 41 | } 42 | 43 | protected function processInternal() 44 | { 45 | parent::processInternal(); 46 | 47 | $pager = $this->ui->getWidget('pager'); 48 | $pager->total_records = SwatDB::queryOne( 49 | $this->app->db, 50 | sprintf( 51 | 'select count(id) from InquisitionQuestion where %s', 52 | $this->getWhereClause() 53 | ) 54 | ); 55 | 56 | $pager->process(); 57 | } 58 | 59 | // build phase 60 | 61 | protected function getTableModel(SwatView $view): ?SwatTableModel 62 | { 63 | switch ($view->id) { 64 | case 'index_view': 65 | return $this->getQuestionTableModel($view); 66 | } 67 | 68 | return null; 69 | } 70 | 71 | protected function getQuestionTableModel(SwatView $view): SwatTableStore 72 | { 73 | $sql = sprintf( 74 | 'select InquisitionQuestion.* 75 | from InquisitionQuestion 76 | where %s 77 | order by %s', 78 | $this->getWhereClause(), 79 | $this->getOrderByClause($view, 'bodytext asc') 80 | ); 81 | 82 | $pager = $this->ui->getWidget('pager'); 83 | $this->app->db->setLimit($pager->page_size, $pager->current_record); 84 | 85 | $questions = SwatDB::query( 86 | $this->app->db, 87 | $sql, 88 | SwatDBClassMap::get(InquisitionQuestionWrapper::class) 89 | ); 90 | 91 | if (count($questions) > 0) { 92 | $this->ui->getWidget('results_message')->content = 93 | $pager->getResultsMessage(); 94 | } 95 | 96 | $store = new SwatTableStore(); 97 | foreach ($questions as $question) { 98 | $ds = new SwatDetailsStore($question); 99 | $ds->bodytext = SwatString::condense($question->bodytext, 100); 100 | $store->add($ds); 101 | } 102 | 103 | return $store; 104 | } 105 | 106 | protected function getWhereClause() 107 | { 108 | $where = '1 = 1'; 109 | 110 | // email 111 | $clause = new AdminSearchClause('bodytext'); 112 | $clause->table = 'InquisitionQuestion'; 113 | $clause->value = $this->ui->getWidget('search_keywords')->value; 114 | $clause->operator = AdminSearchClause::OP_CONTAINS; 115 | $where .= $clause->getClause($this->app->db); 116 | 117 | return $where; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionInquisitionQuestionBinding.php: -------------------------------------------------------------------------------- 1 | question->getView($this); 29 | } 30 | 31 | public function getPosition() 32 | { 33 | $sql = sprintf( 34 | 'select position from ( 35 | select id, rank() over ( 36 | partition by inquisition order by displayorder, id 37 | ) as position from InquisitionInquisitionQuestionBinding 38 | where inquisition = %s 39 | ) as temp where id = %s', 40 | $this->getInternalValue('inquisition'), 41 | $this->id 42 | ); 43 | 44 | return SwatDB::queryOne($this->db, $sql); 45 | } 46 | 47 | public function getDependentOptions() 48 | { 49 | if (!is_array($this->dependent_options)) { 50 | $sql = sprintf( 51 | 'select InquisitionQuestionDependency.option, 52 | InquisitionQuestionDependency.question_binding, 53 | InquisitionInquisitionQuestionBinding.question 54 | from InquisitionQuestionDependency 55 | inner join InquisitionInquisitionQuestionBinding on 56 | InquisitionQuestionDependency.question_binding = 57 | InquisitionInquisitionQuestionBinding.id 58 | where dependent_question_binding = %s', 59 | $this->db->quote($this->id, 'integer') 60 | ); 61 | 62 | $rs = SwatDB::query($this->db, $sql); 63 | 64 | $dependent_options = []; 65 | 66 | foreach ($rs as $row) { 67 | $option = []; 68 | 69 | $id = $row->question_binding . '_' . $row->question; 70 | 71 | $option['binding'] = $row->question_binding; 72 | $option['question'] = $row->question; 73 | $option['options'] = [$row->option]; 74 | 75 | if (array_key_exists($id, $dependent_options)) { 76 | $dependent_options[$id]['options'][] = $row->option; 77 | } else { 78 | $dependent_options[$id] = $option; 79 | } 80 | } 81 | 82 | $this->dependent_options = array_values($dependent_options); 83 | } 84 | 85 | return $this->dependent_options; 86 | } 87 | 88 | protected function init() 89 | { 90 | $this->table = 'InquisitionInquisitionQuestionBinding'; 91 | $this->id_field = 'integer:id'; 92 | 93 | $this->registerInternalProperty( 94 | 'inquisition', 95 | SwatDBClassMap::get(InquisitionInquisition::class) 96 | ); 97 | 98 | // We set autosave so that questions are saved before the binding. 99 | $this->registerInternalProperty( 100 | 'question', 101 | SwatDBClassMap::get(InquisitionQuestion::class), 102 | true 103 | ); 104 | } 105 | 106 | protected function getSerializableSubDataObjects() 107 | { 108 | return array_merge( 109 | parent::getSerializableSubDataObjects(), 110 | ['question'] 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Inquisition/ImageUpload.php: -------------------------------------------------------------------------------- 1 | ui->loadFromXML($this->getUiXml()); 23 | 24 | $this->initInquisition(); 25 | } 26 | 27 | protected function initInquisition() 28 | { 29 | $inquisition_id = SiteApplication::initVar('inquisition'); 30 | 31 | if ($inquisition_id !== null) { 32 | $this->inquisition = $this->loadInquisition($inquisition_id); 33 | } 34 | } 35 | 36 | protected function loadInquisition($inquisition_id) 37 | { 38 | $inquisition = SwatDBClassMap::new(InquisitionInquisition::class); 39 | $inquisition->setDatabase($this->app->db); 40 | 41 | if (!$inquisition->load($inquisition_id)) { 42 | throw new AdminNotFoundException( 43 | sprintf( 44 | 'Inquisition with id ‘%s’ not found.', 45 | $inquisition_id 46 | ) 47 | ); 48 | } 49 | 50 | return $inquisition; 51 | } 52 | 53 | protected function getUiXml() 54 | { 55 | return __DIR__ . '/image-upload.xml'; 56 | } 57 | 58 | // process phase 59 | 60 | protected function saveDBData(): void 61 | { 62 | $original = $this->ui->getWidget('original_image'); 63 | 64 | $image = $this->getImageObject(); 65 | $image->process($original->getTempFileName()); 66 | 67 | $this->updateBindings($image); 68 | 69 | $this->app->messages->add( 70 | new SwatMessage( 71 | sprintf( 72 | Inquisition::_('Image has been saved.'), 73 | $image->title 74 | ) 75 | ) 76 | ); 77 | } 78 | 79 | protected function getImageObject() 80 | { 81 | $class_name = $this->getImageClass(); 82 | 83 | $image = new $class_name(); 84 | $image->setDatabase($this->app->db); 85 | $image->setFileBase('../images'); 86 | 87 | return $image; 88 | } 89 | 90 | abstract protected function getImageClass(); 91 | 92 | abstract protected function updateBindings(SiteImage $image); 93 | 94 | // build phase 95 | 96 | protected function buildForm() 97 | { 98 | parent::buildForm(); 99 | 100 | if ($this->inquisition instanceof InquisitionInquisition) { 101 | $form = $this->ui->getWidget('edit_form'); 102 | $form->addHiddenField('inquisition', $this->inquisition->id); 103 | } 104 | } 105 | 106 | protected function buildNavBar() 107 | { 108 | parent::buildNavBar(); 109 | 110 | $this->navbar->popEntry(); 111 | 112 | if ($this->inquisition instanceof InquisitionInquisition) { 113 | $this->navbar->createEntry( 114 | $this->inquisition->title, 115 | sprintf( 116 | 'Inquisition/Details?id=%s', 117 | $this->inquisition->id 118 | ) 119 | ); 120 | } 121 | } 122 | 123 | protected function getLinkSuffix() 124 | { 125 | $suffix = null; 126 | if ($this->inquisition instanceof InquisitionInquisition) { 127 | $suffix = sprintf( 128 | '&inquisition=%s', 129 | $this->inquisition->id 130 | ); 131 | } 132 | 133 | return $suffix; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Inquisition/Inquisition.php: -------------------------------------------------------------------------------- 1 | addJavaScript( 25 | 'packages/inquisition/javascript/inquisition-checkbox-entry-list.js' 26 | ); 27 | 28 | $yui = new SwatYUI(['dom', 'event']); 29 | $this->html_head_entry_set->addEntrySet($yui->getHtmlHeadEntrySet()); 30 | 31 | $this->classes[] = 'inquisition-checkbox-entry-list'; 32 | } 33 | 34 | /** 35 | * Processes this checkbox list. 36 | */ 37 | public function process() 38 | { 39 | parent::process(); 40 | 41 | foreach ($this->values as $value) { 42 | if ($this->hasEntry($value) 43 | && $this->getEntryValue($value) == '') { 44 | $message = Inquisition::_( 45 | 'The selected option requires a value to be entered.' 46 | ); 47 | 48 | $this->addMessage(new SwatMessage($message, 'error')); 49 | } 50 | } 51 | } 52 | 53 | public function getEntryValue($option_value) 54 | { 55 | $value = null; 56 | 57 | if ($this->hasEntry($option_value)) { 58 | $value = $this->getCompositeWidget('entry_' . $option_value)->value; 59 | } 60 | 61 | return $value; 62 | } 63 | 64 | public function setEntryValue($option_value, $text) 65 | { 66 | if ($this->hasEntry($option_value)) { 67 | $this->getCompositeWidget('entry_' . $option_value)->value = $text; 68 | } 69 | } 70 | 71 | public function setEntryOption($value) 72 | { 73 | $this->entry_option_values[] = $value; 74 | } 75 | 76 | public function hasEntry($value) 77 | { 78 | return in_array($value, $this->entry_option_values); 79 | } 80 | 81 | public function display() 82 | { 83 | parent::display(); 84 | Swat::displayInlineJavaScript($this->getInlineJavaScript()); 85 | } 86 | 87 | protected function displayOptionLabel(SwatOption $option, $index) 88 | { 89 | parent::displayOptionLabel($option, $index); 90 | 91 | if ($this->hasEntry($option->value)) { 92 | echo ''; 93 | $this->getCompositeWidget('entry_' . $option->value)->display(); 94 | echo ''; 95 | } 96 | } 97 | 98 | protected function createCompositeWidgets() 99 | { 100 | parent::createCompositeWidgets(); 101 | 102 | $options = $this->getOptions(); 103 | 104 | foreach ($this->entry_option_values as $value) { 105 | // get index of checkbox 106 | $index = 0; 107 | foreach ($options as $option) { 108 | if ($option->value === $value) { 109 | break; 110 | } 111 | $index++; 112 | } 113 | 114 | $entry = new SwatEntry($this->id . '_entry_' . $index); 115 | $entry->maxlength = 255; 116 | $this->addCompositeWidget($entry, 'entry_' . $value); 117 | } 118 | } 119 | 120 | protected function getInlineJavaScript() 121 | { 122 | return sprintf( 123 | 'var %s_obj = new InquisitionCheckboxEntryList(%s);', 124 | $this->id, 125 | SwatString::quoteJavaScriptString($this->id) 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Question/Order.php: -------------------------------------------------------------------------------- 1 | initInquisition(); 23 | } 24 | 25 | protected function initInquisition() 26 | { 27 | $id = SiteApplication::initVar('id'); 28 | 29 | if ($id == '') { 30 | throw new AdminNotFoundException( 31 | 'No inquisition id specified.' 32 | ); 33 | } 34 | 35 | if (is_numeric($id)) { 36 | $id = intval($id); 37 | } 38 | 39 | $this->inquisition = SwatDBClassMap::new(InquisitionInquisition::class); 40 | $this->inquisition->setDatabase($this->app->db); 41 | 42 | if (!$this->inquisition->load($id)) { 43 | throw new AdminNotFoundException( 44 | sprintf( 45 | 'An inquisition with the id of “%s” does not exist', 46 | $id 47 | ) 48 | ); 49 | } 50 | } 51 | 52 | // process phase 53 | 54 | protected function saveIndex($id, $index) 55 | { 56 | SwatDB::updateColumn( 57 | $this->app->db, 58 | 'InquisitionInquisitionQuestionBinding', 59 | 'integer:displayorder', 60 | $index, 61 | 'integer:id', 62 | [$id] 63 | ); 64 | } 65 | 66 | protected function getUpdatedMessage() 67 | { 68 | return new SwatMessage( 69 | Inquisition::_('Question order has been updated.') 70 | ); 71 | } 72 | 73 | protected function relocate() 74 | { 75 | $this->app->relocate( 76 | sprintf( 77 | 'Inquisition/Details?id=%s', 78 | $this->inquisition->id 79 | ) 80 | ); 81 | } 82 | 83 | // build phase 84 | 85 | protected function buildInternal() 86 | { 87 | $this->ui->getWidget('order_frame')->title = 88 | Inquisition::_('Change Question Order'); 89 | 90 | $this->ui->getWidget('order')->width = '500px'; 91 | $this->ui->getWidget('order')->height = '500px'; 92 | 93 | parent::buildInternal(); 94 | } 95 | 96 | protected function buildNavBar() 97 | { 98 | parent::buildNavBar(); 99 | 100 | $this->navbar->popEntries(2); 101 | 102 | $this->navbar->createEntry( 103 | Inquisition::_('Inquisition'), 104 | 'Inquisition' 105 | ); 106 | 107 | $this->navbar->createEntry( 108 | $this->inquisition->title, 109 | sprintf( 110 | 'Inquisition/Details?id=%s', 111 | $this->inquisition->id 112 | ) 113 | ); 114 | 115 | $this->navbar->createEntry(Inquisition::_('Change Question Order')); 116 | } 117 | 118 | protected function buildForm() 119 | { 120 | parent::buildForm(); 121 | 122 | $form = $this->ui->getWidget('order_form'); 123 | $form->addHiddenField('id', $this->inquisition->id); 124 | } 125 | 126 | protected function loadData() 127 | { 128 | $sum = 0; 129 | $order_widget = $this->ui->getWidget('order'); 130 | 131 | foreach ($this->inquisition->question_bindings as $question_binding) { 132 | $sum += $question_binding->displayorder; 133 | 134 | $order_widget->addOption( 135 | $question_binding->id, 136 | $question_binding->question->bodytext, 137 | 'text/xml' 138 | ); 139 | } 140 | 141 | $options_list = $this->ui->getWidget('options'); 142 | $options_list->value = ($sum == 0) ? 'auto' : 'custom'; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Question/ImageUpload.php: -------------------------------------------------------------------------------- 1 | initQuestion(); 23 | } 24 | 25 | protected function initQuestion() 26 | { 27 | if ($this->id == '') { 28 | throw new AdminNotFoundException( 29 | Inquisition::_('Unable to load a question.') 30 | ); 31 | } 32 | 33 | $this->question = SwatDBClassMap::new(InquisitionQuestion::class); 34 | $this->question->setDatabase($this->app->db); 35 | 36 | if (!$this->question->load($this->id)) { 37 | throw new AdminNotFoundException( 38 | sprintf( 39 | Inquisition::_( 40 | 'Unable to load question with id of “%s”.' 41 | ), 42 | $this->id 43 | ) 44 | ); 45 | } 46 | } 47 | 48 | // process phase 49 | 50 | protected function getImageClass() 51 | { 52 | return SwatDBClassMap::get(InquisitionQuestionImage::class); 53 | } 54 | 55 | protected function updateBindings(SiteImage $image) 56 | { 57 | // set displayorder so the new image appears at the end of the 58 | // list of the current questions by default. 59 | $sql = sprintf( 60 | 'select coalesce(max(displayorder), 0)+10 61 | from InquisitionQuestionImageBinding where question = %s', 62 | $this->app->db->quote($this->question->id, 'integer') 63 | ); 64 | 65 | $displayorder = SwatDB::queryOne($this->app->db, $sql); 66 | 67 | $sql = sprintf( 68 | 'insert into InquisitionQuestionImageBinding 69 | (question, image, displayorder) values (%s, %s, %s)', 70 | $this->app->db->quote($this->question->id, 'integer'), 71 | $this->app->db->quote($image->id, 'integer'), 72 | $this->app->db->quote($displayorder, 'integer') 73 | ); 74 | 75 | SwatDB::exec($this->app->db, $sql); 76 | } 77 | 78 | protected function relocate() 79 | { 80 | $this->app->relocate( 81 | sprintf( 82 | 'Question/Details?id=%s%s', 83 | $this->question->id, 84 | $this->getLinkSuffix() 85 | ) 86 | ); 87 | } 88 | 89 | // build phase 90 | 91 | protected function loadDBData() {} 92 | 93 | protected function buildNavBar() 94 | { 95 | parent::buildNavBar(); 96 | 97 | $this->navbar->createEntry( 98 | $this->getQuestionTitle(), 99 | sprintf( 100 | 'Question/Details?id=%s%s', 101 | $this->question->id, 102 | $this->getLinkSuffix() 103 | ) 104 | ); 105 | 106 | $this->navbar->createEntry(Inquisition::_('Add Image')); 107 | } 108 | 109 | protected function buildFrame() 110 | { 111 | $frame = $this->ui->getWidget('edit_frame'); 112 | $frame->title = $this->getQuestionTitle(); 113 | 114 | $frame->subtitle = Inquisition::_('Add Image'); 115 | } 116 | 117 | protected function getQuestionTitle() 118 | { 119 | // TODO: Update this with some version of getPosition(). 120 | return Inquisition::_('Question'); 121 | } 122 | 123 | protected function getLinkSuffix() 124 | { 125 | $suffix = null; 126 | if ($this->inquisition instanceof InquisitionInquisition) { 127 | $suffix = sprintf( 128 | '&inquisition=%s', 129 | $this->inquisition->id 130 | ); 131 | } 132 | 133 | return $suffix; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Option/ImageUpload.php: -------------------------------------------------------------------------------- 1 | initOption(); 23 | } 24 | 25 | protected function initOption() 26 | { 27 | if ($this->id == '') { 28 | throw new AdminNotFoundException( 29 | Inquisition::_('Unable to load a option.') 30 | ); 31 | } 32 | 33 | $this->option = SwatDBClassMap::new(InquisitionQuestionOption::class); 34 | $this->option->setDatabase($this->app->db); 35 | 36 | if (!$this->option->load($this->id)) { 37 | throw new AdminNotFoundException( 38 | sprintf( 39 | Inquisition::_( 40 | 'Unable to load option with id of “%s”.' 41 | ), 42 | $this->id 43 | ) 44 | ); 45 | } 46 | } 47 | 48 | // process phase 49 | 50 | protected function getImageClass() 51 | { 52 | return SwatDBClassMap::get(InquisitionQuestionOptionImage::class); 53 | } 54 | 55 | protected function updateBindings(SiteImage $image) 56 | { 57 | $sql = sprintf( 58 | 'insert into InquisitionQuestionOptionImageBinding 59 | (question_option, image) values (%s, %s)', 60 | $this->app->db->quote($this->option->id, 'integer'), 61 | $this->app->db->quote($image->id, 'integer') 62 | ); 63 | 64 | SwatDB::exec($this->app->db, $sql); 65 | } 66 | 67 | protected function relocate() 68 | { 69 | $this->app->relocate( 70 | sprintf( 71 | 'Option/Details?id=%s%s', 72 | $this->option->id, 73 | $this->getLinkSuffix() 74 | ) 75 | ); 76 | } 77 | 78 | // build phase 79 | 80 | protected function loadDBData() {} 81 | 82 | protected function buildFrame() 83 | { 84 | $frame = $this->ui->getWidget('edit_frame'); 85 | $frame->title = sprintf($this->getOptionTitle()); 86 | $frame->subtitle = $this->getTitle(); 87 | } 88 | 89 | protected function buildNavBar() 90 | { 91 | parent::buildNavBar(); 92 | 93 | $this->navbar->createEntry( 94 | $this->getQuestionTitle(), 95 | sprintf( 96 | 'Question/Details?id=%s%s', 97 | $this->option->question->id, 98 | $this->getLinkSuffix() 99 | ) 100 | ); 101 | 102 | $this->navbar->createEntry( 103 | $this->getOptionTitle(), 104 | sprintf( 105 | 'Option/Details?id=%s%s', 106 | $this->option->id, 107 | $this->getLinkSuffix() 108 | ) 109 | ); 110 | 111 | $this->navbar->createEntry($this->getTitle()); 112 | } 113 | 114 | protected function getTitle() 115 | { 116 | return Inquisition::_('Add Image'); 117 | } 118 | 119 | protected function getOptionTitle() 120 | { 121 | return sprintf( 122 | Inquisition::_('Option %s'), 123 | $this->option->position 124 | ); 125 | } 126 | 127 | protected function getQuestionTitle() 128 | { 129 | // TODO: Update this with some version of getPosition(). 130 | return Inquisition::_('Question'); 131 | } 132 | 133 | protected function getLinkSuffix() 134 | { 135 | $suffix = null; 136 | if ($this->inquisition instanceof InquisitionInquisition) { 137 | $suffix = sprintf( 138 | '&inquisition=%s', 139 | $this->inquisition->id 140 | ); 141 | } 142 | 143 | return $suffix; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Inquisition/details.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Edit 9 | Inquisition/Edit?id=%s 10 | edit 11 | 12 | 13 | Delete 14 | Inquisition/Delete?id=%s 15 | delete 16 | 17 | 18 | 19 | 20 | 21 | 22 | Title 23 | 24 | title 25 | 26 | 27 | 28 | Created On 29 | 30 | createdate 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Questions 40 | 41 | 42 | 43 | Add Question 44 | Question/Add?id=%s 45 | add 46 | 47 | 48 | Import Questions 49 | Question/Import?id=%s 50 | add 51 | 52 | 53 | Change Question Order 54 | Question/Order?id=%s 55 | change-order 56 | 57 | 58 | 59 | 60 | 61 | id 62 | 63 | 64 | 65 | 66 | title 67 | Question/Details?id=%s 68 | id 69 | 70 | 71 | 72 | Show on Site 73 | false 74 | 75 | enabled 76 | No 77 | 78 | 79 | 80 | # of Options 81 | 82 | option_count 83 | 84 | 85 | 86 | # of Images 87 | 88 | image_count 89 | 90 | 91 | 92 | 93 | bodytext 94 | text/xml 95 | 96 | 97 | 98 | 99 | 100 | delete… 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Inquisition/Delete.php: -------------------------------------------------------------------------------- 1 | initInquisition(); 23 | } 24 | 25 | protected function initInquisition() 26 | { 27 | $this->inquisition = SwatDBClassMap::new(InquisitionInquisition::class); 28 | $this->inquisition->setDatabase($this->app->db); 29 | 30 | $id = $this->getFirstItem(); 31 | 32 | if (!$this->inquisition->load($id)) { 33 | throw new AdminNotFoundException(sprintf( 34 | 'A inquisition with the id of “%s” does not exist', 35 | $id 36 | )); 37 | } 38 | } 39 | 40 | // process phase 41 | 42 | protected function processDBData(): void 43 | { 44 | parent::processDBData(); 45 | 46 | $item_list = $this->getItemList('integer'); 47 | 48 | $this->deleteQuestions($item_list); 49 | 50 | $sql = sprintf( 51 | 'delete from Inquisition where id in (%s)', 52 | $item_list 53 | ); 54 | 55 | $num = SwatDB::exec($this->app->db, $sql); 56 | $this->app->messages->add($this->getDeletedMessage($num)); 57 | } 58 | 59 | protected function deleteQuestions($item_list) 60 | { 61 | // By default delete questions that don't belong to other inquisitions 62 | // instead of leaving orphan questions. 63 | $sql = sprintf( 64 | 'delete from InquisitionQuestion where id in ( 65 | select question from InquisitionInquisitionQuestionBinding 66 | where %s 67 | )', 68 | $this->getSingleQuizQuestionsWhere($item_list) 69 | ); 70 | 71 | SwatDB::exec($this->app->db, $sql); 72 | } 73 | 74 | protected function getDeletedMessage($num) 75 | { 76 | return new SwatMessage( 77 | sprintf( 78 | Inquisition::ngettext( 79 | 'One inquisition has been deleted.', 80 | '%s inquisitions have been deleted.', 81 | $num 82 | ), 83 | SwatString::numberFormat($num) 84 | ) 85 | ); 86 | } 87 | 88 | // build phase 89 | 90 | protected function buildInternal() 91 | { 92 | parent::buildInternal(); 93 | 94 | $item_list = $this->getItemList('integer'); 95 | 96 | $dep = new AdminListDependency(); 97 | $dep->entries = AdminListDependency::queryEntries( 98 | $this->app->db, 99 | 'Inquisition', 100 | 'id', 101 | null, 102 | 'text:title', 103 | 'id', 104 | 'id in (' . $item_list . ')', 105 | AdminDependency::DELETE 106 | ); 107 | 108 | // check inquisition dependencies 109 | $dep_questions = new AdminSummaryDependency(); 110 | $dep_questions->setTitle('question', 'questions'); 111 | $dep_questions->summaries = AdminSummaryDependency::querySummaries( 112 | $this->app->db, 113 | 'InquisitionInquisitionQuestionBinding', 114 | 'integer:question', 115 | 'integer:inquisition', 116 | $this->getSingleQuizQuestionsWhere($item_list), 117 | AdminDependency::DELETE 118 | ); 119 | 120 | $dep->addDependency($dep_questions); 121 | 122 | $message = $this->ui->getWidget('confirmation_message'); 123 | $message->content = $dep->getMessage(); 124 | $message->content_type = 'text/xml'; 125 | 126 | if ($dep->getStatusLevelCount(AdminDependency::DELETE) == 0) { 127 | $this->switchToCancelButton(); 128 | } 129 | } 130 | 131 | protected function buildNavBar() 132 | { 133 | parent::buildNavBar(); 134 | 135 | $last = $this->navbar->popEntry(); 136 | 137 | $this->navbar->createEntry( 138 | $this->inquisition->title, 139 | sprintf( 140 | 'Inquisition/Details?id=%s', 141 | $this->inquisition->id 142 | ) 143 | ); 144 | 145 | $this->navbar->addEntry($last); 146 | } 147 | 148 | // helper methods 149 | 150 | protected function getSingleQuizQuestionsWhere($item_list) 151 | { 152 | return sprintf( 153 | 'inquisition in (%1$s) 154 | and question not in ( 155 | select question from InquisitionInquisitionQuestionBinding 156 | where inquisition not in (%1$s) 157 | )', 158 | $item_list 159 | ); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionQuestion.php: -------------------------------------------------------------------------------- 1 | question_type) { 65 | default: 66 | case self::TYPE_RADIO_LIST: 67 | $view = new InquisitionRadioListQuestionView($binding); 68 | break; 69 | 70 | case self::TYPE_FLYDOWN: 71 | $view = new InquisitionFlydownQuestionView($binding); 72 | break; 73 | 74 | case self::TYPE_RADIO_ENTRY: 75 | $view = new InquisitionRadioEntryQuestionView($binding); 76 | break; 77 | 78 | case self::TYPE_TEXT: 79 | $view = new InquisitionTextQuestionView($binding); 80 | break; 81 | 82 | case self::TYPE_CHECKBOX_LIST: 83 | $view = new InquisitionCheckboxListQuestionView($binding); 84 | break; 85 | 86 | case self::TYPE_CHECKBOX_ENTRY: 87 | $view = new InquisitionCheckboxEntryQuestionView($binding); 88 | break; 89 | } 90 | 91 | return $view; 92 | } 93 | 94 | protected function init() 95 | { 96 | $this->table = 'InquisitionQuestion'; 97 | $this->id_field = 'integer:id'; 98 | 99 | $this->registerInternalProperty( 100 | 'correct_option', 101 | SwatDBClassMap::get(InquisitionQuestionOption::class) 102 | ); 103 | 104 | $this->registerInternalProperty( 105 | 'question_group', 106 | SwatDBClassMap::get(InquisitionQuestionGroup::class) 107 | ); 108 | } 109 | 110 | protected function getSerializableSubDataObjects() 111 | { 112 | return array_merge( 113 | parent::getSerializableSubDataObjects(), 114 | ['options', 'correct_option'] 115 | ); 116 | } 117 | 118 | // loader methods 119 | 120 | protected function loadOptions() 121 | { 122 | $sql = sprintf( 123 | 'select * from InquisitionQuestionOption 124 | where question = %s 125 | order by displayorder', 126 | $this->db->quote($this->id, 'integer') 127 | ); 128 | 129 | return SwatDB::query( 130 | $this->db, 131 | $sql, 132 | SwatDBClassMap::get(InquisitionQuestionOptionWrapper::class) 133 | ); 134 | } 135 | 136 | protected function loadHints() 137 | { 138 | $sql = sprintf( 139 | 'select * from InquisitionQuestionHint 140 | where question = %s 141 | order by displayorder', 142 | $this->db->quote($this->id, 'integer') 143 | ); 144 | 145 | return SwatDB::query( 146 | $this->db, 147 | $sql, 148 | SwatDBClassMap::get(InquisitionQuestionHintWrapper::class) 149 | ); 150 | } 151 | 152 | protected function loadImages() 153 | { 154 | $sql = sprintf( 155 | 'select * from Image 156 | inner join InquisitionQuestionImageBinding 157 | on InquisitionQuestionImageBinding.image = Image.id 158 | where InquisitionQuestionImageBinding.question = %s 159 | order by InquisitionQuestionImageBinding.displayorder, 160 | InquisitionQuestionImageBinding.image', 161 | $this->db->quote($this->id, 'integer') 162 | ); 163 | 164 | return SwatDB::query( 165 | $this->db, 166 | $sql, 167 | SwatDBClassMap::get(InquisitionQuestionImageWrapper::class) 168 | ); 169 | } 170 | 171 | // saver methods 172 | 173 | protected function saveOptions() 174 | { 175 | foreach ($this->options as $option) { 176 | $option->question = $this; 177 | } 178 | 179 | $this->options->setDatabase($this->db); 180 | $this->options->save(); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Question/Edit.php: -------------------------------------------------------------------------------- 1 | ui->loadFromXML($this->getUiXml()); 28 | 29 | $this->initQuestion(); 30 | $this->initInquisition(); 31 | } 32 | 33 | protected function initQuestion() 34 | { 35 | $this->question = SwatDBClassMap::new(InquisitionQuestion::class); 36 | $this->question->setDatabase($this->app->db); 37 | 38 | if ($this->id !== null && !$this->question->load($this->id)) { 39 | throw new AdminNotFoundException( 40 | sprintf( 41 | 'Question with id ‘%s’ not found.', 42 | $this->id 43 | ) 44 | ); 45 | } 46 | } 47 | 48 | protected function initInquisition() 49 | { 50 | $inquisition_id = SiteApplication::initVar('inquisition'); 51 | 52 | if ($inquisition_id !== null) { 53 | $this->inquisition = $this->loadInquisition($inquisition_id); 54 | } 55 | } 56 | 57 | protected function loadInquisition($inquisition_id) 58 | { 59 | $inquisition = SwatDBClassMap::new(InquisitionInquisition::class); 60 | $inquisition->setDatabase($this->app->db); 61 | 62 | if (!$inquisition->load($inquisition_id)) { 63 | throw new AdminNotFoundException( 64 | sprintf( 65 | 'Inquisition with id ‘%s’ not found.', 66 | $inquisition_id 67 | ) 68 | ); 69 | } 70 | 71 | return $inquisition; 72 | } 73 | 74 | protected function getUiXml() 75 | { 76 | return __DIR__ . '/edit.xml'; 77 | } 78 | 79 | // process phase 80 | 81 | protected function saveDBData(): void 82 | { 83 | $this->updateQuestion(); 84 | $this->question->save(); 85 | 86 | $this->app->messages->add( 87 | new SwatMessage( 88 | Inquisition::_('Question has been saved.') 89 | ) 90 | ); 91 | } 92 | 93 | protected function updateQuestion() 94 | { 95 | $values = $this->ui->getValues( 96 | [ 97 | 'bodytext', 98 | 'enabled', 99 | ] 100 | ); 101 | 102 | $this->question->bodytext = $values['bodytext']; 103 | $this->question->enabled = $values['enabled']; 104 | } 105 | 106 | protected function relocate() 107 | { 108 | $this->app->relocate( 109 | sprintf( 110 | 'Question/Details?id=%s%s', 111 | $this->question->id, 112 | $this->getLinkSuffix() 113 | ) 114 | ); 115 | } 116 | 117 | // build phase 118 | 119 | protected function loadDBData() 120 | { 121 | $this->ui->setValues($this->question->getAttributes()); 122 | } 123 | 124 | protected function buildForm() 125 | { 126 | parent::buildForm(); 127 | 128 | if ($this->inquisition instanceof InquisitionInquisition) { 129 | $form = $this->ui->getWidget('edit_form'); 130 | $form->addHiddenField('inquisition', $this->inquisition->id); 131 | } 132 | } 133 | 134 | protected function buildNavBar() 135 | { 136 | parent::buildNavBar(); 137 | 138 | $this->navbar->popEntry(); 139 | 140 | if ($this->inquisition instanceof InquisitionInquisition) { 141 | $this->navbar->createEntry( 142 | $this->inquisition->title, 143 | sprintf( 144 | 'Inquisition/Details?id=%s', 145 | $this->inquisition->id 146 | ) 147 | ); 148 | } 149 | 150 | $this->navbar->createEntry( 151 | $this->getQuestionTitle(), 152 | sprintf( 153 | 'Question/Details?id=%s%s', 154 | $this->question->id, 155 | $this->getLinkSuffix() 156 | ) 157 | ); 158 | 159 | $this->navbar->createEntry(Inquisition::_('Edit Question')); 160 | } 161 | 162 | protected function getQuestionTitle() 163 | { 164 | // TODO: Update this with some version of getPosition(). 165 | return Inquisition::_('Question'); 166 | } 167 | 168 | protected function getLinkSuffix() 169 | { 170 | $suffix = null; 171 | if ($this->inquisition instanceof InquisitionInquisition) { 172 | $suffix = sprintf( 173 | '&inquisition=%s', 174 | $this->inquisition->id 175 | ); 176 | } 177 | 178 | return $suffix; 179 | } 180 | 181 | // finalize phase 182 | 183 | public function finalize() 184 | { 185 | parent::finalize(); 186 | 187 | $this->layout->addHtmlHeadEntry( 188 | 'packages/inquisition/admin/styles/inquisition-question-edit.css' 189 | ); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionResponse.php: -------------------------------------------------------------------------------- 1 | used_hint_bindings as $hint_binding) { 42 | $question_binding_id = $hint_binding->getInternalValue( 43 | 'question_binding' 44 | ); 45 | 46 | if ($question_binding_id === $question_binding->id) { 47 | $wrapper->add($hint_binding); 48 | } 49 | } 50 | 51 | return $wrapper; 52 | } 53 | 54 | protected function init() 55 | { 56 | $this->table = 'InquisitionResponse'; 57 | $this->id_field = 'integer:id'; 58 | 59 | $this->registerDateProperty('createdate'); 60 | $this->registerDateProperty('complete_date'); 61 | 62 | $this->registerInternalProperty( 63 | 'inquisition', 64 | SwatDBClassMap::get(InquisitionInquisition::class) 65 | ); 66 | } 67 | 68 | protected function getSerializableSubDataObjects() 69 | { 70 | return array_merge( 71 | parent::getSerializableSubDataObjects(), 72 | [ 73 | 'values', 74 | 'visible_question_values', 75 | ] 76 | ); 77 | } 78 | 79 | // loader methods 80 | 81 | protected function loadValues() 82 | { 83 | $sql = sprintf( 84 | 'select InquisitionResponseValue.* 85 | from InquisitionResponseValue 86 | inner join InquisitionResponse on 87 | InquisitionResponseValue.response = InquisitionResponse.id 88 | inner join InquisitionInquisitionQuestionBinding on 89 | InquisitionInquisitionQuestionBinding.id = 90 | InquisitionResponseValue.question_binding 91 | and InquisitionInquisitionQuestionBinding.inquisition = 92 | InquisitionResponse.inquisition 93 | where InquisitionResponseValue.response = %s 94 | order by InquisitionInquisitionQuestionBinding.displayorder', 95 | $this->db->quote($this->id, 'integer') 96 | ); 97 | 98 | return SwatDB::query( 99 | $this->db, 100 | $sql, 101 | SwatDBClassMap::get(InquisitionResponseValueWrapper::class) 102 | ); 103 | } 104 | 105 | protected function loadVisibleQuestionValues() 106 | { 107 | $sql = sprintf( 108 | 'select InquisitionResponseValue.* 109 | from InquisitionResponseValue 110 | inner join InquisitionResponse on 111 | InquisitionResponseValue.response = InquisitionResponse.id 112 | inner join InquisitionInquisitionQuestionBinding on 113 | InquisitionInquisitionQuestionBinding.id = 114 | InquisitionResponseValue.question_binding 115 | and InquisitionInquisitionQuestionBinding.inquisition = 116 | InquisitionResponse.inquisition 117 | inner join VisibleInquisitionQuestionView on 118 | InquisitionInquisitionQuestionBinding.question = 119 | VisibleInquisitionQuestionView.question 120 | where InquisitionResponseValue.response = %s 121 | order by InquisitionInquisitionQuestionBinding.displayorder', 122 | $this->db->quote($this->id, 'integer') 123 | ); 124 | 125 | return SwatDB::query( 126 | $this->db, 127 | $sql, 128 | SwatDBClassMap::get(InquisitionResponseValueWrapper::class) 129 | ); 130 | } 131 | 132 | protected function loadUsedHintBindings() 133 | { 134 | $sql = sprintf( 135 | 'select * from InquisitionResponseUsedHintBinding 136 | where InquisitionResponseUsedHintBinding.response = %s 137 | order by InquisitionResponseUsedHintBinding.createdate', 138 | $this->db->quote($this->id, 'integer') 139 | ); 140 | 141 | $bindings = SwatDB::query( 142 | $this->db, 143 | $sql, 144 | SwatDBClassMap::get(InquisitionResponseUsedHintBindingWrapper::class) 145 | ); 146 | 147 | $bindings->loadAllSubDataObjects( 148 | 'question_hint', 149 | $this->db, 150 | 'select * from InquisitionQuestionHint where id in (%s)', 151 | SwatDBClassMap::get(InquisitionQuestionHintWrapper::class) 152 | ); 153 | 154 | return $bindings; 155 | } 156 | 157 | // saver methods 158 | 159 | protected function saveValues() 160 | { 161 | foreach ($this->values as $value) { 162 | $value->response = $this; 163 | } 164 | 165 | $this->values->setDatabase($this->db); 166 | $this->values->save(); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Inquisition/ImageDelete.php: -------------------------------------------------------------------------------- 1 | ui->getWidget('confirmation_form'); 26 | $form->addHiddenField('id', $id); 27 | } 28 | 29 | public function setItems($items, $extended_selected = false) 30 | { 31 | parent::setItems($items, $extended_selected); 32 | 33 | $sql = sprintf( 34 | 'select Image.* from Image where id in (%s)', 35 | $this->getItemList('integer') 36 | ); 37 | 38 | $this->images = SwatDB::query( 39 | $this->app->db, 40 | $sql, 41 | SwatDBClassMap::get(InquisitionQuestionImageWrapper::class) 42 | ); 43 | } 44 | 45 | public function setInquisition(?InquisitionInquisition $inquisition = null) 46 | { 47 | if ($inquisition instanceof InquisitionInquisition) { 48 | $this->inquisition = $inquisition; 49 | 50 | $form = $this->ui->getWidget('confirmation_form'); 51 | $form->addHiddenField('inquisition_id', $this->inquisition->id); 52 | } 53 | } 54 | 55 | abstract protected function getImageWrapper(); 56 | 57 | // init phase 58 | 59 | protected function initInternal() 60 | { 61 | parent::initInternal(); 62 | 63 | $form = $this->ui->getWidget('confirmation_form'); 64 | $id = $form->getHiddenField('id'); 65 | if ($id != '') { 66 | $this->setId($id); 67 | } 68 | 69 | $inquisition_id = $form->getHiddenField('inquisition_id'); 70 | if ($inquisition_id != '') { 71 | $inquisition = $this->loadInquisition($inquisition_id); 72 | $this->setInquisition($inquisition); 73 | } 74 | } 75 | 76 | protected function loadInquisition($inquisition_id) 77 | { 78 | $inquisition = SwatDBClassMap::new(InquisitionInquisition::class); 79 | $inquisition->setDatabase($this->app->db); 80 | 81 | if (!$inquisition->load($inquisition_id)) { 82 | throw new AdminNotFoundException( 83 | sprintf( 84 | 'Inquisition with id ‘%s’ not found.', 85 | $inquisition_id 86 | ) 87 | ); 88 | } 89 | 90 | return $inquisition; 91 | } 92 | 93 | protected function getUiXml() 94 | { 95 | return __DIR__ . '/image-delete.xml'; 96 | } 97 | 98 | // process phase 99 | 100 | protected function processDBData(): void 101 | { 102 | parent::processDBData(); 103 | 104 | $delete_count = 0; 105 | 106 | foreach ($this->images as $image) { 107 | $image->setFileBase('../images'); 108 | $image->delete(); 109 | 110 | $delete_count++; 111 | } 112 | 113 | $this->app->messages->add( 114 | new SwatMessage( 115 | sprintf( 116 | Inquisition::ngettext( 117 | 'One image has been deleted.', 118 | '%s images have been deleted.', 119 | $delete_count 120 | ), 121 | $delete_count 122 | ) 123 | ) 124 | ); 125 | } 126 | 127 | protected function relocate() 128 | { 129 | AdminDBConfirmation::relocate(); 130 | } 131 | 132 | // build phase 133 | 134 | protected function buildInternal() 135 | { 136 | parent::buildInternal(); 137 | 138 | $store = new SwatTableStore(); 139 | foreach ($this->images as $image) { 140 | $ds = new SwatDetailsStore(); 141 | 142 | $ds->image = $image; 143 | 144 | $store->add($ds); 145 | } 146 | 147 | $delete_view = $this->ui->getWidget('delete_view'); 148 | $delete_view->model = $store; 149 | 150 | $message = $this->ui->getWidget('confirmation_message'); 151 | $message->content_type = 'text/xml'; 152 | $message->content = sprintf( 153 | '%s', 154 | Inquisition::ngettext( 155 | 'Are you sure you want to delete the following image?', 156 | 'Are you sure you want to delete the following images?', 157 | count($this->images) 158 | ) 159 | ); 160 | } 161 | 162 | protected function buildForm() 163 | { 164 | parent::buildForm(); 165 | 166 | $yes_button = $this->ui->getWidget('yes_button'); 167 | $yes_button->title = Inquisition::_('Delete'); 168 | } 169 | 170 | protected function buildNavBar() 171 | { 172 | parent::buildNavBar(); 173 | 174 | if ($this->inquisition instanceof InquisitionInquisition) { 175 | $this->navbar->createEntry( 176 | $this->inquisition->title, 177 | sprintf( 178 | 'Inquisition/Details?id=%s', 179 | $this->inquisition->id 180 | ) 181 | ); 182 | } 183 | } 184 | 185 | protected function getLinkSuffix() 186 | { 187 | $suffix = null; 188 | if ($this->inquisition instanceof InquisitionInquisition) { 189 | $suffix = sprintf( 190 | '&inquisition=%s', 191 | $this->inquisition->id 192 | ); 193 | } 194 | 195 | return $suffix; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Option/Order.php: -------------------------------------------------------------------------------- 1 | initQuestion(); 28 | $this->initInquisition(); 29 | } 30 | 31 | protected function initQuestion() 32 | { 33 | $id = SiteApplication::initVar('id'); 34 | 35 | if ($id == '') { 36 | throw new AdminNotFoundException( 37 | 'No question id specified.' 38 | ); 39 | } 40 | 41 | if (is_numeric($id)) { 42 | $id = intval($id); 43 | } 44 | 45 | $this->question = SwatDBClassMap::new(InquisitionQuestion::class); 46 | $this->question->setDatabase($this->app->db); 47 | 48 | if (!$this->question->load($id)) { 49 | throw new AdminNotFoundException( 50 | sprintf( 51 | 'A question with the id of “%s” does not exist', 52 | $id 53 | ) 54 | ); 55 | } 56 | } 57 | 58 | protected function initInquisition() 59 | { 60 | $inquisition_id = SiteApplication::initVar('inquisition'); 61 | 62 | if ($inquisition_id !== null) { 63 | $this->inquisition = $this->loadInquisition($inquisition_id); 64 | } 65 | } 66 | 67 | protected function loadInquisition($inquisition_id) 68 | { 69 | $inquisition = SwatDBClassMap::new(InquisitionInquisition::class); 70 | $inquisition->setDatabase($this->app->db); 71 | 72 | if (!$inquisition->load($inquisition_id)) { 73 | throw new AdminNotFoundException( 74 | sprintf( 75 | 'Inquisition with id ‘%s’ not found.', 76 | $inquisition_id 77 | ) 78 | ); 79 | } 80 | 81 | return $inquisition; 82 | } 83 | 84 | // process phase 85 | 86 | protected function saveIndex($id, $index) 87 | { 88 | SwatDB::updateColumn( 89 | $this->app->db, 90 | 'InquisitionQuestionOption', 91 | 'integer:displayorder', 92 | $index, 93 | 'integer:id', 94 | [$id] 95 | ); 96 | } 97 | 98 | protected function getUpdatedMessage() 99 | { 100 | return new SwatMessage( 101 | Inquisition::_('Option order has been updated.') 102 | ); 103 | } 104 | 105 | protected function relocate() 106 | { 107 | $this->app->relocate( 108 | sprintf( 109 | 'Question/Details?id=%s%s', 110 | $this->question->id, 111 | $this->getLinkSuffix() 112 | ) 113 | ); 114 | } 115 | 116 | // build phase 117 | 118 | protected function loadData() 119 | { 120 | $sum = 0; 121 | $order_widget = $this->ui->getWidget('order'); 122 | 123 | foreach ($this->question->options as $option) { 124 | $sum += $option->displayorder; 125 | 126 | $order_widget->addOption( 127 | $option->id, 128 | $option->title, 129 | 'text/xml' 130 | ); 131 | } 132 | 133 | $options_list = $this->ui->getWidget('options'); 134 | $options_list->value = ($sum == 0) ? 'auto' : 'custom'; 135 | } 136 | 137 | protected function buildInternal() 138 | { 139 | $this->ui->getWidget('order_frame')->title = $this->getTitle(); 140 | 141 | $this->ui->getWidget('order')->width = '500px'; 142 | $this->ui->getWidget('order')->height = '200px'; 143 | 144 | parent::buildInternal(); 145 | } 146 | 147 | protected function buildForm() 148 | { 149 | parent::buildForm(); 150 | 151 | $form = $this->ui->getWidget('order_form'); 152 | $form->addHiddenField('id', $this->question->id); 153 | 154 | if ($this->inquisition instanceof InquisitionInquisition) { 155 | $form->addHiddenField('inquisition', $this->inquisition->id); 156 | } 157 | } 158 | 159 | protected function buildNavBar() 160 | { 161 | parent::buildNavBar(); 162 | 163 | $this->navbar->popEntry(); 164 | 165 | if ($this->inquisition instanceof InquisitionInquisition) { 166 | $this->navbar->createEntry( 167 | $this->inquisition->title, 168 | sprintf( 169 | 'Inquisition/Details?id=%s', 170 | $this->inquisition->id 171 | ) 172 | ); 173 | } 174 | 175 | $this->navbar->createEntry( 176 | $this->getQuestionTitle(), 177 | sprintf( 178 | 'Question/Details?id=%s%s', 179 | $this->question->id, 180 | $this->getLinkSuffix() 181 | ) 182 | ); 183 | 184 | $this->navbar->createEntry($this->getTitle()); 185 | } 186 | 187 | protected function getQuestionTitle() 188 | { 189 | // TODO: Update this with some version of getPosition(). 190 | return Inquisition::_('Question'); 191 | } 192 | 193 | protected function getLinkSuffix() 194 | { 195 | $suffix = null; 196 | if ($this->inquisition instanceof InquisitionInquisition) { 197 | $suffix = sprintf( 198 | '&inquisition=%s', 199 | $this->inquisition->id 200 | ); 201 | } 202 | 203 | return $suffix; 204 | } 205 | 206 | protected function getTitle() 207 | { 208 | return Inquisition::_('Change Option Order'); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Question/HintOrder.php: -------------------------------------------------------------------------------- 1 | initQuestion(); 28 | $this->initInquisition(); 29 | } 30 | 31 | protected function initQuestion() 32 | { 33 | $id = SiteApplication::initVar('id'); 34 | 35 | if ($id == '') { 36 | throw new AdminNotFoundException( 37 | 'No question id specified.' 38 | ); 39 | } 40 | 41 | if (is_numeric($id)) { 42 | $id = intval($id); 43 | } 44 | 45 | $this->question = SwatDBClassMap::new(InquisitionQuestion::class); 46 | $this->question->setDatabase($this->app->db); 47 | 48 | if (!$this->question->load($id)) { 49 | throw new AdminNotFoundException( 50 | sprintf( 51 | 'A question with the id of “%s” does not exist', 52 | $id 53 | ) 54 | ); 55 | } 56 | } 57 | 58 | protected function initInquisition() 59 | { 60 | $inquisition_id = SiteApplication::initVar('inquisition'); 61 | 62 | if ($inquisition_id !== null) { 63 | $this->inquisition = $this->loadInquisition($inquisition_id); 64 | } 65 | } 66 | 67 | protected function loadInquisition($inquisition_id) 68 | { 69 | $inquisition = SwatDBClassMap::new(InquisitionInquisition::class); 70 | $inquisition->setDatabase($this->app->db); 71 | 72 | if (!$inquisition->load($inquisition_id)) { 73 | throw new AdminNotFoundException( 74 | sprintf( 75 | 'Inquisition with id ‘%s’ not found.', 76 | $inquisition_id 77 | ) 78 | ); 79 | } 80 | 81 | return $inquisition; 82 | } 83 | 84 | // process phase 85 | 86 | protected function saveIndex($id, $index) 87 | { 88 | SwatDB::updateColumn( 89 | $this->app->db, 90 | 'InquisitionQuestionHint', 91 | 'integer:displayorder', 92 | $index, 93 | 'integer:id', 94 | [$id] 95 | ); 96 | } 97 | 98 | protected function getUpdatedMessage() 99 | { 100 | return new SwatMessage(Inquisition::_('Hint order has been updated.')); 101 | } 102 | 103 | protected function relocate() 104 | { 105 | $this->app->relocate( 106 | sprintf( 107 | 'Question/Details?id=%s%s', 108 | $this->question->id, 109 | $this->getLinkSuffix() 110 | ) 111 | ); 112 | } 113 | 114 | // build phase 115 | 116 | protected function loadData() 117 | { 118 | $sum = 0; 119 | $order_widget = $this->ui->getWidget('order'); 120 | 121 | foreach ($this->question->hints as $hint) { 122 | $sum += $hint->displayorder; 123 | 124 | $order_widget->addOption( 125 | $hint->id, 126 | SwatString::condense($hint->bodytext, 50), 127 | 'text/xml' 128 | ); 129 | } 130 | 131 | $options_list = $this->ui->getWidget('options'); 132 | $options_list->value = ($sum == 0) ? 'auto' : 'custom'; 133 | } 134 | 135 | protected function buildInternal() 136 | { 137 | $this->ui->getWidget('order_frame')->title = $this->getTitle(); 138 | 139 | $this->ui->getWidget('order')->width = '500px'; 140 | $this->ui->getWidget('order')->height = '200px'; 141 | 142 | parent::buildInternal(); 143 | } 144 | 145 | protected function buildForm() 146 | { 147 | parent::buildForm(); 148 | 149 | $form = $this->ui->getWidget('order_form'); 150 | $form->addHiddenField('id', $this->question->id); 151 | 152 | if ($this->inquisition instanceof InquisitionInquisition) { 153 | $form->addHiddenField('inquisition', $this->inquisition->id); 154 | } 155 | } 156 | 157 | protected function buildNavBar() 158 | { 159 | parent::buildNavBar(); 160 | 161 | $this->navbar->popEntry(); 162 | 163 | if ($this->inquisition instanceof InquisitionInquisition) { 164 | $this->navbar->createEntry( 165 | $this->inquisition->title, 166 | sprintf( 167 | 'Inquisition/Details?id=%s', 168 | $this->inquisition->id 169 | ) 170 | ); 171 | } 172 | 173 | $this->navbar->createEntry( 174 | $this->getQuestionTitle(), 175 | sprintf( 176 | 'Question/Details?id=%s%s', 177 | $this->question->id, 178 | $this->getLinkSuffix() 179 | ) 180 | ); 181 | 182 | $this->navbar->createEntry($this->getTitle()); 183 | } 184 | 185 | protected function getQuestionTitle() 186 | { 187 | // TODO: Update this with some version of getPosition(). 188 | return Inquisition::_('Question'); 189 | } 190 | 191 | protected function getLinkSuffix() 192 | { 193 | $suffix = null; 194 | if ($this->inquisition instanceof InquisitionInquisition) { 195 | $suffix = sprintf( 196 | '&inquisition=%s', 197 | $this->inquisition->id 198 | ); 199 | } 200 | 201 | return $suffix; 202 | } 203 | 204 | protected function getTitle() 205 | { 206 | return Inquisition::_('Change Hint Order'); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Question/CorrectOption.php: -------------------------------------------------------------------------------- 1 | initQuestion(); 28 | $this->initInquisition(); 29 | 30 | $this->ui->loadFromXML($this->getUiXml()); 31 | } 32 | 33 | protected function initQuestion() 34 | { 35 | $this->question = SwatDBClassMap::new(InquisitionQuestion::class); 36 | $this->question->setDatabase($this->app->db); 37 | 38 | if ($this->id == '') { 39 | throw new AdminNotFoundException( 40 | 'Question id not provided.' 41 | ); 42 | } 43 | 44 | if (!$this->question->load($this->id)) { 45 | throw new AdminNotFoundException( 46 | sprintf( 47 | 'Question with id ‘%s’ not found.', 48 | $this->id 49 | ) 50 | ); 51 | } 52 | } 53 | 54 | protected function initInquisition() 55 | { 56 | $inquisition_id = SiteApplication::initVar('inquisition'); 57 | 58 | if ($inquisition_id !== null) { 59 | $this->inquisition = $this->loadInquisition($inquisition_id); 60 | } 61 | } 62 | 63 | protected function loadInquisition($inquisition_id) 64 | { 65 | $inquisition = SwatDBClassMap::new(InquisitionInquisition::class); 66 | $inquisition->setDatabase($this->app->db); 67 | 68 | if (!$inquisition->load($inquisition_id)) { 69 | throw new AdminNotFoundException( 70 | sprintf( 71 | 'Inquisition with id ‘%s’ not found.', 72 | $inquisition_id 73 | ) 74 | ); 75 | } 76 | 77 | return $inquisition; 78 | } 79 | 80 | protected function getUiXml() 81 | { 82 | return __DIR__ . '/correct-option.xml'; 83 | } 84 | 85 | // process phase 86 | 87 | protected function saveDBData(): void 88 | { 89 | $values = $this->ui->getValues([ 90 | 'correct_option', 91 | ]); 92 | 93 | $this->question->correct_option = $values['correct_option']; 94 | $this->question->save(); 95 | 96 | $this->app->messages->add( 97 | new SwatMessage( 98 | Inquisition::_('Correct option has been updated.') 99 | ) 100 | ); 101 | } 102 | 103 | protected function relocate() 104 | { 105 | $uri = sprintf( 106 | 'Question/Details?id=%s', 107 | $this->question->id 108 | ); 109 | 110 | if ($this->inquisition instanceof InquisitionInquisition) { 111 | $uri .= sprintf( 112 | '&inquisition=%s', 113 | $this->inquisition->id 114 | ); 115 | } 116 | 117 | $this->app->relocate($uri); 118 | } 119 | 120 | // build phase 121 | 122 | protected function buildInternal() 123 | { 124 | parent::buildInternal(); 125 | 126 | $list = $this->ui->getWidget('correct_option'); 127 | 128 | foreach ($this->question->options as $option) { 129 | $list->addOption( 130 | $option->id, 131 | sprintf( 132 | '%s. %s', 133 | $option->position, 134 | $option->title 135 | ) 136 | ); 137 | } 138 | } 139 | 140 | protected function buildForm() 141 | { 142 | parent::buildForm(); 143 | 144 | if ($this->inquisition instanceof InquisitionInquisition) { 145 | $form = $this->ui->getWidget('edit_form'); 146 | $form->addHiddenField('inquisition', $this->inquisition->id); 147 | } 148 | } 149 | 150 | protected function loadDBData() 151 | { 152 | if ($this->question->correct_option instanceof InquisitionQuestionOption) { 153 | $this->ui->setValues( 154 | [ 155 | 'correct_option' => $this->question->correct_option->id, 156 | ] 157 | ); 158 | } 159 | } 160 | 161 | protected function buildNavBar() 162 | { 163 | parent::buildNavBar(); 164 | 165 | $this->navbar->popEntry(); 166 | 167 | if ($this->inquisition instanceof InquisitionInquisition) { 168 | $this->navbar->createEntry( 169 | $this->inquisition->title, 170 | sprintf( 171 | 'Inquisition/Details?id=%s', 172 | $this->inquisition->id 173 | ) 174 | ); 175 | } 176 | 177 | $this->navbar->createEntry( 178 | $this->getQuestionTitle(), 179 | sprintf( 180 | 'Question/Details?id=%s%s', 181 | $this->question->id, 182 | $this->getLinkSuffix() 183 | ) 184 | ); 185 | 186 | $this->navbar->createEntry(Inquisition::_('Edit Correct Question')); 187 | } 188 | 189 | protected function getQuestionTitle() 190 | { 191 | // TODO: Update this with some version of getPosition(). 192 | return Inquisition::_('Question'); 193 | } 194 | 195 | protected function getLinkSuffix() 196 | { 197 | $suffix = null; 198 | if ($this->inquisition instanceof InquisitionInquisition) { 199 | $suffix = sprintf( 200 | '&inquisition=%s', 201 | $this->inquisition->id 202 | ); 203 | } 204 | 205 | return $suffix; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /Inquisition/InquisitionQuestionImporter.php: -------------------------------------------------------------------------------- 1 | app = $app; 17 | } 18 | 19 | // questions 20 | 21 | public function importQuestions(InquisitionFileParser $file) 22 | { 23 | $questions = []; 24 | 25 | while (!$file->eof()) { 26 | $question = SwatDBClassMap::new(InquisitionQuestion::class); 27 | $question->setDatabase($this->app->db); 28 | $this->importQuestion($question, $file); 29 | 30 | $questions[] = $question; 31 | } 32 | 33 | return $questions; 34 | } 35 | 36 | protected function importQuestion( 37 | InquisitionQuestion $question, 38 | InquisitionFileParser $file 39 | ) { 40 | $line = $file->line(); 41 | $row = $file->row(); 42 | 43 | $this->importQuestionProperties($question, $file); 44 | $this->importOptions($question, $file); 45 | 46 | if (count($question->options) < 2) { 47 | throw new InquisitionImportException( 48 | sprintf( 49 | Inquisition::_( 50 | 'Question on line %s (CSV row %s) must have at ' . 51 | 'least two options.' 52 | ), 53 | $line, 54 | $row 55 | ), 56 | 0, 57 | $file 58 | ); 59 | } 60 | 61 | if (!$question->correct_option instanceof InquisitionQuestionOption) { 62 | throw new InquisitionImportException( 63 | sprintf( 64 | Inquisition::_( 65 | 'Question on line %s (CSV row %s) must have a ' . 66 | 'correct answer.' 67 | ), 68 | $line, 69 | $row 70 | ), 71 | 0, 72 | $file 73 | ); 74 | } 75 | } 76 | 77 | protected function importQuestionProperties( 78 | InquisitionQuestion $question, 79 | InquisitionFileParser $file 80 | ) { 81 | $line = $file->line(); 82 | $row = $file->row(); 83 | $data = $file->current(); 84 | 85 | $question->required = true; 86 | $question->question_type = InquisitionQuestion::TYPE_RADIO_LIST; 87 | 88 | if (!isset($data[0]) || $data[0] == '') { 89 | throw new InquisitionImportException( 90 | sprintf( 91 | Inquisition::_( 92 | 'Line %s (CSV row %s) has no question text.' 93 | ), 94 | $line, 95 | $row 96 | ), 97 | 0, 98 | $file 99 | ); 100 | } 101 | 102 | $question->bodytext = $data[0]; 103 | } 104 | 105 | // question options 106 | 107 | protected function importOptions( 108 | InquisitionQuestion $question, 109 | InquisitionFileParser $file 110 | ) { 111 | $file->next(); 112 | 113 | $option_class = SwatDBClassMap::get(InquisitionQuestionOption::class); 114 | 115 | while (!$file->eof() && $this->isOptionLine($file)) { 116 | $option = new $option_class(); 117 | $option->setDatabase($this->app->db); 118 | $this->importOption($option, $file); 119 | 120 | $previous_option = $question->options->getLast(); 121 | 122 | if ($previous_option instanceof $option_class) { 123 | $option->displayorder = $previous_option->displayorder + 1; 124 | } else { 125 | $option->displayorder = 1; 126 | } 127 | 128 | $question->options->add($option); 129 | 130 | if ($this->isCorrectOptionLine($file)) { 131 | $line = $file->line(); 132 | $row = $file->row(); 133 | 134 | if ($question->correct_option instanceof $option_class) { 135 | throw new InquisitionImportException( 136 | sprintf( 137 | Inquisition::_( 138 | 'Line %s (CSV row %s) contains a second ' . 139 | 'correct answer.' 140 | ), 141 | $line, 142 | $row 143 | ), 144 | 0, 145 | $file 146 | ); 147 | } 148 | 149 | $question->correct_option = $option; 150 | } 151 | 152 | $file->next(); 153 | } 154 | } 155 | 156 | protected function importOption( 157 | InquisitionQuestionOption $option, 158 | InquisitionFileParser $file 159 | ) { 160 | $line = $file->line(); 161 | $row = $file->row(); 162 | $data = $file->current(); 163 | 164 | if (!isset($data[1]) || $data[1] == '') { 165 | throw new InquisitionImportException( 166 | sprintf( 167 | Inquisition::_('Line %s (CSV row %s) has no option text.'), 168 | $line, 169 | $row 170 | ), 171 | 0, 172 | $file 173 | ); 174 | } 175 | 176 | $option->title = $data[1]; 177 | } 178 | 179 | // helper methods 180 | 181 | protected function isOptionLine(InquisitionFileParser $file) 182 | { 183 | $data = $file->current(); 184 | 185 | return isset($data[0]) && $data[0] === ''; 186 | } 187 | 188 | protected function isCorrectOptionLine(InquisitionFileParser $file) 189 | { 190 | $data = $file->current(); 191 | 192 | return isset($data[2]) && mb_strtolower(trim($data[2])) === 'x'; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /Inquisition/dataobjects/InquisitionInquisition.php: -------------------------------------------------------------------------------- 1 | checkDB(); 46 | 47 | $sql = sprintf( 48 | 'select * from InquisitionResponse 49 | where account = %s and inquisition = %s', 50 | $this->db->quote($account->id, 'integer'), 51 | $this->db->quote($this->id, 'integer') 52 | ); 53 | 54 | $wrapper = $this->getResolvedResponseWrapperClass(); 55 | $response = SwatDB::query($this->db, $sql, $wrapper)->getFirst(); 56 | 57 | if ($response instanceof InquisitionResponse) { 58 | $response->inquisition = $this; 59 | } 60 | 61 | return $response; 62 | } 63 | 64 | public function addQuestionDependency( 65 | InquisitionInquisitionQuestionBinding $dependent_question_binding, 66 | InquisitionInquisitionQuestionBinding $question_binding, 67 | InquisitionQuestionOption $option 68 | ) { 69 | $this->question_dependencies[] = [ 70 | 'dependent_question_binding' => $dependent_question_binding, 71 | 'question_binding' => $question_binding, 72 | 'option' => $option, 73 | ]; 74 | } 75 | 76 | protected function init() 77 | { 78 | $this->table = 'Inquisition'; 79 | $this->id_field = 'integer:id'; 80 | $this->registerDateProperty('createdate'); 81 | } 82 | 83 | protected function getSerializableSubDataObjects() 84 | { 85 | return array_merge( 86 | parent::getSerializableSubDataObjects(), 87 | [ 88 | 'question_bindings', 89 | 'visible_question_bindings', 90 | ] 91 | ); 92 | } 93 | 94 | protected function getResolvedResponseWrapperClass() 95 | { 96 | return SwatDBClassMap::get($this->getResponseWrapperClass()); 97 | } 98 | 99 | protected function getResponseWrapperClass() 100 | { 101 | return InquisitionResponseWrapper::class; 102 | } 103 | 104 | // saver methods 105 | 106 | protected function saveQuestionBindings() 107 | { 108 | foreach ($this->question_bindings as $question_binding) { 109 | $question_binding->inquisition = $this; 110 | } 111 | 112 | $this->question_bindings->setDatabase($this->db); 113 | $this->question_bindings->save(); 114 | 115 | foreach ($this->question_dependencies as $question_dependency) { 116 | $dependent_binding = $question_dependency['dependent_question_binding']; 117 | $binding = $question_dependency['question_binding']; 118 | $option = $question_dependency['option']; 119 | 120 | SwatDB::exec( 121 | $this->db, 122 | sprintf( 123 | 'insert into InquisitionQuestionDependency 124 | (dependent_question_binding, question_binding, option) 125 | values 126 | (%s, %s, %s) 127 | ', 128 | $this->db->quote($dependent_binding->id, 'integer'), 129 | $this->db->quote($binding->id, 'integer'), 130 | $this->db->quote($option->id, 'integer') 131 | ) 132 | ); 133 | } 134 | } 135 | 136 | // loader methods 137 | 138 | protected function loadResponses() 139 | { 140 | $sql = sprintf( 141 | 'select * from InquisitionResponse 142 | where inquisition = %s 143 | order by createdate, id', 144 | $this->db->quote($this->id, 'integer') 145 | ); 146 | 147 | return SwatDB::query( 148 | $this->db, 149 | $sql, 150 | $this->getResolvedResponseWrapperClass() 151 | ); 152 | } 153 | 154 | protected function loadQuestionBindings() 155 | { 156 | $sql = sprintf( 157 | 'select * from InquisitionInquisitionQuestionBinding 158 | where inquisition = %s order by displayorder, id', 159 | $this->db->quote($this->id, 'integer') 160 | ); 161 | 162 | return SwatDB::query( 163 | $this->db, 164 | $sql, 165 | SwatDBClassMap::get(InquisitionInquisitionQuestionBindingWrapper::class) 166 | ); 167 | } 168 | 169 | protected function loadVisibleQuestionBindings() 170 | { 171 | $sql = sprintf( 172 | 'select InquisitionInquisitionQuestionBinding.* 173 | from InquisitionInquisitionQuestionBinding 174 | inner join VisibleInquisitionQuestionView 175 | on InquisitionInquisitionQuestionBinding.question = 176 | VisibleInquisitionQuestionView.question 177 | where InquisitionInquisitionQuestionBinding.inquisition = %s 178 | order by InquisitionInquisitionQuestionBinding.displayorder, 179 | InquisitionInquisitionQuestionBinding.id', 180 | $this->db->quote($this->id, 'integer') 181 | ); 182 | 183 | return SwatDB::query( 184 | $this->db, 185 | $sql, 186 | SwatDBClassMap::get(InquisitionInquisitionQuestionBindingWrapper::class) 187 | ); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Question/ImageOrder.php: -------------------------------------------------------------------------------- 1 | initQuestion(); 28 | $this->initInquisition(); 29 | } 30 | 31 | protected function initQuestion() 32 | { 33 | $id = SiteApplication::initVar('id'); 34 | 35 | if ($id == '') { 36 | throw new AdminNotFoundException( 37 | Inquisition::_('No question id specified.') 38 | ); 39 | } 40 | 41 | if (is_numeric($id)) { 42 | $id = intval($id); 43 | } 44 | 45 | $this->question = SwatDBClassMap::new(InquisitionQuestion::class); 46 | $this->question->setDatabase($this->app->db); 47 | 48 | if (!$this->question->load($id)) { 49 | throw new AdminNotFoundException( 50 | sprintf( 51 | 'A question with the id of “%s” does not exist', 52 | $id 53 | ) 54 | ); 55 | } 56 | } 57 | 58 | protected function initInquisition() 59 | { 60 | $inquisition_id = SiteApplication::initVar('inquisition'); 61 | 62 | if ($inquisition_id !== null) { 63 | $this->inquisition = $this->loadInquisition($inquisition_id); 64 | } 65 | } 66 | 67 | protected function loadInquisition($inquisition_id) 68 | { 69 | $inquisition = SwatDBClassMap::new(InquisitionInquisition::class); 70 | $inquisition->setDatabase($this->app->db); 71 | 72 | if (!$inquisition->load($inquisition_id)) { 73 | throw new AdminNotFoundException( 74 | sprintf( 75 | 'Inquisition with id ‘%s’ not found.', 76 | $inquisition_id 77 | ) 78 | ); 79 | } 80 | 81 | return $inquisition; 82 | } 83 | 84 | // process phase 85 | 86 | protected function saveIndex($id, $index) 87 | { 88 | SwatDB::updateColumn( 89 | $this->app->db, 90 | 'InquisitionQuestionImageBinding', 91 | 'integer:displayorder', 92 | $index, 93 | 'integer:image', 94 | [$id] 95 | ); 96 | } 97 | 98 | protected function getUpdatedMessage() 99 | { 100 | return new SwatMessage(Inquisition::_('Image order has been updated.')); 101 | } 102 | 103 | protected function relocate() 104 | { 105 | $this->app->relocate( 106 | sprintf( 107 | 'Question/Details?id=%s%s', 108 | $this->question->id, 109 | $this->getLinkSuffix() 110 | ) 111 | ); 112 | } 113 | 114 | // build phase 115 | 116 | protected function loadData() 117 | { 118 | $order_widget = $this->ui->getWidget('order'); 119 | 120 | foreach ($this->question->images as $image) { 121 | $order_widget->addOption( 122 | $image->id, 123 | strval($image->getImgTag('thumb', '../')), 124 | 'text/xml' 125 | ); 126 | } 127 | 128 | $sql = sprintf( 129 | 'select sum(displayorder) from 130 | InquisitionQuestionImageBinding where question = %s', 131 | $this->question->id 132 | ); 133 | 134 | $sum = SwatDB::queryOne($this->app->db, $sql, 'integer'); 135 | 136 | $options_list = $this->ui->getWidget('options'); 137 | $options_list->value = ($sum == 0) ? 'auto' : 'custom'; 138 | } 139 | 140 | protected function buildInternal() 141 | { 142 | $this->ui->getWidget('order_frame')->title = $this->getTitle(); 143 | $this->ui->getWidget('order')->width = '150px'; 144 | $this->ui->getWidget('order')->height = '300px'; 145 | 146 | parent::buildInternal(); 147 | } 148 | 149 | protected function buildForm() 150 | { 151 | parent::buildForm(); 152 | 153 | $form = $this->ui->getWidget('order_form'); 154 | $form->addHiddenField('id', $this->question->id); 155 | 156 | if ($this->inquisition instanceof InquisitionInquisition) { 157 | $form->addHiddenField('inquisition', $this->inquisition->id); 158 | } 159 | } 160 | 161 | protected function buildNavBar() 162 | { 163 | parent::buildNavBar(); 164 | 165 | $this->navbar->popEntry(); 166 | 167 | if ($this->inquisition instanceof InquisitionInquisition) { 168 | $this->navbar->createEntry( 169 | $this->inquisition->title, 170 | sprintf( 171 | 'Inquisition/Details?id=%s', 172 | $this->inquisition->id 173 | ) 174 | ); 175 | } 176 | 177 | $this->navbar->createEntry( 178 | $this->getQuestionTitle(), 179 | sprintf( 180 | 'Question/Details?id=%s%s', 181 | $this->question->id, 182 | $this->getLinkSuffix() 183 | ) 184 | ); 185 | 186 | $this->navbar->createEntry($this->getTitle()); 187 | } 188 | 189 | protected function getQuestionTitle() 190 | { 191 | // TODO: Update this with some version of getPosition(). 192 | return Inquisition::_('Question'); 193 | } 194 | 195 | protected function getLinkSuffix() 196 | { 197 | $suffix = null; 198 | if ($this->inquisition instanceof InquisitionInquisition) { 199 | $suffix = sprintf( 200 | '&inquisition=%s', 201 | $this->inquisition->id 202 | ); 203 | } 204 | 205 | return $suffix; 206 | } 207 | 208 | protected function getTitle() 209 | { 210 | return Inquisition::_('Change Image Order'); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Option/ImageOrder.php: -------------------------------------------------------------------------------- 1 | initOption(); 28 | $this->initInquisition(); 29 | } 30 | 31 | protected function initOption() 32 | { 33 | $id = SiteApplication::initVar('id'); 34 | 35 | if ($id == '') { 36 | throw new AdminNotFoundException( 37 | Inquisition::_('No option id specified.') 38 | ); 39 | } 40 | 41 | if (is_numeric($id)) { 42 | $id = intval($id); 43 | } 44 | 45 | $this->option = SwatDBClassMap::new(InquisitionQuestionOption::class); 46 | $this->option->setDatabase($this->app->db); 47 | 48 | if (!$this->option->load($id)) { 49 | throw new AdminNotFoundException( 50 | sprintf( 51 | Inquisition::_( 52 | 'An option with the id of “%s” does not exist' 53 | ), 54 | $id 55 | ) 56 | ); 57 | } 58 | } 59 | 60 | protected function initInquisition() 61 | { 62 | $inquisition_id = SiteApplication::initVar('inquisition'); 63 | 64 | if ($inquisition_id !== null) { 65 | $this->inquisition = $this->loadInquisition($inquisition_id); 66 | } 67 | } 68 | 69 | protected function loadInquisition($inquisition_id) 70 | { 71 | $inquisition = SwatDBClassMap::new(InquisitionInquisition::class); 72 | $inquisition->setDatabase($this->app->db); 73 | 74 | if (!$inquisition->load($inquisition_id)) { 75 | throw new AdminNotFoundException( 76 | sprintf( 77 | 'Inquisition with id ‘%s’ not found.', 78 | $inquisition_id 79 | ) 80 | ); 81 | } 82 | 83 | return $inquisition; 84 | } 85 | 86 | // process phase 87 | 88 | protected function saveIndex($id, $index) 89 | { 90 | SwatDB::updateColumn( 91 | $this->app->db, 92 | 'InquisitionQuestionOptionImageBinding', 93 | 'integer:displayorder', 94 | $index, 95 | 'integer:image', 96 | [$id] 97 | ); 98 | } 99 | 100 | protected function getUpdatedMessage() 101 | { 102 | return new SwatMessage(Inquisition::_('Image order has been updated.')); 103 | } 104 | 105 | protected function relocate() 106 | { 107 | $this->app->relocate( 108 | sprintf( 109 | 'Option/Details?id=%s%s', 110 | $this->option->id, 111 | $this->getLinkSuffix() 112 | ) 113 | ); 114 | } 115 | 116 | // build phase 117 | 118 | protected function loadData() 119 | { 120 | $order_widget = $this->ui->getWidget('order'); 121 | 122 | foreach ($this->option->images as $image) { 123 | $order_widget->addOption( 124 | $image->id, 125 | strval($image->getImgTag('thumb', '../')), 126 | 'text/xml' 127 | ); 128 | } 129 | 130 | $sql = sprintf( 131 | 'select sum(displayorder) 132 | from InquisitionQuestionOptionImageBinding 133 | where question_option = %s', 134 | $this->option->id 135 | ); 136 | 137 | $sum = SwatDB::queryOne($this->app->db, $sql, 'integer'); 138 | 139 | $options_list = $this->ui->getWidget('options'); 140 | $options_list->value = ($sum == 0) ? 'auto' : 'custom'; 141 | } 142 | 143 | protected function buildInternal() 144 | { 145 | $this->ui->getWidget('order_frame')->title = $this->getTitle(); 146 | $this->ui->getWidget('order')->width = '150px'; 147 | $this->ui->getWidget('order')->height = '300px'; 148 | 149 | parent::buildInternal(); 150 | } 151 | 152 | protected function buildForm() 153 | { 154 | parent::buildForm(); 155 | 156 | $form = $this->ui->getWidget('order_form'); 157 | $form->addHiddenField('id', $this->option->id); 158 | 159 | if ($this->inquisition instanceof InquisitionInquisition) { 160 | $form->addHiddenField('inquisition', $this->inquisition->id); 161 | } 162 | } 163 | 164 | protected function buildNavBar() 165 | { 166 | parent::buildNavBar(); 167 | 168 | $this->navbar->popEntry(); 169 | 170 | if ($this->inquisition instanceof InquisitionInquisition) { 171 | $this->navbar->createEntry( 172 | $this->inquisition->title, 173 | sprintf( 174 | 'Inquisition/Details?id=%s', 175 | $this->inquisition->id 176 | ) 177 | ); 178 | } 179 | 180 | $this->navbar->createEntry( 181 | $this->getQuestionTitle(), 182 | sprintf( 183 | 'Question/Details?id=%s%s', 184 | $this->option->question->id, 185 | $this->getLinkSuffix() 186 | ) 187 | ); 188 | 189 | $this->navbar->createEntry( 190 | $this->getOptionTitle(), 191 | sprintf( 192 | 'Option/Details?id=%s%s', 193 | $this->option->id, 194 | $this->getLinkSuffix() 195 | ) 196 | ); 197 | 198 | $this->navbar->createEntry($this->getTitle()); 199 | } 200 | 201 | protected function getOptionTitle() 202 | { 203 | return sprintf( 204 | Inquisition::_('Option %s'), 205 | $this->option->position 206 | ); 207 | } 208 | 209 | protected function getQuestionTitle() 210 | { 211 | // TODO: Update this with some version of getPosition(). 212 | return Inquisition::_('Question'); 213 | } 214 | 215 | protected function getLinkSuffix() 216 | { 217 | $suffix = null; 218 | if ($this->inquisition instanceof InquisitionInquisition) { 219 | $suffix = sprintf( 220 | '&inquisition=%s', 221 | $this->inquisition->id 222 | ); 223 | } 224 | 225 | return $suffix; 226 | } 227 | 228 | protected function getTitle() 229 | { 230 | return Inquisition::_('Change Image Order'); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /Inquisition/admin/components/Option/Delete.php: -------------------------------------------------------------------------------- 1 | question = SwatDBClassMap::new(InquisitionQuestion::class); 26 | $this->question->setDatabase($this->app->db); 27 | 28 | if ($id == '') { 29 | throw new AdminNotFoundException('Question id not provided.'); 30 | } 31 | 32 | if (!$this->question->load($id)) { 33 | throw new AdminNotFoundException( 34 | sprintf('Question with id ‘%s’ not found.', $id) 35 | ); 36 | } 37 | 38 | $form = $this->ui->getWidget('confirmation_form'); 39 | $form->addHiddenField('id', $id); 40 | } 41 | 42 | public function setInquisition(?InquisitionInquisition $inquisition = null) 43 | { 44 | if ($inquisition instanceof InquisitionInquisition) { 45 | $this->inquisition = $inquisition; 46 | 47 | $form = $this->ui->getWidget('confirmation_form'); 48 | $form->addHiddenField('inquisition_id', $this->inquisition->id); 49 | } 50 | } 51 | 52 | // init phase 53 | 54 | protected function initInternal() 55 | { 56 | parent::initInternal(); 57 | 58 | $form = $this->ui->getWidget('confirmation_form'); 59 | $id = $form->getHiddenField('id'); 60 | if ($id != '') { 61 | $this->setId($id); 62 | } 63 | 64 | $inquisition_id = $form->getHiddenField('inquisition_id'); 65 | if ($inquisition_id != '') { 66 | $inquisition = $this->loadInquisition($inquisition_id); 67 | $this->setInquisition($inquisition); 68 | } 69 | } 70 | 71 | protected function loadInquisition($inquisition_id) 72 | { 73 | $inquisition = SwatDBClassMap::new(InquisitionInquisition::class); 74 | $inquisition->setDatabase($this->app->db); 75 | 76 | if (!$inquisition->load($inquisition_id)) { 77 | throw new AdminNotFoundException( 78 | sprintf( 79 | 'Inquisition with id ‘%s’ not found.', 80 | $inquisition_id 81 | ) 82 | ); 83 | } 84 | 85 | return $inquisition; 86 | } 87 | 88 | // process phase 89 | 90 | protected function processDBData(): void 91 | { 92 | parent::processDBData(); 93 | 94 | $locale = SwatI18NLocale::get(); 95 | 96 | $sql = sprintf( 97 | 'delete from InquisitionQuestionOption where id in (%s)', 98 | $this->getItemList('integer') 99 | ); 100 | 101 | $num = SwatDB::exec($this->app->db, $sql); 102 | 103 | $this->app->messages->add( 104 | new SwatMessage( 105 | sprintf( 106 | Inquisition::ngettext( 107 | 'One option has been deleted.', 108 | '%s options have been deleted.', 109 | $num 110 | ), 111 | $locale->formatNumber($num) 112 | ) 113 | ) 114 | ); 115 | } 116 | 117 | protected function relocate() 118 | { 119 | AdminDBConfirmation::relocate(); 120 | } 121 | 122 | // build phase 123 | 124 | protected function buildInternal() 125 | { 126 | parent::buildInternal(); 127 | 128 | $item_list = $this->getItemList('integer'); 129 | 130 | $dep = new AdminListDependency(); 131 | $dep->setTitle( 132 | Inquisition::_('option'), 133 | Inquisition::_('options') 134 | ); 135 | 136 | $dep->entries = AdminListDependency::queryEntries( 137 | $this->app->db, 138 | 'InquisitionQuestionOption', 139 | 'id', 140 | null, 141 | 'text:title', 142 | 'displayorder, id', 143 | 'id in (' . $item_list . ')', 144 | AdminDependency::DELETE 145 | ); 146 | 147 | // check images dependencies 148 | $dep_images = new AdminSummaryDependency(); 149 | $dep_images->setTitle( 150 | Inquisition::_('image'), 151 | Inquisition::_('images') 152 | ); 153 | 154 | $dep_images->summaries = AdminSummaryDependency::querySummaries( 155 | $this->app->db, 156 | 'InquisitionQuestionOptionImageBinding', 157 | 'integer:image', 158 | 'integer:question_option', 159 | 'question_option in (' . $item_list . ')', 160 | AdminDependency::DELETE 161 | ); 162 | 163 | $dep->addDependency($dep_images); 164 | 165 | foreach ($dep->entries as $entry) { 166 | $entry->title = SwatString::condense($entry->title); 167 | } 168 | 169 | $message = $this->ui->getWidget('confirmation_message'); 170 | $message->content = $dep->getMessage(); 171 | $message->content_type = 'text/xml'; 172 | 173 | if ($dep->getStatusLevelCount(AdminDependency::DELETE) == 0) { 174 | $this->switchToCancelButton(); 175 | } 176 | } 177 | 178 | protected function buildNavBar() 179 | { 180 | parent::buildNavBar(); 181 | 182 | $this->navbar->popEntry(); 183 | 184 | if ($this->inquisition instanceof InquisitionInquisition) { 185 | $this->navbar->createEntry( 186 | $this->inquisition->title, 187 | sprintf( 188 | 'Inquisition/Details?id=%s', 189 | $this->inquisition->id 190 | ) 191 | ); 192 | } 193 | 194 | $this->navbar->createEntry( 195 | $this->getQuestionTitle(), 196 | sprintf( 197 | 'Question/Details?id=%s%s', 198 | $this->question->id, 199 | $this->getLinkSuffix() 200 | ) 201 | ); 202 | 203 | $this->navbar->createEntry( 204 | Inquisition::ngettext( 205 | 'Delete Option', 206 | 'Delete Options', 207 | $this->getItemCount() 208 | ) 209 | ); 210 | } 211 | 212 | protected function getQuestionTitle() 213 | { 214 | // TODO: Update this with some version of getPosition(). 215 | return Inquisition::_('Question'); 216 | } 217 | 218 | protected function getLinkSuffix() 219 | { 220 | $suffix = null; 221 | if ($this->inquisition instanceof InquisitionInquisition) { 222 | $suffix = sprintf( 223 | '&inquisition=%s', 224 | $this->inquisition->id 225 | ); 226 | } 227 | 228 | return $suffix; 229 | } 230 | } 231 | --------------------------------------------------------------------------------