diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..d8120359 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,121 @@ +name: Moodle plugin CI +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-22.04 + + services: + postgres: + image: postgres:13 + env: + POSTGRES_USER: 'postgres' + POSTGRES_HOST_AUTH_METHOD: 'trust' + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3 + + mariadb: + image: mariadb:10 + env: + MYSQL_USER: 'root' + MYSQL_ALLOW_EMPTY_PASSWORD: "true" + MYSQL_CHARACTER_SET_SERVER: "utf8mb4" + MYSQL_COLLATION_SERVER: "utf8mb4_unicode_ci" + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 3 + + strategy: + fail-fast: false + matrix: +# include: +# - php: '8.3' +# moodle-branch: 'MOODLE_404_STABLE' +# database: 'pgsql' +# - php: '8.2' +# moodle-branch: 'MOODLE_403_STABLE' +# database: 'mariadb' +# - php: '8.1' +# moodle-branch: 'MOODLE_403_STABLE' +# database: 'pgsql' +# - php: '8.0' +# moodle-branch: 'MOODLE_402_STABLE' +# database: 'mariadb' +# - php: '7.4' +# moodle-branch: 'MOODLE_401_STABLE' +# database: 'pgsql' + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: plugin + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: ${{ matrix.extensions }} + ini-values: max_input_vars=5000 + # none to use phpdbg fallback. Specify pcov (Moodle 3.10 and up) or xdebug to use them instead. + coverage: none + + - name: Deploy moodle-plugin-ci + run: | + composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^4 + echo $(cd ci/bin; pwd) >> $GITHUB_PATH + echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH + sudo locale-gen en_AU.UTF-8 + # Install nvm. + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash + + - name: Install Moodle + # Need explicit IP to stop mysql client fail on attempt to use unix socket. + run: moodle-plugin-ci install --plugin ./plugin --db-host=127.0.0.1 + env: + DB: ${{ matrix.database }} + MOODLE_BRANCH: ${{ matrix.moodle-branch }} + IGNORE_PATHS: 'templates/local/mobile' + PHPDOCCHECKER_IGNORE_PATHS: /^vendor/ + + - name: PHP Lint + if: ${{ always() }} + run: moodle-plugin-ci phplint + + - name: PHP Mess Detector + continue-on-error: true # This step will show errors but will not fail + if: ${{ always() }} + run: moodle-plugin-ci phpmd + + - name: Moodle Code Checker + if: ${{ always() }} + run: moodle-plugin-ci codechecker || true + + - name: Moodle PHPDoc Checker + if: ${{ always() }} + run: moodle-plugin-ci phpdoc || true + + - name: Validating + if: ${{ always() }} + run: moodle-plugin-ci validate + + - name: Check upgrade savepoints + if: ${{ always() }} + run: moodle-plugin-ci savepoints + + - name: Mustache Lint + if: ${{ always() }} + run: moodle-plugin-ci mustache + + - name: Grunt + if: ${{ always() }} + run: moodle-plugin-ci grunt + + - name: PHPUnit tests + if: ${{ always() }} + run: moodle-plugin-ci phpunit + + - name: Behat features + if: ${{ always() }} + run: moodle-plugin-ci behat --profile chrome diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 371390b7..00000000 --- a/.travis.yml +++ /dev/null @@ -1,93 +0,0 @@ -language: php - -sudo: required - -addons: - firefox: "47.0.1" - postgresql: "9.4" - mysql: "8.0.2" - apt: - packages: - - oracle-java9-installer - - oracle-java9-set-default - -cache: - directories: - - $HOME/.composer/cache - - $HOME/.npm - -php: - - 5.6 - - 7.1 - - 7.2 - -env: - - MOODLE_BRANCH=MOODLE_36_STABLE DB=pgsql - - MOODLE_BRANCH=MOODLE_36_STABLE DB=mysqli - - MOODLE_BRANCH=MOODLE_35_STABLE DB=pgsql - - MOODLE_BRANCH=MOODLE_35_STABLE DB=mysqli - - MOODLE_BRANCH=MOODLE_34_STABLE DB=pgsql - - MOODLE_BRANCH=MOODLE_34_STABLE DB=mysqli - - MOODLE_BRANCH=MOODLE_33_STABLE DB=pgsql - - MOODLE_BRANCH=MOODLE_33_STABLE DB=mysqli - -matrix: - exclude: - - php: 7.1 - env: MOODLE_BRANCH=MOODLE_33_STABLE DB=mysqli - - php: 7.1 - env: MOODLE_BRANCH=MOODLE_33_STABLE DB=pgsql - - php: 7.2 - env: MOODLE_BRANCH=MOODLE_33_STABLE DB=mysqli - - php: 7.2 - env: MOODLE_BRANCH=MOODLE_33_STABLE DB=pgsql - - php: 5.6 - env: MOODLE_BRANCH=MOODLE_34_STABLE DB=mysqli - - php: 5.6 - env: MOODLE_BRANCH=MOODLE_34_STABLE DB=pgsql - - php: 7.2 - env: MOODLE_BRANCH=MOODLE_34_STABLE DB=mysqli - - php: 7.2 - env: MOODLE_BRANCH=MOODLE_34_STABLE DB=pgsql - - php: 5.6 - env: MOODLE_BRANCH=MOODLE_35_STABLE DB=mysqli - - php: 5.6 - env: MOODLE_BRANCH=MOODLE_35_STABLE DB=pgsql - - php: 7.2 - env: MOODLE_BRANCH=MOODLE_35_STABLE DB=mysqli - - php: 7.2 - env: MOODLE_BRANCH=MOODLE_35_STABLE DB=pgsql - - php: 5.6 - env: MOODLE_BRANCH=MOODLE_36_STABLE DB=mysqli - - php: 5.6 - env: MOODLE_BRANCH=MOODLE_36_STABLE DB=pgsql - - php: 7.1 - env: MOODLE_BRANCH=MOODLE_36_STABLE DB=mysqli - - php: 7.1 - env: MOODLE_BRANCH=MOODLE_36_STABLE DB=pgsql - -before_install: - - phpenv config-rm xdebug.ini - - nvm install node - - nvm install 8.9 - - nvm use 8.9 - - cd ../.. - - composer selfupdate - - composer create-project -n --no-dev --prefer-dist moodlerooms/moodle-plugin-ci ci ^2 - - export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH" - -install: - - moodle-plugin-ci install - -script: - - moodle-plugin-ci phplint - - moodle-plugin-ci phpcpd - - moodle-plugin-ci phpmd - - moodle-plugin-ci codechecker - - moodle-plugin-ci validate - - moodle-plugin-ci savepoints - - moodle-plugin-ci mustache - - moodle-plugin-ci grunt - - moodle-plugin-ci phpunit - - moodle-plugin-ci behat - - moodle-plugin-ci phpunit --coverage-text \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 00000000..493b4a37 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,38 @@ +Release Notes + +Release 4.1.0 (Build - 2023081100) + +Initial release for Moodle 4.1 forward. + +Release 4.1.1 (Build - 2024082900) + +Improvements: +* Compatible with Moodle 4.3 and 4.4. +* Compatible with PHP8.2. +* PR449 - Allow localized answer options to be displayed correctly in conditions. +* PR506 - Accessibility: improved accessibility for essay box type. +* PR495 - Accessibility: The slider values should be associated with the labels and the slider should be programmatically associated with the question +* PR497 - Accessibility: Rate table does not have a programmatically associated caption. +* PR496 - Accessibility: Numeric instructions not programmatically associated with field. +* PR505 - Accessibility: Rate form controls within the table are not accessible. +* PR501 - Accessibility: Check boxes missing group label. +* PR511 - Accessibility: Radio buttons & Yes/No missing group labels. +* PR517 - Mobile: Update sectiontext questions to display on mobile. +* PR520 - Add Slider question type compatibility with Feedback features. +* PR526 - Have additional info text pass through filters everywhere fixes. +* PR534 - Fix namespace issues with externallib.php file. +* PR536 - Support for user identity fields in Download Responses. +* PR577 - Improved headings in report page. +* PR581 - Mobile: Adapt mobile code to ionic 7. +* PR569 - Adopt icon size to 24×24 with a smaller content as other icons. +* PR586, PR579, PR594 - Various deprecations fixed. +* PR593 - Ensure "pdf" extension force. + +Bug Fixes: +* PR508 - General PHP fixes. +* PR523 - Behat activity completion fix. +* PR514 - Section text qtype should not support feedback. +* PR516 - Course description displays properly. + +(see CHANGES.md in release 4.00 for earlier changes.) + diff --git a/CHANGES.txt b/CHANGES.txt deleted file mode 100644 index 41b4b81f..00000000 --- a/CHANGES.txt +++ /dev/null @@ -1,80 +0,0 @@ -Release Notes - -NOTE - This release will work on Moodle 3.6 and can be backported to Moodle 3.4 and 3.3. If doing so, please ensure that your -questionnaire installation is at the latest version for those releases. For Moodle 3.4, this is questionnaire release 3.4.2. -For Moodle 3.3, this is questionnaire release 3.3.3. - -Version 3.5.3 (Build - 2018121000) -Bug Fixes: -GHI162 - Fixed response by group. - -CONTRIB-7555 - Fixed error on recent activity block. - -CONTRIB-7557 - Allowed compatibility with older releases of Moodle privacy API. - - -Version 3.5.2 (Build - 2018120100) -New features: -Code has been updated to work on Moodle 3.6 - -The two new privacy api functions have been added (get_users_in_context, delete_data_for_users). - -Bug Fixes: -The privacy api polyfill functions have been fixed so that they work correctly with PHP 5.6 under Moodle 3.3. -(Thanks to Paul Holden - https://github.com/paulholden - https://github.com/PoetOS/moodle-mod_questionnaire/pull/166) - -GHI167 - Fixed the feedback scoring for boolean questions. - - -Version 3.5.1 (Build - 2018110100) -NOTE - This release can be backported to Moodle 3.4 and 3.3. If doing so, please ensure that your questionnaire installation is at -the latest version for those releases. For Moodle 3.4, this is questionnaire release 3.4.2. For Moodle 3.3, this is questionnaire -release 3.3.3. - -New features: -The feedback options UI has been completely overhauled. The feedback UI is now accessed from the module settings menu and tabs. -Each section can now be easily accessed and managed on one page. Questions are now assigned to sections rather than sections -assigned to questions. - -Rate question usability improvements, specifically a mouse click on table cell selects the radio button as well. - -Report now uses number of of participants instead of number of responses for averages on questions that have optional responses -(e.g. checkboxes). - -Numeric question responses now allow comma decimal separator as well as period. - -Data export now uses the more of the Moodle export API, and exports with 'CSV' extension rather than 'TXT'. - -Incomplete responses can now be selected for export with the CSV export function. - -In exported data, anonymous responses are assigned unique identifiers, so that responses from the same anonymous submitter can be -grouped. - -Thanks to C. Jobst & Y. Annanias of the University of Leipzig and the Online-Self-Assessment project for the Faculty of Economics -and Management Science, funded by the EU/ESF (The European Social Fund in Germany) for the resources and efforts for these features. - - -Bug Fixes: -CONTRIB-7420 - Renaming 'rank' data field to 'rankvalue' to deal with 'rank' now being a MySQL reserved word. - -GHI154 - Fixing delete question error caused by code improvement regression. - -GHI151 - Fixing download of all public questionnaire instance results, caused by attempt table merge regression. - - -Code improvements: -Merged attempts and response tables into one table. -New functions added to remove repeated code snippets and DB calls. -Numerous response templates added. -Feedback and feedback section classes added. - -Version 3.5.0 (Build - 2018061900) - -New features: -This is an early release providing the GDPR Privacy API implementation. - -Bug fixes: -CONTRIB-7187 - Fixed preview mode with dependencies bug and added tests to verify. -CONTRIB-7300 - Removed database columns from install.xml and readded the upgrade step for them. - -(see CHANGES.TXT in release 3.4 for earlier changes.) diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..20d40b6b --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/README.txt b/README.txt index 8b6d78eb..5a18c293 100644 --- a/README.txt +++ b/README.txt @@ -2,6 +2,14 @@ The questionnaire module allows you to construct questionnaires (surveys) from a variety of question type. It was originally based on phpESP, and Open Source survey tool. +-------------------------------------------------------------------------------- +Developers Note: + +There is no main branch. Questionnaire is maintained in MOODLE_XX_STABLE +branches. Use the latest STABLE branch for development or installation. +The current stable branch is MOODLE_400_STABLE, and supports Moodle 4.0 and up. +Use the MOODLE_311_STABLE branch for Moodle 3.9 through 3.11. + -------------------------------------------------------------------------------- To Install: @@ -13,29 +21,6 @@ To Upgrade: 1. Copy all of the files into your 'mod/questionnaire' directory. 2. Visit your admin page. The database will be updated. -3. As part of the update, all existing surveys are assigned as either 'private', - 'public' or 'temmplate'. Surveys assigned to a single questionnaire are set - to 'private' with the questionnaire's course as the owner. Surveys assigned - to multiple questionnaires in the same course are set to 'public' with the - questionnaire's course as the owner. Surveys assigned to multiple - questionnaires in multiple courses are set to 'public' with the site ID as - the owner. Surveys that are not deleted but have no associated questionnaires - are set to 'template' with the site ID as the owner. - -*** IMPORTANT *** - -IF YOU ARE UPGRADING TO MOODLE 2.3... - -Make sure that you upgrade the questionnaire module to the latest 2.2 version in -a Moodle 2.2 install first. -------------------------------------------------------------------------------- -Version 2.4.1 - Release date 20130519 - -In accordance with current Moodle languages policy, all language folders other than English have been -removed from the lang folder. All translations are now available from AMOS. - --------------------------------------------------------------------------------- -Please read the releasenotes.txt file for more info about successive changes --------------------------------------------------------------------------------- - +Please read the CHANGES.md file for more info about successive changes diff --git a/appjs/uncheckother.js b/appjs/uncheckother.js new file mode 100644 index 00000000..c76a4df6 --- /dev/null +++ b/appjs/uncheckother.js @@ -0,0 +1,4 @@ +(function(t) { + t.toggleRatebox = function($event, $fieldkey) { + } +})(this); \ No newline at end of file diff --git a/backup/moodle1/lib.php b/backup/moodle1/lib.php index c84ce0c8..bd8d7d9b 100644 --- a/backup/moodle1/lib.php +++ b/backup/moodle1/lib.php @@ -17,14 +17,11 @@ /** * Provides support for the conversion of moodle1 backup to the moodle2 format * - * @package mod - * @subpackage questionnaire + * @package mod_questionnaire * @copyright 2011 Robin de Vries * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); - /** * Choice conversion handler */ @@ -33,7 +30,7 @@ class moodle1_mod_questionnaire_handler extends moodle1_mod_handler { /** * Declare the paths in moodle.xml we are able to convert * - * The method returns list of {@link convert_path} instances. For each path returned, + * The method returns list of convert_path instances. For each path returned, * at least one of on_xxx_start(), process_xxx() and on_xxx_end() methods must be * defined. The method process_xxx() is not executed if the associated path element is * empty (i.e. it contains none elements or sub-paths only). @@ -42,7 +39,7 @@ class moodle1_mod_questionnaire_handler extends moodle1_mod_handler { * actually exist in the file. The last element with the module name was * appended by the moodle1_converter class. * - * @return array of {@link convert_path} instances + * @return array of convert_path instances */ public function get_paths() { return array( @@ -62,16 +59,18 @@ public function get_paths() { new convert_path('question_choice', '/MOODLE_BACKUP/COURSE/MODULES/MOD/QUESTIONNAIRE/SURVEY/QUESTION/QUESTION_CHOICE'), ); } + /** * This is executed every time we have one /MOODLE_BACKUP/COURSE/MODULES/MOD/QUESTIONNAIRE - * data available + * data available. + * @param array $data */ public function process_questionnaire($data) { // Get the course module id and context id. $instanceid = $data['id']; - $cminfo = $this->get_cminfo($instanceid); - $moduleid = $cminfo['id']; - $contextid = $this->converter->get_contextid(CONTEXT_MODULE, $moduleid); + $cminfo = $this->get_cminfo($instanceid); + $moduleid = $cminfo['id']; + $contextid = $this->converter->get_contextid(CONTEXT_MODULE, $moduleid); // We now have all information needed to start writing into the file. $this->open_xml_writer("activities/questionnaire_{$moduleid}/questionnaire.xml"); @@ -88,7 +87,6 @@ public function process_questionnaire($data) { /** * This is executed when we reach the closing tag of our 'questionnaire' path */ - public function on_questionnaire_end() { // Close questionnaire.xml. $this->xmlwriter->end_tag('surveys'); @@ -96,9 +94,11 @@ public function on_questionnaire_end() { $this->xmlwriter->end_tag('activity'); $this->close_xml_writer(); } + /** * This is executed every time we have one /MOODLE_BACKUP/COURSE/MODULES/MOD/QUESTIONNAIRE/SURVEY * data available + * @param array $data */ public function process_survey($data) { $this->xmlwriter->begin_tag('survey', array('id' => $data['id'])); @@ -120,6 +120,7 @@ public function on_survey_end() { /** * This is executed every time we have one /MOODLE_BACKUP/COURSE/MODULES/MOD/QUESTIONNAIRE/SURVEY/QUESTION * data available + * @param array $data */ public function process_question($data) { @@ -144,9 +145,9 @@ public function on_question_end() { /** * This is executed every time we have one /MOODLE_BACKUP/COURSE/MODULES/MOD/QUESTIONNAIRE/SURVEY/QUESTION/QUESTION_CHOICE * data available + * @param array $data */ public function process_question_choice($data) { $this->write_xml('quest_choice', $data, array('/question_choice/id')); } - -} \ No newline at end of file +} diff --git a/backup/moodle2/backup_questionnaire_activity_task.class.php b/backup/moodle2/backup_questionnaire_activity_task.class.php index f1473e21..808672e8 100644 --- a/backup/moodle2/backup_questionnaire_activity_task.class.php +++ b/backup/moodle2/backup_questionnaire_activity_task.class.php @@ -14,13 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * @package mod_questionnaire - * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - defined('MOODLE_INTERNAL') || die(); // Because it exists (must). @@ -29,8 +22,11 @@ require_once($CFG->dirroot . '/mod/questionnaire/backup/moodle2/backup_questionnaire_settingslib.php'); /** - * questionnaire backup task that provides all the settings and steps to perform one - * complete backup of the activity + * Questionnaire backup task that provides all the settings and steps to perform one complete backup of the activity. + * @package mod_questionnaire + * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_questionnaire_activity_task extends backup_activity_task { @@ -52,8 +48,10 @@ protected function define_my_steps() { /** * Code the transformations to perform in the activity in * order to get transportable (encoded) links + * @param string $content + * @return array|string|string[]|null */ - static public function encode_content_links($content) { + public static function encode_content_links($content) { global $CFG; $base = preg_quote($CFG->wwwroot, "/"); diff --git a/backup/moodle2/backup_questionnaire_settingslib.php b/backup/moodle2/backup_questionnaire_settingslib.php index 8c7fd054..c8c163e0 100644 --- a/backup/moodle2/backup_questionnaire_settingslib.php +++ b/backup/moodle2/backup_questionnaire_settingslib.php @@ -15,14 +15,13 @@ // along with Moodle. If not, see . /** + * The required settingslib file. * @package mod_questionnaire * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); - // This activity has no particular settings but the inherited from the generic // backup_activity_task so here there isn't any class definition, like the ones // existing in /backup/moodle2/backup_settingslib.php (activities section). diff --git a/backup/moodle2/backup_questionnaire_stepslib.php b/backup/moodle2/backup_questionnaire_stepslib.php index 85a1890b..747b304b 100644 --- a/backup/moodle2/backup_questionnaire_stepslib.php +++ b/backup/moodle2/backup_questionnaire_stepslib.php @@ -15,23 +15,21 @@ // along with Moodle. If not, see . /** + * Define all the backup steps that will be used by the backup_questionnaire_activity_task. + * + * Define the complete choice structure for backup, with file and id annotations. + * * @package mod_questionnaire * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -defined('MOODLE_INTERNAL') || die(); - -/** - * Define all the backup steps that will be used by the backup_questionnaire_activity_task - */ - -/** - * Define the complete choice structure for backup, with file and id annotations - */ class backup_questionnaire_activity_structure_step extends backup_activity_structure_step { + /** + * Defines the backup structure. + * @return backup_nested_element + */ protected function define_structure() { global $DB; // To know if we are including userinfo. @@ -53,7 +51,7 @@ protected function define_structure() { $questions = new backup_nested_element('questions'); $question = new backup_nested_element('question', array('id'), array('surveyid', 'name', 'type_id', 'result_id', - 'length', 'precise', 'position', 'content', 'required', 'deleted')); + 'length', 'precise', 'position', 'content', 'required', 'deleted', 'extradata')); $questchoices = new backup_nested_element('quest_choices'); @@ -172,7 +170,7 @@ protected function define_structure() { $question->set_source_table('questionnaire_question', array('surveyid' => backup::VAR_PARENTID)); $fbsection->set_source_table('questionnaire_fb_sections', array('surveyid' => backup::VAR_PARENTID)); $feedback->set_source_table('questionnaire_feedback', array('sectionid' => backup::VAR_PARENTID)); - $questchoice->set_source_table('questionnaire_quest_choice', array('question_id' => backup::VAR_PARENTID)); + $questchoice->set_source_table('questionnaire_quest_choice', array('question_id' => backup::VAR_PARENTID), 'id ASC'); $questdependency->set_source_table('questionnaire_dependency', array('questionid' => backup::VAR_PARENTID)); // All the rest of elements only happen if we are including user info. @@ -190,10 +188,10 @@ protected function define_structure() { // Define id annotations. $response->annotate_ids('user', 'userid'); } - // Define file annotations + // Define file annotations. $questionnaire->annotate_files('mod_questionnaire', 'intro', null); // This file area hasn't itemid. - $survey->annotate_files('mod_questionnaire', 'info', 'id'); // By survey->id + $survey->annotate_files('mod_questionnaire', 'info', 'id'); // By survey->id. $survey->annotate_files('mod_questionnaire', 'thankbody', 'id'); // By survey->id. $question->annotate_files('mod_questionnaire', 'question', 'id'); // By question->id. @@ -204,4 +202,4 @@ protected function define_structure() { // Return the root element, wrapped into standard activity structure. return $this->prepare_activity_structure($questionnaire); } -} \ No newline at end of file +} diff --git a/backup/moodle2/restore_questionnaire_activity_task.class.php b/backup/moodle2/restore_questionnaire_activity_task.class.php index da6b7d50..ebb066b7 100644 --- a/backup/moodle2/restore_questionnaire_activity_task.class.php +++ b/backup/moodle2/restore_questionnaire_activity_task.class.php @@ -14,21 +14,17 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * @package mod_questionnaire - * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - defined('MOODLE_INTERNAL') || die(); // Because it exists (must). require_once($CFG->dirroot . '/mod/questionnaire/backup/moodle2/restore_questionnaire_stepslib.php'); /** - * questionnaire restore task that provides all the settings and steps to perform one - * complete restore of the activity + * Questionnaire restore task that provides all the settings and steps to perform one complete restore of the activity. + * @package mod_questionnaire + * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_questionnaire_activity_task extends restore_activity_task { @@ -51,13 +47,15 @@ protected function define_my_steps() { * Define the contents in the activity that must be * processed by the link decoder */ - static public function define_decode_contents() { + public static function define_decode_contents() { $contents = array(); $contents[] = new restore_decode_content('questionnaire', array('intro'), 'questionnaire'); $contents[] = new restore_decode_content('questionnaire_survey', - array('info', 'thank_head', 'thank_body', 'thanks_page'), 'questionnaire_survey'); + array('info', 'thank_head', 'thank_body', 'thanks_page', 'feedbacknotes'), 'questionnaire_survey'); $contents[] = new restore_decode_content('questionnaire_question', array('content'), 'questionnaire_question'); + $contents[] = new restore_decode_content('questionnaire_fb_sections', array('sectionheading'), 'questionnaire_fb_sections'); + $contents[] = new restore_decode_content('questionnaire_feedback', array('feedbacktext'), 'questionnaire_feedback'); return $contents; } @@ -66,7 +64,7 @@ static public function define_decode_contents() { * Define the decoding rules for links belonging * to the activity to be executed by the link decoder */ - static public function define_decode_rules() { + public static function define_decode_rules() { $rules = array(); $rules[] = new restore_decode_rule('QUESTIONNAIREVIEWBYID', '/mod/questionnaire/view.php?id=$1', 'course_module'); @@ -78,11 +76,11 @@ static public function define_decode_rules() { /** * Define the restore log rules that will be applied - * by the {@link restore_logs_processor} when restoring + * by the restore_logs_processor when restoring * questionnaire logs. It must return one array - * of {@link restore_log_rule} objects + * of restore_log_rule objects */ - static public function define_restore_log_rules() { + public static function define_restore_log_rules() { $rules = array(); $rules[] = new restore_log_rule('questionnaire', 'add', 'view.php?id={course_module}', '{questionnaire}'); @@ -97,15 +95,15 @@ static public function define_restore_log_rules() { /** * Define the restore log rules that will be applied - * by the {@link restore_logs_processor} when restoring + * by the restore_logs_processor when restoring * course logs. It must return one array - * of {@link restore_log_rule} objects + * of restore_log_rule objects * * Note this rules are applied when restoring course logs * by the restore final task, but are defined here at * activity level. All them are rules not linked to any module instance (cmid = 0) */ - static public function define_restore_log_rules_for_course() { + public static function define_restore_log_rules_for_course() { $rules = array(); // Fix old wrong uses (missing extension). diff --git a/backup/moodle2/restore_questionnaire_stepslib.php b/backup/moodle2/restore_questionnaire_stepslib.php index 0b060b2f..d5235635 100644 --- a/backup/moodle2/restore_questionnaire_stepslib.php +++ b/backup/moodle2/restore_questionnaire_stepslib.php @@ -13,23 +13,18 @@ // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +use mod_questionnaire\feedback\section; /** + * Define all the restore steps that will be used by the restore_questionnaire_activity_task. + * + * Structure step to restore one questionnaire activity. + * * @package mod_questionnaire * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -defined('MOODLE_INTERNAL') || die(); - -/** - * Define all the restore steps that will be used by the restore_questionnaire_activity_task - */ - -/** - * Structure step to restore one questionnaire activity - */ class restore_questionnaire_activity_structure_step extends restore_activity_structure_step { /** @@ -47,6 +42,10 @@ class restore_questionnaire_activity_structure_step extends restore_activity_str */ protected $olddependencies = []; + /** + * Implementation of define_structure. + * @return mixed + */ protected function define_structure() { $paths = array(); @@ -110,6 +109,10 @@ protected function define_structure() { return $this->prepare_activity_structure($paths); } + /** + * Implementation of process_questionnaire. + * @param array $data + */ protected function process_questionnaire($data) { global $DB; @@ -126,6 +129,10 @@ protected function process_questionnaire($data) { $this->apply_activity_instance($newitemid); } + /** + * Process the survey table. + * @param array $data + */ protected function process_questionnaire_survey($data) { global $DB; @@ -146,6 +153,10 @@ protected function process_questionnaire_survey($data) { $DB->set_field('questionnaire', 'sid', $newitemid, array('id' => $this->get_new_parentid('questionnaire'))); } + /** + * Process the questions. + * @param array $data + */ protected function process_questionnaire_question($data) { global $DB; @@ -166,9 +177,9 @@ protected function process_questionnaire_question($data) { } /** + * Process the feedback sections. * $qid is unused, but is needed in order to get the $key elements of the array. Suppress PHPMD warning. - * - * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @param array $data */ protected function process_questionnaire_fb_sections($data) { global $DB; @@ -179,7 +190,7 @@ protected function process_questionnaire_fb_sections($data) { // If this questionnaire has separate sections feedbacks. if (isset($data->scorecalculation)) { - $scorecalculation = unserialize($data->scorecalculation); + $scorecalculation = section::decode_scorecalculation($data->scorecalculation); $newscorecalculation = array(); foreach ($scorecalculation as $qid => $val) { $newqid = $this->get_mappingid('questionnaire_question', $qid); @@ -193,6 +204,10 @@ protected function process_questionnaire_fb_sections($data) { $this->set_mapping('questionnaire_fb_sections', $oldid, $newitemid, true); } + /** + * Process feedback. + * @param array $data + */ protected function process_questionnaire_feedback($data) { global $DB; @@ -205,15 +220,24 @@ protected function process_questionnaire_feedback($data) { $this->set_mapping('questionnaire_feedback', $oldid, $newitemid, true); } + /** + * Process question choices. + * @param array $data + */ protected function process_questionnaire_quest_choice($data) { global $CFG, $DB; $data = (object)$data; - // Replace the = separator with :: separator in quest_choice content. - // This fixes radio button options using old "value"="display" formats. require_once($CFG->dirroot.'/mod/questionnaire/locallib.php'); + // Some old systems had '' instead of NULL. Change it to NULL. + if ($data->value === '') { + $data->value = null; + } + + // Replace the = separator with :: separator in quest_choice content. + // This fixes radio button options using old "value"="display" formats. if (($data->value == null || $data->value == 'NULL') && !preg_match("/^([0-9]{1,3}=.*|!other=.*)$/", $data->content)) { $content = questionnaire_choice_values($data->content); if (strpos($content->text, '=')) { @@ -229,6 +253,10 @@ protected function process_questionnaire_quest_choice($data) { $this->set_mapping('questionnaire_quest_choice', $oldid, $newitemid); } + /** + * Process dependencies. + * @param array $data + */ protected function process_questionnaire_dependency($data) { $data = (object)$data; @@ -240,11 +268,20 @@ protected function process_questionnaire_dependency($data) { } } + /** + * Process attempts (these are no longer used). + * @param array $data + * @return bool + */ protected function process_questionnaire_attempt($data) { // New structure will be completed in process_questionnaire_response. Nothing to do here any more. return true; } + /** + * Process responses. + * @param array $data + */ protected function process_questionnaire_response($data) { global $DB; @@ -264,6 +301,11 @@ protected function process_questionnaire_response($data) { $this->set_mapping('questionnaire_response', $oldid, $newitemid); } + /** + * Process boolean responses. + * @param array $data + * @throws dml_exception + */ protected function process_questionnaire_response_bool($data) { global $DB; @@ -275,6 +317,11 @@ protected function process_questionnaire_response_bool($data) { $DB->insert_record('questionnaire_response_bool', $data); } + /** + * Process date responses. + * @param array $data + * @throws dml_exception + */ protected function process_questionnaire_response_date($data) { global $DB; @@ -286,6 +333,11 @@ protected function process_questionnaire_response_date($data) { $DB->insert_record('questionnaire_response_date', $data); } + /** + * Process multiple responses. + * @param array $data + * @throws dml_exception + */ protected function process_questionnaire_response_multiple($data) { global $DB; @@ -298,6 +350,11 @@ protected function process_questionnaire_response_multiple($data) { $DB->insert_record('questionnaire_resp_multiple', $data); } + /** + * Process other responses. + * @param array $data + * @throws dml_exception + */ protected function process_questionnaire_response_other($data) { global $DB; @@ -310,6 +367,11 @@ protected function process_questionnaire_response_other($data) { $DB->insert_record('questionnaire_response_other', $data); } + /** + * Process rank responses. + * @param array $data + * @throws dml_exception + */ protected function process_questionnaire_response_rank($data) { global $DB; @@ -328,6 +390,11 @@ protected function process_questionnaire_response_rank($data) { $DB->insert_record('questionnaire_response_rank', $data); } + /** + * Process single responses. + * @param array $data + * @throws dml_exception + */ protected function process_questionnaire_response_single($data) { global $DB; @@ -340,6 +407,10 @@ protected function process_questionnaire_response_single($data) { $DB->insert_record('questionnaire_resp_single', $data); } + /** + * Process text answers. + * @param array $data + */ protected function process_questionnaire_response_text($data) { global $DB; @@ -351,6 +422,9 @@ protected function process_questionnaire_response_text($data) { $DB->insert_record('questionnaire_response_text', $data); } + /** + * Stuff to do after execution. + */ protected function after_execute() { global $DB; @@ -396,5 +470,10 @@ protected function after_execute() { $this->add_related_files('mod_questionnaire', 'question', 'questionnaire_question'); $this->add_related_files('mod_questionnaire', 'sectionheading', 'questionnaire_fb_sections'); $this->add_related_files('mod_questionnaire', 'feedback', 'questionnaire_feedback'); + + // Process any old rate question named degree choices after all questions and choices have been restored. + if ($this->task->get_old_moduleversion() < 2018110103) { + \mod_questionnaire\question\rate::move_all_nameddegree_choices($this->get_new_parentid('questionnaire_survey')); + } } -} \ No newline at end of file +} diff --git a/classes/completion/custom_completion.php b/classes/completion/custom_completion.php new file mode 100644 index 00000000..3e7542f8 --- /dev/null +++ b/classes/completion/custom_completion.php @@ -0,0 +1,85 @@ +. +declare(strict_types=1); + +namespace mod_questionnaire\completion; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/questionnaire/lib.php'); + +use coding_exception; +use core_completion\activity_custom_completion; +use moodle_exception; + +/** + * Activity custom completion subclass for the data activity. + * + * Class for defining mod_oucontent's custom completion rules and fetching the completion statuses + * of the custom completion rules for a given data instance and a user. + * + * @package mod_questionnaire + * @copyright 2022 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class custom_completion extends activity_custom_completion { + /** + * Fetches the completion state for a given completion rule. + * + * @param string $rule + * @return int + */ + public function get_state(string $rule): int { + $this->validate_rule($rule); + $userid = $this->userid; + $cm = $this->cm; + $status = questionnaire_get_completion_state($cm, $userid, $rule); + return $status ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE; + } + + /** + * Fetch the list of custom completion rules that this module defines. + * + * @return array + */ + public static function get_defined_custom_rules(): array { + return [ + 'completionsubmit' + ]; + } + + /** + * Returns an associative array of the descriptions of custom completion rules. + * + * @return array + */ + public function get_custom_rule_descriptions(): array { + return [ + 'completionsubmit' => get_string('completionsubmit', 'questionnaire') + ]; + } + + /** + * Returns an array of all completion rules, in the order they should be displayed to users. + * + * @return array + */ + public function get_sort_order(): array { + return [ + 'completionsubmit', + ]; + } +} diff --git a/classes/db/bulk_sql_config.php b/classes/db/bulk_sql_config.php index 1069c0c9..dce187cd 100644 --- a/classes/db/bulk_sql_config.php +++ b/classes/db/bulk_sql_config.php @@ -16,8 +16,6 @@ namespace mod_questionnaire\db; -defined('MOODLE_INTERNAL') || die(); - /** * For bulk sql operations on useresponses. * @@ -25,7 +23,7 @@ * @copyright 2015 Guy Thomas * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class bulk_sql_config { +class bulk_sql_config { /** * @var string $table @@ -53,6 +51,7 @@ class bulk_sql_config { protected $userank = false; /** + * The class constructor. * @param string $table * @param string $tablealias * @param bool $usechoiceid @@ -78,4 +77,4 @@ public function get_extra_select() { 'rankvalue' => $this->userank ]; } -} \ No newline at end of file +} diff --git a/classes/edit_question_form.php b/classes/edit_question_form.php index 71f65a41..92098e84 100644 --- a/classes/edit_question_form.php +++ b/classes/edit_question_form.php @@ -15,11 +15,11 @@ // along with Moodle. If not, see . /** + * The form class for editing questions. * @package mod_questionnaire * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) * @author Mike Churchward & Joseph Rézeau * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questionnaire */ namespace mod_questionnaire; @@ -36,6 +36,9 @@ */ class edit_question_form extends \moodleform { + /** + * Form definition. + */ public function definition() { // TODO - Find a way to not use globals. Maybe the base class allows more parameters to be passed? global $questionnaire, $question, $SESSION; @@ -46,15 +49,23 @@ public function definition() { $question->required = $SESSION->questionnaire->required; } if (!isset($question->type_id)) { - print_error('undefinedquestiontype', 'questionnaire'); + throw new \moodle_exception('undefinedquestiontype', 'mod_questionnaire'); } // Each question can provide its own form elements to the provided form, or use the default ones. if (!$question->edit_form($this, $questionnaire)) { - print_error("Question type had an unknown error in the edit_form method."); + throw new \moodle_exception('Question type had an unknown error in the edit_form method.', 'mod_questionnaire'); } } + /** + * Form validation. + * + * @param array $data array of ("fieldname"=>value) of submitted data + * @param array $files array of uploaded files "element_name"=>tmp_file_path + * @return array of "element_name"=>"error_description" if there are errors, + * or an empty array if everything is OK (true allowed for backwards compatibility too). + */ public function validation($data, $files) { $errors = parent::validation($data, $files); @@ -79,6 +90,36 @@ public function validation($data, $files) { } } + // If this is a slider question. + if ($data['type_id'] == QUESSLIDER) { + if (isset($data['minrange']) && isset($data['maxrange']) && isset($data['startingvalue']) && + isset($data['stepvalue'])) { + if ($data['minrange'] >= $data['maxrange']) { + $errors['maxrange'] = get_string('invalidrange', 'questionnaire'); + } + + if (($data['startingvalue'] > $data['maxrange']) || ($data['startingvalue'] < $data['minrange'])) { + $errors['startingvalue'] = get_string('invalidstartingvalue', 'questionnaire'); + } + + if ($data['startingvalue'] > 100 || $data['startingvalue'] < -100) { + $errors['startingvalue'] = get_string('invalidstartingvalue', 'questionnaire'); + } + + if (($data['stepvalue'] > $data['maxrange']) || $data['stepvalue'] < 1) { + $errors['stepvalue'] = get_string('invalidincrement', 'questionnaire'); + } + + if ($data['minrange'] < -100) { + $errors['minrange'] = get_string('invalidminmaxrange', 'questionnaire'); + } + + if ($data['maxrange'] > 100) { + $errors['maxrange'] = get_string('invalidminmaxrange', 'questionnaire'); + } + } + } + return $errors; } @@ -86,7 +127,6 @@ public function validation($data, $files) { * Magic method for getting the protected $_form MoodleQuickForm and $_customdata array properties. * @param string $name * @return mixed - * @throws \coding_exception */ public function __get($name) { if ($name == '_form') { @@ -97,4 +137,4 @@ public function __get($name) { throw new \coding_exception($name.' is not a publicly accessible property of '.get_class($this)); } } -} \ No newline at end of file +} diff --git a/classes/event/all_responses_deleted.php b/classes/event/all_responses_deleted.php index ce634f0b..af2b1227 100644 --- a/classes/event/all_responses_deleted.php +++ b/classes/event/all_responses_deleted.php @@ -24,8 +24,6 @@ namespace mod_questionnaire\event; -defined('MOODLE_INTERNAL') || die(); - /** * The mod_questionnaire all responses deleted event class. * diff --git a/classes/event/all_responses_saved_as_text.php b/classes/event/all_responses_saved_as_text.php index 867982a6..860b27c0 100644 --- a/classes/event/all_responses_saved_as_text.php +++ b/classes/event/all_responses_saved_as_text.php @@ -24,8 +24,6 @@ namespace mod_questionnaire\event; -defined('MOODLE_INTERNAL') || die(); - /** * The mod_questionnaire all_responses_saved_as_text event class. * diff --git a/classes/event/all_responses_viewed.php b/classes/event/all_responses_viewed.php index 44961418..98b59ce3 100644 --- a/classes/event/all_responses_viewed.php +++ b/classes/event/all_responses_viewed.php @@ -24,8 +24,6 @@ namespace mod_questionnaire\event; -defined('MOODLE_INTERNAL') || die(); - /** * The mod_questionnaire all_responses_viewed event class. * @@ -86,14 +84,4 @@ public function get_url() { } return new \moodle_url("/mod/questionnaire/report.php", $params); } - - /** - * Return the legacy event log data. - * - * @return array - */ - protected function get_legacy_logdata() { - return array($this->courseid, "questionnaire", "view report", "report.php?id=" . $this->contextinstanceid, $this->objectid, - $this->contextinstanceid); - } } diff --git a/classes/event/attempt_resumed.php b/classes/event/attempt_resumed.php index ba4efccc..c2057e2c 100644 --- a/classes/event/attempt_resumed.php +++ b/classes/event/attempt_resumed.php @@ -24,8 +24,6 @@ namespace mod_questionnaire\event; -defined('MOODLE_INTERNAL') || die(); - /** * The mod_questionnaire attempt_resumed event. * @@ -73,4 +71,4 @@ public function get_url() { return new \moodle_url("/mod/questionnaire/view.php", array('id' => $this->contextinstanceid)); } -} \ No newline at end of file +} diff --git a/classes/event/attempt_saved.php b/classes/event/attempt_saved.php index 95d28013..c5ca4cea 100644 --- a/classes/event/attempt_saved.php +++ b/classes/event/attempt_saved.php @@ -24,8 +24,6 @@ namespace mod_questionnaire\event; -defined('MOODLE_INTERNAL') || die(); - /** * The mod_questionnaire attempt_saved event class. * @@ -72,16 +70,6 @@ public function get_url() { return new \moodle_url("/mod/questionnaire/view.php", array('id' => $this->contextinstanceid)); } - /** - * Return the legacy event log data. - * - * @return array - */ - protected function get_legacy_logdata() { - return array($this->courseid, "questionnaire", "save", "view.php?id=" . - $this->contextinstanceid, $this->other['questionnaireid'], $this->contextinstanceid); - } - /** * Custom validation. * diff --git a/classes/event/attempt_submitted.php b/classes/event/attempt_submitted.php index d36366e1..274bdcbb 100644 --- a/classes/event/attempt_submitted.php +++ b/classes/event/attempt_submitted.php @@ -24,8 +24,6 @@ namespace mod_questionnaire\event; -defined('MOODLE_INTERNAL') || die(); - /** * The mod_questionnaire attempt_submitted event class. * @@ -72,16 +70,6 @@ public function get_url() { return new \moodle_url("/mod/questionnaire/view.php", array('id' => $this->contextinstanceid)); } - /** - * Return the legacy event log data. - * - * @return array - */ - protected function get_legacy_logdata() { - return array($this->courseid, "questionnaire", "submit", "view.php?id=" . - $this->contextinstanceid, $this->other['questionnaireid'], $this->contextinstanceid); - } - /** * Custom validation. * diff --git a/classes/event/course_module_instance_list_viewed.php b/classes/event/course_module_instance_list_viewed.php index c7797f4e..f4781d73 100644 --- a/classes/event/course_module_instance_list_viewed.php +++ b/classes/event/course_module_instance_list_viewed.php @@ -24,7 +24,6 @@ */ namespace mod_questionnaire\event; -defined('MOODLE_INTERNAL') || die(); /** * The mod_questionnaire instance list viewed event class. @@ -37,4 +36,4 @@ */ class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { // No code required here as the parent class handles it all. -} \ No newline at end of file +} diff --git a/classes/event/course_module_viewed.php b/classes/event/course_module_viewed.php index 2f39845d..3c6dbb42 100644 --- a/classes/event/course_module_viewed.php +++ b/classes/event/course_module_viewed.php @@ -24,8 +24,6 @@ namespace mod_questionnaire\event; -defined('MOODLE_INTERNAL') || die(); - /** * The mod_survery course module viewed event class. * @@ -44,14 +42,4 @@ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } - - /** - * Return the legacy event log data. - * - * @return array - */ - protected function get_legacy_logdata() { - return array($this->courseid, $this->objecttable, 'view '. $this->other['viewed'], 'view.php?id=' . - $this->contextinstanceid, $this->objectid, $this->contextinstanceid); - } } diff --git a/classes/event/non_respondents_viewed.php b/classes/event/non_respondents_viewed.php index 462bec23..cc27edf7 100644 --- a/classes/event/non_respondents_viewed.php +++ b/classes/event/non_respondents_viewed.php @@ -24,8 +24,6 @@ namespace mod_questionnaire\event; -defined('MOODLE_INTERNAL') || die(); - /** * The mod_questionnaire non_respondents_viewed event class. * diff --git a/classes/event/question_created.php b/classes/event/question_created.php index 69aff135..b480b678 100644 --- a/classes/event/question_created.php +++ b/classes/event/question_created.php @@ -14,38 +14,25 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * The mod_questionnaire question_created event. - * - * @package mod_questionnaire - * @copyright 2014 Joseph Rézeau - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_questionnaire\event; -defined('MOODLE_INTERNAL') || die(); - /** * The mod_questionnaire question_created event class. * * @package mod_questionnaire - * @since Moodle 2.7 * @copyright 2014 Joseph Rézeau * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - class question_created extends \core\event\base { - /* + /** * Set basic properties for the event. */ - protected function init() { $this->data['crud'] = 'c'; $this->data['edulevel'] = self::LEVEL_TEACHING; } - /* + /** * Return localised event name. * * @return string @@ -54,7 +41,7 @@ public static function get_name() { return get_string('event_question_created', 'mod_questionnaire'); } - /* + /** * Returns description of what happened. * * @return string @@ -64,4 +51,4 @@ public function get_description() { return "The user with id '$this->userid' has created or modified a question of type '$questiontype' for the questionnaire with course module id '$this->contextinstanceid'."; } -} \ No newline at end of file +} diff --git a/classes/event/question_deleted.php b/classes/event/question_deleted.php index 456d4691..900172b3 100644 --- a/classes/event/question_deleted.php +++ b/classes/event/question_deleted.php @@ -14,39 +14,25 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * The mod_questionnaire question_deleted event. - * - * @package mod_questionnaire - * @copyright 2014 Joseph Rézeau - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_questionnaire\event; -defined('MOODLE_INTERNAL') || die(); - /** * The mod_questionnaire question_deleted event class. * * @package mod_questionnaire - * @since Moodle 2.7 * @copyright 2014 Joseph Rézeau * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - - class question_deleted extends \core\event\base { - /* + /** * Set basic properties for the event. */ - protected function init() { $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_TEACHING; } - /* + /** * Return localised event name. * * @return string @@ -55,7 +41,7 @@ public static function get_name() { return get_string('event_question_deleted', 'mod_questionnaire'); } - /* + /** * Returns description of what happened. * * @return string @@ -65,4 +51,4 @@ public function get_description() { return "The user with id '$this->userid' has deleted a question of type '$questiontype' for the questionnaire with course module id '$this->contextinstanceid'."; } -} \ No newline at end of file +} diff --git a/classes/event/questionnaire_previewed.php b/classes/event/questionnaire_previewed.php index 296b19f2..c4607e81 100644 --- a/classes/event/questionnaire_previewed.php +++ b/classes/event/questionnaire_previewed.php @@ -24,8 +24,6 @@ namespace mod_questionnaire\event; -defined('MOODLE_INTERNAL') || die(); - /** * The mod_questionnaire questionnaire_previewed event class. * diff --git a/classes/event/response_deleted.php b/classes/event/response_deleted.php index 393c62ca..2453579d 100644 --- a/classes/event/response_deleted.php +++ b/classes/event/response_deleted.php @@ -24,8 +24,6 @@ namespace mod_questionnaire\event; -defined('MOODLE_INTERNAL') || die(); - /** * The mod_questionnaire response_deleted event class. * diff --git a/classes/event/response_viewed.php b/classes/event/response_viewed.php index 2ae67a88..404f3955 100644 --- a/classes/event/response_viewed.php +++ b/classes/event/response_viewed.php @@ -24,8 +24,6 @@ namespace mod_questionnaire\event; -defined('MOODLE_INTERNAL') || die(); - /** * The mod_questionnaire response_viewed event class. * @@ -77,14 +75,4 @@ public function get_url() { $params['group'] = $this->other['currentgroupid']; return new \moodle_url("/mod/questionnaire/report.php", $params); } - - /** - * Return the legacy event log data. - * - * @return array - */ - protected function get_legacy_logdata() { - return array($this->courseid, "questionnaire", "view report", "report.php?id=" . $this->contextinstanceid, $this->objectid, - $this->contextinstanceid); - } } diff --git a/classes/feedback/section.php b/classes/feedback/section.php index 2c8c1b4f..e4040702 100644 --- a/classes/feedback/section.php +++ b/classes/feedback/section.php @@ -14,41 +14,46 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Manage feedback sections. - * - * @package mod_questionnaire - * @copyright 2018 onward Mike Churchward (mike.churchward@poetopensource.org) - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_questionnaire\feedback; + defined('MOODLE_INTERNAL') || die(); use invalid_parameter_exception; use coding_exception; +#[\AllowDynamicProperties] /** * Class for describing a feedback section. * - * @author Mike Churchward - * @package feedback + * @package mod_questionnaire + * @copyright 2018 onward Mike Churchward (mike.churchward@poetopensource.org) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - class section { + /** @var int */ public $id = 0; + /** @var int */ public $surveyid = 0; + /** @var int */ public $section = 1; + /** @var array */ public $scorecalculation = []; + /** @var string */ public $sectionlabel = ''; + /** @var string */ public $sectionheading = ''; + /** @var string */ public $sectionheadingformat = FORMAT_HTML; + /** @var array */ public $sectionfeedback = []; + /** @var array */ public $questions = []; + /** The table name. */ const TABLE = 'questionnaire_fb_sections'; + /** Represents the "no score" setting. */ const NOSCORE = -1; /** @@ -58,13 +63,13 @@ class section { * 'surveyid' - the surveyid field of the fb_sections table (required if no 'id' field), * 'sectionnum' - the section field of the fb_sections table (ignored if 'id' is present; defaults to 1). * - * @param array $params As above * @param array $questions Array of mod_questionnaire\question objects. + * @param array $params As above * @throws \dml_exception * @throws coding_exception * @throws invalid_parameter_exception */ - public function __construct($params = [], $questions) { + public function __construct($questions, $params = []) { if (!is_array($params) || !is_array($questions)) { throw new coding_exception('Invalid data provided.'); @@ -80,7 +85,9 @@ public function __construct($params = [], $questions) { /** * Factory method to create a new, empty section and return an instance. - * + * @param int $surveyid + * @param string $sectionlabel + * @return section */ public static function new_section($surveyid, $sectionlabel = '') { global $DB; @@ -93,7 +100,7 @@ public static function new_section($surveyid, $sectionlabel = '') { $newsection->surveyid = $surveyid; $newsection->section = $maxsection + 1; $newsection->sectionlabel = $sectionlabel; - $newsection->scorecalculation = ''; + $newsection->scorecalculation = $newsection->encode_scorecalculation([]); $newsecid = $DB->insert_record(self::TABLE, $newsection); $newsection->id = $newsecid; $newsection->scorecalculation = []; @@ -139,7 +146,7 @@ public function load_section($params) { $this->id = $feedbackrec->id; $this->surveyid = $feedbackrec->surveyid; $this->section = $feedbackrec->section; - $this->scorecalculation = $this->decode_scorecalculation($feedbackrec->scorecalculation); + $this->scorecalculation = $this->get_valid_scorecalculation($feedbackrec->scorecalculation); $this->sectionlabel = $feedbackrec->sectionlabel; $this->sectionheading = $feedbackrec->sectionheading; $this->sectionheadingformat = $feedbackrec->sectionheadingformat; @@ -154,11 +161,8 @@ public function load_section($params) { /** * Loads the section feedback record into the proper array location. * - * @param $feedbackrec + * @param \stdClass $feedbackrec * @return int The id of the section feedback record. - * @throws \dml_exception - * @throws coding_exception - * @throws invalid_parameter_exception */ public function load_sectionfeedback($feedbackrec) { if (!isset($feedbackrec->id) || empty($feedbackrec->id)) { @@ -174,8 +178,7 @@ public function load_sectionfeedback($feedbackrec) { /** * Updates the object and data record with a new scorecalculation. If no new score provided, uses what's in the object. * - * @param $scorecalculation - * @throws \dml_exception + * @param array $scorecalculation * @throws coding_exception */ public function set_new_scorecalculation($scorecalculation = null) { @@ -219,13 +222,14 @@ public function delete() { $DB->delete_records(self::TABLE, ['id' => $this->id]); // Resequence the section numbers as necessary. - $allsections = $DB->get_records(self::TABLE, ['surveyid' => $this->surveyid], 'section ASC'); - $count = 1; - foreach ($allsections as $id => $section) { - if ($section->section != $count) { - $DB->set_field(self::TABLE, 'section', $count, ['id' => $id]); + if ($allsections = $DB->get_records(self::TABLE, ['surveyid' => $this->surveyid], 'section ASC')) { + $count = 1; + foreach ($allsections as $id => $section) { + if ($section->section != $count) { + $DB->set_field(self::TABLE, 'section', $count, ['id' => $id]); + } + $count++; } - $count++; } } @@ -253,7 +257,7 @@ public function update() { $this->scorecalculation = $this->encode_scorecalculation($this->scorecalculation); $DB->update_record(self::TABLE, $this); - $this->scorecalculation = $this->decode_scorecalculation($this->scorecalculation); + $this->scorecalculation = $this->get_valid_scorecalculation($this->scorecalculation); foreach ($this->sectionfeedback as $sectionfeedback) { $sectionfeedback->update(); @@ -261,11 +265,12 @@ public function update() { } /** - * @param string $codedstring - * @return mixed + * Decode and ensure scorecalculation is what we expect. + * @param string|null $codedstring + * @return array * @throws coding_exception */ - protected function decode_scorecalculation($codedstring) { + public static function decode_scorecalculation(?string $codedstring): array { // Expect a serialized data string. if (($codedstring == null)) { $codedstring = ''; @@ -274,11 +279,33 @@ protected function decode_scorecalculation($codedstring) { throw new coding_exception('Invalid scorecalculation format.'); } if (!empty($codedstring)) { - $scorecalculation = unserialize($codedstring); + $scorecalculation = unserialize_array($codedstring) ?: []; } else { $scorecalculation = []; } + if (!is_array($scorecalculation)) { + throw new coding_exception('Invalid scorecalculation format.'); + } + + foreach ($scorecalculation as $score) { + if (!empty($score) && !is_numeric($score)) { + throw new coding_exception('Invalid scorecalculation format.'); + } + } + + return $scorecalculation; + } + + /** + * Return the decoded and validated calculation array. + * @param string $codedstring + * @return mixed + * @throws coding_exception + */ + protected function get_valid_scorecalculation($codedstring) { + $scorecalculation = static::decode_scorecalculation($codedstring); + // Check for deleted questions and questions that don't support scores. foreach ($scorecalculation as $qid => $score) { if (!isset($this->questions[$qid])) { @@ -292,6 +319,7 @@ protected function decode_scorecalculation($codedstring) { } /** + * Return the encoded score array as a serialized string. * @param string $scorearray * @return mixed * @throws coding_exception @@ -306,4 +334,4 @@ protected function encode_scorecalculation($scorearray) { return $scorecalculation; } -} \ No newline at end of file +} diff --git a/classes/feedback/sectionfeedback.php b/classes/feedback/sectionfeedback.php index bc479359..69e56291 100644 --- a/classes/feedback/sectionfeedback.php +++ b/classes/feedback/sectionfeedback.php @@ -14,17 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Manage feedback sections. - * - * @package mod_questionnaire - * @copyright 2018 onward Mike Churchward (mike.churchward@poetopensource.org) - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_questionnaire\feedback; -defined('MOODLE_INTERNAL') || die(); use invalid_parameter_exception; use coding_exception; @@ -32,28 +22,34 @@ /** * Class for describing a feedback section's feedback definition. * - * @author Mike Churchward - * @package feedback + * @package mod_questionnaire + * @copyright 2018 onward Mike Churchward (mike.churchward@poetopensource.org) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - class sectionfeedback { - + /** @var int */ public $id = 0; + /** @var int */ public $sectionid = 0; + /** @var string */ public $feedbacklabel = ''; // I don't think this is actually used? + /** @var string */ public $feedbacktext = ''; + /** @var string */ public $feedbacktextformat = FORMAT_HTML; + /** @var float */ public $minscore = 0.0; + /** @var float */ public $maxscore = 0.0; + /** The table name. */ const TABLE = 'questionnaire_feedback'; /** + * Class constructor. * @param int $id * @param null|object $record - * @throws \dml_exception - * @throws coding_exception - * @throws invalid_parameter_exception */ public function __construct($id = 0, $record = null) { // Return a new section based on the data id. @@ -70,7 +66,8 @@ public function __construct($id = 0, $record = null) { /** * Factory method to create a new sectionfeedback from the provided data and return an instance. - * + * @param \stdClass $data + * @return sectionfeedback */ public static function new_sectionfeedback($data) { global $DB; @@ -99,9 +96,9 @@ public function update() { } /** - * @param $id + * Return the record specified by the id. + * @param int $id * @return mixed - * @throws \dml_exception */ protected function get_sectionfeedback($id) { global $DB; @@ -134,4 +131,4 @@ protected function get_section($id) { return $DB->get_record(self::TABLE, ['id' => $id]); } -} \ No newline at end of file +} diff --git a/classes/feedback_form.php b/classes/feedback_form.php index 4ce52137..25260847 100644 --- a/classes/feedback_form.php +++ b/classes/feedback_form.php @@ -14,6 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->dirroot.'/mod/questionnaire/lib.php'); + /** * Print the form to manage feedback settings. * @@ -22,25 +29,20 @@ * @author Joseph Rezeau * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ - -namespace mod_questionnaire; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->libdir . '/formslib.php'); -require_once($CFG->dirroot.'/mod/questionnaire/lib.php'); - class feedback_form extends \moodleform { + /** + * Defition of the form. + */ public function definition() { global $questionnaire; $mform =& $this->_form; // Questionnaire Feedback Sections and Messages. + $mform->addElement('header', 'submithdr', get_string('feedbackoptions', 'questionnaire')); $feedbackoptions = []; $feedbackoptions[0] = get_string('feedbacknone', 'questionnaire'); - $mform->addElement('header', 'submithdr', get_string('feedbackoptions', 'questionnaire')); $feedbackoptions[1] = get_string('feedbackglobal', 'questionnaire'); $feedbackoptions[2] = get_string('feedbacksections', 'questionnaire'); @@ -130,8 +132,14 @@ public function definition() { } } + /** + * Validate the data submitted. + * @param array $data + * @param array $files + * @return array + */ public function validation($data, $files) { $errors = parent::validation($data, $files); return $errors; } -} \ No newline at end of file +} diff --git a/classes/feedback_section_form.php b/classes/feedback_section_form.php index 0f8fe73b..e566d04c 100644 --- a/classes/feedback_section_form.php +++ b/classes/feedback_section_form.php @@ -14,6 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->dirroot.'/mod/questionnaire/lib.php'); + /** * Print the form to add or edit a questionnaire-instance * @@ -22,18 +29,18 @@ * @author Joseph Rezeau (based on Quiz by Tim Hunt) * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ - -namespace mod_questionnaire; - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->libdir . '/formslib.php'); -require_once($CFG->dirroot.'/mod/questionnaire/lib.php'); - class feedback_section_form extends \moodleform { + /** @var mixed $_feedbacks */ protected $_feedbacks; + /** + * @var \context $context The used context. + */ + public $context; + /** + * Form definition. + */ public function definition() { global $questionnaire; @@ -175,6 +182,10 @@ public function definition() { $mform->closeHeaderBefore('buttonar'); } + /** + * Form preprocessing. + * @param array $toform + */ public function data_preprocessing(&$toform) { if (count($this->_feedbacks)) { $key = 0; @@ -200,6 +211,13 @@ public function data_preprocessing(&$toform) { } } } + + /** + * Form validation. + * @param array $data + * @param array $files + * @return array + */ public function validation($data, $files) { $errors = parent::validation($data, $files); @@ -250,7 +268,7 @@ public function validation($data, $files) { * form definition (new entry form); this function is used to load in data where values * already exist and data is being edited (edit entry form). * - * @param mixed $default_values object or array of default values + * @param array $defaultvalues */ public function set_data($defaultvalues) { if (is_object($defaultvalues)) { @@ -259,4 +277,4 @@ public function set_data($defaultvalues) { $this->data_preprocessing($defaultvalues); parent::set_data($defaultvalues); } -} \ No newline at end of file +} diff --git a/classes/file_storage.php b/classes/file_storage.php new file mode 100644 index 00000000..329e9dff --- /dev/null +++ b/classes/file_storage.php @@ -0,0 +1,60 @@ +. + +namespace mod_questionnaire; + +/** + * Defines the file stoeage class for questionnaire. + * @package mod_questionnaire + * @copyright 2020 onwards Mike Churchward (mike.churchward@poetopensource.org) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class file_storage extends \file_storage { + + /** + * Copy all the files in a file area from one context to another. + * + * @param int $oldcontextid the context the files are being moved from. + * @param int $newcontextid the context the files are being moved to. + * @param string $component the plugin that these files belong to. + * @param string $filearea the name of the file area. + * @param int|boolean $olditemid The identifier for the old file area if required. + * @param int|boolean $newitemid The identifier for the new file area if different than old. + * @return int the number of files copied, for information. + * @throws \coding_exception + * @throws \file_exception + * @throws \stored_file_creation_exception + */ + public function copy_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $olditemid = false, + $newitemid = false) { + $count = 0; + + $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $olditemid, 'id', false); + foreach ($oldfiles as $oldfile) { + $filerecord = new \stdClass(); + $filerecord->contextid = $newcontextid; + if ($newitemid !== false) { + $filerecord->itemid = $newitemid; + } else { + $filerecord->itemid = $olditemid; + } + $this->create_file_from_storedfile($filerecord, $oldfile); + $count += 1; + } + return $count; + } +} diff --git a/classes/generator/question_response.php b/classes/generator/question_response.php index 77050825..fc4c7221 100644 --- a/classes/generator/question_response.php +++ b/classes/generator/question_response.php @@ -14,22 +14,30 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\generator; + +use mod_questionnaire\responsetype\response\response; + /** * Question response class * @author gthomas2 + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package mod_questionnaire */ - -namespace mod_questionnaire\generator; - -defined('MOODLE_INTERNAL') || die(); - class question_response { + /** @var int $questionid */ public $questionid; + /** @var response $response */ public $response; + /** + * Class constructor. + * @param int $questionid + * @param response $response + */ public function __construct($questionid, $response) { $this->questionid = $questionid; $this->response = $response; } -} \ No newline at end of file +} diff --git a/classes/generator/question_response_rank.php b/classes/generator/question_response_rank.php index 65edd13f..4ff25a7e 100644 --- a/classes/generator/question_response_rank.php +++ b/classes/generator/question_response_rank.php @@ -14,22 +14,30 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\generator; + +use mod_questionnaire\question\choice; + /** * Question response rank class * @author gthomas2 + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package mod_questionnaire */ - -namespace mod_questionnaire\generator; - -defined('MOODLE_INTERNAL') || die(); - class question_response_rank { + /** @var choice $choice */ public $choice; + /** @var int $rankvalue */ public $rankvalue; + /** + * Class constructor. + * @param choice $choice + * @param int $rank + */ public function __construct($choice, $rank) { $this->choice = $choice; $this->rankvalue = $rank; } -} \ No newline at end of file +} diff --git a/classes/output/completepage.php b/classes/output/completepage.php index d3edd3db..03192113 100644 --- a/classes/output/completepage.php +++ b/classes/output/completepage.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\output; + /** * Contains class mod_questionnaire\output\viewpage * @@ -22,11 +24,6 @@ * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -namespace mod_questionnaire\output; - -defined('MOODLE_INTERNAL') || die(); - class completepage implements \renderable, \templatable { /** @@ -49,8 +46,8 @@ public function __construct($data = null) { /** * Add data for export. - * @param string The index for the data. - * @param string The content for the index. + * @param string $element The index for the data. + * @param string $content The content for the index. */ public function add_to_page($element, $content) { if ($element !== 'questions') { @@ -68,4 +65,4 @@ public function export_for_template(\renderer_base $output) { return $this->data; } -} \ No newline at end of file +} diff --git a/classes/output/fbsectionspage.php b/classes/output/fbsectionspage.php index ccacc497..2c3b0cb1 100644 --- a/classes/output/fbsectionspage.php +++ b/classes/output/fbsectionspage.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\output; + /** * Contains class mod_questionnaire\output\fbsectionspage * @@ -22,11 +24,6 @@ * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -namespace mod_questionnaire\output; - -defined('MOODLE_INTERNAL') || die(); - class fbsectionspage implements \renderable, \templatable { /** @@ -49,8 +46,8 @@ public function __construct($data = null) { /** * Add data for export. - * @param string The index for the data. - * @param string The content for the index. + * @param string $element The index for the data. + * @param string $content The content for the index. */ public function add_to_page($element, $content) { $this->data->{$element} = empty($this->data->{$element}) ? $content : ($this->data->{$element} . $content); @@ -64,4 +61,4 @@ public function export_for_template(\renderer_base $output) { return $this->data; } -} \ No newline at end of file +} diff --git a/classes/output/feedbackpage.php b/classes/output/feedbackpage.php index 5d464553..21eed2c3 100644 --- a/classes/output/feedbackpage.php +++ b/classes/output/feedbackpage.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\output; + /** * Contains class mod_questionnaire\output\feedback * @@ -22,11 +24,6 @@ * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -namespace mod_questionnaire\output; - -defined('MOODLE_INTERNAL') || die(); - class feedbackpage implements \renderable, \templatable { /** @@ -49,8 +46,8 @@ public function __construct($data = null) { /** * Add data for export. - * @param string The index for the data. - * @param string The content for the index. + * @param string $element The index for the data. + * @param string $content The content for the index. */ public function add_to_page($element, $content) { $this->data->{$element} = empty($this->data->{$element}) ? $content : ($this->data->{$element} . $content); @@ -64,4 +61,4 @@ public function export_for_template(\renderer_base $output) { return $this->data; } -} \ No newline at end of file +} diff --git a/classes/output/mobile.php b/classes/output/mobile.php index 7ec783d1..0e3d0d28 100644 --- a/classes/output/mobile.php +++ b/classes/output/mobile.php @@ -14,16 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\output; + +use mod_questionnaire\responsetype\response\response; + /** * Mobile output class for mod_questionnaire. * - * @copyright 2018 Igor Sazonov - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package mod_questionnaire + * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -namespace mod_questionnaire\output; - -defined('MOODLE_INTERNAL') || die(); - class mobile { /** @@ -33,261 +35,162 @@ class mobile { * @return array HTML, javascript and other data */ public static function mobile_view_activity($args) { - global $OUTPUT, $USER, $CFG, $DB, $SESSION; - + global $OUTPUT, $USER, $CFG, $DB; require_once($CFG->dirroot.'/mod/questionnaire/questionnaire.class.php'); + $args = (object) $args; + + $versionname = $args->appversioncode >= 44000 ? 'latest' : 'ionic5'; $cmid = $args->cmid; + $rid = isset($args->rid) ? $args->rid : 0; + $action = isset($args->action) ? $args->action : 'index'; $pagenum = (isset($args->pagenum) && !empty($args->pagenum)) ? intval($args->pagenum) : 1; - $prevpage = 0; - if (!empty($SESSION->prevpage)) { - $prevpage = $SESSION->prevpage; - if (!$prevpage) { - $SESSION->prevpage = $pagenum; - if ($pagenum == 1) { - $prevpage = 0; - } else { - $prevpage = $pagenum; - } - } - } + $userid = isset($args->userid) ? $args->userid : $USER->id; + $submit = isset($args->submit) ? $args->submit : false; + $completed = isset($args->completed) ? $args->completed : false; + + list($cm, $course, $questionnaire) = questionnaire_get_standard_page_items($cmid); + $questionnaire = new \questionnaire($course, $cm, 0, $questionnaire); + + $data = []; + $data['cmid'] = $cmid; + $data['userid'] = $userid; + $data['intro'] = $questionnaire->intro; + $data['autonumquestions'] = $questionnaire->autonum; + $data['id'] = $questionnaire->id; + $data['rid'] = $rid; + $data['surveyid'] = $questionnaire->survey->id; + $data['pagenum'] = $pagenum; + $data['prevpage'] = 0; + $data['nextpage'] = 0; + // Capabilities check. - $cm = get_coursemodule_from_id('questionnaire', $cmid); $context = \context_module::instance($cmid); self::require_capability($cm, $context, 'mod/questionnaire:view'); - // Set some variables we are going to be using. - $questionnaire = get_questionnaire_data($cmid, $USER->id); - if (isset($questionnaire['questions'][$pagenum - 1]) && !empty($questionnaire['questions'][$pagenum - 1])) { - $prevpage = $pagenum - 1; - } - $data = [ - 'questionnaire' => $questionnaire, - 'cmid' => $cmid, - 'courseid' => intval($cm->course), - 'pagenum' => $pagenum, - 'userid' => $USER->id, - 'nextpage' => 0, - 'prevpage' => 0, - 'emptypage' => false - ]; - $pagebreaks = false; - $branching = check_mobile_branching_logic($questionnaire); - if ($branching) { - $pagebreaks = true; - } - $break = false; - // Checking for completion below, cmid is a complettion variable. - // Checks whether a questionnaire was touched, so created a proper completed check coming from the DB. - if ($cmid) { - $data['completed'] = (isset($questionnaire['response']['complete']) - && $questionnaire['response']['complete'] == 'y') ? 1 : 0; - $data['complete_userdate'] = (isset($questionnaire['response']['complete']) - && $questionnaire['response']['complete'] == 'y') ? - userdate($questionnaire['response']['submitted']) : ''; - if (isset($questionnaire['questions'][$pagenum]) && $branching == false) { - $i = 0; - foreach ($questionnaire['questions'][$pagenum] as $questionid => $choices) { - if (isset($questionnaire['questionsinfo'][$pagenum][$questionid]) - && !empty($questionnaire['questionsinfo'][$pagenum][$questionid])) { - $data['questions'][$pagenum][$i]['info'] = $questionnaire['questionsinfo'][$pagenum][$questionid]; - if ($data['questions'][$pagenum][$i]['info']['required'] == 'n') { - unset($data['questions'][$pagenum][$i]['info']['required']); - } - $ii = 0; - foreach ($choices as $k => $v) { - $data['questions'][$pagenum][$i]['choices'][$ii] = (array) $v; - $ii++; - } - if (count($choices) == 1) { - $data['questions'][$pagenum][$i]['value'] = $data['questions'][$pagenum][$i]['choices'][0]['value']; - } - $i++; - } + // Any notifications will be displayed on top of main page, and prevent questionnaire from being completed. This also checks + // appropriate capabilities. + $data['notifications'] = $questionnaire->user_access_messages($userid); + $responses = []; + $result = ''; + + $data['emptypage'] = 1; + $template = "mod_questionnaire/local/mobile/$versionname/main_index_page"; + + switch ($action) { + case 'index': + self::add_index_data($questionnaire, $data, $userid); + $template = "mod_questionnaire/local/mobile/$versionname/main_index_page"; + break; + + case 'submit': + case 'nextpage': + case 'previouspage': + if (!$data['notifications']) { + $result = $questionnaire->save_mobile_data($userid, $pagenum, $completed, $rid, $submit, $action, (array)$args); } - if (isset($data['questions'][$pagenum]) && !empty($data['questions'][$pagenum])) { - $i = 0; - foreach ($data['questions'][$pagenum] as $arr) { - $data['pagequestions'][$i] = $arr; - $i++; + + case 'respond': + case 'resume': + // Completing a questionnaire. + if (!$data['notifications']) { + if ($questionnaire->user_has_saved_response($userid)) { + if (empty($rid)) { + $rid = $questionnaire->get_latest_responseid($userid); + } + $questionnaire->add_response($rid); + $data['rid'] = $rid; } - } - if (isset($questionnaire['questions'][$pagenum + 1]) && !empty($questionnaire['questions'][$pagenum + 1])) { - $data['nextpage'] = $pagenum + 1; - } - if ($prevpage) { - $data['prevpage'] = $prevpage; - } - } else if (isset($questionnaire['questions'][$pagenum]) && $branching == true - && $questionnaire['completed'] == false ) { - $i = 0; - foreach ($questionnaire['questions'][$pagenum] as $questionid => $choices) { - if (isset($questionnaire['questionsinfo'][$pagenum][$questionid]) && - !empty($questionnaire['questionsinfo'][$pagenum][$questionid])) { - $data['questions'][$pagenum][$i]['info'] = $questionnaire['questionsinfo'][$pagenum][$questionid]; - if ($data['questions'][$pagenum][$i]['info']['required'] == 'n') { - unset($data['questions'][$pagenum][$i]['info']['required']); + $response = (isset($questionnaire->responses) && !empty($questionnaire->responses)) ? + end($questionnaire->responses) : \mod_questionnaire\responsetype\response\response::create_from_data([]); + $response->sec = $pagenum; + if (isset($result['warnings'])) { + if ($action == 'submit') { + $response = $result['response']; } - $ii = 0; - foreach ($choices as $k => $v) { - $data['questions'][$pagenum][$i]['choices'][$ii] = (array) $v; - $ii++; + $data['notifications'] = $result['warnings']; + } else if ($action == 'nextpage') { + $pageresult = $result['nextpagenum']; + if ($pageresult === false) { + $pagenum = count($questionnaire->questionsbysec); + } else if (is_string($pageresult)) { + $data['notifications'] .= !empty($data['notifications']) ? "\n
$pageresult" : $pageresult; + } else { + $pagenum = $pageresult; } - if (count($choices) == 1) { - $data['questions'][$pagenum][$i]['value'] = $data['questions'][$pagenum][$i]['choices'][0]['value']; + } else if ($action == 'previouspage') { + $prevpage = $result['nextpagenum']; + if ($prevpage === false) { + $pagenum = 1; + } else { + $pagenum = $prevpage; } - $i++; - } - } - if (isset($data['questions'][$pagenum]) && !empty($data['questions'][$pagenum])) { - $i = 0; - foreach ($data['questions'][$pagenum] as $arr) { - $data['pagequestions'][$i] = $arr; - $i++; + } else if ($action == 'submit') { + self::add_index_data($questionnaire, $data, $userid); + $data['action'] = 'index'; + $template = "mod_questionnaire/local/mobile/$versionname/main_index_page"; + break; } - } - if (isset($questionnaire['questions'][$pagenum + 1]) && !empty($questionnaire['questions'][$pagenum + 1])) { - $data['nextpage'] = $pagenum + 1; - } - if ($prevpage) { - $data['prevpage'] = $prevpage; - } - } - - if ($questionnaire['completed'] == true) { - // Branching specific logic. - // If we are branching and the questionnaire is complete, display all the responses on one page. - $pagecounter = 1; - foreach ($questionnaire['questions'] as $question) { - $i = 0; - foreach ($questionnaire['questions'][$pagecounter] as $questionid => $choices) { - if (isset($questionnaire['questionsinfo'][$pagecounter][$questionid]) && - !empty($questionnaire['questionsinfo'][$pagecounter][$questionid])) { - $data['questions'][$pagecounter][$i]['info'] - = $questionnaire['questionsinfo'][$pagecounter][$questionid]; - if ($data['questions'][$pagecounter][$i]['info']['required'] == 'n') { - unset($data['questions'][$pagecounter][$i]['info']['required']); - } - $ii = 0; - foreach ($choices as $k => $v) { - $data['questions'][$pagecounter][$i]['choices'][$ii] = (array) $v; - $ii++; - } - if (count($choices) == 1) { - $data['questions'][$pagecounter][$i]['value'] - = $data['questions'][$pagecounter][$i]['choices'][0]['value']; - } - $i++; + $pagequestiondata = self::add_pagequestion_data($questionnaire, $pagenum, $response); + $data['pagequestions'] = $pagequestiondata['pagequestions']; + $responses = $pagequestiondata['responses']; + $numpages = count($questionnaire->questionsbysec); + // Set some variables we are going to be using. + if (!empty($questionnaire->questionsbysec) && ($numpages > 1)) { + if ($pagenum > 1) { + $data['prevpage'] = true; } - if ($pagecounter > count($questionnaire['questions'])) { - break; + if ($pagenum < $numpages) { + $data['nextpage'] = true; } } - $pagecounter++; - $x = 0; - $questioncounter = 1; - foreach ($data['questions'] as $dataq) { - foreach ($dataq as $arr) { - $data['pagequestions'][$x] = $arr; - $x++; - if ($questioncounter >= count($questionnaire['questions'])) { - break; - } - } - $questioncounter++; - } + $data['pagenum'] = $pagenum; + $data['completed'] = 0; + $data['emptypage'] = 0; + $template = "mod_questionnaire/local/mobile/$versionname/view_activity_page"; } + break; - $data['prevpage'] = 0; - $data['nextpage'] = 0; - $pagebreaks = false; - } - } else { - $data['emptypage'] = true; - $data['emptypage_content'] = get_string('questionnaire:submit', 'questionnaire'); - } - // Let each pagequestions know it's current required step, and fill up the final required step. - // Logic states that we get all the required steps and give them an counter. - // We get the final required count and check it againts the input once it's sent to a js file. - // If its the final required count we display the button. - $currentrequiredresponse = 0; - $counter = 0; - $multichoiceflag = false; - $completedchoices = 0; - $finalpagerequired = false; - $completeddisabledflag = false; - foreach ($data['pagequestions'] as &$pagequestion) { - if ($pagequestion['info']['required'] == 'y') { - if (!empty($pagequestion['choices']) && $pagequestion['info']['response_table'] == 'response_rank') { - foreach ($pagequestion['choices'] as &$choice) { - if (empty($choice['value'])) { - if ($currentrequiredresponse > 0 && empty($counter)) { - $counter = $currentrequiredresponse; + case 'review': + // If reviewing a submission. + if ($questionnaire->capabilities->readownresponses && isset($args->submissionid) && !empty($args->submissionid)) { + $questionnaire->add_response($args->submissionid); + $response = $questionnaire->responses[$args->submissionid]; + $qnum = 1; + $pagequestions = []; + foreach ($questionnaire->questions as $question) { + if ($question->supports_mobile()) { + $pagequestions[] = $question->mobile_question_display($qnum, $questionnaire->autonum); + $responses = array_merge($responses, $question->get_mobile_response_data($response)); + if ($question->is_numbered()) { + $qnum++; } - $counter++; - $choice['current_required_resp'] = $counter; - } else { - $completedchoices++; - $completeddisabledflag = true; } } - $currentrequiredresponse = $counter; - } else { - $currentrequiredresponse++; - $pagequestion['info']['current_required_resp'] = $currentrequiredresponse; + $data['prevpage'] = 0; + $data['nextpage'] = 0; + $data['pagequestions'] = $pagequestions; + $data['completed'] = 1; + $data['emptypage'] = 0; + $template = "mod_questionnaire/local/mobile/$versionname/view_activity_page"; } - if ($pagequestion['info']['qnum'] === count($data['pagequestions'])) { - $finalpagerequired = true; - } - } - } - - $disablesavebutton = true; - if ($completedchoices == $currentrequiredresponse && !$finalpagerequired) { - $disablesavebutton = false; - } else if ($completeddisabledflag) { - $disablesavebutton = false; - } else { - $disablesavebutton = true; + break; } - // Let each pagequestions know what the final required field is. - foreach ($data['pagequestions'] as &$pagequestion) { - $pagequestion['info']['final_required_resp'] = $currentrequiredresponse - $completedchoices; - } - - $mobileviewactivity = 'mod_questionnaire/mobile_view_activity_page'; - if ($branching) { - $mobileviewactivity = 'mod_questionnaire/mobile_view_activity_branching_page'; - } + $data['hasmorepages'] = $data['prevpage'] || $data['nextpage']; - $data['pagebreak'] = true; - - return [ + $return = [ 'templates' => [ [ 'id' => 'main', - 'html' => $OUTPUT->render_from_template($mobileviewactivity, $data) + 'html' => $OUTPUT->render_from_template($template, $data) ], ], - 'javascript' => file_get_contents($CFG->dirroot . '/mod/questionnaire/javascript/mobile_questionnaire.js'), - 'otherdata' => [ - 'fields' => json_encode($questionnaire['fields']), - 'questionsinfo' => json_encode($questionnaire['questionsinfo']), - 'questions' => json_encode($questionnaire['questions']), - 'pagequestions' => json_encode($data['pagequestions']), - 'responses' => json_encode($questionnaire['responses']), - 'pagenum' => $pagenum, - 'nextpage' => $data['nextpage'], - 'prevpage' => $data['prevpage'], - 'completed' => $data['completed'], - 'intro' => $questionnaire['questionnaire']['intro'], - 'string_required' => get_string('required'), - 'string_dropdown' => get_string('selectdropdowntext', 'mod_questionnaire'), - 'disable_save' => $disablesavebutton, - ], + 'javascript' => file_get_contents($CFG->dirroot . '/mod/questionnaire/appjs/uncheckother.js'), + 'otherdata' => $responses, 'files' => null ]; + return $return; } /** @@ -302,169 +205,74 @@ protected static function require_capability(\stdClass $cm, \context $context, s require_capability($cap, $context); } - public static function mobile_view_activity_branching($args) { - global $OUTPUT, $USER, $CFG, $DB, $SESSION; - - require_once($CFG->dirroot.'/mod/questionnaire/questionnaire.class.php'); - require_once($CFG->dirroot . '/mod/questionnaire/lib.php'); - $args = (object) $args; - $cmid = $args->cmid; - $pagenum = (isset($args->pagenum) && !empty($args->pagenum)) ? intval($args->pagenum) : 1; - $prevpage = 0; - if (!empty($SESSION->prevpage)) { - $prevpage = $SESSION->prevpage; - if (!$prevpage) { - $SESSION->prevpage = $pagenum; - if ($pagenum == 1) { - $prevpage = 0; - } else { - $prevpage = $pagenum; - } + /** + * Add the submissions. + * @param \questionnaire $questionnaire + * @param array $data + * @param int $userid + */ + protected static function add_index_data($questionnaire, &$data, $userid) { + // List any existing submissions, if user is allowed to review them. + if ($questionnaire->capabilities->readownresponses) { + $questionnaire->add_user_responses(); + $submissions = []; + foreach ($questionnaire->responses as $response) { + $submissions[] = ['submissiondate' => userdate($response->submitted), 'submissionid' => $response->id]; } + if (!empty($submissions)) { + $data['submissions'] = $submissions; + } else { + $data['emptypage'] = 1; + } + if ($questionnaire->user_has_saved_response($userid)) { + $data['resume'] = 1; + } + $data['emptypage'] = 0; } - $quesitonnaireresponses = (!empty($args->responses)) ? $args->responses : []; - $branching = (isset($args->branching) && !empty($args->branching)) ? intval($args->branching) : 0; - // Capabilities check. - $cm = get_coursemodule_from_id('questionnaire', $cmid); - $context = \context_module::instance($cmid); - self::require_capability($cm, $context, 'mod/questionnaire:view'); - // Set some variables we are going to be using. - $questionnaire = get_questionnaire_data($cmid, $USER->id); - if (isset($questionnaire['questions'][$pagenum - 1]) && !empty($questionnaire['questions'][$pagenum - 1])) { - $prevpage = $pagenum - 1; - } + } - $branching = check_mobile_branching_logic($questionnaire); - $pagenum = get_mobile_questionnaire($questionnaire, $pagenum, $branching); - $newpagenum = $pagenum['pagenum']; - $newprevpagenum = $prevpage; - $newnextpagenum = $pagenum['nextpage']; - $pagenum = $newpagenum; + /** + * Ass the questions for the page. + * @param \questionnaire $questionnaire + * @param int $pagenum + * @param response $response + * @return array + */ + protected static function add_pagequestion_data($questionnaire, $pagenum, $response=null) { + $qnum = 1; + $pagequestions = []; + $responses = []; - $data = [ - 'questionnaire' => $questionnaire, - 'cmid' => $cmid, - 'courseid' => intval($cm->course), - 'pagenum' => $pagenum, - 'userid' => $USER->id, - 'nextpage' => 0, - 'prevpage' => 0, - 'emptypage' => false - ]; - $data['completed'] = (isset($questionnaire['response']['complete']) - && $questionnaire['response']['complete'] == 'y') ? 1 : 0; - $data['complete_userdate'] = (isset($questionnaire['response']['complete']) - && $questionnaire['response']['complete'] == 'y') ? - userdate($questionnaire['response']['submitted']) : ''; - if (isset($questionnaire['questions'][$pagenum])) { - $i = 0; - foreach ($questionnaire['questions'][$pagenum] as $questionid => $choices) { - if (isset($questionnaire['questionsinfo'][$pagenum][$questionid]) - && !empty($questionnaire['questionsinfo'][$pagenum][$questionid])) { - $data['questions'][$pagenum][$i]['info'] = $questionnaire['questionsinfo'][$pagenum][$questionid]; - if ($data['questions'][$pagenum][$i]['info']['required'] == 'n') { - unset($data['questions'][$pagenum][$i]['info']['required']); - } - $ii = 0; - foreach ($choices as $k => $v) { - $data['questions'][$pagenum][$i]['choices'][$ii] = (array) $v; - $ii++; - } - if (count($choices) == 1) { - $data['questions'][$pagenum][$i]['value'] = $data['questions'][$pagenum][$i]['choices'][0]['value']; + // Find out what question number we are on $i New fix for question numbering. + $i = 0; + if ($pagenum > 1) { + for ($j = 2; $j <= $pagenum; $j++) { + foreach ($questionnaire->questionsbysec[$j - 1] as $questionid) { + if ($questionnaire->questions[$questionid]->type_id < QUESPAGEBREAK) { + $i++; } - $i++; } } - if (isset($data['questions'][$pagenum]) && !empty($data['questions'][$pagenum])) { - $i = 0; - foreach ($data['questions'][$pagenum] as $arr) { - $data['pagequestions'][$i] = $arr; - $i++; - } - } - if (isset($questionnaire['questions'][$pagenum + 1]) && !empty($questionnaire['questions'][$pagenum + 1])) { - $data['nextpage'] = $pagenum + 1; - } - if ($prevpage) { - $data['prevpage'] = $prevpage; - } } - // Let each pagequestions know it's current required step, and fill up the final required step. - // Logic states that we get all the required steps and give them an counter. - // We get the final required count and check it againts the input once it's sent to a js file. - // If its the final required count we display the button. - $currentrequiredresponse = 0; - $counter = 0; - $multichoiceflag = false; - $completedchoices = 0; - $finalpagerequired = false; - $completeddisabledflag = false; - foreach ($data['pagequestions'] as &$pagequestion) { - if ($pagequestion['info']['required'] == 'y') { - if (!empty($pagequestion['choices']) && $pagequestion['info']['response_table'] == 'response_rank') { - foreach ($pagequestion['choices'] as &$choice) { - if (empty($choice['value'])) { - if ($currentrequiredresponse > 0 && empty($counter)) { - $counter = $currentrequiredresponse; - } - $counter++; - $choice['current_required_resp'] = $counter; - } else { - $completedchoices++; - $completeddisabledflag = true; - } - } - $currentrequiredresponse = $counter; - } else { - $currentrequiredresponse++; - $pagequestion['info']['current_required_resp'] = $currentrequiredresponse; + $qnum = $i + 1; + + foreach ($questionnaire->questionsbysec[$pagenum] as $questionid) { + $question = $questionnaire->questions[$questionid]; + if ($question->supports_mobile()) { + $pagequestions[] = $question->mobile_question_display($qnum, $questionnaire->autonum); + $mobileotherdata = $question->mobile_otherdata(); + if (!empty($mobileotherdata)) { + $responses = array_merge($responses, $mobileotherdata); } - if ($pagequestion['info']['qnum'] === count($data['pagequestions'])) { - $finalpagerequired = true; + if (($response !== null) && isset($response->answers[$questionid])) { + $responses = array_merge($responses, $question->get_mobile_response_data($response)); + } + if ($question->is_numbered()) { + $qnum++; } } } - $disablesavebutton = true; - if ($completedchoices == $currentrequiredresponse && !$finalpagerequired) { - $disablesavebutton = false; - } else if ($completeddisabledflag) { - $disablesavebutton = false; - } else { - $disablesavebutton = true; - } - - // Let each pagequestions know what the final required field is. - foreach ($data['pagequestions'] as &$pagequestion) { - $pagequestion['info']['final_required_resp'] = $currentrequiredresponse; - } - $data['pagebreak'] = true; // Branching logic is always true. - - return [ - 'templates' => [ - [ - 'id' => 'main', - 'html' => $OUTPUT->render_from_template('mod_questionnaire/mobile_view_activity_branching_page', $data) - ], - ], - 'javascript' => file_get_contents($CFG->dirroot . '/mod/questionnaire/javascript/mobile_questionnaire.js'), - 'otherdata' => [ - 'fields' => json_encode($questionnaire['fields']), - 'questionsinfo' => json_encode($questionnaire['questionsinfo']), - 'questions' => json_encode($questionnaire['questions']), - 'pagequestions' => json_encode($data['pagequestions']), - 'responses' => json_encode($questionnaire['responses']), - 'pagenum' => $pagenum, - 'nextpage' => $newnextpagenum, - 'prevpage' => $newprevpagenum, - 'completed' => $data['completed'], - 'intro' => $questionnaire['questionnaire']['intro'], - 'string_required' => get_string('required'), - 'string_dropdown' => get_string('selectdropdowntext', 'mod_questionnaire'), - 'disable_save' => $disablesavebutton, - ], - 'files' => null - ]; + return ['pagequestions' => $pagequestions, 'responses' => $responses]; } -} \ No newline at end of file +} diff --git a/classes/output/nonrespondentspage.php b/classes/output/nonrespondentspage.php index 2f8b6312..4771ebf0 100644 --- a/classes/output/nonrespondentspage.php +++ b/classes/output/nonrespondentspage.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\output; + /** * Contains class mod_questionnaire\output\viewpage * @@ -22,11 +24,6 @@ * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -namespace mod_questionnaire\output; - -defined('MOODLE_INTERNAL') || die(); - class nonrespondentspage implements \renderable, \templatable { /** @@ -49,8 +46,8 @@ public function __construct($data = null) { /** * Add data for export. - * @param string The index for the data. - * @param string The content for the index. + * @param string $element The index for the data. + * @param string $content The content for the index. */ public function add_to_page($element, $content) { $this->data->{$element} = empty($this->data->{$element}) ? $content : ($this->data->{$element} . $content); @@ -64,4 +61,4 @@ public function export_for_template(\renderer_base $output) { return $this->data; } -} \ No newline at end of file +} diff --git a/classes/output/previewpage.php b/classes/output/previewpage.php index af2c5ba9..82267de3 100644 --- a/classes/output/previewpage.php +++ b/classes/output/previewpage.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\output; + /** * Contains class mod_questionnaire\output\viewpage * @@ -22,11 +24,6 @@ * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -namespace mod_questionnaire\output; - -defined('MOODLE_INTERNAL') || die(); - class previewpage implements \renderable, \templatable { /** @@ -49,8 +46,8 @@ public function __construct($data = null) { /** * Add data for export. - * @param string The index for the data. - * @param string The content for the index. + * @param string $element The index for the data. + * @param string $content The content for the index. */ public function add_to_page($element, $content) { if ($element === 'questions') { @@ -68,4 +65,4 @@ public function export_for_template(\renderer_base $output) { return $this->data; } -} \ No newline at end of file +} diff --git a/classes/output/qsettingspage.php b/classes/output/qsettingspage.php index 7b4be1ad..311a18d1 100644 --- a/classes/output/qsettingspage.php +++ b/classes/output/qsettingspage.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\output; + /** * Contains class mod_questionnaire\output\viewpage * @@ -22,11 +24,6 @@ * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -namespace mod_questionnaire\output; - -defined('MOODLE_INTERNAL') || die(); - class qsettingspage implements \renderable, \templatable { /** @@ -49,8 +46,8 @@ public function __construct($data = null) { /** * Add data for export. - * @param string The index for the data. - * @param string The content for the index. + * @param string $element The index for the data. + * @param string $content The content for the index. */ public function add_to_page($element, $content) { $this->data->{$element} = empty($this->data->{$element}) ? $content : ($this->data->{$element} . $content); @@ -64,4 +61,4 @@ public function export_for_template(\renderer_base $output) { return $this->data; } -} \ No newline at end of file +} diff --git a/classes/output/questionspage.php b/classes/output/questionspage.php index 2624687c..09ea8dd7 100644 --- a/classes/output/questionspage.php +++ b/classes/output/questionspage.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\output; + /** * Contains class mod_questionnaire\output\viewpage * @@ -22,11 +24,6 @@ * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -namespace mod_questionnaire\output; - -defined('MOODLE_INTERNAL') || die(); - class questionspage implements \renderable, \templatable { /** @@ -49,8 +46,8 @@ public function __construct($data = null) { /** * Add data for export. - * @param string The index for the data. - * @param string The content for the index. + * @param string $element The index for the data. + * @param string $content The content for the index. */ public function add_to_page($element, $content) { $this->data->{$element} = empty($this->data->{$element}) ? $content : ($this->data->{$element} . $content); @@ -63,4 +60,4 @@ public function add_to_page($element, $content) { public function export_for_template(\renderer_base $output) { return $this->data; } -} \ No newline at end of file +} diff --git a/classes/output/renderer.php b/classes/output/renderer.php old mode 100644 new mode 100755 index 1dbbc99c..456370e3 --- a/classes/output/renderer.php +++ b/classes/output/renderer.php @@ -14,6 +14,10 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\output; + +use mod_questionnaire\question\question; + /** * Contains class mod_questionnaire\output\renderer * @@ -22,11 +26,6 @@ * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -namespace mod_questionnaire\output; - -defined('MOODLE_INTERNAL') || die(); - class renderer extends \plugin_renderer_base { /** * Main view page. @@ -58,6 +57,16 @@ public function render_reportpage($page) { return $this->render_from_template('mod_questionnaire/reportpage', $data); } + /** + * Fill out the PDF report page. + * @param \templateable $page + * @return string | boolean + */ + public function render_reportpagepdf($page) { + $data = $page->export_for_template($this); + return $this->render_from_template('mod_questionnaire/reportpagepdf', $data); + } + /** * Fill out the qsettings page. * @param \templateable $page @@ -138,6 +147,7 @@ public function complete_formstart($action, $hiddeninputs=[]) { foreach ($hiddeninputs as $name => $value) { $output .= \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => $name, 'value' => $value]) . "\n"; } + $this->page->requires->js_init_call('M.mod_questionnaire.init_attempt_form', null, false, questionnaire_get_js_module()); return $output; } @@ -157,14 +167,14 @@ public function complete_formend($inputs=[]) { /** * Render the completion form control buttons. - * @param array | string $inputs Name/(Type/attribute) array of input types and values used by the form. + * @param array|string $inputs Name/(Type/attribute) array of input types and values used by the form. * @return string The output for the page. */ public function complete_controlbuttons($inputs=null) { $output = ''; if (is_array($inputs)) { foreach ($inputs as $name => $attributes) { - $output .= \html_writer::empty_tag('input', array_merge(['name' => $name], $attributes)); + $output .= \html_writer::empty_tag('input', array_merge(['name' => $name], $attributes)) . ' '; } } else if (is_string($inputs)) { $output .= \html_writer::tag('p', $inputs); @@ -172,18 +182,51 @@ public function complete_controlbuttons($inputs=null) { return $output; } + /** + * Calculate the progress and return it. + * @param int $section + * @param array $questionsbysec + * @return float + */ + private function calculate_progress($section, $questionsbysec) { + $done = 0; + $todo = 0; + for ($i = 1; $i <= count($questionsbysec); $i++) { + if ($i < $section) { + $done += count($questionsbysec[$i]); + } else { + $todo += count($questionsbysec[$i]); + } + } + + return round($done / ($done + $todo) * 100); + } + + /** + * Render the progress bar and return it. + * @param int $section + * @param array $questionsbysec + * @return bool|string + */ + public function render_progress_bar($section, $questionsbysec) { + $templatecontext['percent'] = $this->calculate_progress($section, $questionsbysec); + $helpicon = new \help_icon('progresshelp', 'mod_questionnaire'); + $templatecontext['progresshelp'] = $helpicon->export_for_template($this); + return $this->render_from_template('mod_questionnaire/progressbar', $templatecontext); + } + /** * Render a question for a survey. - * @param mod_questionnaire\question\base $question The question object. - * @param array $formdata Any returned form data. - * @param array $dependants Array of all questions/choices depending on $question. + * @param \mod_questionnaire\question\question $question The question object. + * @param \mod_questionnaire\responsetype\response\response $response Any current response data. * @param int $qnum The question number. * @param boolean $blankquestionnaire Used for printing a blank one. + * @param array $dependants Array of all questions/choices depending on $question. * @return string The output for the page. */ - public function question_output($question, $formdata, $dependants=[], $qnum, $blankquestionnaire) { + public function question_output($question, $response, $qnum, $blankquestionnaire, $dependants=[]) { - $pagetags = $question->question_output($formdata, $dependants, $qnum, $blankquestionnaire); + $pagetags = $question->question_output($response, $blankquestionnaire, $dependants, $qnum); // If the question has a template, then render it from the 'qformelement' context. If no template, then 'qformelement' // already contains HTML. @@ -203,17 +246,22 @@ public function question_output($question, $formdata, $dependants=[], $qnum, $bl /** * Render a question response. - * @param mod_questionnaire\question\base $question The question object. - * @param stdClass $data All of the response data. + * @param \mod_questionnaire\question\question $question The question object. + * @param \mod_questionnaire\responsetype\response\response $response The response object. * @param int $qnum The question number. + * @param bool $pdf * @return string The output for the page. + * @throws \moodle_exception */ - public function response_output($question, $data, $qnum=null) { - $pagetags = $question->response_output($data, $qnum); + public function response_output($question, $response, $qnum=null, $pdf=false) { + $pagetags = $question->response_output($response, $qnum); // If the response has a template, then render it from the 'qformelement' context. If no template, then 'qformelement' // already contains HTML. if (($template = $question->response_template())) { + if ($pdf) { + $pagetags->qformelement->pdf = 1; + } $pagetags->qformelement = $this->render_from_template($template, $pagetags->qformelement); } @@ -223,34 +271,40 @@ public function response_output($question, $data, $qnum=null) { $pagetags->notifications = $this->notification($notification, \core\output\notification::NOTIFY_ERROR); } } - return $this->render_from_template('mod_questionnaire/question_container', $pagetags); + if (!$pdf) { + return $this->render_from_template('mod_questionnaire/question_container', $pagetags); + } else { + return $this->render_from_template('mod_questionnaire/questionpdf_container', $pagetags); + } } /** * Render all responses for a question. - * @param stdClass $data All of the response data. + * @param array|string $responses + * @param array $questions * @return string The output for the page. */ - public function all_response_output($data=null) { + public function all_response_output($responses, $questions = null) { $output = ''; - if (is_string($data)) { - $output .= $data; + if (is_string($responses)) { + $output .= $responses; } else { - foreach ($data as $qnum => $responses) { - $question = $responses['question']; - $pagetags = $question->questionstart_survey_display($qnum); - foreach ($responses as $item => $response) { - if ($item !== 'question') { - $resptags = $question->response_output($response['respdata']); - // If the response has a template, then render it from the 'qformelement' context. - // If no template, then 'qformelement' already contains HTML. - if (($template = $question->response_template())) { - $resptags->qformelement = $this->render_from_template($template, $resptags->qformelement); - } - $resptags->respdate = $response['respdate']; - $pagetags->responses[] = $resptags; + $qnum = 1; + foreach ($questions as $question) { + if (empty($pagetags = $question->questionstart_survey_display($qnum))) { + continue; + } + foreach ($responses as $response) { + $resptags = $question->response_output($response); + // If the response has a template, then render it from the 'qformelement' context. + // If no template, then 'qformelement' already contains HTML. + if (($template = $question->response_template())) { + $resptags->qformelement = $this->render_from_template($template, $resptags->qformelement); } + $resptags->respdate = userdate($response->submitted); + $pagetags->responses[] = $resptags; } + $qnum++; $output .= $this->render_from_template('mod_questionnaire/response_container', $pagetags); } } @@ -259,17 +313,18 @@ public function all_response_output($data=null) { /** * Render a question results summary. - * @param mod_questionnaire\question\base $question The question object. + * @param question $question The question object. * @param array $rids The response ids. * @param string $sort The sort order being used. * @param string $anonymous The value of the anonymous setting. + * @param bool $pdf * @return string The output for the page. */ - public function results_output($question, $rids, $sort, $anonymous) { + public function results_output($question, $rids, $sort, $anonymous, $pdf = false) { $pagetags = $question->display_results($rids, $sort, $anonymous); // If the response has a template, then render it from $pagetags. If no template, then $pagetags already contains HTML. - if (($template = $question->results_template())) { + if (($template = $question->results_template($pdf))) { return $this->render_from_template($template, $pagetags); } else { return $pagetags; @@ -322,9 +377,10 @@ public function print_preview_pagenumber($content) { public function print_preview_formend($url, $submitstr, $resetstr) { $output = ''; $output .= \html_writer::start_tag('div'); - $output .= \html_writer::empty_tag('input', ['type' => 'submit', 'name' => 'submit', 'value' => $submitstr]); + $output .= \html_writer::empty_tag('input', ['type' => 'submit', 'name' => 'submit', 'value' => $submitstr, + 'class' => 'btn btn-primary']); $output .= ' '; - $output .= \html_writer::tag('a', $resetstr, ['href' => $url]); + $output .= \html_writer::tag('a', $resetstr, ['href' => $url, 'class' => 'btn btn-secondary mr-1']); $output .= \html_writer::end_tag('div') . "\n"; $output .= \html_writer::end_tag('form') . "\n"; return $output; @@ -339,16 +395,17 @@ public function print_preview_formend($url, $submitstr, $resetstr) { public function homelink($url, $text) { $output = ''; $output .= \html_writer::start_tag('div', ['class' => 'homelink']); - $output .= \html_writer::tag('a', $text, ['href' => $url]); + $output .= \html_writer::tag('a', $text, ['href' => $url, 'class' => 'btn btn-primary']); $output .= \html_writer::end_tag('div'); return $output; } /** - * @param $children - * @param $langstring - * @param $strnum + * Generate and return any dependency warnings. + * @param array $children + * @param string $langstring + * @param int $strnum * @return string */ public function dependency_warnings($children, $langstring, $strnum) { @@ -408,8 +465,8 @@ public function dependency_warnings($children, $langstring, $strnum) { /** * Get displayable list of parents for the question in questions_form. - * @param $qid The question id. - * @param $dependencies Array of dependency records for a question. + * @param int $qid The question id. + * @param array $dependencies Array of dependency records for a question. * @return string */ public function get_dependency_html($qid, $dependencies) { @@ -461,4 +518,50 @@ public function flexible_table(\flexible_table $table, $buffering = false) { return $o; } -} \ No newline at end of file + + /** + * Returns a dataformat selection and download form + * + * @param string $label A text label + * @param moodle_url|string $base The download page url + * @param string $name The query param which will hold the type of the download + * @param array $params Extra params sent to the download page + * @param string $extrafields HTML for extra form fields + * @return string HTML fragment + */ + public function download_dataformat_selector($label, $base, $name = 'dataformat', $params = array(), $extrafields = '') { + + $formats = \core_plugin_manager::instance()->get_plugins_of_type('dataformat'); + $options = array(); + foreach ($formats as $format) { + if ($format->is_enabled()) { + $options[] = array( + 'value' => $format->name, + 'label' => get_string('dataformat', $format->component), + ); + } + } + $hiddenparams = array(); + foreach ($params as $key => $value) { + $hiddenparams[] = array( + 'name' => $key, + 'value' => $value, + ); + } + $data = array( + 'label' => $label, + 'base' => $base, + 'name' => $name, + 'params' => $hiddenparams, + 'options' => $options, + 'extrafields' => $extrafields, + 'sesskey' => sesskey(), + 'submit' => get_string('download'), + 'emailroleshelp' => $this->help_icon('emailroles', 'questionnaire'), + 'emailextrahelp' => $this->help_icon('emailextra', 'questionnaire'), + 'allowemailreporting' => get_config('questionnaire', 'allowemailreporting'), + ); + + return $this->render_from_template('mod_questionnaire/dataformat_selector', $data); + } +} diff --git a/classes/output/reportpage.php b/classes/output/reportpage.php index 0d7c52b9..48a218a8 100644 --- a/classes/output/reportpage.php +++ b/classes/output/reportpage.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\output; + /** * Contains class mod_questionnaire\output\viewpage * @@ -22,11 +24,6 @@ * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -namespace mod_questionnaire\output; - -defined('MOODLE_INTERNAL') || die(); - class reportpage implements \renderable, \templatable { /** @@ -49,8 +46,8 @@ public function __construct($data = null) { /** * Add data for export. - * @param string The index for the data. - * @param string The content for the index. + * @param string $element The index for the data. + * @param string $content The content for the index. */ public function add_to_page($element, $content) { if ($element === 'responses') { @@ -68,4 +65,4 @@ public function export_for_template(\renderer_base $output) { return $this->data; } -} \ No newline at end of file +} diff --git a/classes/output/reportpagepdf.php b/classes/output/reportpagepdf.php new file mode 100644 index 00000000..9e74f6cf --- /dev/null +++ b/classes/output/reportpagepdf.php @@ -0,0 +1,68 @@ +. + +namespace mod_questionnaire\output; + +/** + * Contains class mod_questionnaire\output\reportpagepdf + * + * @package mod_questionnaire + * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class reportpagepdf implements \renderable, \templatable { + + /** + * The data to be exported. + * @var array + */ + protected $data; + + /** + * Construct the renderable. + * @param object $data The template data for export. + */ + public function __construct($data = null) { + if ($data !== null) { + $this->data = $data; + } else { + $this->data = new \stdClass(); + } + } + + /** + * Add data for export. + * @param string $element The index for the data. + * @param string $content The content for the index. + */ + public function add_to_page($element, $content) { + if ($element === 'responses') { + $this->data->{$element}[] = $content; + } else { + $this->data->{$element} = empty($this->data->{$element}) ? $content : ($this->data->{$element} . $content); + } + } + + /** + * Export the data for template. + * @param \renderer_base $output + */ + public function export_for_template(\renderer_base $output) { + return $this->data; + } + +} diff --git a/classes/output/responsepagepdf.php b/classes/output/responsepagepdf.php new file mode 100644 index 00000000..ce7cbfab --- /dev/null +++ b/classes/output/responsepagepdf.php @@ -0,0 +1,68 @@ +. + +namespace mod_questionnaire\output; + +/** + * Contains class mod_questionnaire\output\responsepagepdf + * + * @package mod_questionnaire + * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class responsepagepdf implements \renderable, \templatable { + + /** + * The data to be exported. + * @var array + */ + protected $data; + + /** + * Construct the renderable. + * @param object $data The template data for export. + */ + public function __construct($data = null) { + if ($data !== null) { + $this->data = $data; + } else { + $this->data = new \stdClass(); + } + } + + /** + * Add data for export. + * @param string $element The index for the data. + * @param string $content The content for the index. + */ + public function add_to_page($element, $content) { + if ($element === 'responses') { + $this->data->{$element}[] = $content; + } else { + $this->data->{$element} = empty($this->data->{$element}) ? $content : ($this->data->{$element} . $content); + } + } + + /** + * Export the data for template. + * @param \renderer_base $output + */ + public function export_for_template(\renderer_base $output) { + return $this->data; + } + +} diff --git a/classes/output/viewpage.php b/classes/output/viewpage.php index 3939810f..ab7462f5 100644 --- a/classes/output/viewpage.php +++ b/classes/output/viewpage.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\output; + /** * Contains class mod_questionnaire\output\viewpage * @@ -22,11 +24,6 @@ * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -namespace mod_questionnaire\output; - -defined('MOODLE_INTERNAL') || die(); - class viewpage implements \renderable, \templatable { /** @@ -49,8 +46,8 @@ public function __construct($data = null) { /** * Add data for export. - * @param string The index for the data. - * @param string The content for the index. + * @param string $element The index for the data. + * @param string $content The content for the index. */ public function add_to_page($element, $content) { $this->data->{$element} = empty($this->data->{$element}) ? $content : ($this->data->{$element} . $content); @@ -64,4 +61,4 @@ public function export_for_template(\renderer_base $output) { return $this->data; } -} \ No newline at end of file +} diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index f1dfe97b..3388857d 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -14,6 +14,14 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\privacy; + +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\contextlist; +use \core_privacy\local\request\userlist; +use \core_privacy\local\request\approved_contextlist; +use \core_privacy\local\request\approved_userlist; + /** * Contains class mod_questionnaire\privacy\provider * @@ -22,45 +30,23 @@ * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +class provider implements + // This plugin has data. + \core_privacy\local\metadata\provider, -namespace mod_questionnaire\privacy; - -defined('MOODLE_INTERNAL') || die(); - -// The core_userlist_provider was introduced in 3.6, 3.5.3 and 3.4.6. It will not work in any release supporting the privacy API -// below those. This code will not use it if it does not exist and continue to work on under 3.5.3, under 3.4.6 and 3.3.*. -if (interface_exists('\core_privacy\local\request\core_userlist_provider')) { - abstract class provider_helper implements - // This plugin has data. - \core_privacy\local\metadata\provider, - - // This plugin is capable of determining which users have data within it. - \core_privacy\local\request\core_userlist_provider, + // This plugin is capable of determining which users have data within it. + \core_privacy\local\request\core_userlist_provider, - // This plugin currently implements the original plugin_provider interface. - \core_privacy\local\request\plugin\provider { - } -} else { - abstract class provider_helper implements - // This plugin has data. - \core_privacy\local\metadata\provider, - - // This plugin currently implements the original plugin_provider interface. - \core_privacy\local\request\plugin\provider { - } -} - -class provider extends provider_helper { - - use \core_privacy\local\legacy_polyfill; + // This plugin currently implements the original plugin_provider interface. + \core_privacy\local\request\plugin\provider { /** * Returns meta data about this system. * - * @param collection $items The collection to add metadata to. + * @param collection $collection The collection to add metadata to. * @return collection The array of metadata */ - public static function _get_metadata(\core_privacy\local\metadata\collection $collection) { + public static function get_metadata(collection $collection): collection { // Add all of the relevant tables and fields to the collection. $collection->add_database_table('questionnaire_response', [ @@ -124,8 +110,8 @@ public static function _get_metadata(\core_privacy\local\metadata\collection $co * @param int $userid The user to search. * @return contextlist $contextlist The list of contexts used in this plugin. */ - public static function _get_contexts_for_userid($userid) { - $contextlist = new \core_privacy\local\request\contextlist(); + public static function get_contexts_for_userid(int $userid): contextlist { + $contextlist = new contextlist(); $sql = "SELECT c.id FROM {context} c @@ -134,7 +120,7 @@ public static function _get_contexts_for_userid($userid) { INNER JOIN {questionnaire} q ON q.id = cm.instance INNER JOIN {questionnaire_response} qr ON qr.questionnaireid = q.id WHERE qr.userid = :attemptuserid - "; + "; $params = [ 'modname' => 'questionnaire', @@ -153,7 +139,7 @@ public static function _get_contexts_for_userid($userid) { * @param \core_privacy\local\request\userlist $userlist The userlist containing the list of users who have data in this * context/plugin combination. */ - public static function get_users_in_context(\core_privacy\local\request\userlist $userlist) { + public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!$context instanceof \context_module) { @@ -177,7 +163,7 @@ public static function get_users_in_context(\core_privacy\local\request\userlist * * @param approved_contextlist $contextlist The approved contexts to export information for. */ - public static function _export_user_data(\core_privacy\local\request\approved_contextlist $contextlist) { + public static function export_user_data(approved_contextlist $contextlist) { global $DB, $CFG; require_once($CFG->dirroot . '/mod/questionnaire/questionnaire.class.php'); @@ -222,7 +208,7 @@ public static function _export_user_data(\core_privacy\local\request\approved_co $lastcmid = $response->cmid; $course = $DB->get_record("course", ["id" => $response->qcourse]); $cm = get_coursemodule_from_instance("questionnaire", $response->qid, $course->id); - $questionnaire = new \questionnaire($response->qid, null, $course, $cm); + $questionnaire = new \questionnaire($course, $cm, $response->qid, null); } $responsedata['responses'][] = [ 'complete' => (($response->complete == 'y') ? get_string('yes') : get_string('no')), @@ -248,7 +234,7 @@ public static function _export_user_data(\core_privacy\local\request\approved_co * * @param context $context Context to delete data from. */ - public static function _delete_data_for_all_users_in_context(\context $context) { + public static function delete_data_for_all_users_in_context(\context $context) { global $DB; if (!($context instanceof \context_module)) { @@ -275,7 +261,7 @@ public static function _delete_data_for_all_users_in_context(\context $context) * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ - public static function _delete_data_for_user(\core_privacy\local\request\approved_contextlist $contextlist) { + public static function delete_data_for_user(approved_contextlist $contextlist) { global $DB; if (empty($contextlist->count())) { @@ -310,7 +296,7 @@ public static function _delete_data_for_user(\core_privacy\local\request\approve * @param \core_privacy\local\request\approved_userlist $userlist The approved context and user information to delete * information for. */ - public static function delete_data_for_users(\core_privacy\local\request\approved_userlist $userlist) { + public static function delete_data_for_users(approved_userlist $userlist) { global $DB; $context = $userlist->get_context(); @@ -334,9 +320,9 @@ public static function delete_data_for_users(\core_privacy\local\request\approve /** * Helper function to delete all the response records for a recordset array of responses. * - * @param recordset $responses The list of response records to delete for. + * @param \moodle_recordset $responses The list of response records to delete for. */ - private static function delete_responses($responses) { + private static function delete_responses(\moodle_recordset $responses) { global $DB; foreach ($responses as $response) { @@ -349,4 +335,4 @@ private static function delete_responses($responses) { $DB->delete_records('questionnaire_response_text', ['response_id' => $response->id]); } } -} \ No newline at end of file +} diff --git a/classes/question/check.php b/classes/question/check.php index 387524d0..7af517fe 100644 --- a/classes/question/check.php +++ b/classes/question/check.php @@ -14,24 +14,30 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\question; + /** * This file contains the parent class for check question types. * * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes + * @package mod_questionnaire */ +class check extends question { -namespace mod_questionnaire\question; -defined('MOODLE_INTERNAL') || die(); -use \html_writer; - -class check extends base { - + /** + * Return the responseclass used. + * @return string + */ protected function responseclass() { - return '\\mod_questionnaire\\response\\multiple'; + return '\\mod_questionnaire\\responsetype\\multiple'; } + /** + * Return the help name. + * @return string + */ public function helpname() { return 'checkboxes'; } @@ -45,7 +51,7 @@ public function has_choices() { /** * Override and return a form template if provided. Output of question_survey_display is iterpreted based on this. - * @return boolean | string + * @return string */ public function question_template() { return 'mod_questionnaire/question_check'; @@ -53,7 +59,7 @@ public function question_template() { /** * Override and return a form template if provided. Output of response_survey_display is iterpreted based on this. - * @return boolean | string + * @return string */ public function response_template() { return 'mod_questionnaire/response_check'; @@ -69,31 +75,24 @@ public function allows_dependents() { /** * Return the context tags for the check question template. - * @param object $data + * @param \mod_questionnaire\responsetype\response\response $response * @param array $dependants Array of all questions/choices depending on this question. * @param boolean $blankquestionnaire - * @return object The check question context tags. + * @return \stdClass The check question context tags. * */ - protected function question_survey_display($data, $dependants, $blankquestionnaire=false) { + protected function question_survey_display($response, $dependants, $blankquestionnaire = false) { // Check boxes. $otherempty = false; - if (!empty($data) ) { - if (!isset($data->{'q'.$this->id}) || !is_array($data->{'q'.$this->id})) { - $data->{'q'.$this->id} = array(); - } + if (!empty($response)) { // Verify that number of checked boxes (nbboxes) is within set limits (length = min; precision = max). - if ( $data->{'q'.$this->id} ) { + if (!empty($response->answers[$this->id])) { $otherempty = false; - $boxes = $data->{'q'.$this->id}; - $nbboxes = count($boxes); - foreach ($boxes as $box) { - $pos = strpos($box, 'other_'); - if (is_int($pos) == true) { - $resp = 'q'.$this->id.''.substr($box, 5); - if (isset($data->$resp) && (trim($data->$resp) == false)) { - $otherempty = true; - } + $nbboxes = count($response->answers[$this->id]); + foreach ($response->answers[$this->id] as $answer) { + $choice = $this->choices[$answer->choiceid]; + if ($choice->is_other_choice()) { + $otherempty = empty($answer->value); } } $nbchoices = count($this->choices); @@ -109,7 +108,7 @@ protected function question_survey_display($data, $dependants, $blankquestionnai if ($nbboxes < $min || $nbboxes > $max) { $msg = get_string('boxesnbreq', 'questionnaire'); if ($min == $max) { - $msg .= ' '.get_string('boxesnbexact', 'questionnaire', $min); + $msg .= get_string('boxesnbexact', 'questionnaire', $min); } else { if ($min && ($nbboxes < $min)) { $msg .= get_string('boxesnbmin', 'questionnaire', $min); @@ -130,160 +129,191 @@ protected function question_survey_display($data, $dependants, $blankquestionnai $choicetags = new \stdClass(); $choicetags->qelements = []; foreach ($this->choices as $id => $choice) { - - $other = strpos($choice->content, '!other'); $checkbox = new \stdClass(); - if ($other !== 0) { // This is a normal check box. - $contents = questionnaire_choice_values($choice->content); - $checked = false; - if (!empty($data) ) { - $checked = in_array($id, $data->{'q'.$this->id}); - } - $checkbox->name = 'q'.$this->id.'[]'; - $checkbox->value = $id; - $checkbox->id = 'checkbox_'.$id; - $checkbox->label = format_text($contents->text, FORMAT_HTML, ['noclean' => true]).$contents->image; - if ($checked) { - $checkbox->checked = $checked; - } - } else { // Check box with associated !other text field. - // In case length field has been used to enter max number of choices, set it to 20. - $othertext = preg_replace( - array("/^!other=/", "/^!other/"), - array('', get_string('other', 'questionnaire')), - $choice->content); - $cid = 'q'.$this->id.'_'.$id; - if (!empty($data) && isset($data->$cid) && (trim($data->$cid) != false)) { - $checked = true; - } else { - $checked = false; - } - $name = 'q'.$this->id.'[]'; - $value = 'other_'.$id; - - $checkbox->name = $name; - $checkbox->oname = $cid; - $checkbox->value = $value; - $checkbox->ovalue = (isset($data->$cid) && !empty($data->$cid) ? stripslashes($data->$cid) : ''); - $checkbox->id = 'checkbox_'.$id; - $checkbox->label = format_text($othertext.'', FORMAT_HTML, ['noclean' => true]); - if ($checked) { - $checkbox->checked = $checked; - } + $contents = questionnaire_choice_values($choice->content); + $checked = false; + if (!empty($response->answers[$this->id]) ) { + $checked = isset($response->answers[$this->id][$id]); + } + $checkbox->name = 'q'.$this->id.'['.$id.']'; + $checkbox->value = $id; + $checkbox->id = 'checkbox_'.$id; + $checkbox->label = format_text($contents->text, FORMAT_HTML, ['noclean' => true]).$contents->image; + if ($checked) { + $checkbox->checked = $checked; + } + if ($choice->is_other_choice()) { + $checkbox->oname = 'q'.$this->id.'['.$choice->other_choice_name().']'; + $checkbox->ovalue = (isset($response->answers[$this->id][$id]) && !empty($response->answers[$this->id][$id]) ? + format_string(stripslashes($response->answers[$this->id][$id]->value)) : ''); + $checkbox->label = format_text($choice->other_choice_display().'', FORMAT_HTML, ['noclean' => true]); + } + if (!empty($this->qlegend)) { + $checkbox->alabel = strip_tags("{$this->qlegend} {$checkbox->label}"); } $choicetags->qelements[] = (object)['choice' => $checkbox]; } if ($otherempty) { $this->add_notification(get_string('otherempty', 'questionnaire')); } - return $choicetags; } /** * Return the context tags for the check response template. - * @param object $data - * @return object The check question response context tags. - * + * @param \mod_questionnaire\responsetype\response\response $response + * @return \stdClass The check question response context tags. */ - protected function response_survey_display($data) { + protected function response_survey_display($response) { static $uniquetag = 0; // To make sure all radios have unique names. $resptags = new \stdClass(); $resptags->choices = []; - if (!isset($data->{'q'.$this->id}) || !is_array($data->{'q'.$this->id})) { - $data->{'q'.$this->id} = array(); + if (!isset($response->answers[$this->id])) { + $response->answers[$this->id][] = new \mod_questionnaire\responsetype\answer\answer(); } foreach ($this->choices as $id => $choice) { $chobj = new \stdClass(); - if (strpos($choice->content, '!other') !== 0) { + if (!$choice->is_other_choice()) { $contents = questionnaire_choice_values($choice->content); $choice->content = $contents->text.$contents->image; - if (in_array($id, $data->{'q'.$this->id})) { + if (isset($response->answers[$this->id][$id])) { $chobj->selected = 1; } $chobj->name = $id.$uniquetag++; $chobj->content = (($choice->content === '') ? $id : format_text($choice->content, FORMAT_HTML, ['noclean' => true])); } else { - $othertext = preg_replace( - array("/^!other=/", "/^!other/"), - array('', get_string('other', 'questionnaire')), - $choice->content); - $cid = 'q'.$this->id.'_'.$id; - - if (isset($data->$cid)) { + $othertext = $choice->other_choice_display(); + if (isset($response->answers[$this->id][$id])) { + $oresp = $response->answers[$this->id][$id]->value; $chobj->selected = 1; - $chobj->othercontent = (!empty($data->$cid) ? htmlspecialchars($data->$cid) : ' '); + $chobj->othercontent = (!empty($oresp) ? htmlspecialchars($oresp) : ' '); } $chobj->name = $id.$uniquetag++; $chobj->content = (($othertext === '') ? $id : $othertext); } + if (!empty($this->qlegend)) { + $chobj->alabel = strip_tags("{$this->qlegend} {$chobj->content}"); + } $resptags->choices[] = $chobj; } return $resptags; } /** - * Check question's form data for valid response. Override this is type has specific format requirements. + * Check question's form data for complete response. * * @param object $responsedata The data entered into the response. * @return boolean */ + public function response_complete($responsedata) { + if (isset($responsedata->{'q'.$this->id}) && $this->required() && + is_array($responsedata->{'q'.$this->id})) { + foreach ($responsedata->{'q' . $this->id} as $key => $choice) { + // If only an 'other' choice is selected and empty, question is not completed. + if ((strpos($key, 'o') === 0) && empty($choice)) { + return false; + } else { + return true; + } + } + } + return parent::response_complete($responsedata); + } + + /** + * Check question's form data for valid response. Override this is type has specific format requirements. + * + * @param \stdClass $responsedata The data entered into the response. + * @return boolean + */ public function response_valid($responsedata) { + $nbrespchoices = 0; $valid = true; - if (isset($responsedata->{'q'.$this->id})) { - $nbrespchoices = 0; - foreach ($responsedata->{'q'.$this->id} as $resp) { - if (strpos($resp, 'other_') !== false) { + if (is_a($responsedata, 'mod_questionnaire\responsetype\response\response')) { + // If $responsedata is a response object, look through the answers. + if (isset($responsedata->answers[$this->id]) && !empty($responsedata->answers[$this->id])) { + foreach ($responsedata->answers[$this->id] as $answer) { + if (isset($this->choices[$answer->choiceid]) && $this->choices[$answer->choiceid]->is_other_choice()) { + $valid = !empty($answer->value); + } else { + $nbrespchoices++; + } + } + } + } else if (isset($responsedata->{'q'.$this->id})) { + foreach ($responsedata->{'q'.$this->id} as $key => $answer) { + if (strpos($key, 'o') === 0) { // ..."other" choice is checked but text box is empty. - $othercontent = "q".$this->id.substr($resp, 5); - if (trim($responsedata->$othercontent) == false) { + $okey = substr($key, 1); + if (isset($responsedata->{'q'.$this->id}[$okey]) && empty(trim($answer))) { $valid = false; break; } - $nbrespchoices++; - } else if (is_numeric($resp)) { + } else if (is_numeric($key)) { $nbrespchoices++; } } - $nbquestchoices = count($this->choices); - $min = $this->length; - $max = $this->precise; - if ($max == 0) { - $max = $nbquestchoices; - } - if ($min > $max) { - $min = $max; // Sanity check. - } - $min = min($nbquestchoices, $min); - if ($nbrespchoices && (($nbrespchoices < $min) || ($nbrespchoices > $max))) { - // Number of ticked boxes is not within min and max set limits. - $valid = false; - } } else { - $valid = parent::response_valid($responsedata); + return parent::response_valid($responsedata); + } + + $nbquestchoices = count($this->choices); + $min = $this->length; + $max = $this->precise; + if ($max == 0) { + $max = $nbquestchoices; + } + if ($min > $max) { + $min = $max; // Sanity check. + } + $min = min($nbquestchoices, $min); + if ($nbrespchoices && (($nbrespchoices < $min) || ($nbrespchoices > $max))) { + // Number of ticked boxes is not within min and max set limits. + $valid = false; } return $valid; } + /** + * Return the length form element. + * @param \MoodleQuickForm $mform + * @param string $helptext + */ protected function form_length(\MoodleQuickForm $mform, $helptext = '') { return parent::form_length($mform, 'minforcedresponses'); } + /** + * Return the precision form element. + * @param \MoodleQuickForm $mform + * @param string $helptext + */ protected function form_precise(\MoodleQuickForm $mform, $helptext = '') { return parent::form_precise($mform, 'maxforcedresponses'); } + /** + * True if question provides mobile support. + * + * @return bool + */ + public function supports_mobile() { + return true; + } + /** * Preprocess choice data. + * @param \stdClass $formdata + * @return bool */ protected function form_preprocess_choicedata($formdata) { if (empty($formdata->allchoices)) { - error (get_string('enterpossibleanswers', 'questionnaire')); + throw new \moodle_exception('enterpossibleanswers', 'mod_questionnaire'); } else { // Sanity checks for min and max checked boxes. $allchoices = $formdata->allchoices; @@ -296,8 +326,61 @@ protected function form_preprocess_choicedata($formdata) { if ($formdata->precise > $nbvalues) { $formdata->precise = $nbvalues; } - $formdata->precise = max($formdata->length, $formdata->precise); + if ($formdata->precise != 0) { + $formdata->precise = max($formdata->length, $formdata->precise); + } } return true; } -} \ No newline at end of file + + /** + * Return the mobile question display. + * @param int $qnum + * @param bool $autonum + * @return \stdClass + */ + public function mobile_question_display($qnum, $autonum = false) { + $mobiledata = parent::mobile_question_display($qnum, $autonum); + $mobiledata->ischeckbox = true; + return $mobiledata; + } + + /** + * Return the mobile question choices display. + * @return array + */ + public function mobile_question_choices_display() { + $choices = parent::mobile_question_choices_display(); + foreach ($choices as $choicenum => $choice) { + // Add a fieldkey for each choice. + $choices[$choicenum]->choicefieldkey = $this->mobile_fieldkey($choice->id); + if ($choice->is_other_choice()) { + $choices[$choicenum]->otherchoicekey = $this->mobile_fieldkey($choice->other_choice_name()); + $choices[$choicenum]->content = format_text($choice->other_choice_display(), FORMAT_HTML, ['noclean' => true]); + } + } + return $choices; + } + + /** + * Return the mobile response data. + * @param \stdClass $response + * @return array + */ + public function get_mobile_response_data($response) { + $resultdata = []; + if (isset($response->answers[$this->id])) { + foreach ($response->answers[$this->id] as $answer) { + if (isset($this->choices[$answer->choiceid])) { + // Add a fieldkey for each choice. + $resultdata[$this->mobile_fieldkey($answer->choiceid)] = 1; + if ($this->choices[$answer->choiceid]->is_other_choice()) { + $resultdata[$this->mobile_fieldkey($this->choices[$answer->choiceid]->other_choice_name())] = + $answer->value; + } + } + } + } + return $resultdata; + } +} diff --git a/classes/question/choice.php b/classes/question/choice.php new file mode 100644 index 00000000..567d271c --- /dev/null +++ b/classes/question/choice.php @@ -0,0 +1,210 @@ +. + +namespace mod_questionnaire\question; + +/** + * This defines a structured class to hold question choices. + * + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire + * @copyright 2019, onwards Poet + */ +class choice { + + // Class properties. + + /** The table name. */ + const TABLE = 'questionnaire_quest_choice'; + + /** @var int $id The id of the question choice this applies to. */ + public $id; + + /** @var int $questionid The id of the question this choice applies to. */ + public $questionid; + + /** @var string $content The display content for this choice. */ + public $content; + + /** @var string $value Optional value assigned to this choice. */ + public $value; + + /** + * Choice constructor. + * @param int $id + * @param int $questionid + * @param string $content + * @param mixed $value + */ + public function __construct($id = null, $questionid = null, $content = null, $value = null) { + $this->id = $id; + $this->questionid = $questionid; + $this->content = $content; + $this->value = $value; + } + + /** + * Create and return a choice object from a data id. If not found, an empty object is returned. + * + * @param int $id The data id to load. + * @return choice + */ + public static function create_from_id($id) { + global $DB; + + // Rename the data field question_id to questionid to conform with code conventions. Eventually, data table should be + // changed. + if ($record = $DB->get_record(self::tablename(), ['id' => $id], 'id,question_id as questionid,content,value')) { + return new choice($id, $record->questionid, $record->content, $record->value); + } else { + return new choice(); + } + } + + /** + * Create and return a choice object from data. + * + * @param \stdclass|array $choicedata The data to load. + * @return choice + */ + public static function create_from_data($choicedata) { + if (!is_array($choicedata)) { + $choicedata = (array)$choicedata; + } + + $properties = array_keys(get_class_vars(__CLASS__)); + foreach ($properties as $property) { + if (!isset($choicedata[$property])) { + $choicedata[$property] = null; + } + } + // Since the data table uses 'question_id' instead of 'questionid', look for that field as well. Hack that should be fixed + // by renaming the data table column. + if (!empty($choicedata['question_id'])) { + $choicedata['questionid'] = $choicedata['question_id']; + } + + return new choice($choicedata['id'], $choicedata['questionid'], $choicedata['content'], $choicedata['value']); + } + + /** + * Return the table name for choice. + */ + public static function tablename() { + return self::TABLE; + } + + /** + * Delete the choice record. + * @param int $id + * @return bool + */ + public static function delete_from_db_by_id($id) { + global $DB; + return $DB->delete_records(self::tablename(), ['id' => $id]); + } + + /** + * Delete this record from the DB. + * @return bool + */ + public function delete_from_db() { + return self::delete_from_db_by_id($this->id); + } + + /** + * Return true if the content string is an "other" choice. + * + * @param string $content + * @return bool + */ + public static function content_is_other_choice($content) { + return (strpos($content, '!other') === 0); + } + + /** + * Return true if the choice object is an "other" choice. + * + * @return bool + */ + public function is_other_choice() { + return (self::content_is_other_choice($this->content)); + } + + /** + * Return the string to display for an "other" option content string. If the option is not an "other", return false. + * + * @param string $content + * @return string|bool + */ + public static function content_other_choice_display($content) { + if (!self::content_is_other_choice($content)) { + return false; + } + + // If there is a defined string display after the "=", return it. Otherwise the "other" language string. + return preg_replace(["/^!other=/", "/^!other/"], ['', get_string('other', 'questionnaire')], $content); + } + + /** + * Return the string to display for an "other" option for this object. If the option is not an "other", return false. + * + * @return string|bool + */ + public function other_choice_display() { + return self::content_other_choice_display($this->content); + } + + /** + * Is the content a named degree rate choice. + * @param string $content + * @return array|bool + */ + public static function content_is_named_degree_choice($content) { + if (preg_match("/^([0-9]{1,3})=(.*)$/", $content, $ndegrees)) { + return [$ndegrees[1] => $ndegrees[2]]; + } else { + return false; + } + } + + /** + * Is the choice object a named degree rate choice. + * @return array|bool + */ + public function is_named_degree_choice() { + return self::content_is_named_degree_choice($this->content); + } + + /** + * Return the string to use as an input name for an other choice. + * + * @param int $choiceid + * @return string + */ + public static function id_other_choice_name($choiceid) { + return 'o' . $choiceid; + } + + /** + * Return the string to use as an input name for an other choice. + * @return string + */ + public function other_choice_name() { + return self::id_other_choice_name($this->id); + } +} diff --git a/classes/question/date.php b/classes/question/date.php index 1462d061..c564c34b 100644 --- a/classes/question/date.php +++ b/classes/question/date.php @@ -14,24 +14,30 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\question; + /** * This file contains the parent class for date question types. * * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes + * @package mod_questionnaire */ +class date extends question { -namespace mod_questionnaire\question; -defined('MOODLE_INTERNAL') || die(); -use \html_writer; - -class date extends base { - + /** + * Return the responseclass used. + * @return string + */ protected function responseclass() { - return '\\mod_questionnaire\\response\\date'; + return '\\mod_questionnaire\\responsetype\\date'; } + /** + * Return the help name. + * @return string + */ public function helpname() { return 'date'; } @@ -54,32 +60,29 @@ public function response_template() { /** * Return the context tags for the check question template. - * @param object $data - * @param string $descendantdata + * @param \mod_questionnaire\responsetype\response\response $response + * @param array $descendantsdata * @param boolean $blankquestionnaire * @return object The check question context tags. - * */ - protected function question_survey_display($data, $descendantsdata, $blankquestionnaire=false) { + protected function question_survey_display($response, $descendantsdata, $blankquestionnaire=false) { // Date. $questiontags = new \stdClass(); - if (!empty($data->{'q'.$this->id})) { - $dateentered = $data->{'q'.$this->id}; - $setdate = questionnaire_check_date ($dateentered, false); - if ($setdate == 'wrongdateformat') { + if (!empty($response->answers[$this->id])) { + $dateentered = $response->answers[$this->id][0]->value; + $setdate = $this->check_date_format($dateentered); + if (!$setdate) { $msg = get_string('wrongdateformat', 'questionnaire', $dateentered); $this->add_notification($msg); - } else if ($setdate == 'wrongdaterange') { - $msg = get_string('wrongdaterange', 'questionnaire'); - $this->add_notification($msg); } else { - $data->{'q'.$this->id} = $setdate; + $response->answers[$this->id][0]->value = $dateentered; } } $choice = new \stdClass(); + $choice->type = 'date'; // Using HTML5 date input. $choice->onkeypress = 'return event.keyCode != 13;'; $choice->name = 'q'.$this->id; - $choice->value = (isset($data->{'q'.$this->id}) ? $data->{'q'.$this->id} : ''); + $choice->value = (isset($response->answers[$this->id][0]->value) ? $response->answers[$this->id][0]->value : ''); $questiontags->qelements = new \stdClass(); $questiontags->qelements->choice = $choice; return $questiontags; @@ -87,14 +90,14 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti /** * Return the context tags for the check response template. - * @param object $data + * @param \mod_questionnaire\responsetype\response\response $response * @return object The check question response context tags. - * */ - protected function response_survey_display($data) { + protected function response_survey_display($response) { $resptags = new \stdClass(); - if (isset($data->{'q'.$this->id})) { - $resptags->content = $data->{'q'.$this->id}; + if (isset($response->answers[$this->id])) { + $answer = reset($response->answers[$this->id]); + $resptags->content = $answer->value; } return $resptags; } @@ -102,26 +105,128 @@ protected function response_survey_display($data) { /** * Check question's form data for valid response. Override this is type has specific format requirements. * - * @param object $responsedata The data entered into the response. + * @param \stdClass $responsedata The data entered into the response. * @return boolean */ public function response_valid($responsedata) { - if (isset($responsedata->{'q'.$this->id})) { - $checkdateresult = ''; - if ($responsedata->{'q'.$this->id} != '') { - $checkdateresult = questionnaire_check_date($responsedata->{'q'.$this->id}); + $responseval = false; + if (is_a($responsedata, 'mod_questionnaire\responsetype\response\response')) { + // If $responsedata is a response object, look through the answers. + if (isset($responsedata->answers[$this->id]) && !empty($responsedata->answers[$this->id])) { + $answer = $responsedata->answers[$this->id][0]; + $responseval = $answer->value; } - return (substr($checkdateresult, 0, 5) != 'wrong'); + } else if (isset($responsedata->{'q'.$this->id})) { + $responseval = $responsedata->{'q' . $this->id}; + } + if ($responseval !== false) { + $checkdateresult = true; + if ($responseval != '') { + $checkdateresult = $this->check_date_format($responseval); + } + return $checkdateresult; } else { return parent::response_valid($responsedata); } } + /** + * Return the form length. + * @param \MoodleQuickForm $mform + * @param string $helpname + * @return \MoodleQuickForm|void + */ protected function form_length(\MoodleQuickForm $mform, $helpname = '') { - return base::form_length_hidden($mform); + return question::form_length_hidden($mform); } + /** + * Return the form precision. + * @param \MoodleQuickForm $mform + * @param string $helpname + * @return \MoodleQuickForm|void + */ protected function form_precise(\MoodleQuickForm $mform, $helpname = '') { - return base::form_precise_hidden($mform); + return question::form_precise_hidden($mform); + } + + /** + * Verify that the date provided is in the proper YYYY-MM-DD format. + * @param string $date + * @return bool + */ + public function check_date_format($date) { + $datepieces = explode('-', $date); + $return = true; + if (count($datepieces) != 3) { + $return = false; + } else { + foreach ($datepieces as $piece => $datepiece) { + if (!is_numeric($datepiece)) { + $return = false; + break; + } + switch ($piece) { + // Year check. + case 0: + if ((strlen($datepiece) != 4) || ($datepiece <= 0)) { + $return = false; + break 2; + } + break; + // Month check. + case 1: + if ((strlen($datepiece) != 2) || ((int)$datepiece < 1) || ((int)$datepiece > 12)) { + $return = false; + break 2; + } + break; + // Day check. + case 2: + if ((strlen($datepiece) != 2) || ((int)$datepiece < 1) || ((int)$datepiece > 31)) { + $return = false; + break 2; + } + break; + } + } + } + return $return; + } + + /** + * True if question provides mobile support. + * + * @return bool + */ + public function supports_mobile() { + return true; + } + + /** + * Does the question support mobile display. + * @param int $qnum + * @param bool $autonum + * @return \stdClass + */ + public function mobile_question_display($qnum, $autonum = false) { + $mobiledata = parent::mobile_question_display($qnum, $autonum); + $mobiledata->isdate = true; + return $mobiledata; + } + + /** + * Does the question have mobile choices. + * @return mixed + */ + public function mobile_question_choices_display() { + $choices = []; + $choices[0] = new \stdClass(); + $choices[0]->id = 0; + $choices[0]->choice_id = 0; + $choices[0]->question_id = $this->id; + $choices[0]->content = ''; + $choices[0]->value = null; + return $choices; } -} \ No newline at end of file +} diff --git a/classes/question/drop.php b/classes/question/drop.php index 635e83cb..17c49667 100644 --- a/classes/question/drop.php +++ b/classes/question/drop.php @@ -14,24 +14,33 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\question; +use \html_writer; +use mod_questionnaire\question\choice; +use mod_questionnaire\responsetype\response\response; + /** * This file contains the parent class for drop question types. * * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes + * @package mod_questionnaire */ +class drop extends question { -namespace mod_questionnaire\question; -defined('MOODLE_INTERNAL') || die(); -use \html_writer; - -class drop extends base { - + /** + * Each question type must define its response class. + * @return string The response object based off of questionnaire_response_base. + */ protected function responseclass() { - return '\\mod_questionnaire\\response\\single'; + return '\\mod_questionnaire\\responsetype\\single'; } + /** + * Short name for this question type - no spaces, etc.. + * @return string + */ public function helpname() { return 'dropdown'; } @@ -45,7 +54,7 @@ public function has_choices() { /** * Override and return a form template if provided. Output of question_survey_display is iterpreted based on this. - * @return boolean | string + * @return string */ public function question_template() { return 'mod_questionnaire/question_drop'; @@ -53,7 +62,7 @@ public function question_template() { /** * Override and return a form template if provided. Output of response_survey_display is iterpreted based on this. - * @return boolean | string + * @return string */ public function response_template() { return 'mod_questionnaire/response_drop'; @@ -61,7 +70,7 @@ public function response_template() { /** * Override this and return true if the question type allows dependent questions. - * @return boolean + * @return bool */ public function allows_dependents() { return true; @@ -69,6 +78,7 @@ public function allows_dependents() { /** * True if question type supports feedback options. False by default. + * @return bool */ public function supports_feedback() { return true; @@ -76,19 +86,18 @@ public function supports_feedback() { /** * Return the context tags for the check question template. - * @param object $data + * @param \mod_questionnaire\responsetype\response\response $response * @param array $dependants Array of all questions/choices depending on this question. * @param boolean $blankquestionnaire * @return object The check question context tags. * */ - protected function question_survey_display($data, $dependants, $blankquestionnaire=false) { + protected function question_survey_display($response, $dependants, $blankquestionnaire=false) { // Drop. $options = []; $choicetags = new \stdClass(); $choicetags->qelements = new \stdClass(); - $selected = isset($data->{'q'.$this->id}) ? $data->{'q'.$this->id} : false; $options[] = (object)['value' => '', 'label' => get_string('choosedots')]; foreach ($this->choices as $key => $choice) { if ($pos = strpos($choice->content, '=')) { @@ -96,8 +105,8 @@ protected function question_survey_display($data, $dependants, $blankquestionnai } $option = new \stdClass(); $option->value = $key; - $option->label = $choice->content; - if (($selected !== false) && ($key == $selected)) { + $option->label = format_string($choice->content); + if (isset($response->answers[$this->id][$key])) { $option->selected = true; } $options[] = $option; @@ -114,11 +123,10 @@ protected function question_survey_display($data, $dependants, $blankquestionnai /** * Return the context tags for the drop response template. - * @param object $data - * @return object The check question response context tags. - * + * @param \mod_questionnaire\responsetype\response\response $response + * @return \stdClass The check question response context tags. */ - protected function response_survey_display($data) { + protected function response_survey_display($response) { static $uniquetag = 0; // To make sure all radios have unique names. $resptags = new \stdClass(); @@ -126,12 +134,18 @@ protected function response_survey_display($data) { $resptags->id = 'menu' . $resptags->name; $resptags->class = 'select custom-select ' . $resptags->id; $resptags->options = []; + $resptags->options[] = (object)['value' => '', 'label' => get_string('choosedots')]; + + if (!isset($response->answers[$this->id])) { + $response->answers[$this->id][] = new \mod_questionnaire\responsetype\answer\answer(); + } + foreach ($this->choices as $id => $choice) { $contents = questionnaire_choice_values($choice->content); $chobj = new \stdClass(); $chobj->value = $id; $chobj->label = format_text($contents->text, FORMAT_HTML, ['noclean' => true]); - if (isset($data->{'q'.$this->id}) && ($id == $data->{'q'.$this->id})) { + if (isset($response->answers[$this->id][$id])) { $chobj->selected = 1; $resptags->selectedlabel = $chobj->label; } @@ -141,11 +155,57 @@ protected function response_survey_display($data) { return $resptags; } + /** + * Return the length form element. + * @param \MoodleQuickForm $mform + * @param string $helpname + */ protected function form_length(\MoodleQuickForm $mform, $helpname = '') { - return base::form_length_hidden($mform); + return question::form_length_hidden($mform); } + /** + * Return the precision form element. + * @param \MoodleQuickForm $mform + * @param string $helpname + */ protected function form_precise(\MoodleQuickForm $mform, $helpname = '') { - return base::form_precise_hidden($mform); + return question::form_precise_hidden($mform); + } + + /** + * True if question provides mobile support. + * @return bool + */ + public function supports_mobile() { + return true; + } + + /** + * Return the mobile question display. + * @param int $qnum + * @param bool $autonum + * @return \stdClass + */ + public function mobile_question_display($qnum, $autonum = false) { + $mobiledata = parent::mobile_question_display($qnum, $autonum); + $mobiledata->isselect = true; + return $mobiledata; + } + + /** + * Return the mobile response data. + * @param response $response + * @return array + */ + public function get_mobile_response_data($response) { + $resultdata = []; + if (isset($response->answers[$this->id])) { + foreach ($response->answers[$this->id] as $answer) { + // Add a fieldkey for each choice. + $resultdata[$this->mobile_fieldkey()] = $answer->choiceid; + } + } + return $resultdata; } -} \ No newline at end of file +} diff --git a/classes/question/essay.php b/classes/question/essay.php index 042a4564..3e8591bc 100644 --- a/classes/question/essay.php +++ b/classes/question/essay.php @@ -14,29 +14,60 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\question; +use \html_writer; +use mod_questionnaire\responsetype\response\response; + /** * This file contains the parent class for essay question types. * * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes + * @package mod_questionnaire */ +class essay extends text { -namespace mod_questionnaire\question; -defined('MOODLE_INTERNAL') || die(); -use \html_writer; - -class essay extends base { - + /** + * Each question type must define its response class. + * @return object The response object based off of questionnaire_response_base. + */ protected function responseclass() { - return '\\mod_questionnaire\\response\\text'; + return '\\mod_questionnaire\\responsetype\\text'; } + /** + * Short name for this question type - no spaces, etc.. + * @return string + */ public function helpname() { return 'essaybox'; } - protected function question_survey_display($data, $descendantsdata, $blankquestionnaire=false) { + /** + * Override and return a form template if provided. Output of question_survey_display is iterpreted based on this. + * @return boolean | string + */ + public function question_template() { + return false; + } + + /** + * Override and return a response template if provided. Output of response_survey_display is iterpreted based on this. + * @return boolean | string + */ + public function response_template() { + return false; + } + + /** + * Question specific display method. + * @param response $response + * @param array $descendantsdata + * @param bool $blankquestionnaire + * + */ + protected function question_survey_display($response, $descendantsdata, $blankquestionnaire=false) { $output = ''; // Essay. @@ -53,8 +84,8 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti $rows = $this->precise > 1 ? $this->precise : $this->length; } $name = 'q'.$this->id; - if (isset($data->{'q'.$this->id})) { - $value = $data->{'q'.$this->id}; + if (isset($response->answers[$this->id][0])) { + $value = $response->answers[$this->id][0]->value; } else { $value = ''; } @@ -62,38 +93,67 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti $editor = editors_get_preferred_editor(); $editor->use_editor($name, questionnaire_get_editor_options($this->context)); $texteditor = html_writer::tag('textarea', $value, - array('id' => $name, 'name' => $name, 'rows' => $rows, 'cols' => $cols, 'aria-labelledby' => 'Text box')); + ['id' => $name, 'name' => $name, 'rows' => $rows, 'cols' => $cols, 'class' => 'form-control']); } else { $editor = FORMAT_PLAIN; $texteditor = html_writer::tag('textarea', $value, - array('id' => $name, 'name' => $name, 'rows' => $rows, 'cols' => $cols, 'aria-labelledby' => 'Text box')); + ['id' => $name, 'name' => $name, 'rows' => $rows, 'cols' => $cols]); } $output .= $texteditor; return $output; } - protected function response_survey_display($data) { + /** + * Question specific response display method. + * @param \stdClass $response + * + */ + protected function response_survey_display($response) { + if (isset($response->answers[$this->id])) { + $answer = reset($response->answers[$this->id]); + $answer = format_text($answer->value, FORMAT_HTML); + } else { + $answer = ' '; + } $output = ''; $output .= '
'; - $output .= !empty($data->{'q'.$this->id}) ? format_text($data->{'q'.$this->id}, FORMAT_HTML) : ' '; + $output .= $answer; $output .= '
'; return $output; } // Note - intentianally returning 'precise' for length and 'length' for precise. + /** + * Return the length form element. + * @param \MoodleQuickForm $mform + * @param string $helptext + */ protected function form_length(\MoodleQuickForm $mform, $helptext = '') { - $responseformats = array( + $responseformats = [ "0" => get_string('formateditor', 'questionnaire'), - "1" => get_string('formatplain', 'questionnaire')); + "1" => get_string('formatplain', 'questionnaire')]; $mform->addElement('select', 'precise', get_string('responseformat', 'questionnaire'), $responseformats); $mform->setType('precise', PARAM_INT); return $mform; } + /** + * True if question provides mobile support. + * @return bool + */ + public function supports_mobile() { + return true; + } + + /** + * Return the precision form element. + * @param \MoodleQuickForm $mform + * @param string $helptext + */ protected function form_precise(\MoodleQuickForm $mform, $helptext = '') { - $choices = array(); + $choices = []; for ($lines = 5; $lines <= 40; $lines += 5) { $choices[$lines] = get_string('nlines', 'questionnaire', $lines); } @@ -101,4 +161,4 @@ protected function form_precise(\MoodleQuickForm $mform, $helptext = '') { $mform->setType('length', PARAM_INT); return $mform; } -} \ No newline at end of file +} diff --git a/classes/question/numerical.php b/classes/question/numerical.php index ca74ff21..d86bbedb 100644 --- a/classes/question/numerical.php +++ b/classes/question/numerical.php @@ -14,39 +14,51 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\question; + /** * This file contains the parent class for numeric question types. * * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes + * @package mod_questionnaire */ - -namespace mod_questionnaire\question; -defined('MOODLE_INTERNAL') || die(); - -class numerical extends base { +class numerical extends question { /** * Constructor. Use to set any default properties. - * + * @param int $id + * @param \stdClass $question + * @param string $context + * @param array $params */ public function __construct($id = 0, $question = null, $context = null, $params = []) { $this->length = 10; return parent::__construct($id, $question, $context, $params); } + /** + * Each question type must define its response class. + * + * @return string The response object based off of questionnaire_response_base. + * + */ protected function responseclass() { - return '\\mod_questionnaire\\response\\text'; + return '\\mod_questionnaire\\responsetype\\numericaltext'; } + /** + * Short name for this question type - no spaces, etc.. + * @return string + */ public function helpname() { return 'numeric'; } /** * Override and return a form template if provided. Output of question_survey_display is iterpreted based on this. - * @return boolean | string + * @return string */ public function question_template() { return 'mod_questionnaire/question_numeric'; @@ -54,7 +66,7 @@ public function question_template() { /** * Override and return a response template if provided. Output of response_survey_display is iterpreted based on this. - * @return boolean | string + * @return string */ public function response_template() { return 'mod_questionnaire/response_numeric'; @@ -62,19 +74,18 @@ public function response_template() { /** * Return the context tags for the check question template. - * @param object $data - * @param string $descendantdata + * @param \mod_questionnaire\responsetype\response\response $response + * @param array $descendantsdata * @param boolean $blankquestionnaire - * @return object The check question context tags. - * + * @return \stdClass The check question context tags. */ - protected function question_survey_display($data, $descendantsdata, $blankquestionnaire=false) { + protected function question_survey_display($response, $descendantsdata, $blankquestionnaire=false) { // Numeric. $questiontags = new \stdClass(); $precision = $this->precise; - $a = ''; - if (isset($data->{'q'.$this->id})) { - $mynumber = $data->{'q'.$this->id}; + $a = new \StdClass(); + if (isset($response->answers[$this->id][0])) { + $mynumber = $response->answers[$this->id][0]->value; if ($mynumber != '') { $mynumber0 = $mynumber; if (!is_numeric($mynumber) ) { @@ -100,58 +111,115 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti } } if ($mynumber != '') { - $data->{'q'.$this->id} = $mynumber; + $response->answers[$this->id][0]->value = $mynumber; } } $choice = new \stdClass(); $choice->onkeypress = 'return event.keyCode != 13;'; $choice->size = $this->length; + // Add a 'thousands separator' instruction if there is a size setting greater than three. + $choice->instruction = (empty($choice->size) || ($choice->size > 3)) ? get_string('thousands', 'mod_questionnaire') : ''; $choice->name = 'q'.$this->id; $choice->maxlength = $this->length; - $choice->value = (isset($data->{'q'.$this->id}) ? $data->{'q'.$this->id} : ''); + $choice->value = (isset($response->answers[$this->id][0]) ? $response->answers[$this->id][0]->value : ''); $choice->id = self::qtypename($this->type_id) . $this->id; $questiontags->qelements = new \stdClass(); $questiontags->qelements->choice = $choice; return $questiontags; } - /** - * Return the context tags for the numeric response template. - * @param object $data - * @return object The numeric question response context tags. - * - */ - protected function response_survey_display($data) { - $resptags = new \stdClass(); - if (isset($data->{'q'.$this->id})) { - $resptags->content = $data->{'q'.$this->id}; - } - return $resptags; - } - /** * Check question's form data for valid response. Override this is type has specific format requirements. * - * @param object $responsedata The data entered into the response. + * @param \stdClass $responsedata The data entered into the response. * @return boolean */ public function response_valid($responsedata) { - if (isset($responsedata->{'q'.$this->id})) { + $responseval = false; + if (is_a($responsedata, 'mod_questionnaire\responsetype\response\response')) { + // If $responsedata is a response object, look through the answers. + if (isset($responsedata->answers[$this->id]) && !empty($responsedata->answers[$this->id])) { + $answer = $responsedata->answers[$this->id][0]; + $responseval = $answer->value; + } + } else if (isset($responsedata->{'q'.$this->id})) { + $responseval = $responsedata->{'q' . $this->id}; + } + if ($responseval !== false) { // If commas are present, replace them with periods, in case that was meant as the European decimal place. - $responseval = str_replace(',', '.', $responsedata->{'q'.$this->id}); + $responseval = str_replace(',', '.', $responseval); return (($responseval == '') || is_numeric($responseval)); } else { return parent::response_valid($responsedata); } } + /** + * Return the context tags for the numeric response template. + * @param \mod_questionnaire\responsetype\response\response $response + * @return \stdClass The numeric question response context tags. + */ + protected function response_survey_display($response) { + $resptags = new \stdClass(); + if (isset($response->answers[$this->id])) { + $answer = reset($response->answers[$this->id]); + $resptags->content = $answer->value; + } + return $resptags; + } + + /** + * Return the length form element. + * @param \MoodleQuickForm $mform + * @param string $helptext + */ protected function form_length(\MoodleQuickForm $mform, $helptext = '') { $this->length = isset($this->length) ? $this->length : 10; return parent::form_length($mform, 'maxdigitsallowed'); } + /** + * Return the precision form element. + * @param \MoodleQuickForm $mform + * @param string $helptext + */ protected function form_precise(\MoodleQuickForm $mform, $helptext = '') { return parent::form_precise($mform, 'numberofdecimaldigits'); } -} \ No newline at end of file + + /** + * True if question provides mobile support. + * @return bool + */ + public function supports_mobile() { + return true; + } + + /** + * Return the mobile question display. + * @param int $qnum + * @param bool $autonum + * @return \stdClass + */ + public function mobile_question_display($qnum, $autonum = false) { + $mobiledata = parent::mobile_question_display($qnum, $autonum); + $mobiledata->isnumeric = true; + return $mobiledata; + } + + /** + * Return the mobile question choices display. + * @return array + */ + public function mobile_question_choices_display() { + $choices = []; + $choices[0] = new \stdClass(); + $choices[0]->id = 0; + $choices[0]->choice_id = 0; + $choices[0]->question_id = $this->id; + $choices[0]->content = ''; + $choices[0]->value = null; + return $choices; + } +} diff --git a/classes/question/pagebreak.php b/classes/question/pagebreak.php index fbb659de..930639df 100644 --- a/classes/question/pagebreak.php +++ b/classes/question/pagebreak.php @@ -14,38 +14,100 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\question; +use mod_questionnaire\edit_question_form; +use \questionnaire; + /** * This file contains the parent class for pagebreak question types. * * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes + * @package mod_questionnaire */ +class pagebreak extends question { -namespace mod_questionnaire\question; -use mod_questionnaire\edit_question_form; -use \questionnaire; -defined('MOODLE_INTERNAL') || die(); - -class pagebreak extends base { - + /** + * Each question type must define its response class. + * @return object The response object based off of questionnaire_response_base. + */ protected function responseclass() { return ''; } + /** + * Short name for this question type - no spaces, etc.. + * @return string + */ public function helpname() { return ''; } + /** + * Get the output for the start of the questions in a survey. + * @param int $qnum + * @param \mod_questionnaire\responsetype\response\response $response + * @return \stdClass + */ + public function questionstart_survey_display($qnum, $response=null) { + return ''; + } + + /** + * Question specific display method. + * @param \stdClass $data + * @param array $descendantsdata + * @param bool $blankquestionnaire + * + */ protected function question_survey_display($data, $descendantsdata, $blankquestionnaire=false) { return ''; } + /** + * Question specific response display method. + * @param \stdClass $data + * + */ protected function response_survey_display($data) { return ''; } + /** + * Override this, or any of the internal methods, to provide specific form data for editing the question type. + * The structure of the elements here is the default layout for the question form. + * @param edit_question_form $form The main moodleform object. + * @param questionnaire $questionnaire The questionnaire being edited. + * @return bool + */ public function edit_form(edit_question_form $form, questionnaire $questionnaire) { return false; } -} \ No newline at end of file + + /** + * True if question provides mobile support. + * @return bool + */ + public function supports_mobile() { + return false; + } + + /** + * Override and return false if not supporting mobile app. + * @param int $qnum + * @param bool $autonum + * @return \stdClass + */ + public function mobile_question_display($qnum, $autonum = false) { + return false; + } + + /** + * Override and return false if a number should not be rendered for this question in any context. + * @return bool + */ + public function is_numbered() { + return false; + } +} diff --git a/classes/question/base.php b/classes/question/question.php similarity index 72% rename from classes/question/base.php rename to classes/question/question.php index beaa48a7..7b4594b5 100644 --- a/classes/question/base.php +++ b/classes/question/question.php @@ -16,6 +16,7 @@ namespace mod_questionnaire\question; use mod_questionnaire\edit_question_form; +use mod_questionnaire\responsetype\response\response; use \questionnaire; defined('MOODLE_INTERNAL') || die(); @@ -25,17 +26,10 @@ * This file contains the parent class for questionnaire question types. * * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes + * @package mod_questionnaire */ - -/** - * Class for describing a question - * - * @author Mike Churchward - * @package questiontypes - */ - // Constants. define('QUESCHOOSE', 0); define('QUESYESNO', 1); @@ -47,6 +41,7 @@ define('QUESRATE', 8); define('QUESDATE', 9); define('QUESNUMERIC', 10); +define('QUESSLIDER', 11); define('QUESPAGEBREAK', 99); define('QUESSECTIONTEXT', 100); @@ -55,23 +50,31 @@ require_once($CFG->dirroot.'/mod/questionnaire/locallib.php'); -abstract class base { +#[\AllowDynamicProperties] +/** + * Class for describing a question + * + * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) + * @package mod_questionnaire + */ +abstract class question { // Class Properties. /** @var int $id The database id of this question. */ - public $id = 0; + public $id = 0; /** @var int $surveyid The database id of the survey this question belongs to. */ - public $surveyid = 0; + public $surveyid = 0; /** @var string $name The name of this question. */ - public $name = ''; + public $name = ''; /** @var string $type The name of the question type. */ - public $type = ''; + public $type = ''; /** @var array $choices Array holding any choices for this question. */ - public $choices = []; + public $choices = []; /** @var array $dependencies Array holding any dependencies for this question. */ public $dependencies = []; @@ -80,25 +83,31 @@ abstract class base { public $responsetable = ''; /** @var int $length The length field. */ - public $length = 0; + public $length = 0; /** @var int $precise The precision field. */ - public $precise = 0; + public $precise = 0; /** @var int $position Position in the questionnaire */ - public $position = 0; + public $position = 0; /** @var string $content The question's content. */ - public $content = ''; + public $content = ''; + + /** @var string $qlegend The question's legend. */ + public $qlegend = ''; /** @var string $allchoices The list of all question's choices. */ - public $allchoices = ''; + public $allchoices = ''; /** @var boolean $required The required flag. */ - public $required = 'n'; + public $required = 'n'; /** @var boolean $deleted The deleted flag. */ - public $deleted = 'n'; + public $deleted = 'n'; + + /** @var mixed $extradata Any custom data for the question type. */ + public $extradata = ''; /** @var array $qtypenames List of all question names. */ private static $qtypenames = [ @@ -112,7 +121,8 @@ abstract class base { QUESDATE => 'date', QUESNUMERIC => 'numerical', QUESPAGEBREAK => 'pagebreak', - QUESSECTIONTEXT => 'sectiontext' + QUESSECTIONTEXT => 'sectiontext', + QUESSLIDER => 'slider', ]; /** @var array $notifications Array of extra messages for display purposes. */ @@ -122,7 +132,10 @@ abstract class base { /** * The class constructor - * + * @param int $id + * @param \stdClass $question + * @param \context $context + * @param array $params */ public function __construct($id = 0, $question = null, $context = null, $params = []) { global $DB; @@ -130,7 +143,7 @@ public function __construct($id = 0, $question = null, $context = null, $params if ($qtypes === null) { $qtypes = $DB->get_records('questionnaire_question_type', [], 'typeid', - 'typeid, type, has_choices, response_table'); + 'typeid, type, has_choices, response_table') ?? []; } if ($id) { @@ -147,11 +160,15 @@ public function __construct($id = 0, $question = null, $context = null, $params $this->content = $question->content; $this->required = $question->required; $this->deleted = $question->deleted; + $this->extradata = $question->extradata; $this->type_id = $question->type_id; $this->type = $qtypes[$this->type_id]->type; $this->responsetable = $qtypes[$this->type_id]->response_table; - if ($qtypes[$this->type_id]->has_choices == 'y') { + + if (!empty($question->choices)) { + $this->choices = $question->choices; + } else if ($qtypes[$this->type_id]->has_choices == 'y') { $this->get_choices(); } // Added for dependencies. @@ -164,7 +181,7 @@ public function __construct($id = 0, $question = null, $context = null, $params } if ($respclass = $this->responseclass()) { - $this->response = new $respclass($this); + $this->responsetype = new $respclass($this); } } @@ -176,12 +193,12 @@ abstract public function helpname(); /** * Build a question from data. - * @var int $qtype The question type code. - * @var int|array|object $qdata Either the id of the record, or a structure containing the question data, or null. - * @var object $context The context for the question. - * @return A question object. + * @param int $qtype + * @param int|array $qdata + * @param \stdClass $context + * @return mixed */ - static public function question_builder($qtype, $qdata = null, $context = null) { + public static function question_builder($qtype, $qdata = null, $context = null) { $qclassname = '\\mod_questionnaire\\question\\'.self::qtypename($qtype); $qid = 0; if (!empty($qdata) && is_array($qdata)) { @@ -194,9 +211,10 @@ static public function question_builder($qtype, $qdata = null, $context = null) /** * Return the different question type names. - * @return array + * @param int $qtype + * @return string */ - static public function qtypename($qtype) { + public static function qtypename($qtype) { if (array_key_exists($qtype, self::$qtypenames)) { return self::$qtypenames[$qtype]; } else { @@ -204,21 +222,32 @@ static public function qtypename($qtype) { } } + /** + * Return all of the different question type names. + * @return array + */ + public static function qtypenames() { + return self::$qtypenames; + } + /** * Override and return true if the question has choices. + * @return bool */ public function has_choices() { return false; } + /** + * Load any choices into the object. + * @throws \dml_exception + */ private function get_choices() { global $DB; if ($choices = $DB->get_records('questionnaire_quest_choice', ['question_id' => $this->id], 'id ASC')) { foreach ($choices as $choice) { - $this->choices[$choice->id] = new \stdClass(); - $this->choices[$choice->id]->content = $choice->content; - $this->choices[$choice->id]->value = $choice->value; + $this->choices[$choice->id] = \mod_questionnaire\question\choice::create_from_data($choice); } } else { $this->choices = []; @@ -227,7 +256,7 @@ private function get_choices() { /** * Return true if this question has been marked as required. - * @return boolean + * @return bool */ public function required() { return ($this->required == 'y'); @@ -235,7 +264,7 @@ public function required() { /** * Return true if the question has defined dependencies. - * @return boolean + * @return bool */ public function has_dependencies() { return !empty($this->dependencies); @@ -243,24 +272,28 @@ public function has_dependencies() { /** * Override this and return true if the question type allows dependent questions. - * @return boolean + * @return bool */ public function allows_dependents() { return false; } + /** + * Load any dependencies. + */ private function get_dependencies() { global $DB; $this->dependencies = []; - $dependencies = $DB->get_records('questionnaire_dependency', - ['questionid' => $this->id , 'surveyid' => $this->surveyid], 'id ASC'); - foreach ($dependencies as $dependency) { - $this->dependencies[$dependency->id] = new \stdClass(); - $this->dependencies[$dependency->id]->dependquestionid = $dependency->dependquestionid; - $this->dependencies[$dependency->id]->dependchoiceid = $dependency->dependchoiceid; - $this->dependencies[$dependency->id]->dependlogic = $dependency->dependlogic; - $this->dependencies[$dependency->id]->dependandor = $dependency->dependandor; + if ($dependencies = $DB->get_records('questionnaire_dependency', + ['questionid' => $this->id , 'surveyid' => $this->surveyid], 'id ASC')) { + foreach ($dependencies as $dependency) { + $this->dependencies[$dependency->id] = new \stdClass(); + $this->dependencies[$dependency->id]->dependquestionid = $dependency->dependquestionid; + $this->dependencies[$dependency->id]->dependchoiceid = $dependency->dependchoiceid; + $this->dependencies[$dependency->id]->dependlogic = $dependency->dependlogic; + $this->dependencies[$dependency->id]->dependandor = $dependency->dependandor; + } } } @@ -277,9 +310,9 @@ protected function get_dependency_options() { if (!empty($contents->modname)) { $choice->content = $contents->modname; } else if (!empty($contents->title)) { // Must be an image; use its title for the dropdown list. - $choice->content = $contents->title; + $choice->content = format_string($contents->title); } else { - $choice->content = $contents->text; + $choice->content = format_string($contents->text); } $options[$this->id . ',' . $key] = $this->name . '->' . $choice->content; } @@ -349,30 +382,36 @@ public function dependency_fulfilled($rid, $questions) { return $fulfilled; } + /** + * Return the responsetype table for this question. + * @return string + */ public function response_table() { - return $this->response->response_table(); + return $this->responsetype->response_table(); } /** * Return true if the specified response for this question contains the specified choice. - * @param $rid - * @param $choiceid + * @param int $rid + * @param int $choiceid * @return bool */ public function response_has_choice($rid, $choiceid) { global $DB; - $choiceval = $this->response->transform_choiceid($choiceid); + $choiceval = $this->responsetype->transform_choiceid($choiceid); return $DB->record_exists($this->response_table(), ['response_id' => $rid, 'question_id' => $this->id, 'choice_id' => $choiceval]); } /** * Insert response data method. + * @param \stdClass $responsedata All of the responsedata. + * @return bool */ - public function insert_response($rid, $val) { - if (isset ($this->response) && is_object($this->response) && - is_subclass_of($this->response, '\\mod_questionnaire\\response\\base')) { - return $this->response->insert_response($rid, $val); + public function insert_response($responsedata) { + if (isset($this->responsetype) && is_object($this->responsetype) && + is_subclass_of($this->responsetype, '\\mod_questionnaire\\responsetype\\responsetype')) { + return $this->responsetype->insert_response($responsedata); } else { return false; } @@ -380,11 +419,13 @@ public function insert_response($rid, $val) { /** * Get results data method. + * @param array|bool $rids + * @return array|false */ public function get_results($rids = false) { - if (isset ($this->response) && is_object($this->response) && - is_subclass_of($this->response, '\\mod_questionnaire\\response\\base')) { - return $this->response->get_results($rids); + if (isset ($this->responsetype) && is_object($this->responsetype) && + is_subclass_of($this->responsetype, '\\mod_questionnaire\\responsetype\\responsetype')) { + return $this->responsetype->get_results($rids); } else { return false; } @@ -392,11 +433,15 @@ public function get_results($rids = false) { /** * Display results method. + * @param bool $rids + * @param string $sort + * @param bool $anonymous + * @return false|string */ public function display_results($rids=false, $sort='', $anonymous=false) { - if (isset ($this->response) && is_object($this->response) && - is_subclass_of($this->response, '\\mod_questionnaire\\response\\base')) { - return $this->response->display_results($rids, $sort, $anonymous); + if (isset ($this->responsetype) && is_object($this->responsetype) && + is_subclass_of($this->responsetype, '\\mod_questionnaire\\responsetype\\responsetype')) { + return $this->responsetype->display_results($rids, $sort, $anonymous); } else { return false; } @@ -424,14 +469,13 @@ public function get_notifications() { /** * Each question type must define its response class. - * * @return object The response object based off of questionnaire_response_base. - * */ abstract protected function responseclass(); /** * True if question type allows responses. + * @return bool */ public function supports_responses() { return !empty($this->responseclass()); @@ -439,6 +483,7 @@ public function supports_responses() { /** * True if question type supports feedback options. False by default. + * @return bool */ public function supports_feedback() { return false; @@ -446,13 +491,23 @@ public function supports_feedback() { /** * True if question type supports feedback scores and weights. Same as supports_feedback() by default. + * @return bool */ public function supports_feedback_scores() { return $this->supports_feedback(); } + /** + * Override and return false if a number should not be rendered for this question in any context. + * @return bool + */ + public function is_numbered() { + return true; + } + /** * True if the question supports feedback and has valid settings for feedback. Override if the default logic is not enough. + * @return bool */ public function valid_feedback() { if ($this->supports_feedback() && $this->has_choices() && $this->required() && !empty($this->name)) { @@ -461,20 +516,19 @@ public function valid_feedback() { return true; } } - } else { - return false; } + return false; } /** * Provide the feedback scores for all requested response id's. This should be provided only by questions that provide feedback. * @param array $rids - * @return array | boolean + * @return array|bool */ public function get_feedback_scores(array $rids) { - if ($this->valid_feedback() && isset($this->response) && is_object($this->response) && - is_subclass_of($this->response, '\\mod_questionnaire\\response\\base')) { - return $this->response->get_feedback_scores($rids); + if ($this->valid_feedback() && isset($this->responsetype) && is_object($this->responsetype) && + is_subclass_of($this->responsetype, '\\mod_questionnaire\\responsetype\\responsetype')) { + return $this->responsetype->get_feedback_scores($rids); } else { return false; } @@ -482,7 +536,7 @@ public function get_feedback_scores(array $rids) { /** * Get the maximum score possible for feedback if appropriate. Override if default behaviour is not correct. - * @return int | boolean + * @return int|bool */ public function get_feedback_maxscore() { if ($this->valid_feedback()) { @@ -502,20 +556,34 @@ public function get_feedback_maxscore() { /** * Check question's form data for complete response. - * - * @param object $responsedata The data entered into the response. - * @return boolean + * @param \stdClass $responsedata The data entered into the response. + * @return bool */ public function response_complete($responsedata) { - return !($this->required() && ($this->deleted == 'n') && - (!isset($responsedata->{'q'.$this->id}) || $responsedata->{'q'.$this->id} == '')); + if (is_a($responsedata, 'mod_questionnaire\responsetype\response\response')) { + // If $responsedata is a response object, look through the answers. + if (isset($responsedata->answers[$this->id]) && !empty($responsedata->answers[$this->id])) { + $answer = $responsedata->answers[$this->id][0]; + if (!empty($answer->choiceid) && isset($this->choices[$answer->choiceid]) && + $this->choices[$answer->choiceid]->is_other_choice()) { + $answered = !empty($answer->value); + } else { + $answered = (!empty($answer->choiceid) || !empty($answer->value)); + } + } else { + $answered = false; + } + } else { + // If $responsedata is webform data, check that its not empty. + $answered = isset($responsedata->{'q'.$this->id}) && ($responsedata->{'q'.$this->id} != ''); + } + return !($this->required() && ($this->deleted == 'n') && !$answered); } /** * Check question's form data for valid response. Override this if type has specific format requirements. - * - * @param object $responsedata The data entered into the response. - * @return boolean + * @param \stdClass $responsedata The data entered into the response. + * @return bool */ public function response_valid($responsedata) { return true; @@ -523,9 +591,8 @@ public function response_valid($responsedata) { /** * Update data record from object or optional question data. - * - * @param object $questionrecord An object with all updated question record data. - * @param boolean $updatechoices True if choices should also be updated. + * @param \stdClass $questionrecord An object with all updated question record data. + * @param bool $updatechoices True if choices should also be updated. */ public function update($questionrecord = null, $updatechoices = true) { global $DB; @@ -543,6 +610,7 @@ public function update($questionrecord = null, $updatechoices = true) { $questionrecord->content = $this->content; $questionrecord->required = $this->required; $questionrecord->deleted = $this->deleted; + $questionrecord->extradata = $this->extradata; $questionrecord->dependquestion = $this->dependquestion; $questionrecord->dependchoice = $this->dependchoice; } else { @@ -562,7 +630,7 @@ public function update($questionrecord = null, $updatechoices = true) { /** * Add the question to the database from supplied arguments. - * @param object $questionrecord The required data for adding the question. + * @param \stdClass $questionrecord The required data for adding the question. * @param array $choicerecords An array of choice records with 'content' and 'value' properties. * @param boolean $calcposition Whether or not to calculate the next available position in the survey. */ @@ -598,6 +666,10 @@ public function add($questionrecord, array $choicerecords = null, $calcposition } } + /** + * Update all choices. + * @return bool + */ public function update_choices() { $retvalue = true; if ($this->has_choices() && isset($this->choices)) { @@ -619,11 +691,21 @@ public function update_choices() { return $retvalue; } + /** + * Update the choice with the choicerecord. + * @param \stdClass $choicerecord + * @return bool + */ public function update_choice($choicerecord) { global $DB; return $DB->update_record('questionnaire_quest_choice', $choicerecord); } + /** + * Add a new choice to the database. + * @param \stdClass $choicerecord + * @return bool + */ public function add_choice($choicerecord) { global $DB; $retvalue = true; @@ -639,19 +721,16 @@ public function add_choice($choicerecord) { /** * Delete the choice from the question object and the database. - * - * @param integer|object $choice Either the integer id of the choice, or the choice record. + * @param int|\stdClass $choice Either the integer id of the choice, or the choice record. */ public function delete_choice($choice) { - global $DB; - $retvalue = true; if (is_int($choice)) { $cid = $choice; } else { $cid = $choice->id; } - if ($DB->delete_records('questionnaire_quest_choice', ['id' => $cid])) { + if (\mod_questionnaire\question\choice::delete_from_db_by_id($cid)) { unset($this->choices[$cid]); } else { $retvalue = false; @@ -659,11 +738,31 @@ public function delete_choice($choice) { return $retvalue; } + /** + * Insert extradata field into db. This will be stored as a string. If a question needs a different format, override this. + * @param string $extradata + * @return bool + */ + public function insert_extradata($extradata) { + global $DB; + return $DB->set_field('questionnaire_question', 'extradata', $extradata, ['id' => $this->id]); + } + + /** + * Update the dependency record. + * @param \stdClass $dependencyrecord + * @return bool + */ public function update_dependency($dependencyrecord) { global $DB; return $DB->update_record('questionnaire_dependency', $dependencyrecord); } + /** + * Add a dependency record. + * @param \stdClass $dependencyrecord + * @return bool + */ public function add_dependency($dependencyrecord) { global $DB; @@ -682,8 +781,7 @@ public function add_dependency($dependencyrecord) { /** * Delete the dependency from the question object and the database. - * - * @param integer|object $dependency Either the integer id of the dependency, or the dependency record. + * @param int|\stdClass $dependency Either the integer id of the dependency, or the dependency record. */ public function delete_dependency($dependency) { global $DB; @@ -704,8 +802,7 @@ public function delete_dependency($dependency) { /** * Set the question required field in the object and database. - * - * @param boolean $required Whether question should be required or not. + * @param bool $required Whether question should be required or not. */ public function set_required($required) { global $DB; @@ -722,26 +819,23 @@ public function set_required($required) { /** * Question specific display method. - * - * @param object $formdata - * @param array $descendantdata - * @param boolean $blankquestionnaire + * @param \stdClass $formdata + * @param array $descendantsdata + * @param bool $blankquestionnaire * */ abstract protected function question_survey_display($formdata, $descendantsdata, $blankquestionnaire); /** * Question specific response display method. - * - * @param object $data - * @param integer $qnum + * @param \stdClass $data * */ abstract protected function response_survey_display($data); /** * Override and return a form template if provided. Output of question_survey_display is iterpreted based on this. - * @return boolean | string + * @return bool|string */ public function question_template() { return false; @@ -749,7 +843,7 @@ public function question_template() { /** * Override and return a form template if provided. Output of response_survey_display is iterpreted based on this. - * @return boolean | string + * @return bool|string */ public function response_template() { return false; @@ -757,12 +851,13 @@ public function response_template() { /** * Override and return a form template if provided. Output of results_output is iterpreted based on this. - * @return boolean | string + * @param bool $pdf + * @return bool|string */ - public function results_template() { - if (isset ($this->response) && is_object($this->response) && - is_subclass_of($this->response, '\\mod_questionnaire\\response\\base')) { - return $this->response->results_template(); + public function results_template($pdf = false) { + if (isset ($this->responsetype) && is_object($this->responsetype) && + is_subclass_of($this->responsetype, '\\mod_questionnaire\\responsetype\\responsetype')) { + return $this->responsetype->results_template($pdf); } else { return false; } @@ -770,55 +865,60 @@ public function results_template() { /** * Get the output for question renderers / templates. - * @param object $formdata - * @param array $dependants Array of all questions/choices depending on this question. - * @param integer $qnum + * @param \mod_questionnaire\responsetype\response\response $response * @param boolean $blankquestionnaire + * @param array $dependants Array of all questions/choices depending on this question. + * @param int $qnum + * @return \stdClass */ - public function question_output($formdata, $dependants=[], $qnum='', $blankquestionnaire) { - $pagetags = $this->questionstart_survey_display($qnum, $formdata); - $pagetags->qformelement = $this->question_survey_display($formdata, $dependants, $blankquestionnaire); + public function question_output($response, $blankquestionnaire, $dependants=[], $qnum='') { + $pagetags = $this->questionstart_survey_display($qnum, $response); + $pagetags->qformelement = $this->question_survey_display($response, $dependants, $blankquestionnaire); return $pagetags; } /** * Get the output for question renderers / templates. - * @param object $formdata - * @param string $descendantdata - * @param integer $qnum - * @param boolean $blankquestionnaire + * @param \mod_questionnaire\responsetype\response\response $response + * @param string $qnum + * @return \stdClass */ - public function response_output($data, $qnum='') { - $pagetags = $this->questionstart_survey_display($qnum, $data); - $pagetags->qformelement = $this->response_survey_display($data); + public function response_output($response, $qnum='') { + $pagetags = $this->questionstart_survey_display($qnum, $response); + $pagetags->qformelement = $this->response_survey_display($response); return $pagetags; } /** * Get the output for the start of the questions in a survey. - * @param integer $qnum - * @param object $formdata + * @param int $qnum + * @param \mod_questionnaire\responsetype\response\response $response + * @return \stdClass */ - public function questionstart_survey_display($qnum, $formdata='') { + public function questionstart_survey_display($qnum, $response=null) { global $OUTPUT, $SESSION, $questionnaire, $PAGE; $pagetags = new \stdClass(); $currenttab = $SESSION->questionnaire->current_tab; $pagetype = $PAGE->pagetype; - $skippedquestion = false; $skippedclass = ''; - $autonum = $questionnaire->autonum; // If no questions autonumbering. $nonumbering = false; - if ($autonum != 1 && $autonum != 3) { + if (!$questionnaire->questions_autonumbered()) { $qnum = ''; $nonumbering = true; } + + // For now, check what the response type is until we've got it all refactored. + if ($response instanceof \mod_questionnaire\responsetype\response\response) { + $skippedquestion = !isset($response->answers[$this->id]); + } else { + $skippedquestion = !empty($response) && !isset($response->{'q'.$this->id}); + } + // If we are on report page and this questionnaire has dependquestions and this question was skipped. if (($pagetype == 'mod-questionnaire-myreport' || $pagetype == 'mod-questionnaire-report') && - ($nonumbering == false) && !empty($formdata) && !empty($this->dependencies) && - !array_key_exists('q'.$this->id, $formdata)) { - $skippedquestion = true; + ($nonumbering == false) && !empty($this->dependencies) && $skippedquestion) { $skippedclass = ' unselected'; $qnum = '('.$qnum.')'; } @@ -857,17 +957,19 @@ public function questionstart_survey_display($qnum, $formdata='') { $this->content = ''; } $pagetags->skippedclass = $skippedclass; - if ($this->type_id == QUESNUMERIC || $this->type_id == QUESTEXT) { + if ($this->type_id == QUESNUMERIC || $this->type_id == QUESTEXT || $this->type_id == QUESSLIDER) { $pagetags->label = (object)['for' => self::qtypename($this->type_id) . $this->id]; } else if ($this->type_id == QUESDROP) { $pagetags->label = (object)['for' => self::qtypename($this->type_id) . $this->name]; } else if ($this->type_id == QUESESSAY) { - $pagetags->label = (object)['for' => 'edit-q' . $this->id]; + $pagetags->label = (object)['for' => 'q' . $this->id]; } + $content = file_rewrite_pluginfile_urls($this->content, 'pluginfile.php', + $this->context->id, 'mod_questionnaire', 'question', $this->id); $options = ['noclean' => true, 'para' => false, 'filter' => true, 'context' => $this->context, 'overflowdiv' => true]; - $content = format_text(file_rewrite_pluginfile_urls($this->content, 'pluginfile.php', - $this->context->id, 'mod_questionnaire', 'question', $this->id), FORMAT_HTML, $options); - $pagetags->qcontent = $content; + $pagetags->qcontent = format_text($content, FORMAT_HTML, $options); + $this->qlegend = strip_tags($content); + $pagetags->qlegend = $this->qlegend; return $pagetags; } @@ -891,12 +993,15 @@ public function edit_form(edit_question_form $form, questionnaire $questionnaire $this->form_required($mform); $this->form_length($mform); $this->form_precise($mform); - $this->form_question_text($mform, $form->_customdata['modcontext']); + $this->form_question_text($mform, ($form->_customdata['modcontext'] ?? '')); if ($this->has_choices()) { - $this->allchoices = $this->form_choices($mform, $this->choices); + // This is used only by the question editing form. + $this->allchoices = $this->form_choices($mform); } + $this->form_extradata($mform); + // Added for advanced dependencies, parameter $editformobject is needed to use repeat_elements. if ($questionnaire->navigate > 0) { $this->form_dependencies($form, $questionnaire->questions); @@ -928,6 +1033,11 @@ public function edit_form(edit_question_form $form, questionnaire $questionnaire return true; } + /** + * Add the form header. + * @param \MoodleQuickForm $mform + * @param string $helpname + */ protected function form_header(\MoodleQuickForm $mform, $helpname = '') { // Display different messages for new question creation and existing question modification. if (isset($this->qid) && !empty($this->qid)) { @@ -943,6 +1053,11 @@ protected function form_header(\MoodleQuickForm $mform, $helpname = '') { $mform->addHelpButton('questionhdredit', $helpname, 'questionnaire'); } + /** + * Add the form name field. + * @param \MoodleQuickForm $mform + * @return \MoodleQuickForm + */ protected function form_name(\MoodleQuickForm $mform) { $mform->addElement('text', 'name', get_string('optionalname', 'questionnaire'), ['size' => '30', 'maxlength' => '30']); @@ -951,6 +1066,11 @@ protected function form_name(\MoodleQuickForm $mform) { return $mform; } + /** + * Add the form required field. + * @param \MoodleQuickForm $mform + * @return \MoodleQuickForm + */ protected function form_required(\MoodleQuickForm $mform) { $reqgroup = []; $reqgroup[] =& $mform->createElement('radio', 'required', '', get_string('yes'), 'y'); @@ -960,18 +1080,28 @@ protected function form_required(\MoodleQuickForm $mform) { return $mform; } + /** + * Return the length form element. + * @param \MoodleQuickForm $mform + * @param string $helpname + */ protected function form_length(\MoodleQuickForm $mform, $helpname = '') { self::form_length_text($mform, $helpname); } + /** + * Return the precision form element. + * @param \MoodleQuickForm $mform + * @param string $helpname + */ protected function form_precise(\MoodleQuickForm $mform, $helpname = '') { self::form_precise_text($mform, $helpname); } /** - * @param \MoodleQuickForm $mform The moodle form to add elements to. - * @param $questionnaire - * @param $editquestionformobject + * Determine form dependencies. + * @param \MoodleQuickForm $form The moodle form to add elements to. + * @param array $questions * @return bool */ protected function form_dependencies($form, $questions) { @@ -1057,6 +1187,12 @@ protected function form_dependencies($form, $questions) { return true; } + /** + * Return the question text element. + * @param \MoodleQuickForm $mform + * @param string $context + * @return \MoodleQuickForm + */ protected function form_question_text(\MoodleQuickForm $mform, $context) { $editoroptions = ['maxfiles' => EDITOR_UNLIMITED_FILES, 'trusttext' => true, 'context' => $context]; $mform->addElement('editor', 'content', get_string('text', 'questionnaire'), null, $editoroptions); @@ -1065,40 +1201,71 @@ protected function form_question_text(\MoodleQuickForm $mform, $context) { return $mform; } - protected function form_choices(\MoodleQuickForm $mform, array $choices, $helpname = '') { - $numchoices = count($choices); - $allchoices = ''; - foreach ($choices as $choice) { - if (!empty($allchoices)) { - $allchoices .= "\n"; + /** + * Add the choices to the form. + * @param \MoodleQuickForm $mform + * @return string + */ + protected function form_choices(\MoodleQuickForm $mform) { + if ($this->has_choices()) { + $numchoices = count($this->choices); + $allchoices = ''; + foreach ($this->choices as $choice) { + if (!empty($allchoices)) { + $allchoices .= "\n"; + } + $allchoices .= $choice->content; } - $allchoices .= $choice->content; - } - if (empty($helpname)) { + $helpname = $this->helpname(); - } - $mform->addElement('html', '
'); - $options = ['wrap' => 'virtual', 'class' => 'qopts']; - $mform->addElement('textarea', 'allchoices', get_string('possibleanswers', 'questionnaire'), $options); - $mform->setType('allchoices', PARAM_RAW); - $mform->addRule('allchoices', null, 'required', null, 'client'); - $mform->addHelpButton('allchoices', $helpname, 'questionnaire'); - $mform->addElement('html', '
'); - $mform->addElement('hidden', 'num_choices', $numchoices); - $mform->setType('num_choices', PARAM_INT); + $mform->addElement('html', '
'); + $options = ['wrap' => 'virtual', 'class' => 'qopts']; + $mform->addElement('textarea', 'allchoices', get_string('possibleanswers', 'questionnaire'), $options); + $mform->setType('allchoices', PARAM_RAW); + $mform->addRule('allchoices', null, 'required', null, 'client'); + $mform->addHelpButton('allchoices', $helpname, 'questionnaire'); + $mform->addElement('html', '
'); + $mform->addElement('hidden', 'num_choices', $numchoices); + $mform->setType('num_choices', PARAM_INT); + } return $allchoices; } + /** + * Override if the question uses the extradata field. + * @param \MoodleQuickForm $mform + * @param string $helpname + * @return \MoodleQuickForm + */ + protected function form_extradata(\MoodleQuickForm $mform, $helpname = '') { + $mform->addElement('hidden', 'extradata'); + $mform->setType('extradata', PARAM_INT); + return $mform; + } + // Helper functions for commonly used editing functions. - static public function form_length_hidden(\MoodleQuickForm $mform, $value = 0) { + /** + * Add the length element as hidden. + * @param \MoodleQuickForm $mform + * @param int $value + * @return \MoodleQuickForm + */ + public static function form_length_hidden(\MoodleQuickForm $mform, $value = 0) { $mform->addElement('hidden', 'length', $value); $mform->setType('length', PARAM_INT); return $mform; } - static public function form_length_text(\MoodleQuickForm $mform, $helpname = '', $value = 0) { + /** + * Add the length element as text. + * @param \MoodleQuickForm $mform + * @param string $helpname + * @param int $value + * @return \MoodleQuickForm + */ + public static function form_length_text(\MoodleQuickForm $mform, $helpname = '', $value = 0) { $mform->addElement('text', 'length', get_string($helpname, 'questionnaire'), ['size' => '1'], $value); $mform->setType('length', PARAM_INT); if (!empty($helpname)) { @@ -1107,13 +1274,27 @@ static public function form_length_text(\MoodleQuickForm $mform, $helpname = '', return $mform; } - static public function form_precise_hidden(\MoodleQuickForm $mform, $value = 0) { + /** + * Add the precise element as hidden. + * @param \MoodleQuickForm $mform + * @param int $value + * @return \MoodleQuickForm + */ + public static function form_precise_hidden(\MoodleQuickForm $mform, $value = 0) { $mform->addElement('hidden', 'precise', $value); $mform->setType('precise', PARAM_INT); return $mform; } - static public function form_precise_text(\MoodleQuickForm $mform, $helpname = '', $value = 0) { + /** + * Add the precise element as text. + * @param \MoodleQuickForm $mform + * @param string $helpname + * @param int $value + * @return \MoodleQuickForm + * @throws \coding_exception + */ + public static function form_precise_text(\MoodleQuickForm $mform, $helpname = '', $value = 0) { $mform->addElement('text', 'precise', get_string($helpname, 'questionnaire'), ['size' => '1']); $mform->setType('precise', PARAM_INT); if (!empty($helpname)) { @@ -1124,6 +1305,8 @@ static public function form_precise_text(\MoodleQuickForm $mform, $helpname = '' /** * Create and update question data from the forms. + * @param \stdClass $formdata + * @param questionnaire $questionnaire */ public function form_update($formdata, $questionnaire) { global $DB; @@ -1133,13 +1316,13 @@ public function form_update($formdata, $questionnaire) { // Update existing question. // Handle any attachments in the content. - $formdata->itemid = $formdata->content['itemid']; - $formdata->format = $formdata->content['format']; + $formdata->itemid = $formdata->content['itemid']; + $formdata->format = $formdata->content['format']; $formdata->content = $formdata->content['text']; $formdata->content = file_save_draft_area_files($formdata->itemid, $questionnaire->context->id, 'mod_questionnaire', 'question', $formdata->qid, ['subdirs' => true], $formdata->content); - $fields = ['name', 'type_id', 'length', 'precise', 'required', 'content']; + $fields = ['name', 'type_id', 'length', 'precise', 'required', 'content', 'extradata']; $questionrecord = new \stdClass(); $questionrecord->id = $formdata->qid; foreach ($fields as $f) { @@ -1157,7 +1340,7 @@ public function form_update($formdata, $questionnaire) { // Create new question: // Need to update any image content after the question is created, so create then update the content. $formdata->surveyid = $formdata->sid; - $fields = ['surveyid', 'name', 'type_id', 'length', 'precise', 'required', 'position']; + $fields = ['surveyid', 'name', 'type_id', 'length', 'precise', 'required', 'position', 'extradata']; $questionrecord = new \stdClass(); foreach ($fields as $f) { if (isset($formdata->$f)) { @@ -1169,10 +1352,10 @@ public function form_update($formdata, $questionnaire) { $this->add($questionrecord); // Handle any attachments in the content. - $formdata->itemid = $formdata->content['itemid']; - $formdata->format = $formdata->content['format']; + $formdata->itemid = $formdata->content['itemid']; + $formdata->format = $formdata->content['format']; $formdata->content = $formdata->content['text']; - $content = file_save_draft_area_files($formdata->itemid, $questionnaire->context->id, 'mod_questionnaire', + $content = file_save_draft_area_files($formdata->itemid, $questionnaire->context->id, 'mod_questionnaire', 'question', $this->qid, ['subdirs' => true], $formdata->content); $DB->set_field('questionnaire_question', 'content', $content, ['id' => $this->qid]); } @@ -1215,7 +1398,7 @@ public function form_update($formdata, $questionnaire) { } while ($nidx < $newcount) { - // New choices... + // New choices. $choicerecord = new \stdClass(); $choicerecord->question_id = $this->qid; $choicerecord->content = trim($newchoices[$nidx]); @@ -1305,6 +1488,8 @@ public function form_update($formdata, $questionnaire) { /** * Any preprocessing of general data. + * @param \stdClass $formdata + * @return bool */ protected function form_preprocess_data($formdata) { if ($this->has_choices()) { @@ -1349,6 +1534,8 @@ protected function form_preprocess_data($formdata) { /** * Override this function for question specific choice preprocessing. + * @param \stdClass $formdata + * @return false */ protected function form_preprocess_choicedata($formdata) { if (empty($formdata->allchoices)) { @@ -1356,4 +1543,115 @@ protected function form_preprocess_choicedata($formdata) { } return false; } -} \ No newline at end of file + + /** + * True if question provides mobile support. + * @return bool + */ + public function supports_mobile() { + return false; + } + + /** + * Override and return false if not supporting mobile app. + * @param int $qnum + * @param bool $autonum + * @return \stdClass + */ + public function mobile_question_display($qnum, $autonum = false) { + $options = ['noclean' => true, 'para' => false, 'filter' => true, + 'context' => $this->context, 'overflowdiv' => true]; + $mobiledata = (object)[ + 'id' => $this->id, + 'name' => $this->name, + 'type_id' => $this->type_id, + 'length' => $this->length, + 'content' => format_text(file_rewrite_pluginfile_urls($this->content, 'pluginfile.php', $this->context->id, + 'mod_questionnaire', 'question', $this->id), FORMAT_HTML, $options), + 'content_stripped' => strip_tags($this->content), + 'required' => ($this->required == 'y') ? 1 : 0, + 'deleted' => $this->deleted, + 'response_table' => $this->responsetable, + 'fieldkey' => $this->mobile_fieldkey(), + 'precise' => $this->precise, + 'qnum' => $qnum, + 'errormessage' => get_string('required') . ': ' . $this->name + ]; + $mobiledata->choices = $this->mobile_question_choices_display(); + + if ($this->mobile_question_extradata_display()) { + $mobiledata->extradata = json_decode($this->extradata); + } + if ($autonum) { + $mobiledata->content = $qnum . '. ' . $mobiledata->content; + $mobiledata->content_stripped = $qnum . '. ' . $mobiledata->content_stripped; + } + $mobiledata->responses = ''; + return $mobiledata; + } + + /** + * Override and return false if not supporting mobile app. + * @return array + */ + public function mobile_question_choices_display() { + $choices = []; + $cnum = 0; + if ($this->has_choices()) { + foreach ($this->choices as $choice) { + $choices[$cnum] = clone($choice); + $contents = questionnaire_choice_values($choice->content); + $choices[$cnum]->content = format_text($contents->text, FORMAT_HTML, ['noclean' => true]).$contents->image; + $cnum++; + } + } + return $choices; + } + + /** + * Return a field key to be used by the mobile app. + * @param int $choiceid + * @return string + */ + public function mobile_fieldkey($choiceid = 0) { + $choicefield = ''; + if ($choiceid !== 0) { + $choicefield = '_' . $choiceid; + } + return 'response_' . $this->type_id . '_' . $this->id . $choicefield; + } + + /** + * Return the mobile response data. + * @param response $response + * @return array + */ + public function get_mobile_response_data($response) { + $resultdata = []; + if (isset($response->answers[$this->id][0])) { + $resultdata[$this->mobile_fieldkey()] = $response->answers[$this->id][0]->value; + } else { + $resultdata[$this->mobile_fieldkey()] = false; + } + + return $resultdata; + } + + /** + * True if question need extradata for mobile app. + * + * @return bool + */ + public function mobile_question_extradata_display() { + return false; + } + + /** + * Return the otherdata to be used by the mobile app. + * + * @return array + */ + public function mobile_otherdata() { + return []; + } +} diff --git a/classes/question/radio.php b/classes/question/radio.php index 99888fed..4d818d7a 100644 --- a/classes/question/radio.php +++ b/classes/question/radio.php @@ -14,23 +14,30 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\question; + /** * This file contains the parent class for radio question types. * * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes + * @package mod_questionnaire */ +class radio extends question { -namespace mod_questionnaire\question; -defined('MOODLE_INTERNAL') || die(); - -class radio extends base { - + /** + * Each question type must define its response class. + * @return object The response object based off of questionnaire_response_base. + */ protected function responseclass() { - return '\\mod_questionnaire\\response\\single'; + return '\\mod_questionnaire\\responsetype\\single'; } + /** + * Short name for this question type - no spaces, etc.. + * @return string + */ public function helpname() { return 'radiobuttons'; } @@ -75,42 +82,36 @@ public function supports_feedback() { /** * Return the context tags for the check question template. - * @param object $data + * @param \mod_questionnaire\responsetype\response\response $response * @param array $dependants Array of all questions/choices depending on this question. * @param boolean $blankquestionnaire * @return object The check question context tags. * */ - protected function question_survey_display($data, $dependants=[], $blankquestionnaire=false) { - // Radio buttons + protected function question_survey_display($response, $dependants=[], $blankquestionnaire=false) { + // Radio buttons. global $idcounter; // To make sure all radio buttons have unique ids. // JR 20 NOV 2007. $otherempty = false; - // Find out which radio button is checked (if any); yields choice ID. - if (isset($data->{'q'.$this->id})) { - $checked = $data->{'q'.$this->id}; - } else { - $checked = ''; - } $horizontal = $this->length; $ischecked = false; $choicetags = new \stdClass(); $choicetags->qelements = []; + foreach ($this->choices as $id => $choice) { $radio = new \stdClass(); - $other = strpos($choice->content, '!other'); if ($horizontal) { $radio->horizontal = $horizontal; } - if ($other !== 0) { // This is a normal radio button. + if (!$choice->is_other_choice()) { // This is a normal radio button. $htmlid = 'auto-rb'.sprintf('%04d', ++$idcounter); $radio->name = 'q'.$this->id; $radio->id = $htmlid; $radio->value = $id; - if ($id == $checked) { + if (isset($response->answers[$this->id][$id])) { $radio->checked = true; $ischecked = true; } @@ -121,33 +122,34 @@ protected function question_survey_display($data, $dependants=[], $blankquestion } $contents = questionnaire_choice_values($choice->content); $radio->label = $value.format_text($contents->text, FORMAT_HTML, ['noclean' => true]).$contents->image; - } else { // Radio button with associated !other text field. - $othertext = preg_replace(["/^!other=/", "/^!other/"], ['', get_string('other', 'questionnaire')], - $choice->content); - $cid = 'q'.$this->id.'_'.$id; - $otherempty = false; - if (substr($checked, 0, 6) == 'other_') { // Fix bug CONTRIB-222. - $checked = substr($checked, 6); + if (!empty($this->qlegend)) { + $radio->alabel = strip_tags("{$this->qlegend} {$radio->label}"); } + } else { // Radio button with associated !other text field. + $othertext = $choice->other_choice_display(); + $cname = choice::id_other_choice_name($id); + $odata = isset($response->answers[$this->id][$id]) ? $response->answers[$this->id][$id]->value : ''; $htmlid = 'auto-rb'.sprintf('%04d', ++$idcounter); $radio->name = 'q'.$this->id; $radio->id = $htmlid; - $radio->value = 'other_'.$id; - if (($id == $checked) || !empty($data->$cid)) { + $radio->value = $id; + if (isset($response->answers[$this->id][$id]) || !empty($odata)) { $radio->checked = true; $ischecked = true; - if (isset($data->$cid) && (trim($data->$cid) == false)) { - $otherempty = true; - } } + $otherempty = !empty($radio->checked) && empty($odata); $radio->label = format_text($othertext, FORMAT_HTML, ['noclean' => true]); - $radio->oname = $cid; + $radio->oname = 'q'.$this->id.choice::id_other_choice_name($id); $radio->oid = $htmlid.'-other'; - if (isset($data->$cid)) { - $radio->ovalue = stripslashes($data->$cid); + if (isset($odata)) { + $radio->ovalue = format_string(stripslashes($odata)); } $radio->olabel = 'Text for '.format_text($othertext, FORMAT_HTML, ['noclean' => true]); + if (!empty($this->qlegend)) { + $radio->alabel = strip_tags("{$this->qlegend} {$radio->label}"); + $radio->aolabel = strip_tags("{$this->qlegend} {$radio->olabel}"); + } } $choicetags->qelements[] = (object)['choice' => $radio]; } @@ -155,7 +157,6 @@ protected function question_survey_display($data, $dependants=[], $blankquestion // CONTRIB-846. if (!$this->required()) { $radio = new \stdClass(); - $id = ''; $htmlid = 'auto-rb'.sprintf('%04d', ++$idcounter); if ($horizontal) { $radio->horizontal = $horizontal; @@ -163,13 +164,16 @@ protected function question_survey_display($data, $dependants=[], $blankquestion $radio->name = 'q'.$this->id; $radio->id = $htmlid; - $radio->value = $id; + $radio->value = 0; if (!$ischecked && !$blankquestionnaire) { $radio->checked = true; } $content = get_string('noanswer', 'questionnaire'); $radio->label = format_text($content, FORMAT_HTML, ['noclean' => true]); + if (!empty($this->qlegend)) { + $radio->alabel = strip_tags("{$this->qlegend} {$radio->label}"); + } $choicetags->qelements[] = (object)['choice' => $radio]; } @@ -183,40 +187,44 @@ protected function question_survey_display($data, $dependants=[], $blankquestion /** * Return the context tags for the radio response template. - * @param object $data + * @param \mod_questionnaire\responsetype\response\response $response * @return object The radio question response context tags. - * */ - protected function response_survey_display($data) { + protected function response_survey_display($response) { static $uniquetag = 0; // To make sure all radios have unique names. $resptags = new \stdClass(); $resptags->choices = []; + $qdata = new \stdClass(); $horizontal = $this->length; - $checked = (isset($data->{'q'.$this->id}) ? $data->{'q'.$this->id} : ''); + if (isset($response->answers[$this->id])) { + $answer = reset($response->answers[$this->id]); + $checked = $answer->choiceid; + } else { + $checked = null; + } foreach ($this->choices as $id => $choice) { $chobj = new \stdClass(); if ($horizontal) { $chobj->horizontal = 1; } $chobj->name = $id.$uniquetag++; - if (strpos($choice->content, '!other') !== 0) { - $contents = questionnaire_choice_values($choice->content); - $choice->content = $contents->text.$contents->image; - if ($id == $checked) { - $chobj->selected = 1; + $contents = questionnaire_choice_values($choice->content); + $choice->content = $contents->text.$contents->image; + if ($id == $checked) { + $chobj->selected = 1; + if ($choice->is_other_choice()) { + $chobj->othercontent = $answer->value; } - $chobj->content = ($choice->content === '' ? $id : format_text($choice->content, FORMAT_HTML, ['noclean' => true])); + } + if ($choice->is_other_choice()) { + $chobj->content = $choice->other_choice_display(); } else { - $othertext = preg_replace(["/^!other=/", "/^!other/"], ['', get_string('other', 'questionnaire')], - $choice->content); - $cid = 'q'.$this->id.'_'.$id; - if (isset($data->{'q'.$this->id.'_'.$id})) { - $chobj->selected = 1; - $chobj->othercontent = (!empty($data->$cid) ? htmlspecialchars($data->$cid) : ' '); - } - $chobj->content = $othertext; + $chobj->content = ($choice->content === '' ? $id : format_text($choice->content, FORMAT_HTML, ['noclean' => true])); + } + if (!empty($this->qlegend)) { + $chobj->alabel = strip_tags("{$this->qlegend} {$chobj->content}"); } $resptags->choices[] = $chobj; } @@ -240,20 +248,10 @@ public function response_complete($responsedata) { } /** - * Check question's form data for valid response. Override this is type has specific format requirements. - * - * @param object $responsedata The data entered into the response. - * @return boolean + * Return the length form element. + * @param \MoodleQuickForm $mform + * @param string $helptext */ - public function response_valid($responsedata) { - if (isset($responsedata->{'q'.$this->id}) && (strpos($responsedata->{'q'.$this->id}, 'other_') !== false)) { - // False if "other" choice is checked but text box is empty. - return (trim($responsedata->{'q'.$this->id.''.substr($responsedata->{'q'.$this->id}, 5)}) != false); - } else { - return parent::response_valid($responsedata); - } - } - protected function form_length(\MoodleQuickForm $mform, $helptext = '') { $lengroup = []; $lengroup[] =& $mform->createElement('radio', 'length', '', get_string('vertical', 'questionnaire'), '0'); @@ -265,7 +263,67 @@ protected function form_length(\MoodleQuickForm $mform, $helptext = '') { return $mform; } + /** + * Return the precision form element. + * @param \MoodleQuickForm $mform + * @param string $helptext + */ protected function form_precise(\MoodleQuickForm $mform, $helptext = '') { - return base::form_precise_hidden($mform); + return question::form_precise_hidden($mform); + } + + /** + * True if question provides mobile support. + * + * @return bool + */ + public function supports_mobile() { + return true; + } + + /** + * Override and return false if not supporting mobile app. + * @param int $qnum + * @param bool $autonum + * @return \stdClass + */ + public function mobile_question_display($qnum, $autonum = false) { + $mobiledata = parent::mobile_question_display($qnum, $autonum); + $mobiledata->isradiobutton = true; + return $mobiledata; + } + + /** + * Override and return false if not supporting mobile app. + * @return array + */ + public function mobile_question_choices_display() { + $choices = parent::mobile_question_choices_display(); + foreach ($choices as $choicenum => $choice) { + if ($choice->is_other_choice()) { + $choices[$choicenum]->otherchoicekey = $this->mobile_fieldkey($choice->other_choice_name()); + $choices[$choicenum]->content = format_text($choice->other_choice_display(), FORMAT_HTML, ['noclean' => true]); + } + } + return $choices; + } + + /** + * Return the mobile response data. + * @param response $response + * @return array + */ + public function get_mobile_response_data($response) { + $resultdata = []; + if (isset($response->answers[$this->id])) { + foreach ($response->answers[$this->id] as $answer) { + // Add a fieldkey for each choice. + $resultdata[$this->mobile_fieldkey()] = $answer->choiceid; + if ($this->choices[$answer->choiceid]->is_other_choice()) { + $resultdata[$this->mobile_fieldkey($this->choices[$answer->choiceid]->other_choice_name())] = $answer->value; + } + } + } + return $resultdata; } -} \ No newline at end of file +} diff --git a/classes/question/rate.php b/classes/question/rate.php index 940bd6b3..a82962be 100644 --- a/classes/question/rate.php +++ b/classes/question/rate.php @@ -14,33 +14,52 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\question; + /** * This file contains the parent class for rate question types. * * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes + * @package mod_questionnaire */ +class rate extends question { -namespace mod_questionnaire\question; -defined('MOODLE_INTERNAL') || die(); -use \html_writer; + /** @var array $nameddegrees */ + public $nameddegrees = []; + + /** @var int Row start position of the rate table. */ + public const ROW_START = 2; -class rate extends base { + /** @var int Column start position of the rate table. */ + public const COL_START = 2; /** - * Constructor. Use to set any default properties. - * + * The class constructor + * @param int $id + * @param \stdClass $question + * @param \context $context + * @param array $params */ public function __construct($id = 0, $question = null, $context = null, $params = array()) { $this->length = 5; - return parent::__construct($id, $question, $context, $params); + parent::__construct($id, $question, $context, $params); + $this->add_nameddegrees_from_extradata(); } + /** + * Each question type must define its response class. + * @return object The response object based off of questionnaire_response_base. + */ protected function responseclass() { - return '\\mod_questionnaire\\response\\rank'; + return '\\mod_questionnaire\\responsetype\\rank'; } + /** + * Short name for this question type - no spaces, etc.. + * @return string + */ public function helpname() { return 'ratescale'; } @@ -68,6 +87,74 @@ public function response_template() { return 'mod_questionnaire/response_rate'; } + /** + * Return true if rate scale type is set to "Normal". + * @param int $scaletype + * @return bool + */ + public static function type_is_normal_rate_scale($scaletype) { + return ($scaletype == 0); + } + + /** + * Return true if rate scale type is set to "N/A column". + * @param int $scaletype + * @return bool + */ + public static function type_is_na_column($scaletype) { + return ($scaletype == 1); + } + + /** + * Return true if rate scale type is set to "No duplicate choices". + * @param int $scaletype + * @return bool + */ + public static function type_is_no_duplicate_choices($scaletype) { + return ($scaletype == 2); + } + + /** + * Return true if rate scale type is set to "Osgood". + * @param int $scaletype + * @return bool + */ + public static function type_is_osgood_rate_scale($scaletype) { + return ($scaletype == 3); + } + + /** + * Return true if rate scale type is set to "Normal". + * @return bool + */ + public function normal_rate_scale() { + return self::type_is_normal_rate_scale($this->precise); + } + + /** + * Return true if rate scale type is set to "N/A column". + * @return bool + */ + public function has_na_column() { + return self::type_is_na_column($this->precise); + } + + /** + * Return true if rate scale type is set to "No duplicate choices". + * @return bool + */ + public function no_duplicate_choices() { + return self::type_is_no_duplicate_choices($this->precise); + } + + /** + * Return true if rate scale type is set to "Osgood". + * @return bool + */ + public function osgood_rate_scale() { + return self::type_is_osgood_rate_scale($this->precise); + } + /** * True if question type supports feedback options. False by default. */ @@ -79,7 +166,8 @@ public function supports_feedback() { * True if the question supports feedback and has valid settings for feedback. Override if the default logic is not enough. */ public function valid_feedback() { - return parent::valid_feedback() && (($this->precise == 0) || ($this->precise == 3)); + return $this->supports_feedback() && $this->has_choices() && $this->required() && !empty($this->name) && + ($this->normal_rate_scale() || $this->osgood_rate_scale()) && !empty($this->nameddegrees); } /** @@ -89,14 +177,10 @@ public function valid_feedback() { public function get_feedback_maxscore() { if ($this->valid_feedback()) { $maxscore = 0; - $nbchoices = 0; - foreach ($this->choices as $choice) { - if (isset($choice->value) && ($choice->value != null)) { - if ($choice->value > $maxscore) { - $maxscore = $choice->value; - } - } else { - $nbchoices++; + $nbchoices = count($this->choices); + foreach ($this->nameddegrees as $value => $label) { + if ($value > $maxscore) { + $maxscore = $value; } } // The maximum score needs to be multiplied by the number of items to rate. @@ -109,15 +193,18 @@ public function get_feedback_maxscore() { /** * Return the context tags for the check question template. - * @param object $data - * @param string $descendantdata + * @param \mod_questionnaire\responsetype\response\response $response + * @param string $descendantsdata * @param boolean $blankquestionnaire * @return object The check question context tags. * + * TODO: This function needs to be rewritten. It is a mess! + * */ - protected function question_survey_display($data, $descendantsdata, $blankquestionnaire=false) { + protected function question_survey_display($response, $descendantsdata, $blankquestionnaire=false) { $choicetags = new \stdClass(); $choicetags->qelements = []; + $choicetags->qelements['caption'] = strip_tags($this->content); $disabled = ''; if ($blankquestionnaire) { @@ -127,12 +214,9 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti $data->{'q'.$this->id} = []; } - $isna = $this->precise == 1; - $osgood = $this->precise == 3; - // Check if rate question has one line only to display full width columns of choices. $nocontent = false; - $nameddegrees = 0; + $nameddegrees = count($this->nameddegrees); $n = []; $v = []; $maxndlen = 0; @@ -141,18 +225,8 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti if (!$nocontent && $content == '') { $nocontent = true; } - // Check for number from 1 to 3 digits, followed by the equal sign = (to accomodate named degrees). - if (preg_match("/^([0-9]{1,3})=(.*)$/", $content, $ndd)) { - $n[$nameddegrees] = format_text($ndd[2], FORMAT_HTML, ['noclean' => true]); - if (strlen($n[$nameddegrees]) > $maxndlen) { - $maxndlen = strlen($n[$nameddegrees]); - } - $v[$nameddegrees] = $ndd[1]; - $this->choices[$cid] = ''; - $nameddegrees++; - } else { - // Something wrong here. $choice->content is being set, but it will never be used. This code exists as far back as - // 2.0. + if ($nameddegrees == 0) { + // Determine if the choices have named values. $contents = questionnaire_choice_values($content); if ($contents->modname) { $choice->content = $contents->text; @@ -166,17 +240,18 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti $choicetags->qelements['twidth'] = $width; $choicetags->qelements['headerrow'] = []; // If Osgood, adjust central columns to width of named degrees if any. - if ($osgood) { + if ($this->osgood_rate_scale()) { if ($maxndlen < 4) { - $width = '45%'; + $width = 45; } else if ($maxndlen < 13) { - $width = '40%'; + $width = 40; } else { - $width = '30%'; + $width = 30; } $nn = 100 - ($width * 2); $colwidth = ($nn / $this->length).'%'; $textalign = 'right'; + $width = $width . '%'; } else if ($nocontent) { $width = '0%'; $colwidth = (100 / $this->length).'%'; @@ -189,38 +264,41 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti $choicetags->qelements['headerrow']['col1width'] = $width; - if ($isna) { + if ($this->has_na_column()) { $na = get_string('notapplicable', 'questionnaire'); } else { $na = ''; } - if ($this->precise == 2) { + if ($this->no_duplicate_choices()) { $order = 'other_rate_uncheck(name, value)'; } else { $order = ''; } - if ($this->precise != 2) { - $nbchoices = count($this->choices) - $nameddegrees; + if (!$this->no_duplicate_choices()) { + $nbchoices = count($this->choices); } else { // If "No duplicate choices", can restrict nbchoices to number of rate items specified. $nbchoices = $this->length; } // Display empty td for Not yet answered column. - if ($nbchoices > 1 && $this->precise != 2 && !$blankquestionnaire) { + if (($nbchoices > 1) && !$this->no_duplicate_choices() && !$blankquestionnaire) { $choicetags->qelements['headerrow']['colnya'] = true; } $collabel = []; - for ($j = 0; $j < $this->length; $j++) { + if ($nameddegrees > 0) { + $currentdegree = reset($this->nameddegrees); + } + for ($j = 1; $j <= $this->length; $j++) { $col = []; - if (isset($n[$j])) { - $str = $n[$j]; - $val = $v[$j]; + if (($nameddegrees > 0) && ($currentdegree !== false)) { + $str = format_text($currentdegree, FORMAT_HTML, ['noclean' => true]); + $currentdegree = next($this->nameddegrees); } else { - $str = $j + 1; - $val = $j + 1; + $str = $j; } + $val = $j; if ($blankquestionnaire) { $val = '
('.$val.')'; } else { @@ -238,8 +316,7 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti $num = 0; foreach ($this->choices as $cid => $choice) { - $str = 'q'."{$this->id}_$cid"; - $num += (isset($data->$str) && ($data->$str != -999)); + $num += (isset($response->answers[$this->id][$cid]) && ($response->answers[$this->id][$cid]->value != -999)); } $notcomplete = false; @@ -248,36 +325,32 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti $notcomplete = true; } - $row = 0; + $rowstart = self::ROW_START; $choicetags->qelements['rows'] = []; foreach ($this->choices as $cid => $choice) { $cols = []; if (isset($choice->content)) { - $row++; $str = 'q'."{$this->id}_$cid"; $content = $choice->content; - if ($osgood) { + $rendercontent = format_text($choice->content, FORMAT_PLAIN); + if ($this->osgood_rate_scale()) { list($content, $contentright) = array_merge(preg_split('/[|]/', $content), array(' ')); } - - $arialtitle = 'Choice'; - if ($notcomplete && isset($data->$str) && ($data->$str == -999)) { - $arialtitle = get_string('pleasecomplete', 'questionnaire'); - } - $cols[] = ['colstyle' => 'text-align: '.$textalign.';', 'coltext' => format_text($content, FORMAT_HTML, ['noclean' => true]).' ']; $bg = 'c0 raterow'; - if ($nbchoices > 1 && $this->precise != 2 && !$blankquestionnaire) { + $hasnotansweredchoice = false; + if (($nbchoices > 1) && !$this->no_duplicate_choices() && !$blankquestionnaire) { + $hasnotansweredchoice = true; $checked = ' checked="checked"'; $completeclass = 'notanswered'; $title = ''; - if ($notcomplete && isset($data->$str) && ($data->$str == -999)) { + if ($notcomplete && isset($response->answers[$this->id][$cid]) && + ($response->answers[$this->id][$cid]->value == -999)) { $completeclass = 'notcompleted'; $title = get_string('pleasecomplete', 'questionnaire'); } - // Set value of notanswered button to -999 in order to eliminate it from form submit later on. $colinput = ['name' => $str, 'value' => -999]; if (!empty($checked)) { @@ -286,25 +359,35 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti if (!empty($order)) { $colinput['onclick'] = $order; } + $colinput['label'] = $this->set_label($rowstart, $rendercontent, self::COL_START, + get_string('unanswered', 'questionnaire')); $cols[] = ['colstyle' => 'width:1%;', 'colclass' => $completeclass, 'coltitle' => $title, 'colinput' => $colinput]; - $choicetags->qelements['title'] = $title; - } - for ($j = 0; $j < $this->length + $isna; $j++) { + if ($nameddegrees > 0) { + reset($this->nameddegrees); + } + $colstart = $hasnotansweredchoice ? self::COL_START + 1 : self::COL_START; + for ($j = 1; $j <= $this->length + $this->has_na_column(); $j++) { + if (!isset($collabel[$j])) { + // If not using this value, continue. + continue; + } $col = []; - $checked = ((isset($data->$str) && ($j == $data->$str || - $j == $this->length && $data->$str == -1)) ? ' checked="checked"' : ''); $checked = ''; - if (isset($data->$str) && ($j == $data->$str || $j == $this->length && $data->$str == -1)) { + // If isna column then set na choice to -1 value. This needs work! + if (!empty($this->nameddegrees) && (key($this->nameddegrees) !== null)) { + $value = key($this->nameddegrees); + next($this->nameddegrees); + } else { + $value = ($j <= $this->length ? $j : -1); + } + if (isset($response->answers[$this->id][$cid]) && ($value == $response->answers[$this->id][$cid]->value)) { $checked = ' checked="checked"'; } $col['colstyle'] = 'text-align:center'; $col['colclass'] = $bg; - $i = $j + 1; - $col['colhiddentext'] = get_string('option', 'questionnaire', $i); - // If isna column then set na choice to -1 value. - $value = ($j < $this->length ? $j : - 1); + $col['colhiddentext'] = get_string('option', 'questionnaire', $j); $col['colinput']['name'] = $str; $col['colinput']['value'] = $value; $col['colinput']['id'] = $str.'_'.$value; @@ -317,18 +400,20 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti if (!empty($order)) { $col['colinput']['onclick'] = $order; } - $col['colinput']['label'] = 'Choice '.$collabel[$j].' for row '.format_text($content, FORMAT_PLAIN); + $col['colinput']['label'] = $this->set_label($rowstart, $rendercontent, $colstart, $collabel[$j]); if ($bg == 'c0 raterow') { $bg = 'c1 raterow'; } else { $bg = 'c0 raterow'; } + $colstart++; $cols[] = $col; } - if ($osgood) { + if ($this->osgood_rate_scale()) { $cols[] = ['coltext' => ' '.format_text($contentright, FORMAT_HTML, ['noclean' => true])]; } - $choicetags->qelements['rows'][] = ['cols' => $cols, 'title' => $arialtitle]; + $choicetags->qelements['rows'][] = ['cols' => $cols]; + $rowstart++; } } @@ -337,24 +422,23 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti /** * Return the context tags for the rate response template. - * @param object $data - * @return object The rate question response context tags. - * + * @param \mod_questionnaire\responsetype\response\response $response + * @return \stdClass The rate question response context tags. + * @throws \coding_exception */ - protected function response_survey_display($data) { + protected function response_survey_display($response) { static $uniquetag = 0; // To make sure all radios have unique names. $resptags = new \stdClass(); $resptags->headers = []; $resptags->rows = []; - if (!isset($data->{'q'.$this->id}) || !is_array($data->{'q'.$this->id})) { - $data->{'q'.$this->id} = array(); + if (!isset($response->answers[$this->id])) { + $response->answers[$this->id][] = new \mod_questionnaire\responsetype\answer\answer(); } // Check if rate question has one line only to display full width columns of choices. $nocontent = false; foreach ($this->choices as $cid => $choice) { - $content = $choice->content; if ($choice->content == '') { $nocontent = true; break; @@ -362,35 +446,24 @@ protected function response_survey_display($data) { } $resptags->twidth = $nocontent ? "50%" : "99.9%"; - $osgood = $this->precise == 3; $bg = 'c0'; $nameddegrees = 0; $cidnamed = array(); - $n = array(); // Max length of potential named degree in column head. $maxndlen = 0; - foreach ($this->choices as $cid => $choice) { - $content = $choice->content; - if (preg_match("/^[0-9]{1,3}=/", $content, $ndd)) { - $ndd = format_text(substr($content, strlen($ndd[0])), FORMAT_HTML, ['noclean' => true]); - $n[$nameddegrees] = $ndd; - if (strlen($ndd) > $maxndlen) { - $maxndlen = strlen($ndd); - } - $cidnamed[$cid] = true; - $nameddegrees++; - } - } - if ($osgood) { + if ($this->osgood_rate_scale()) { $resptags->osgood = 1; if ($maxndlen < 4) { $sidecolwidth = '45%'; + $sidecolwidthn = 45; } else if ($maxndlen < 13) { $sidecolwidth = '40%'; + $sidecolwidthn = 40; } else { $sidecolwidth = '30%'; + $sidecolwidthn = 30; } - $nn = 100 - ($sidecolwidth * 2); + $nn = 100 - ($sidecolwidthn * 2); $resptags->sidecolwidth = $sidecolwidth; $resptags->colwidth = ($nn / $this->length).'%'; $resptags->textalign = 'right'; @@ -399,13 +472,18 @@ protected function response_survey_display($data) { $resptags->colwidth = (50 / $this->length).'%'; $resptags->textalign = 'left'; } - for ($j = 0; $j < $this->length; $j++) { + if (!empty($this->nameddegrees)) { + $this->length = count($this->nameddegrees); + reset($this->nameddegrees); + } + for ($j = 1; $j <= $this->length; $j++) { $cellobj = new \stdClass(); $cellobj->bg = $bg; - if (isset($n[$j])) { - $cellobj->str = $n[$j]; + if (!empty($this->nameddegrees)) { + $cellobj->str = current($this->nameddegrees); + next($this->nameddegrees); } else { - $cellobj->str = $j + 1; + $cellobj->str = $j; } if ($bg == 'c0') { $bg = 'c1'; @@ -414,7 +492,7 @@ protected function response_survey_display($data) { } $resptags->headers[] = $cellobj; } - if ($this->precise == 1) { + if ($this->has_na_column()) { $cellobj = new \stdClass(); $cellobj->bg = $bg; $cellobj->str = get_string('notapplicable', 'questionnaire'); @@ -431,21 +509,32 @@ protected function response_survey_display($data) { if ($contents->modname) { $content = $contents->text; } - if ($osgood) { + if ($this->osgood_rate_scale()) { list($content, $contentright) = array_merge(preg_split('/[|]/', $content), array(' ')); } $rowobj->content = format_text($content, FORMAT_HTML, ['noclean' => true]).' '; $bg = 'c0'; $cols = []; - for ($j = 0; $j < $this->length; $j++) { + if (!empty($this->nameddegrees)) { + $this->length = count($this->nameddegrees); + reset($this->nameddegrees); + } + for ($j = 1; $j <= $this->length; $j++) { $cellobj = new \stdClass(); - if (isset($data->$str) && ($j == $data->$str)) { - $cellobj->checked = 1; + if (isset($response->answers[$this->id][$cid])) { + if (!empty($this->nameddegrees)) { + if ($response->answers[$this->id][$cid]->value == key($this->nameddegrees)) { + $cellobj->checked = 1; + } + next($this->nameddegrees); + } else if ($j == $response->answers[$this->id][$cid]->value) { + $cellobj->checked = 1; + } } $cellobj->str = $str.$j.$uniquetag++; $cellobj->bg = $bg; // N/A column checked. - $checkedna = (isset($data->$str) && ($data->$str == -1)); + $checkedna = (isset($response->answers[$this->id][$cid]) && ($response->answers[$this->id][$cid]->value == -1)); if ($bg == 'c0') { $bg = 'c1'; } else { @@ -453,7 +542,7 @@ protected function response_survey_display($data) { } $cols[] = $cellobj; } - if ($this->precise == 1) { // N/A column. + if ($this->has_na_column()) { // N/A column. $cellobj = new \stdClass(); if ($checkedna) { $cellobj->checked = 1; @@ -463,7 +552,7 @@ protected function response_survey_display($data) { $cols[] = $cellobj; } $rowobj->cols = $cols; - if ($osgood) { + if ($this->osgood_rate_scale()) { $rowobj->osgoodstr = ' '.format_text($contentright, FORMAT_HTML, ['noclean' => true]); } $resptags->rows[] = $rowobj; @@ -475,15 +564,29 @@ protected function response_survey_display($data) { /** * Check question's form data for complete response. * - * @param object $responsedata The data entered into the response. + * @param \stdClass $responsedata The data entered into the response. * @return boolean * */ public function response_complete($responsedata) { + if (!is_a($responsedata, 'mod_questionnaire\responsetype\response\response')) { + $response = \mod_questionnaire\responsetype\response\response::response_from_webform($responsedata, [$this]); + } else { + $response = $responsedata; + } + + // To make it easier, create an array of answers by choiceid. + $answers = []; + if (isset($response->answers[$this->id])) { + foreach ($response->answers[$this->id] as $answer) { + $answers[$answer->choiceid] = $answer; + } + } + + $answered = true; $num = 0; $nbchoices = count($this->choices); $na = get_string('notapplicable', 'questionnaire'); - $complete = true; foreach ($this->choices as $cid => $choice) { // In case we have named degrees on the Likert scale, count them to substract from nbchoices. $nameddegrees = 0; @@ -491,36 +594,50 @@ public function response_complete($responsedata) { if (preg_match("/^[0-9]{1,3}=/", $content)) { $nameddegrees++; } else { - $str = 'q'."{$this->id}_$cid"; - if (isset($responsedata->$str) && $responsedata->$str == $na) { - $responsedata->$str = -1; + if (isset($answers[$cid]) && !empty($answers[$cid]) && ($answers[$cid]->value == $na)) { + $answers[$cid]->value = -1; } // If choice value == -999 this is a not yet answered choice. - $num += (isset($responsedata->$str) && ($responsedata->$str != -999)); + $num += (isset($answers[$cid]) && ($answers[$cid]->value != -999)); } $nbchoices -= $nameddegrees; } if ($num == 0) { - if (!$this->has_dependencies()) { - if ($this->required()) { - $complete = false; - } + if ($this->required()) { + $answered = false; } } - return $complete; + return $answered; } /** * Check question's form data for valid response. Override this is type has specific format requirements. * - * @param object $responsedata The data entered into the response. + * @param \stdClass $responsedata The data entered into the response. * @return boolean */ public function response_valid($responsedata) { + // Work with a response object. + if (!is_a($responsedata, 'mod_questionnaire\responsetype\response\response')) { + $response = \mod_questionnaire\responsetype\response\response::response_from_webform($responsedata, [$this]); + } else { + $response = $responsedata; + } $num = 0; $nbchoices = count($this->choices); $na = get_string('notapplicable', 'questionnaire'); + + // Create an answers array indexed by choiceid for ease. + $answers = []; + $nodups = []; + if (isset($response->answers[$this->id])) { + foreach ($response->answers[$this->id] as $answer) { + $answers[$answer->choiceid] = $answer; + $nodups[] = $answer->value; + } + } + foreach ($this->choices as $cid => $choice) { // In case we have named degrees on the Likert scale, count them to substract from nbchoices. $nameddegrees = 0; @@ -528,20 +645,29 @@ public function response_valid($responsedata) { if (preg_match("/^[0-9]{1,3}=/", $content)) { $nameddegrees++; } else { - $str = 'q'."{$this->id}_$cid"; - if (isset($responsedata->$str) && ($responsedata->$str == $na)) { - $responsedata->$str = -1; + if (isset($answers[$cid]) && ($answers[$cid]->value == $na)) { + $answers[$cid]->value = -1; } // If choice value == -999 this is a not yet answered choice. - $num += (isset($responsedata->$str) && ($responsedata->$str != -999)); + $num += (isset($answers[$cid]) && ($answers[$cid]->value != -999)); } $nbchoices -= $nameddegrees; } // If nodupes and nb choice restricted, nbchoices may be > actual choices, so limit it to $question->length. - $isrestricted = ($this->length < count($this->choices)) && ($this->precise == 2); + $isrestricted = ($this->length < count($this->choices)) && $this->no_duplicate_choices(); if ($isrestricted) { $nbchoices = min ($nbchoices, $this->length); } + + // Test for duplicate answers in a no duplicate question type. + if ($this->no_duplicate_choices()) { + foreach ($answers as $answer) { + if (count(array_keys($nodups, $answer->value)) > 1) { + return false; + } + } + } + if (($num != $nbchoices) && ($num != 0)) { return false; } else { @@ -549,10 +675,20 @@ public function response_valid($responsedata) { } } + /** + * Return the length form element. + * @param \MoodleQuickForm $mform + * @param string $helptext + */ protected function form_length(\MoodleQuickForm $mform, $helptext = '') { return parent::form_length($mform, 'numberscaleitems'); } + /** + * Return the precision form element. + * @param \MoodleQuickForm $mform + * @param string $helptext + */ protected function form_precise(\MoodleQuickForm $mform, $helptext = '') { $precoptions = array("0" => get_string('normal', 'questionnaire'), "1" => get_string('notapplicablecolumn', 'questionnaire'), @@ -566,7 +702,54 @@ protected function form_precise(\MoodleQuickForm $mform, $helptext = '') { } /** - * Preprocess choice data. + * Override if the question uses the extradata field. + * @param \MoodleQuickForm $mform + * @param string $helpname + * @return \MoodleQuickForm + */ + protected function form_extradata(\MoodleQuickForm $mform, $helpname = '') { + $defaultvalue = ''; + foreach ($this->nameddegrees as $value => $label) { + $defaultvalue .= $value . '=' . $label . "\n"; + } + + $options = ['wrap' => 'virtual']; + $mform->addElement('textarea', 'allnameddegrees', get_string('allnameddegrees', 'questionnaire'), $options); + $mform->setDefault('allnameddegrees', $defaultvalue); + $mform->setType('allnameddegrees', PARAM_RAW); + $mform->addHelpButton('allnameddegrees', 'allnameddegrees', 'questionnaire'); + + return $mform; + } + + /** + * Any preprocessing of general data. + * @param \stdClass $formdata + * @return bool + */ + protected function form_preprocess_data($formdata) { + $nameddegrees = []; + // Named degrees are put one per line in the form "[value]=[label]". + if (!empty($formdata->allnameddegrees)) { + $nameddegreelines = explode("\n", $formdata->allnameddegrees); + foreach ($nameddegreelines as $nameddegreeline) { + $nameddegreeline = trim($nameddegreeline); + if (($nameddegree = \mod_questionnaire\question\choice::content_is_named_degree_choice($nameddegreeline)) !== + false) { + $nameddegrees += $nameddegree; + } + } + } + + // Now store the new named degrees in extradata. + $formdata->extradata = json_encode($nameddegrees); + return parent::form_preprocess_data($formdata); + } + + /** + * Override this function for question specific choice preprocessing. + * @param \stdClass $formdata + * @return false */ protected function form_preprocess_choicedata($formdata) { if (empty($formdata->allchoices)) { @@ -601,10 +784,341 @@ protected function form_preprocess_choicedata($formdata) { $formdata->length = $nbnameddegrees; } // Sanity check for "no duplicate choices"". - if ($formdata->precise == 2 && ($formdata->length > $nbvalues || !$formdata->length)) { + if (self::type_is_no_duplicate_choices($formdata->precise) && ($formdata->length > $nbvalues || !$formdata->length)) { $formdata->length = $nbvalues; } } return true; } -} \ No newline at end of file + + /** + * Update the choice with the choicerecord. + * @param \stdClass $choicerecord + * @return bool + */ + public function update_choice($choicerecord) { + if ($nameddegree = \mod_questionnaire\question\choice::content_is_named_degree_choice($choicerecord->content)) { + // Preserve any existing value from the new array. + $this->nameddegrees = $nameddegree + $this->nameddegrees; + $this->insert_nameddegrees($this->nameddegrees); + } + return parent::update_choice($choicerecord); + } + + /** + * Add a new choice to the database. + * @param \stdClass $choicerecord + * @return bool + */ + public function add_choice($choicerecord) { + if ($nameddegree = \mod_questionnaire\question\choice::content_is_named_degree_choice($choicerecord->content)) { + // Preserve any existing value from the new array. + $this->nameddegrees = $nameddegree + $this->nameddegrees; + $this->insert_nameddegrees($this->nameddegrees); + } + return parent::add_choice($choicerecord); + } + + /** + * True if question provides mobile support. + * + * @return bool + */ + public function supports_mobile() { + return true; + } + + /** + * Override and return false if not supporting mobile app. + * @param int $qnum + * @param bool $autonum + * @return \stdClass + */ + public function mobile_question_display($qnum, $autonum = false) { + $mobiledata = parent::mobile_question_display($qnum, $autonum); + $mobiledata->rates = $this->mobile_question_rates_display(); + if ($this->has_na_column()) { + $mobiledata->hasnacolumn = (object)['value' => -1, 'label' => get_string('notapplicable', 'questionnaire')]; + } + + $mobiledata->israte = true; + return $mobiledata; + } + + /** + * Override and return false if not supporting mobile app. + * @return array + */ + public function mobile_question_choices_display() { + $choices = []; + $excludes = []; + $vals = $extracontents = []; + $cnum = 0; + foreach ($this->choices as $choiceid => $choice) { + $choice->na = false; + $choice->choice_id = $choiceid; + $choice->id = $choiceid; + $choice->question_id = $this->id; + + // Add a fieldkey for each choice. + $choice->fieldkey = $this->mobile_fieldkey($choiceid); + + if ($this->osgood_rate_scale()) { + list($choice->leftlabel, $choice->rightlabel) = array_merge(preg_split('/[|]/', $choice->content), []); + } + + if ($this->normal_rate_scale() || $this->no_duplicate_choices()) { + $choices[$cnum] = $choice; + if ($this->required()) { + $choices[$cnum]->min = 0; + $choices[$cnum]->minstr = 1; + } else { + $choices[$cnum]->min = 0; + $choices[$cnum]->minstr = 1; + } + $choices[$cnum]->max = intval($this->length) - 1; + $choices[$cnum]->maxstr = intval($this->length); + + } else if ($this->has_na_column()) { + $choices[$cnum] = $choice; + if ($this->required()) { + $choices[$cnum]->min = 0; + $choices[$cnum]->minstr = 1; + } else { + $choices[$cnum]->min = 0; + $choices[$cnum]->minstr = 1; + } + $choices[$cnum]->max = intval($this->length); + $choices[$cnum]->na = true; + + } else { + $excludes[$choiceid] = $choiceid; + if ($choice->value == null) { + if ($arr = explode('|', $choice->content)) { + if (count($arr) == 2) { + $choices[$cnum] = $choice; + $choices[$cnum]->content = ''; + $choices[$cnum]->minstr = $arr[0]; + $choices[$cnum]->maxstr = $arr[1]; + } + } + } else { + $val = intval($choice->value); + $vals[$val] = $val; + $extracontents[] = $choice->content; + } + } + if ($vals) { + if ($q = $choices) { + foreach (array_keys($q) as $itemid) { + $choices[$itemid]->min = min($vals); + $choices[$itemid]->max = max($vals); + } + } + } + if ($extracontents) { + $extracontents = array_unique($extracontents); + $extrahtml = '
    '; + foreach ($extracontents as $extracontent) { + $extrahtml .= '
  • '.$extracontent.'
  • '; + } + $extrahtml .= '
'; + $options = ['noclean' => true, 'para' => false, 'filter' => true, + 'context' => $this->context, 'overflowdiv' => true]; + $choice->content .= format_text($extrahtml, FORMAT_HTML, $options); + } + + if (!in_array($choiceid, $excludes)) { + $choice->choice_id = $choiceid; + if ($choice->value == null) { + $choice->value = ''; + } + $choices[$cnum] = $choice; + } + $cnum++; + } + + return $choices; + } + + /** + * Display the rates question for mobile. + * @return array + */ + public function mobile_question_rates_display() { + $rates = []; + if (!empty($this->nameddegrees)) { + foreach ($this->nameddegrees as $value => $label) { + $rates[] = (object)['value' => $value, 'label' => $label]; + } + } else { + for ($i = 1; $i <= $this->length; $i++) { + $rates[] = (object)['value' => $i, 'label' => $i]; + } + } + return $rates; + } + + /** + * Return the mobile response data. + * @param response $response + * @return array + */ + public function get_mobile_response_data($response) { + $resultdata = []; + if (isset($response->answers[$this->id])) { + foreach ($response->answers[$this->id] as $answer) { + // Add a fieldkey for each choice. + if (!empty($this->nameddegrees)) { + if (isset($this->nameddegrees[$answer->value])) { + $resultdata[$this->mobile_fieldkey($answer->choiceid)] = $this->nameddegrees[$answer->value]; + } else { + $resultdata[$this->mobile_fieldkey($answer->choiceid)] = $answer->value; + } + } else { + $resultdata[$this->mobile_fieldkey($answer->choiceid)] = $answer->value; + } + } + } + return $resultdata; + } + + /** + * Add the nameddegrees property. + */ + private function add_nameddegrees_from_extradata() { + if (!empty($this->extradata)) { + $this->nameddegrees = json_decode($this->extradata, true); + } + } + + /** + * Insert nameddegress to the extradata database field. + * @param array $nameddegrees + * @return bool + * @throws \dml_exception + */ + public function insert_nameddegrees(array $nameddegrees) { + return $this->insert_extradata(json_encode($nameddegrees)); + } + + /** + * Helper function used to move existing named degree choices for the specified question from the "quest_choice" table to the + * "question" table. + * @param int $qid + * @param null|\stdClass $questionrec + */ + public static function move_nameddegree_choices(int $qid = 0, \stdClass $questionrec = null) { + global $DB; + + if ($qid !== 0) { + $question = new rate($qid); + } else { + $question = new rate(0, $questionrec); + } + $nameddegrees = []; + $oldchoiceids = []; + // There was an issue where rate values were being stored as 1..n, no matter what the named degree value was. We need to fix + // the old responses now. This also assumes that the values are now 1 based rather than 0 based. + $newvalues = []; + $oldval = 1; + foreach ($question->choices as $choice) { + if ($nameddegree = $choice->is_named_degree_choice()) { + $nameddegrees += $nameddegree; + $oldchoiceids[] = $choice->id; + reset($nameddegree); + $newvalues[$oldval++] = key($nameddegree); + } + } + + if (!empty($nameddegrees)) { + if ($question->insert_nameddegrees($nameddegrees)) { + // Remove the old named desgree from the choices table. + foreach ($oldchoiceids as $choiceid) { + \mod_questionnaire\question\choice::delete_from_db_by_id($choiceid); + } + + // First get all existing rank responses for this question. + $responses = $DB->get_recordset('questionnaire_response_rank', ['question_id' => $question->id]); + // Iterating over each response record ensures we won't change an existing record more than once. + foreach ($responses as $response) { + // Then, if the old value exists, set it to the new one. + if (isset($newvalues[$response->rankvalue])) { + $DB->set_field('questionnaire_response_rank', 'rankvalue', $newvalues[$response->rankvalue], + ['id' => $response->id]); + } + } + $responses->close(); + } + } + } + + /** + * Helper function to move named degree choices for all questions, optionally for a specific surveyid. + * This should only be called for an upgrade from before '2018110103', or from a restore operation for a version of a + * questionnaire before '2018110103'. + * @param int|null $surveyid + */ + public static function move_all_nameddegree_choices(int $surveyid = null) { + global $DB; + + // This operation might take a while. Cancel PHP timeouts for this. + \core_php_time_limit::raise(); + + // First, let's adjust all rate answers from zero based to one based (see GHI223). + // If a specific survey is being dealt with, only use the questions from that survey. + $skip = false; + if ($surveyid !== null) { + $qids = $DB->get_records_menu('questionnaire_question', ['surveyid' => $surveyid, 'type_id' => QUESRATE], + '', 'id,surveyid'); + if (!empty($qids)) { + list($qsql, $qparams) = $DB->get_in_or_equal(array_keys($qids)); + } else { + // No relevant questions, so no need to do this step. + $skip = true; + } + } + + // If we're doing this step, let's do it. + if (!$skip) { + $select = 'UPDATE {questionnaire_response_rank} ' . + 'SET rankvalue = (rankvalue + 1) ' . + 'WHERE (rankvalue >= 0)'; + if ($surveyid !== null) { + $select .= ' AND (question_id ' . $qsql . ')'; + } else { + $qparams = []; + } + $DB->execute($select, $qparams); + } + + $args = ['type_id' => QUESRATE]; + if ($surveyid !== null) { + $args['surveyid'] = $surveyid; + } + $ratequests = $DB->get_recordset('questionnaire_question', $args); + foreach ($ratequests as $questionrec) { + self::move_nameddegree_choices(0, $questionrec); + } + $ratequests->close(); + } + + /** + * Set label for per column inside rate table. + * + * @param int $rowposition + * @param string $choicetitle + * @param int $colposition + * @param string $choiceanswer + * @return string + */ + private function set_label(int $rowposition, string $choicetitle, int $colposition, string $choiceanswer): string { + $a = (object) [ + 'rowposition' => $rowposition, + 'choicetitle' => $choicetitle, + 'colposition' => $colposition, + 'choiceanswer' => $choiceanswer, + ]; + return get_string('accessibility:rate:choice', 'questionnaire', $a); + } +} diff --git a/classes/question/sectiontext.php b/classes/question/sectiontext.php index f0ee6273..107e6845 100644 --- a/classes/question/sectiontext.php +++ b/classes/question/sectiontext.php @@ -14,23 +14,32 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\question; + +use mod_questionnaire\feedback\section; + /** * This file contains the parent class for sectiontext question types. * * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes + * @package mod_questionnaire */ +class sectiontext extends question { -namespace mod_questionnaire\question; -defined('MOODLE_INTERNAL') || die(); - -class sectiontext extends base { - + /** + * Each question type must define its response class. + * @return object The response object based off of questionnaire_response_base. + */ protected function responseclass() { return ''; } + /** + * Short name for this question type - no spaces, etc.. + * @return string + */ public function helpname() { return 'sectiontext'; } @@ -47,9 +56,47 @@ public function required() { * True if question type supports feedback options. False by default. */ public function supports_feedback() { + return false; + } + + /** + * True if question provides mobile support. + * @return bool + */ + public function supports_mobile() { return true; } + /** + * Display on mobile. + * + * @param int $qnum + * @param bool $autonum + */ + public function mobile_question_display($qnum, $autonum = false) { + $options = ['noclean' => true, 'para' => false, 'filter' => true, + 'context' => $this->context, 'overflowdiv' => true]; + $mobiledata = (object)[ + 'id' => $this->id, + 'name' => $this->name, + 'type_id' => $this->type_id, + 'length' => $this->length, + 'content' => format_text(file_rewrite_pluginfile_urls($this->content, 'pluginfile.php', $this->context->id, + 'mod_questionnaire', 'question', $this->id), FORMAT_HTML, $options), + 'content_stripped' => strip_tags($this->content), + 'required' => false, + 'deleted' => $this->deleted, + 'response_table' => $this->responsetable, + 'fieldkey' => $this->mobile_fieldkey(), + 'precise' => $this->precise, + 'qnum' => '', + 'errormessage' => get_string('required') . ': ' . $this->name + ]; + + $mobiledata->issectiontext = true; + return $mobiledata; + } + /** * True if question type supports feedback scores and weights. Same as supports_feedback() by default. */ @@ -61,7 +108,7 @@ public function supports_feedback_scores() { * True if the question supports feedback and has valid settings for feedback. Override if the default logic is not enough. */ public function valid_feedback() { - return true; + return false; } /** @@ -72,23 +119,41 @@ public function question_template() { return 'mod_questionnaire/question_sectionfb'; } - protected function question_survey_display($data, $descendantsdata, $blankquestionnaire=false) { + /** + * Override and return false if a number should not be rendered for this question in any context. + * @return bool + */ + public function is_numbered() { + return false; + } + + /** + * Return the context tags for the check question template. + * @param \mod_questionnaire\responsetype\response\response $response + * @param array $descendantsdata Array of all questions/choices depending on this question. + * @param boolean $blankquestionnaire + * @return object The check question context tags. + * + */ + protected function question_survey_display($response, $descendantsdata, $blankquestionnaire=false) { global $DB, $CFG, $PAGE; require_once($CFG->dirroot.'/mod/questionnaire/questionnaire.class.php'); // If !isset then normal behavior as sectiontext question. - if (!isset($data->questionnaire_id)) { + if (!isset($response->questionnaireid)) { return ''; } - $fbsections = $DB->get_records('questionnaire_fb_sections', ['surveyid' => $this->surveyid]); $filteredsections = []; // In which section(s) is this question? - foreach ($fbsections as $key => $fbsection) { - $scorecalculation = unserialize($fbsection->scorecalculation); - if (array_key_exists($this->id, $scorecalculation)) { - array_push($filteredsections, $fbsection->section); + if ($fbsections = $DB->get_records('questionnaire_fb_sections', ['surveyid' => $this->surveyid])) { + foreach ($fbsections as $key => $fbsection) { + if ($scorecalculation = section::decode_scorecalculation($fbsection->scorecalculation)) { + if (array_key_exists($this->id, $scorecalculation)) { + array_push($filteredsections, $fbsection->section); + } + } } } @@ -97,8 +162,8 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti return ''; } - list($cm, $course, $questionnaire) = questionnaire_get_standard_page_items(null, $data->questionnaire_id); - $questionnaire = new \questionnaire(0, $questionnaire, $course, $cm); + list($cm, $course, $questionnaire) = questionnaire_get_standard_page_items(null, $response->questionnaireid); + $questionnaire = new \questionnaire($course, $cm, 0, $questionnaire); $questionnaire->add_renderer($PAGE->get_renderer('mod_questionnaire')); $questionnaire->add_page(new \mod_questionnaire\output\reportpage()); @@ -106,8 +171,8 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti $allresponses = false; $currentgroupid = 0; $isgroupmember = false; - $resps = [$data->rid => null]; - $rid = $data->rid; + $rid = (isset($response->id) && !empty($response->id)) ? $response->id : 0; + $resps = [$rid => null]; // For $filteredsections -> get the feedback messages only for this sections! $feedbackmessages = $questionnaire->response_analysis($rid, $resps, $compare, $isgroupmember, $allresponses, $currentgroupid, $filteredsections); @@ -121,9 +186,13 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti $questiontags->qelements->choice = $choice; return $questiontags; - } + /** + * Question specific response display method. + * @param \stdClass $data + * + */ protected function response_survey_display($data) { return ''; } @@ -138,22 +207,30 @@ public function response_complete($responsedata) { return true; } - /* - //name is required for feedbacksections and better organization of different sectiontext questions - protected function form_name(\MoodleQuickForm $mform) { - return $mform; - } - */ - + /** + * Add the form required field. + * @param \MoodleQuickForm $mform + * @return \MoodleQuickForm + */ protected function form_required(\MoodleQuickForm $mform) { return $mform; } + /** + * Return the length form element. + * @param \MoodleQuickForm $mform + * @param string $helpname + */ protected function form_length(\MoodleQuickForm $mform, $helpname = '') { - return base::form_length_hidden($mform); + return question::form_length_hidden($mform); } + /** + * Return the precision form element. + * @param \MoodleQuickForm $mform + * @param string $helpname + */ protected function form_precise(\MoodleQuickForm $mform, $helpname = '') { - return base::form_precise_hidden($mform); + return question::form_precise_hidden($mform); } -} \ No newline at end of file +} diff --git a/classes/question/slider.php b/classes/question/slider.php new file mode 100644 index 00000000..005c6bab --- /dev/null +++ b/classes/question/slider.php @@ -0,0 +1,337 @@ +. + +namespace mod_questionnaire\question; + +/** + * This file contains the parent class for slider question types. + * + * @author Hieu Vu Van + * @copyright 2022 The Open University. + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire + */ +class slider extends question { + + /** + * Return the responseclass used. + * @return string + */ + protected function responseclass() { + return '\\mod_questionnaire\\responsetype\\slider'; + } + + /** + * Return the help name. + * @return string + */ + public function helpname() { + return 'slider'; + } + + /** + * Return true if the question has choices. + */ + public function has_choices() { + return false; + } + + /** + * Override and return a form template if provided. Output of question_survey_display is iterpreted based on this. + * + * @return boolean | string + */ + public function question_template() { + return 'mod_questionnaire/question_slider'; + } + + /** + * Override and return a response template if provided. Output of response_survey_display is iterpreted based on this. + * + * @return boolean | string + */ + public function response_template() { + return 'mod_questionnaire/response_slider'; + } + + /** + * True if question type supports feedback options. False by default. + * @return bool + */ + public function supports_feedback() { + return true; + } + + /** + * True if the question supports feedback and has valid settings for feedback. Override if the default logic is not enough. + * @return bool + */ + public function valid_feedback() { + $extradata = json_decode($this->extradata); + $minrange = $extradata->minrange; + // Negative scores are not accepted in Feedback. + return $this->supports_feedback() && !empty($this->name) && $minrange >= 0; + } + + /** + * Get the maximum score possible for feedback if appropriate. Override if default behaviour is not correct. + * @return int | boolean + */ + public function get_feedback_maxscore() { + if ($this->valid_feedback()) { + $extradata = json_decode($this->extradata); + $maxscore = $extradata->maxrange; + } else { + $maxscore = false; + } + return $maxscore; + } + + /** + * Return the context tags for the check question template. + * + * @param \mod_questionnaire\responsetype\response\response $response + * @param array $dependants Array of all questions/choices depending on this question. + * @param boolean $blankquestionnaire + * @return object The check question context tags. + * + */ + protected function question_survey_display($response, $dependants = [], $blankquestionnaire = false) { + global $PAGE; + $PAGE->requires->js_init_call('M.mod_questionnaire.init_slider', null, false, questionnaire_get_js_module()); + $extradata = json_decode($this->extradata); + $questiontags = new \stdClass(); + if (isset($response->answers[$this->id][0])) { + $extradata->startingvalue = $response->answers[$this->id][0]->value; + } + $extradata->name = 'q' . $this->id; + $extradata->id = self::qtypename($this->type_id) . $this->id; + $questiontags->qelements = new \stdClass(); + $questiontags->qelements->extradata = $extradata; + return $questiontags; + } + + /** + * Return the context tags for the slider response template. + * @param \mod_questionnaire\responsetype\response\response $response + * @return \stdClass The check question response context tags. + */ + protected function response_survey_display($response) { + global $PAGE; + $PAGE->requires->js_init_call('M.mod_questionnaire.init_slider', null, false, questionnaire_get_js_module()); + + $resptags = new \stdClass(); + if (isset($response->answers[$this->id])) { + $answer = reset($response->answers[$this->id]); + $resptags->content = format_text($answer->value, FORMAT_HTML); + if (!empty($response->answers[$this->id]['extradata'])) { + $resptags->extradata = $response->answers[$this->id]['extradata']; + } else { + $extradata = json_decode($this->extradata); + $resptags->extradata = $extradata; + } + } + return $resptags; + } + + /** + * Add the form required field. + * @param \MoodleQuickForm $mform + * @return \MoodleQuickForm + */ + protected function form_required(\MoodleQuickForm $mform) { + return $mform; + } + + /** + * Return the form precision. + * @param \MoodleQuickForm $mform + * @param string $helptext + * @return \MoodleQuickForm|void + */ + protected function form_precise(\MoodleQuickForm $mform, $helptext = '') { + return question::form_precise_hidden($mform); + } + + /** + * Return the form length. + * @param \MoodleQuickForm $mform + * @param string $helptext + * @return \MoodleQuickForm|void + */ + protected function form_length(\MoodleQuickForm $mform, $helptext = '') { + return question::form_length_hidden($mform); + } + + /** + * Override if the question uses the extradata field. + * @param \MoodleQuickForm $mform + * @param string $helpname + * @return \MoodleQuickForm + */ + protected function form_extradata(\MoodleQuickForm $mform, $helpname = '') { + $minelementname = 'minrange'; + $maxelementname = 'maxrange'; + $startingvalue = 'startingvalue'; + $stepvalue = 'stepvalue'; + + $ranges = []; + if (!empty($this->extradata)) { + $ranges = json_decode($this->extradata); + } + $mform->addElement('text', 'leftlabel', get_string('leftlabel', 'questionnaire')); + $mform->setType('leftlabel', PARAM_RAW); + if (isset($ranges->leftlabel)) { + $mform->setDefault('leftlabel', $ranges->leftlabel); + } + $mform->addElement('text', 'centerlabel', get_string('centerlabel', 'questionnaire')); + $mform->setType('centerlabel', PARAM_RAW); + if (isset($ranges->centerlabel)) { + $mform->setDefault('centerlabel', $ranges->centerlabel); + } + $mform->addElement('text', 'rightlabel', get_string('rightlabel', 'questionnaire')); + $mform->setType('rightlabel', PARAM_RAW); + if (isset($ranges->rightlabel)) { + $mform->setDefault('rightlabel', $ranges->rightlabel); + } + + $patterint = '/^-?\d+$/'; + $mform->addElement('text', $minelementname, get_string($minelementname, 'questionnaire'), ['size' => '3']); + $mform->setType($minelementname, PARAM_RAW); + $mform->addRule($minelementname, get_string('err_required', 'form'), 'required', null, 'client'); + $mform->addRule($minelementname, get_string('err_numeric', 'form'), 'numeric', '', 'client'); + $mform->addRule($minelementname, get_string('err_numeric', 'form'), 'regex', $patterint, 'client'); + $mform->addHelpButton($minelementname, $minelementname, 'questionnaire'); + if (isset($ranges->minrange)) { + $mform->setDefault($minelementname, $ranges->minrange); + } else { + $mform->setDefault($minelementname, 1); + } + + $mform->addElement('text', $maxelementname, get_string($maxelementname, 'questionnaire'), ['size' => '3']); + $mform->setType($maxelementname, PARAM_RAW); + $mform->addHelpButton($maxelementname, $maxelementname, 'questionnaire'); + $mform->addRule($maxelementname, get_string('err_required', 'form'), 'required', null, 'client'); + $mform->addRule($maxelementname, get_string('err_numeric', 'form'), 'numeric', '', 'client'); + $mform->addRule($maxelementname, get_string('err_numeric', 'form'), 'regex', $patterint, 'client'); + if (isset($ranges->maxrange)) { + $mform->setDefault($maxelementname, $ranges->maxrange); + } else { + $mform->setDefault($maxelementname, 10); + } + + $mform->addElement('text', $startingvalue, get_string($startingvalue, 'questionnaire'), ['size' => '3']); + $mform->setType($startingvalue, PARAM_RAW); + $mform->addHelpButton($startingvalue, $startingvalue, 'questionnaire'); + $mform->addRule($startingvalue, get_string('err_required', 'form'), 'required', null, 'client'); + $mform->addRule($startingvalue, get_string('err_numeric', 'form'), 'numeric', '', 'client'); + $mform->addRule($startingvalue, get_string('err_numeric', 'form'), 'regex', $patterint, 'client'); + if (isset($ranges->startingvalue)) { + $mform->setDefault($startingvalue, $ranges->startingvalue); + } else { + $mform->setDefault($startingvalue, 5); + } + + $mform->addElement('text', $stepvalue, get_string($stepvalue, 'questionnaire'), ['size' => '3']); + $mform->setType($stepvalue, PARAM_RAW); + $mform->addHelpButton($stepvalue, $stepvalue, 'questionnaire'); + $mform->addRule($stepvalue, get_string('err_required', 'form'), 'required', null, 'client'); + $mform->addRule($stepvalue, get_string('err_numeric', 'form'), 'numeric', '', 'client'); + $mform->addRule($stepvalue, get_string('err_numeric', 'form'), 'regex', '/^-?\d+$/', 'client'); + + if (isset($ranges->stepvalue)) { + $mform->setDefault($stepvalue, $ranges->stepvalue); + } else { + $mform->setDefault($stepvalue, 1); + } + return $mform; + } + + /** + * Any preprocessing of general data. + * @param \stdClass $formdata + * @return bool + */ + protected function form_preprocess_data($formdata) { + $ranges = []; + if (isset($formdata->minrange)) { + $ranges['minrange'] = $formdata->minrange; + } + if (isset($formdata->maxrange)) { + $ranges['maxrange'] = $formdata->maxrange; + } + if (isset($formdata->startingvalue)) { + $ranges['startingvalue'] = $formdata->startingvalue; + } + if (isset($formdata->stepvalue)) { + $ranges['stepvalue'] = $formdata->stepvalue; + } + if (isset($formdata->leftlabel)) { + $ranges['leftlabel'] = $formdata->leftlabel; + } + if (isset($formdata->rightlabel)) { + $ranges['rightlabel'] = $formdata->rightlabel; + } + if (isset($formdata->centerlabel)) { + $ranges['centerlabel'] = $formdata->centerlabel; + } + + // Now store the new named degrees in extradata. + $formdata->extradata = json_encode($ranges); + return parent::form_preprocess_data($formdata); + } + + /** + * True if question provides mobile support. + * + * @return bool + */ + public function supports_mobile() { + return true; + } + + /** + * True if question need extradata for mobile app. + * + * @return bool + */ + public function mobile_question_extradata_display() { + return true; + } + + /** + * Return the mobile question display. + * + * @param int $qnum + * @param bool $autonum + * @return \stdClass + */ + public function mobile_question_display($qnum, $autonum = false) { + $mobiledata = parent::mobile_question_display($qnum, $autonum); + $mobiledata->isslider = true; + return $mobiledata; + } + + /** + * Return the otherdata to be used by the mobile app. + * + * @return array + */ + public function mobile_otherdata() { + $extradata = json_decode($this->extradata); + return [$this->mobile_fieldkey() => $extradata->startingvalue]; + } +} diff --git a/classes/question/text.php b/classes/question/text.php index f10de144..3bd12661 100644 --- a/classes/question/text.php +++ b/classes/question/text.php @@ -14,22 +14,24 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\question; + /** * This file contains the parent class for text question types. * * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes + * @package mod_questionnaire */ - -namespace mod_questionnaire\question; -defined('MOODLE_INTERNAL') || die(); - -class text extends base { +class text extends question { /** - * Constructor. Use to set any default properties. - * + * The class constructor + * @param int $id + * @param \stdClass $question + * @param \context $context + * @param array $params */ public function __construct($id = 0, $question = null, $context = null, $params = []) { $this->length = 20; @@ -37,10 +39,18 @@ public function __construct($id = 0, $question = null, $context = null, $params return parent::__construct($id, $question, $context, $params); } + /** + * Each question type must define its response class. + * @return object The response object based off of questionnaire_response_base. + */ protected function responseclass() { - return '\\mod_questionnaire\\response\\text'; + return '\\mod_questionnaire\\responsetype\\text'; } + /** + * Short name for this question type - no spaces, etc.. + * @return string + */ public function helpname() { return 'textbox'; } @@ -62,14 +72,13 @@ public function response_template() { } /** - * Return the context tags for the check question template. - * @param object $data - * @param string $descendantdata - * @param boolean $blankquestionnaire - * @return object The check question context tags. + * Question specific display method. + * @param \stdClass $response + * @param array $descendantsdata + * @param bool $blankquestionnaire * */ - protected function question_survey_display($data, $descendantsdata, $blankquestionnaire=false) { + protected function question_survey_display($response, $descendantsdata, $blankquestionnaire=false) { // Text Box. $questiontags = new \stdClass(); $questiontags->qelements = new \stdClass(); @@ -80,31 +89,76 @@ protected function question_survey_display($data, $descendantsdata, $blankquesti if ($this->precise > 0) { $choice->maxlength = $this->precise; } - $choice->value = (isset($data->{'q'.$this->id}) ? stripslashes($data->{'q'.$this->id}) : ''); + $choice->value = (isset($response->answers[$this->id][0]) ? + format_string(stripslashes($response->answers[$this->id][0]->value)) : ''); $choice->id = self::qtypename($this->type_id) . $this->id; $questiontags->qelements->choice = $choice; return $questiontags; } /** - * Return the context tags for the text response template. - * @param object $data - * @return object The radio question response context tags. - * + * Question specific response display method. + * @param \stdClass $response */ - protected function response_survey_display($data) { + protected function response_survey_display($response) { $resptags = new \stdClass(); - if (isset($data->{'q'.$this->id})) { - $resptags->content = format_text($data->{'q'.$this->id}, FORMAT_HTML); + if (isset($response->answers[$this->id])) { + $answer = reset($response->answers[$this->id]); + $resptags->content = format_text($answer->value, FORMAT_HTML); } return $resptags; } + /** + * Return the length form element. + * @param \MoodleQuickForm $mform + * @param string $helptext + */ protected function form_length(\MoodleQuickForm $mform, $helptext = '') { return parent::form_length($mform, 'fieldlength'); } + /** + * Return the precision form element. + * @param \MoodleQuickForm $mform + * @param string $helptext + */ protected function form_precise(\MoodleQuickForm $mform, $helptext = '') { return parent::form_precise($mform, 'maxtextlength'); } -} \ No newline at end of file + + /** + * True if question provides mobile support. + * @return bool + */ + public function supports_mobile() { + return true; + } + + /** + * Override and return false if not supporting mobile app. + * @param int $qnum + * @param bool $autonum + * @return \stdClass + */ + public function mobile_question_display($qnum, $autonum = false) { + $mobiledata = parent::mobile_question_display($qnum, $autonum); + $mobiledata->istextessay = true; + return $mobiledata; + } + + /** + * Override and return false if not supporting mobile app. + * @return array + */ + public function mobile_question_choices_display() { + $choices = []; + $choices[0] = new \stdClass(); + $choices[0]->id = 0; + $choices[0]->choice_id = 0; + $choices[0]->question_id = $this->id; + $choices[0]->content = ''; + $choices[0]->value = null; + return $choices; + } +} diff --git a/classes/question/yesno.php b/classes/question/yesno.php index 4a53d2af..8ecc5373 100644 --- a/classes/question/yesno.php +++ b/classes/question/yesno.php @@ -14,30 +14,37 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\question; + /** * This file contains the parent class for yesno question types. * * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes + * @package mod_questionnaire */ +class yesno extends question { -namespace mod_questionnaire\question; -defined('MOODLE_INTERNAL') || die(); - -class yesno extends base { - + /** + * Each question type must define its response class. + * @return object The response object based off of questionnaire_response_base. + */ protected function responseclass() { - return '\\mod_questionnaire\\response\\boolean'; + return '\\mod_questionnaire\\responsetype\\boolean'; } + /** + * Short name for this question type - no spaces, etc.. + * @return string + */ public function helpname() { return 'yesno'; } /** * Override and return a form template if provided. Output of question_survey_display is iterpreted based on this. - * @return boolean | string + * @return string */ public function question_template() { return 'mod_questionnaire/question_yesno'; @@ -45,7 +52,7 @@ public function question_template() { /** * Override and return a response template if provided. Output of question_survey_display is iterpreted based on this. - * @return boolean | string + * @return string */ public function response_template() { return 'mod_questionnaire/response_yesno'; @@ -53,7 +60,7 @@ public function response_template() { /** * Override this and return true if the question type allows dependent questions. - * @return boolean + * @return bool */ public function allows_dependents() { return true; @@ -61,6 +68,7 @@ public function allows_dependents() { /** * True if question type supports feedback options. False by default. + * @return bool */ public function supports_feedback() { return true; @@ -68,6 +76,7 @@ public function supports_feedback() { /** * True if the question supports feedback and has valid settings for feedback. Override if the default logic is not enough. + * @return bool */ public function valid_feedback() { return $this->required(); @@ -102,13 +111,13 @@ protected function get_dependency_options() { /** * Return the context tags for the check question template. - * @param object $data + * @param \mod_questionnaire\responsetype\response\response $response * @param array $dependants Array of all questions/choices depending on this question. * @param boolean $blankquestionnaire * @return object The check question context tags. - * + * @throws \coding_exception */ - protected function question_survey_display($data, $dependants=[], $blankquestionnaire=false) { + protected function question_survey_display($response, $dependants=[], $blankquestionnaire=false) { global $idcounter; // To make sure all radio buttons have unique ids. // JR 20 NOV 2007. $stryes = get_string('yes'); @@ -124,7 +133,7 @@ protected function question_survey_display($data, $dependants=[], $blankquestion $options = [$val1 => $stryes, $val2 => $strno]; $name = 'q'.$this->id; - $checked = (isset($data->{'q'.$this->id}) ? $data->{'q'.$this->id} : ''); + $checked = (isset($response->answers[$this->id][0]) ? $response->answers[$this->id][0]->value : ''); $ischecked = false; $choicetags = new \stdClass(); @@ -145,6 +154,9 @@ protected function question_survey_display($data, $dependants=[], $blankquestion if ($blankquestionnaire) { $option->disabled = true; } + if (!empty($this->qlegend)) { + $option->alabel = strip_tags("{$this->qlegend} {$option->label}"); + } $choicetags->qelements->choice[] = $option; } // CONTRIB-846. @@ -160,6 +172,9 @@ protected function question_survey_display($data, $dependants=[], $blankquestion if (!$ischecked && !$blankquestionnaire) { $option->checked = true; } + if (!empty($this->qlegend)) { + $option->alabel = strip_tags("{$this->qlegend} {$option->label}"); + } $choicetags->qelements->choice[] = $option; } // End CONTRIB-846. @@ -169,11 +184,11 @@ protected function question_survey_display($data, $dependants=[], $blankquestion /** * Return the context tags for the text response template. - * @param object $data + * @param \mod_questionnaire\responsetype\response\response $response * @return object The radio question response context tags. - * + * @throws \coding_exception */ - protected function response_survey_display($data) { + protected function response_survey_display($response) { static $uniquetag = 0; // To make sure all radios have unique names. $resptags = new \stdClass(); @@ -182,21 +197,104 @@ protected function response_survey_display($data) { $resptags->noname = 'q'.$this->id.$uniquetag++.'n'; $resptags->stryes = get_string('yes'); $resptags->strno = get_string('no'); - if (isset($data->{'q'.$this->id}) && ($data->{'q'.$this->id} == 'y')) { + if (!isset($response->answers[$this->id])) { + $response->answers[$this->id][] = new \mod_questionnaire\responsetype\answer\answer(); + } + $answer = reset($response->answers[$this->id]); + if ($answer->value == 'y') { $resptags->yesselected = 1; } - if (isset($data->{'q'.$this->id}) && ($data->{'q'.$this->id} == 'n')) { + if ($answer->value == 'n') { $resptags->noselected = 1; } + if (!empty($this->qlegend)) { + $resptags->alabelyes = strip_tags("{$this->qlegend} {$resptags->stryes}"); + $resptags->alabelno = strip_tags("{$this->qlegend} {$resptags->strno}"); + } return $resptags; } + /** + * Return the length form element. + * @param \MoodleQuickForm $mform + * @param string $helpname + */ protected function form_length(\MoodleQuickForm $mform, $helpname = '') { - return base::form_length_hidden($mform); + return question::form_length_hidden($mform); } + /** + * Return the precision form element. + * @param \MoodleQuickForm $mform + * @param string $helpname + */ protected function form_precise(\MoodleQuickForm $mform, $helpname = '') { - return base::form_precise_hidden($mform); + return question::form_precise_hidden($mform); + } + + /** + * True if question provides mobile support. + * + * @return bool + */ + public function supports_mobile() { + return true; + } + + /** + * Override and return false if not supporting mobile app. + * @param int $qnum + * @param bool $autonum + * @return \stdClass + */ + public function mobile_question_display($qnum, $autonum = false) { + $mobiledata = parent::mobile_question_display($qnum, $autonum); + $mobiledata->isbool = true; + return $mobiledata; + } + + /** + * Override and return false if not supporting mobile app. + * @return array + */ + public function mobile_question_choices_display() { + $choices = []; + $choices[0] = new \stdClass(); + $choices[0]->id = 0; + $choices[0]->choice_id = 'n'; + $choices[0]->question_id = $this->id; + $choices[0]->value = null; + $choices[0]->content = get_string('no'); + $choices[0]->isbool = true; + $choices[1] = new \stdClass(); + $choices[1]->id = 1; + $choices[1]->choice_id = 'y'; + $choices[1]->question_id = $this->id; + $choices[1]->value = null; + $choices[1]->content = get_string('yes'); + $choices[1]->isbool = true; + if ($this->required()) { + $choices[1]->value = 'y'; + $choices[1]->firstone = true; + } + + return $choices; + } + + /** + * Return the mobile response data. + * @param response $response + * @return array + */ + public function get_mobile_response_data($response) { + $resultdata = []; + if (isset($response->answers[$this->id][0]) && ($response->answers[$this->id][0]->value == 'n')) { + $resultdata[$this->mobile_fieldkey()] = 0; + } else if (isset($response->answers[$this->id][0]) && ($response->answers[$this->id][0]->value == 'y')) { + $resultdata[$this->mobile_fieldkey()] = 1; + } + + return $resultdata; } -} \ No newline at end of file +} diff --git a/classes/questions_form.php b/classes/questions_form.php index 766a0845..0bb2b469 100644 --- a/classes/questions_form.php +++ b/classes/questions_form.php @@ -14,26 +14,36 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * @package mod_questionnaire - * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) - * @author Mike Churchward & Joseph Rézeau - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_questionnaire; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/formslib.php'); +#[\AllowDynamicProperties] +/** + * The form definition class for questions. + * + * @package mod_questionnaire + * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) + * @author Mike Churchward & Joseph Rézeau + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class questions_form extends \moodleform { + /** + * The constructor. + * @param mixed $action + * @param bool $moveq + */ public function __construct($action, $moveq=false) { $this->moveq = $moveq; return parent::__construct($action); } + /** + * Form definition. + */ public function definition() { global $CFG, $questionnaire, $SESSION; global $DB; @@ -137,7 +147,7 @@ public function definition() { redirect($CFG->wwwroot.'/mod/questionnaire/questions.php?id='.$questionnaire->cm->id); } - if ($tid != QUESPAGEBREAK && $tid != QUESSECTIONTEXT) { + if ($question->is_numbered()) { $qnum++; } @@ -158,7 +168,13 @@ public function definition() { $spacer = $questionnaire->renderer->image_url('spacer'); if (!$this->moveq) { - $mform->addElement('html', '
'); // Begin div qn-container. + if ($dependencies) { + // Begin div qn-container with indent if questionnaire has child. + $mform->addElement('html', '
'); + } else { + $mform->addElement('html', '
'); // Begin div qn-container. + } + $mextra = array('value' => $question->id, 'alt' => $strmove, 'title' => $strmove); @@ -241,7 +257,7 @@ public function definition() { $manageqgroup[] =& $mform->createElement('image', 'editbutton['.$question->id.']', $esrc, $eextra); $manageqgroup[] =& $mform->createElement('image', 'removebutton['.$question->id.']', $rsrc, $rextra); - if ($tid != QUESPAGEBREAK && $tid != QUESSECTIONTEXT) { + if ($tid != QUESPAGEBREAK && $tid != QUESSECTIONTEXT && $tid != QUESSLIDER) { if ($required == 'y') { $reqsrc = $questionnaire->renderer->image_url('t/stop'); $strrequired = get_string('required', 'questionnaire'); @@ -315,12 +331,10 @@ public function definition() { $mform->addElement('static', 'qdepend_' . $question->id, '', $dependencies); } - if ($tid != QUESPAGEBREAK) { - if ($tid != QUESSECTIONTEXT) { + if ($question->is_numbered()) { $qnumber = '

'.$qnum.'

'; - } else { + } else { $qnumber = ''; - } } if ($this->moveq && $pos < $moveqposition) { @@ -361,9 +375,14 @@ public function definition() { $mform->addElement('html', '
'); } + /** + * Form validation. + * @param array $data + * @param array $files + * @return array + */ public function validation($data, $files) { $errors = parent::validation($data, $files); return $errors; } - -} \ No newline at end of file +} diff --git a/classes/response/date.php b/classes/response/date.php deleted file mode 100644 index 45a8712a..00000000 --- a/classes/response/date.php +++ /dev/null @@ -1,204 +0,0 @@ -. - -/** - * This file contains the parent class for questionnaire question types. - * - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes - */ - -namespace mod_questionnaire\response; -defined('MOODLE_INTERNAL') || die(); - -use mod_questionnaire\db\bulk_sql_config; - -/** - * Class for date response types. - * - * @author Mike Churchward - * @package responsetypes - */ - -class date extends base { - static public function response_table() { - return 'questionnaire_response_date'; - } - - public function insert_response($rid, $val) { - global $DB; - $checkdateresult = questionnaire_check_date($val); - $thisdate = $val; - if (substr($checkdateresult, 0, 5) == 'wrong') { - return false; - } - // Now use ISO date formatting. - $checkdateresult = questionnaire_check_date($thisdate, true); - $record = new \stdClass(); - $record->response_id = $rid; - $record->question_id = $this->question->id; - $record->response = $checkdateresult; - return $DB->insert_record(self::response_table(), $record); - } - - public function get_results($rids=false, $anonymous=false) { - global $DB; - - $rsql = ''; - $params = array($this->question->id); - if (!empty($rids)) { - list($rsql, $rparams) = $DB->get_in_or_equal($rids); - $params = array_merge($params, $rparams); - $rsql = ' AND response_id ' . $rsql; - } - - $sql = 'SELECT id, response ' . - 'FROM {'.self::response_table().'} ' . - 'WHERE question_id= ? ' . $rsql; - - return $DB->get_records_sql($sql, $params); - } - - /** - * Provide a template for results screen if defined. - * @return mixed The template string or false/ - */ - public function results_template() { - return 'mod_questionnaire/results_date'; - } - - /** - * @param bool $rids - * @param string $sort - * @param bool $anonymous - * @return string - * @throws \coding_exception - */ - public function display_results($rids=false, $sort='', $anonymous=false) { - $numresps = count($rids); - if ($rows = $this->get_results($rids, $anonymous)) { - $numrespondents = count($rows); - foreach ($rows as $row) { - // Count identical answers (case insensitive). - $this->text = $row->response; - if (!empty($this->text)) { - $dateparts = preg_split('/-/', $this->text); - $this->text = make_timestamp($dateparts[0], $dateparts[1], $dateparts[2]); // Unix timestamp. - $textidx = clean_text($this->text); - $this->counts[$textidx] = !empty($this->counts[$textidx]) ? ($this->counts[$textidx] + 1) : 1; - } - } - $pagetags = $this->get_results_tags($this->counts, $numresps, $numrespondents); - } else { - $pagetags = new \stdClass(); - } - return $pagetags; - } - - /** - * Override the results tags function for templates for questions with dates. - * - * @param $weights - * @param $participants Number of questionnaire participants. - * @param $respondents Number of question respondents. - * @param $showtotals - * @param string $sort - * @return \stdClass - * @throws \coding_exception - */ - public function get_results_tags($weights, $participants, $respondents, $showtotals = 1, $sort = '') { - $dateformat = get_string('strfdate', 'questionnaire'); - - $pagetags = new \stdClass(); - if ($respondents == 0) { - return $pagetags; - } - - if (!empty($weights) && is_array($weights)) { - $pagetags->responses = []; - $numresps = 0; - ksort ($weights); // Sort dates into chronological order. - foreach ($weights as $content => $num) { - $response = new \stdClass(); - $response->text = userdate($content, $dateformat, '', false); // Change timestamp into readable dates. - $numresps += $num; - $response->total = $num; - $pagetags->responses[] = (object)['response' => $response]; - } - - if ($showtotals == 1) { - $pagetags->total = new \stdClass(); - $pagetags->total->total = "$numresps/$participants"; - } - } - - return $pagetags; - } - - /** - * Return an array of answers by question/choice for the given response. Must be implemented by the subclass. - * - * @param int $rid The response id. - * @param null $col Other data columns to return. - * @param bool $csvexport Using for CSV export. - * @param int $choicecodes CSV choicecodes are required. - * @param int $choicetext CSV choicetext is required. - * @return array - */ - static public function response_select($rid, $col = null, $csvexport = false, $choicecodes = 0, $choicetext = 1) { - global $DB; - - $values = []; - $sql = 'SELECT q.id '.$col.', a.response as aresponse '. - 'FROM {'.self::response_table().'} a, {questionnaire_question} q '. - 'WHERE a.response_id=? AND a.question_id=q.id '; - $records = $DB->get_records_sql($sql, [$rid]); - $dateformat = get_string('strfdate', 'questionnaire'); - foreach ($records as $qid => $row) { - unset ($row->id); - $row = (array)$row; - $newrow = array(); - foreach ($row as $key => $val) { - if (!is_numeric($key)) { - $newrow[] = $val; - // Convert date from yyyy-mm-dd database format to actual questionnaire dateformat. - // does not work with dates prior to 1900 under Windows. - if (preg_match('/\d\d\d\d-\d\d-\d\d/', $val)) { - $dateparts = preg_split('/-/', $val); - $val = make_timestamp($dateparts[0], $dateparts[1], $dateparts[2]); // Unix timestamp. - $val = userdate ( $val, $dateformat); - $newrow[] = $val; - } - } - } - $values["$qid"] = $newrow; - $val = array_pop($values["$qid"]); - array_push($values["$qid"], '', '', $val); - } - - return $values; - } - - /** - * Configure bulk sql - * @return bulk_sql_config - */ - protected function bulk_sql_config() { - return new bulk_sql_config(self::response_table(), 'qrd', false, true, false); - } -} - diff --git a/classes/response/display_support.php b/classes/response/display_support.php deleted file mode 100644 index 83dd0b9c..00000000 --- a/classes/response/display_support.php +++ /dev/null @@ -1,426 +0,0 @@ -. - -/** - * This file contains the parent class for questionnaire question types. - * - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes - */ - -namespace mod_questionnaire\response; -defined('MOODLE_INTERNAL') || die(); -use \html_writer; -use \html_table; - -/** - * Class for response display support. - * - * @author Mike Churchward - * @package display_support - */ - -class display_support { - - /* {{{ proto void mkresavg(array weights, int total, int precision, bool show_totals) - Builds HTML showing AVG results. */ - - public static function mkresavg($counts, $total, $choices, $precision, $showtotals, $length, $sort, $stravgvalue='') { - global $CFG; - $stravgrank = get_string('averagerank', 'questionnaire'); - $osgood = false; - if ($precision == 3) { // Osgood's semantic differential. - $osgood = true; - $stravgrank = get_string('averageposition', 'questionnaire'); - } - $stravg = '
'.$stravgrank.$stravgvalue.'
'; - - $isna = $precision == 1; - $isnahead = ''; - $nbchoices = count ($counts); - $isrestricted = ($length < $nbchoices) && $precision == 2; - - if ($isna) { - $isnahead = get_string('notapplicable', 'questionnaire'); - } - $table = new html_table(); - - $table->align = array('', '', 'center', 'right'); - $table->width = ' 99%'; - if ($isna) { - $table->head = array('', $stravg, '⇓', $isnahead); - } else { - if ($osgood) { - $stravg = '
'.$stravgrank.'
'; - $table->head = array('', $stravg, ''); - } else { - $table->head = array('', $stravg, '⇓'); - } - } - // TODO JR please calculate the correct width of the question text column (col #1). - $rightcolwidth = '5%'; - $table->size = array('60%', '*', $rightcolwidth); - if ($isna) { - $table->size = array('55%', '*', $rightcolwidth, $rightcolwidth); - } - if ($osgood) { - $table->size = array('25%', '50%', '25%'); - } - - $imageurl = $CFG->wwwroot.'/mod/questionnaire/images/'; - $llength = $length; - if (!$llength) { - $llength = 5; - } - // Add an extra column to accomodate lower ranks in this case. - $llength += $isrestricted; - $width = 100 / $llength; - $n = array(); - $nameddegrees = 0; - foreach ($choices as $choice) { - // To take into account languages filter. - $content = (format_text($choice->content, FORMAT_HTML, ['noclean' => true])); - if (preg_match("/^[0-9]{1,3}=/", $content, $ndd)) { - $n[$nameddegrees] = substr($content, strlen($ndd[0])); - $nameddegrees++; - } - } - $nbchoices = $length; - for ($j = 0; $j < $length; $j++) { - if (isset($n[$j])) { - $str = $n[$j]; - } else { - $str = $j + 1; - } - } - $out = ''; - for ($i = 0; $i <= $llength - 1; $i++) { - if (isset($n[$i])) { - $str = $n[$i]; - } else { - $str = $i + 1; - } - if ($isrestricted && $i == $llength - 1) { - $str = "..."; - } - $out .= ''; - } - $out .= '
'.$str.'
'; - $table->data[] = array('', $out, ''); - - switch ($sort) { - case 'ascending': - uasort($counts, 'self::sortavgasc'); - break; - case 'descending': - uasort($counts, 'self::sortavgdesc'); - break; - } - reset ($counts); - - if (!empty($counts) && is_array($counts)) { - foreach ($counts as $content => $contentobj) { - // Eliminate potential named degrees on Likert scale. - if (!preg_match("/^[0-9]{1,3}=/", $content)) { - - if (isset($contentobj->avg)) { - $avg = $contentobj->avg; - if (isset($contentobj->avgvalue)) { - $avgvalue = $contentobj->avgvalue; - } else { - $avgvalue = ''; - } - } else { - $avg = ''; - } - $nbna = $contentobj->nbna; - - if ($avg) { - $out = ''; - if (($j = $avg * $width) > 0) { - $marginposition = ($avg - 0.5 ) / ($length + $isrestricted) * 100; - } - if (!right_to_left()) { - $out .= ''; - } else { - $out .= ''; - } - } else { - $out = ''; - } - - if ($osgood) { - // Ensure there are two bits of content. - list($content, $contentright) = array_merge(preg_split('/[|]/', $content), array(' ')); - } else { - $contents = questionnaire_choice_values($content); - if ($contents->modname) { - $content = $contents->text; - } - } - if ($osgood) { - $table->data[] = array('
'. - format_text($content, FORMAT_HTML, ['noclean' => true]).'
', $out, - '
'.format_text($contentright, FORMAT_HTML, ['noclean' => true]).'
'); - // JR JUNE 2012 do not display meaningless average rank values for Osgood. - } else { - if ($avg) { - $stravgval = ''; - if ($stravgvalue) { - $stravgval = '('.sprintf('%.1f', $avgvalue).')'; - } - if ($isna) { - $table->data[] = [format_text($content, FORMAT_HTML, ['noclean' => true]), $out, - sprintf('%.1f', $avg).' '.$stravgval, $nbna]; - } else { - $table->data[] = [format_text($content, FORMAT_HTML, ['noclean' => true]), $out, - sprintf('%.1f', $avg).' '.$stravgval]; - } - } else if ($nbna != 0) { - $table->data[] = array(format_text($content, FORMAT_HTML, ['noclean' => true]), $out, '', $nbna); - } - } - } // End if named degrees. - } // End foreach. - } else { - $table->data[] = array('', get_string('noresponsedata', 'questionnaire')); - } - return html_writer::table($table); - } - - public static function mkrescount($counts, $rids, $rows, $question, $precision, $length, $sort) { - // Display number of responses to Rate questions - see http://moodle.org/mod/forum/discuss.php?d=185106. - global $DB; - $nbresponses = count($rids); - // Prepare data to be displayed. - $isrestricted = ($length < count($question->choices)) && $precision == 2; - - $rsql = ''; - if (!empty($rids)) { - list($rsql, $params) = $DB->get_in_or_equal($rids); - $rsql = ' AND response_id ' . $rsql; - } - - array_unshift($params, $question->id); // This is question_id. - $sql = 'SELECT r.id, c.content, r.rankvalue, c.id AS choiceid ' . - 'FROM {questionnaire_quest_choice} c , ' . - '{questionnaire_response_rank} r ' . - 'WHERE c.question_id = ?' . - ' AND r.question_id = c.question_id' . - ' AND r.choice_id = c.id ' . - $rsql . - ' ORDER BY choiceid, rankvalue ASC'; - $choices = $DB->get_records_sql($sql, $params); - - // Sort rows (results) by average value. - if ($sort != 'default') { - $sortarray = array(); - foreach ($rows as $row) { - foreach ($row as $key => $value) { - if (!isset($sortarray[$key])) { - $sortarray[$key] = array(); - } - $sortarray[$key][] = $value; - } - } - $orderby = "average"; - switch ($sort) { - case 'ascending': - array_multisort($sortarray[$orderby], SORT_ASC, $rows); - break; - case 'descending': - array_multisort($sortarray[$orderby], SORT_DESC, $rows); - break; - } - } - $nbranks = $length; - $ranks = array(); - foreach ($rows as $row) { - $choiceid = $row->id; - foreach ($choices as $choice) { - if ($choice->choiceid == $choiceid) { - $n = 0; - for ($i = 0; $i < $nbranks; $i++) { - if ($choice->rankvalue == $i) { - $n++; - if (!isset($ranks[$choice->content][$i])) { - $ranks[$choice->content][$i] = 0; - } - $ranks[$choice->content][$i] += $n; - } - } - } - } - } - - // Psettings for display. - $strtotal = ''.get_string('total', 'questionnaire').''; - $isna = $precision == 1; - $isnahead = ''; - $osgood = false; - if ($precision == 3) { // Osgood's semantic differential. - $osgood = true; - } - if ($isna) { - $isnahead = get_string('notapplicable', 'questionnaire').'
(#)'; - } - if ($precision == 1) { - $na = get_string('notapplicable', 'questionnaire'); - } else { - $na = ''; - } - $nameddegrees = 0; - $n = array(); - foreach ($question->choices as $choice) { - $content = $choice->content; - // Check for number from 1 to 3 digits, followed by the equal sign = (to accomodate named degrees). - if (preg_match("/^([0-9]{1,3})=(.*)$/", $content, $ndd)) { - $n[$nameddegrees] = format_text($ndd[2], FORMAT_HTML, ['noclean' => true]); - $nameddegrees++; - } else { - $contents = questionnaire_choice_values($content); - if ($contents->modname) { - $choice->content = $contents->text; - } - } - } - - $headings = array(''.get_string('responses', 'questionnaire').''); - if ($osgood) { - $align = array('right'); - } else { - $align = array('left'); - } - - // Display the column titles. - for ($j = 0; $j < $length; $j++) { - if (isset($n[$j])) { - $str = $n[$j]; - } else { - $str = $j + 1; - } - array_push($headings, ''.$str.''); - array_push($align, 'center'); - } - if ($osgood) { - array_push($headings, ''); - array_push($align, 'left'); - } - array_push($headings, $strtotal); - if ($isrestricted) { - array_push($headings, get_string('notapplicable', 'questionnaire')); - array_push($align, 'center'); - } - array_push($align, 'center'); - if ($na) { - array_push($headings, $na); - array_push($align, 'center'); - } - - $table = new html_table(); - $table->head = $headings; - $table->align = $align; - $table->attributes['class'] = 'generaltable'; - // Now display the responses. - foreach ($ranks as $content => $rank) { - $data = array(); - // Eliminate potential named degrees on Likert scale. - if (!preg_match("/^[0-9]{1,3}=/", $content)) { - // First display the list of degrees (named or un-named) - // number of NOT AVAILABLE responses for this possible answer. - $nbna = $counts[$content]->nbna; - // TOTAL number of responses for this possible answer. - $total = $counts[$content]->num; - $nbresp = ''.$total.''; - if ($osgood) { - // Ensure there are two bits of content. - list($content, $contentright) = array_merge(preg_split('/[|]/', $content), array(' ')); - $data[] = format_text($content, FORMAT_HTML, ['noclean' => true]); - } else { - // Eliminate potentially short-named choices. - $contents = questionnaire_choice_values($content); - if ($contents->modname) { - $content = $contents->text; - } - $data[] = format_text($content, FORMAT_HTML, ['noclean' => true]); - } - // Display ranks/rates numbers. - $maxrank = max($rank); - for ($i = 0; $i <= $length - 1; $i++) { - $percent = ''; - if (isset($rank[$i])) { - $str = $rank[$i]; - if ($total !== 0 && $str !== 0) { - $percent = ' ('.number_format(($str * 100) / $total).'%)'; - } - // Emphasize responses with max rank value. - if ($str == $maxrank) { - $str = ''.$str.''; - } - } else { - $str = 0; - } - $data[] = $str.$percent; - } - if ($osgood) { - $data[] = format_text($contentright, FORMAT_HTML, ['noclean' => true]); - } - $data[] = $nbresp; - if ($isrestricted) { - $data[] = $nbresponses - $total; - } - if (!$osgood) { - if ($na) { - $data[] = $nbna; - } - } - } // End named degrees. - $table->data[] = $data; - } - return html_writer::table($table); - } - - /** - * Sorting functions for ascending and descending. - * - */ - static private function sortavgasc($a, $b) { - if (isset($a->avg) && isset($b->avg)) { - if ( $a->avg < $b->avg ) { - return -1; - } else if ($a->avg > $b->avg ) { - return 1; - } else { - return 0; - } - } - } - - static private function sortavgdesc($a, $b) { - if (isset($a->avg) && isset($b->avg)) { - if ( $a->avg > $b->avg ) { - return -1; - } else if ($a->avg < $b->avg) { - return 1; - } else { - return 0; - } - } - } -} diff --git a/classes/response/multiple.php b/classes/response/multiple.php deleted file mode 100644 index 0d338acd..00000000 --- a/classes/response/multiple.php +++ /dev/null @@ -1,332 +0,0 @@ -. - -/** - * This file contains the parent class for questionnaire question types. - * - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes - */ - -namespace mod_questionnaire\response; -defined('MOODLE_INTERNAL') || die(); - -use mod_questionnaire\db\bulk_sql_config; - -/** - * Class for multiple response types. - * - * @author Mike Churchward - * @package responsetypes - */ - -class multiple extends single { - /** - * The only differences between multuple and single responses are the - * response table and the insert logic. - */ - static public function response_table() { - return 'questionnaire_resp_multiple'; - } - - public function insert_response($rid, $val) { - global $DB; - $resid = ''; - foreach ($this->question->choices as $cid => $choice) { - if (strpos($choice->content, '!other') === 0) { - $other = optional_param('q'.$this->question->id.'_'.$cid, '', PARAM_CLEAN); - if (empty($other)) { - continue; - } - if (!isset($val) || !is_array($val)) { - $val = array($cid); - } else { - array_push($val, $cid); - } - if (preg_match("/[^ \t\n]/", $other)) { - $record = new \stdClass(); - $record->response_id = $rid; - $record->question_id = $this->question->id; - $record->choice_id = $cid; - $record->response = $other; - $resid = $DB->insert_record('questionnaire_response_other', $record); - } - } - } - - if (!isset($val) || !is_array($val)) { - return false; - } - - foreach ($val as $cid) { - $cid = clean_param($cid, PARAM_CLEAN); - if ($cid != 0) { // Do not save response if choice is empty. - if (preg_match("/other_q[0-9]+/", $cid)) { - continue; - } - $record = new \stdClass(); - $record->response_id = $rid; - $record->question_id = $this->question->id; - $record->choice_id = $cid; - $resid = $DB->insert_record(self::response_table(), $record); - } - } - return $resid; - } - - /** - * Return an array of answers by question/choice for the given response. Must be implemented by the subclass. - * - * @param int $rid The response id. - * @param null $col Other data columns to return. - * @param bool $csvexport Using for CSV export. - * @param int $choicecodes CSV choicecodes are required. - * @param int $choicetext CSV choicetext is required. - * @return array - */ - static public function response_select($rid, $col = null, $csvexport = false, $choicecodes = 0, $choicetext = 1) { - global $DB; - - $stringother = get_string('other', 'questionnaire'); - $values = []; - $sql = 'SELECT a.id as aid, q.id as qid '.$col.',c.content as ccontent,c.id as cid '. - 'FROM {'.self::response_table().'} a, {questionnaire_question} q, {questionnaire_quest_choice} c '. - 'WHERE a.response_id = ? AND a.question_id=q.id AND a.choice_id=c.id '. - 'ORDER BY a.id,a.question_id,c.id'; - $records = $DB->get_records_sql($sql, [$rid]); - if ($csvexport) { - $tmp = null; - if (!empty($records)) { - $qids2 = array(); - $oldqid = ''; - foreach ($records as $qid => $row) { - if ($row->qid != $oldqid) { - $qids2[] = $row->qid; - $oldqid = $row->qid; - } - } - list($qsql, $params) = $DB->get_in_or_equal($qids2); - $sql = 'SELECT * FROM {questionnaire_quest_choice} WHERE question_id ' . $qsql . ' ORDER BY id'; - $records2 = $DB->get_records_sql($sql, $params); - foreach ($records2 as $qid => $row2) { - $selected = '0'; - $qid2 = $row2->question_id; - $cid2 = $row2->id; - $c2 = $row2->content; - $otherend = false; - if ($c2 == '!other') { - $c2 = '!other='.get_string('other', 'questionnaire'); - } - if (preg_match('/^!other/', $c2)) { - $otherend = true; - } else { - $contents = questionnaire_choice_values($c2); - if ($contents->modname) { - $c2 = $contents->modname; - } else if ($contents->title) { - $c2 = $contents->title; - } - } - $sql = 'SELECT a.name as name, a.type_id as q_type, a.position as pos ' . - 'FROM {questionnaire_question} a WHERE id = ?'; - $currentquestion = $DB->get_records_sql($sql, [$qid2]); - foreach ($currentquestion as $question) { - $name1 = $question->name; - $type1 = $question->q_type; - } - $newrow = []; - foreach ($records as $qid => $row1) { - $qid1 = $row1->qid; - $cid1 = $row1->cid; - // If available choice has been selected by student. - if ($qid1 == $qid2 && $cid1 == $cid2) { - $selected = '1'; - } - } - if ($otherend) { - $newrow2 = array(); - $newrow2[] = $question->pos; - $newrow2[] = $type1; - $newrow2[] = $name1; - $newrow2[] = '['.get_string('other', 'questionnaire').']'; - $newrow2[] = $selected; - $tmp2 = $qid2.'_other'; - $values["$tmp2"] = $newrow2; - } - $newrow[] = $question->pos; - $newrow[] = $type1; - $newrow[] = $name1; - $newrow[] = $c2; - $newrow[] = $selected; - $tmp = $qid2.'_'.$cid2; - $values["$tmp"] = $newrow; - } - } - unset($tmp); - unset($row); - - } else { - $arr = []; - $tmp = null; - foreach ($records as $row) { - $qid = $row->qid; - $cid = $row->cid; - unset($row->aid); - unset($row->qid); - unset($row->cid); - $arow = get_object_vars($row); - $newrow = []; - foreach ($arow as $key => $val) { - if (!is_numeric($key)) { - $newrow[] = $val; - } - } - if (preg_match('/^!other/', $row->ccontent)) { - $newrow[] = 'other_' . $cid; - } else { - $newrow[] = (int)$cid; - } - if ($tmp == $qid) { - $arr[] = $newrow; - continue; - } - if ($tmp != null) { - $values["$tmp"] = $arr; - } - $tmp = $qid; - $arr = array($newrow); - } - if ($tmp != null) { - $values["$tmp"] = $arr; - } - unset($arr); - unset($tmp); - unset($row); - } - - // Response_other. - // This will work even for multiple !other fields within one question - // AND for identical !other responses in different questions JR. - $sql = 'SELECT c.id as cid, c.content as content, a.response as aresponse, q.id as qid, q.position as position, - q.type_id as type_id, q.name as name '. - 'FROM {questionnaire_response_other} a, {questionnaire_question} q, {questionnaire_quest_choice} c '. - 'WHERE a.response_id= ? AND a.question_id=q.id AND a.choice_id=c.id '. - 'ORDER BY a.question_id,c.id '; - $records = $DB->get_records_sql($sql, [$rid]); - foreach ($records as $record) { - $newrow = []; - $position = $record->position; - $typeid = $record->type_id; - $name = $record->name; - $cid = $record->cid; - $qid = $record->qid; - $content = $record->content; - - // The !other modality with no label. - if ($content == '!other') { - $content = '!other='.$stringother; - } - $content = substr($content, 7); - $aresponse = $record->aresponse; - // The first two empty values are needed for compatibility with "normal" (non !other) responses. - // They are only needed for the CSV export, in fact. - $newrow[] = $position; - $newrow[] = $typeid; - $newrow[] = $name; - $content = $stringother; - $newrow[] = $content; - $newrow[] = $aresponse; - $values["${qid}_${cid}"] = $newrow; - } - - return $values; - } - - /** - * Return sql and params for getting responses in bulk. - * @author Guy Thomas - * @param int|array $questionnaireids One id, or an array of ids. - * @param bool|int $responseid - * @param bool|int $userid - * @return array - */ - public function get_bulk_sql($questionnaireids, $responseid = false, $userid = false, $groupid = false, $showincompletes = 0) { - global $DB; - - $sql = $this->bulk_sql(); - - if (($groupid !== false) && ($groupid > 0)) { - $groupsql = ' INNER JOIN {groups_members} gm ON gm.groupid = ? AND gm.userid = qr.userid '; - $gparams = [$groupid]; - } else { - $groupsql = ''; - $gparams = []; - } - - if (is_array($questionnaireids)) { - list($qsql, $params) = $DB->get_in_or_equal($questionnaireids); - } else { - $qsql = ' = ? '; - $params = [$questionnaireids]; - } - if ($showincompletes == 1) { - $showcompleteonly = ''; - } else { - $showcompleteonly = 'AND qr.complete = ? '; - $params[] = 'y'; - } - - $sql .= " - AND qr.questionnaireid $qsql $showcompleteonly - LEFT JOIN {questionnaire_response_other} qro ON qro.response_id = qr.id AND qro.choice_id = qrm.choice_id - LEFT JOIN {user} u ON u.id = qr.userid - $groupsql - "; - $params = array_merge($params, $gparams); - - if ($responseid) { - $sql .= " WHERE qr.id = ?"; - $params[] = $responseid; - } else if ($userid) { - $sql .= " WHERE qr.userid = ?"; - $params[] = $userid; - } - return [$sql, $params]; - } - - /** - * Return sql for getting responses in bulk. - * @author Guy Thomas - * @return string - */ - protected function bulk_sql() { - global $DB; - - $userfields = $this->user_fields_sql(); - $alias = 'qrm'; - $extraselect = ''; - $extraselect .= 'qrm.choice_id, ' . $DB->sql_order_by_text('qro.response', 1000) . ' AS response, 0 AS rankvalue'; - - return " - SELECT " . $DB->sql_concat_join("'_'", ['qr.id', "'".$this->question->helpname()."'", $alias.'.id']) . " AS id, - qr.submitted, qr.complete, qr.grade, qr.userid, $userfields, qr.id AS rid, $alias.question_id, - $extraselect - FROM {questionnaire_response} qr - JOIN {".self::response_table()."} $alias ON $alias.response_id = qr.id - "; - } -} diff --git a/classes/response/rank.php b/classes/response/rank.php deleted file mode 100644 index afa3fe3d..00000000 --- a/classes/response/rank.php +++ /dev/null @@ -1,364 +0,0 @@ -. - -/** - * This file contains the parent class for questionnaire question types. - * - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes - */ - -namespace mod_questionnaire\response; -defined('MOODLE_INTERNAL') || die(); - -use mod_questionnaire\db\bulk_sql_config; - -/** - * Class for rank responses. - * - * @author Mike Churchward - * @package responsetypes - */ - -class rank extends base { - static public function response_table() { - return 'questionnaire_response_rank'; - } - - /** - * @param int $rid - * @param mixed $val - * @return bool|int - */ - public function insert_response($rid, $val) { - global $DB; - if ($this->question->type_id == QUESRATE) { - $resid = false; - foreach ($this->question->choices as $cid => $choice) { - $other = optional_param('q'.$this->question->id.'_'.$cid, null, PARAM_CLEAN); - // Choice not set or not answered. - if (!isset($other) || $other == '') { - continue; - } - if ($other == get_string('notapplicable', 'questionnaire')) { - $rank = -1; - } else { - $rank = intval($other); - } - $record = new \stdClass(); - $record->response_id = $rid; - $record->question_id = $this->question->id; - $record->choice_id = $cid; - $record->rankvalue = $rank; - $resid = $DB->insert_record(self::response_table(), $record); - } - return $resid; - } else { // THIS SHOULD NEVER HAPPEN. - if ($val == get_string('notapplicable', 'questionnaire')) { - $rank = -1; - } else { - $rank = intval($val); - } - $record = new \stdClass(); - $record->response_id = $rid; - $record->question_id = $this->question->id; - $record->rankvalue = $rank; - return $DB->insert_record(self::response_table(), $record); - } - } - - /** - * @param bool $rids - * @param bool $anonymous - * @return array - */ - public function get_results($rids=false, $anonymous=false) { - global $DB; - - $rsql = ''; - if (!empty($rids)) { - list($rsql, $params) = $DB->get_in_or_equal($rids); - $rsql = ' AND response_id ' . $rsql; - } - - if ($this->question->type_id == QUESRATE) { - // JR there can't be an !other field in rating questions ??? - $rankvalue = array(); - $select = 'question_id=' . $this->question->id . ' AND content NOT LIKE \'!other%\' ORDER BY id ASC'; - if ($rows = $DB->get_records_select('questionnaire_quest_choice', $select)) { - foreach ($rows as $row) { - $this->counts[$row->content] = new \stdClass(); - $nbna = $DB->count_records(self::response_table(), array('question_id' => $this->question->id, - 'choice_id' => $row->id, 'rankvalue' => '-1')); - $this->counts[$row->content]->nbna = $nbna; - // The $row->value may be null (i.e. empty) or have a 'NULL' value. - if ($row->value !== null && $row->value !== 'NULL') { - $rankvalue[] = $row->value; - } - } - } - - $isrestricted = ($this->question->length < count($this->question->choices)) && $this->question->precise == 2; - // Usual case. - if (!$isrestricted) { - if (!empty ($rankvalue)) { - $sql = "SELECT r.id, c.content, r.rankvalue, c.id AS choiceid - FROM {questionnaire_quest_choice} c, {".self::response_table()."} r - WHERE r.choice_id = c.id - AND c.question_id = " . $this->question->id . " - AND r.rankvalue >= 0{$rsql} - ORDER BY choiceid"; - $results = $DB->get_records_sql($sql, $params); - $value = array(); - foreach ($results as $result) { - if (isset ($value[$result->choiceid])) { - $value[$result->choiceid] += $rankvalue[$result->rankvalue]; - } else { - $value[$result->choiceid] = $rankvalue[$result->rankvalue]; - } - } - } - - $sql = "SELECT c.id, c.content, a.average, a.num - FROM {questionnaire_quest_choice} c - INNER JOIN - (SELECT c2.id, AVG(a2.rankvalue+1) AS average, COUNT(a2.response_id) AS num - FROM {questionnaire_quest_choice} c2, {".self::response_table()."} a2 - WHERE c2.question_id = ? AND a2.question_id = ? AND a2.choice_id = c2.id AND a2.rankvalue >= 0{$rsql} - GROUP BY c2.id) a ON a.id = c.id - order by c.id"; - $results = $DB->get_records_sql($sql, array_merge(array($this->question->id, $this->question->id), $params)); - if (!empty ($rankvalue)) { - foreach ($results as $key => $result) { - $result->averagevalue = $value[$key] / $result->num; - } - } - // Reindex by 'content'. Can't do this from the query as it won't work with MS-SQL. - foreach ($results as $key => $result) { - $results[$result->content] = $result; - unset($results[$key]); - } - return $results; - // Case where scaleitems is less than possible choices. - } else { - $sql = "SELECT c.id, c.content, a.sum, a.num - FROM {questionnaire_quest_choice} c - INNER JOIN - (SELECT c2.id, SUM(a2.rankvalue+1) AS sum, COUNT(a2.response_id) AS num - FROM {questionnaire_quest_choice} c2, {".self::response_table()."} a2 - WHERE c2.question_id = ? AND a2.question_id = ? AND a2.choice_id = c2.id AND a2.rankvalue >= 0{$rsql} - GROUP BY c2.id) a ON a.id = c.id"; - $results = $DB->get_records_sql($sql, array_merge(array($this->question->id, $this->question->id), $params)); - // Formula to calculate the best ranking order. - $nbresponses = count($rids); - foreach ($results as $key => $result) { - $result->average = ($result->sum + ($nbresponses - $result->num) * ($this->length + 1)) / $nbresponses; - $results[$result->content] = $result; - unset($results[$key]); - } - return $results; - } - } else { - $sql = 'SELECT A.rankvalue, COUNT(A.response_id) AS num ' . - 'FROM {'.self::response_table().'} A ' . - 'WHERE A.question_id= ? ' . $rsql . ' ' . - 'GROUP BY A.rankvalue'; - return $DB->get_records_sql($sql, array_merge(array($this->question->id), $params)); - } - } - - /** - * Provide the feedback scores for all requested response id's. This should be provided only by questions that provide feedback. - * @param array $rids - * @return array | boolean - */ - public function get_feedback_scores(array $rids) { - global $DB; - - $rsql = ''; - $params = [$this->question->id]; - if (!empty($rids)) { - list($rsql, $rparams) = $DB->get_in_or_equal($rids); - $params = array_merge($params, $rparams); - $rsql = ' AND response_id ' . $rsql; - } - $params[] = 'y'; - - $sql = 'SELECT r.id, r.response_id as rid, r.question_id AS qid, r.choice_id AS cid, r.rankvalue ' . - 'FROM {'.$this->response_table().'} r ' . - 'INNER JOIN {questionnaire_quest_choice} c ON r.choice_id = c.id ' . - 'WHERE r.question_id= ? ' . $rsql . ' ' . - 'ORDER BY rid,cid ASC'; - $responses = $DB->get_recordset_sql($sql, $params); - - $sql = 'SELECT id, value ' . - 'FROM {questionnaire_quest_choice} ' . - 'WHERE question_id = ? AND value IS NOT NULL ' . - 'ORDER BY id ASC '; - $scorerecs = $DB->get_records_sql($sql, $params); - - // Reindex $scores as a zero starting array. - $scores = []; - foreach ($scorerecs as $scorerec) { - $scores[] = $scorerec->value; - } - - $rid = 0; - $feedbackscores = []; - foreach ($responses as $response) { - if ($rid != $response->rid) { - $rid = $response->rid; - $feedbackscores[$rid] = new \stdClass(); - $feedbackscores[$rid]->rid = $rid; - $feedbackscores[$rid]->score = 0; - } - $feedbackscores[$rid]->score += isset($scores[$response->rankvalue]) ? $scores[$response->rankvalue] : 0; - } - - return (!empty($feedbackscores) ? $feedbackscores : false); - } - - /** - * @param bool $rids - * @param string $sort - * @param bool $anonymous - * @return string - */ - public function display_results($rids=false, $sort='', $anonymous=false) { - $output = ''; - - if (is_array($rids)) { - $prtotal = 1; - } else if (is_int($rids)) { - $prtotal = 0; - } - - if ($rows = $this->get_results($rids, $sort, $anonymous)) { - $stravgvalue = ''; // For printing table heading. - foreach ($this->counts as $key => $value) { - $ccontent = $key; - $avgvalue = ''; - if (array_key_exists($ccontent, $rows)) { - $avg = $rows[$ccontent]->average; - $this->counts[$ccontent]->num = $rows[$ccontent]->num; - if (isset($rows[$ccontent]->averagevalue)) { - $avgvalue = $rows[$ccontent]->averagevalue; - $osgood = false; - if ($this->question->precise == 3) { // Osgood's semantic differential. - $osgood = true; - } - if ($stravgvalue == '' && !$osgood) { - $stravgvalue = ' ('.get_string('andaveragevalues', 'questionnaire').')'; - } - } else { - $avgvalue = null; - } - } else { - $avg = 0; - } - $this->counts[$ccontent]->avg = $avg; - $this->counts[$ccontent]->avgvalue = $avgvalue; - } - $output .= \mod_questionnaire\response\display_support::mkresavg($this->counts, count($rids), $this->question->choices, - $this->question->precise, $prtotal, $this->question->length, $sort, $stravgvalue); - - $output .= \mod_questionnaire\response\display_support::mkrescount($this->counts, $rids, $rows, $this->question, - $this->question->precise, $this->question->length, $sort); - } else { - $output .= '

 '.get_string('noresponsedata', 'questionnaire').'

'; - } - return $output; - } - - /** - * Return an array of answers by question/choice for the given response. Must be implemented by the subclass. - * - * @param int $rid The response id. - * @param null $col Other data columns to return. - * @param bool $csvexport Using for CSV export. - * @param int $choicecodes CSV choicecodes are required. - * @param int $choicetext CSV choicetext is required. - * @return array - */ - static public function response_select($rid, $col = null, $csvexport = false, $choicecodes = 0, $choicetext = 1) { - global $DB; - - $values = []; - $sql = 'SELECT a.id as aid, q.id AS qid, q.precise AS precise, c.id AS cid '.$col.', c.content as ccontent, - a.rankvalue as arank '. - 'FROM {'.self::response_table().'} a, {questionnaire_question} q, {questionnaire_quest_choice} c '. - 'WHERE a.response_id= ? AND a.question_id=q.id AND a.choice_id=c.id '. - 'ORDER BY aid, a.question_id, c.id'; - $records = $DB->get_records_sql($sql, [$rid]); - foreach ($records as $row) { - // Next two are 'qid' and 'cid', each with numeric and hash keys. - $osgood = false; - if ($row->precise == 3) { - $osgood = true; - } - $qid = $row->qid.'_'.$row->cid; - unset($row->aid); // Get rid of the answer id. - unset($row->qid); - unset($row->cid); - unset($row->precise); - $row = (array)$row; - $newrow = []; - foreach ($row as $key => $val) { - if ($key != 'content') { // No need to keep question text - ony keep choice text and rank. - if ($key == 'ccontent') { - if ($osgood) { - list($contentleft, $contentright) = array_merge(preg_split('/[|]/', $val), [' ']); - $contents = questionnaire_choice_values($contentleft); - if ($contents->title) { - $contentleft = $contents->title; - } - $contents = questionnaire_choice_values($contentright); - if ($contents->title) { - $contentright = $contents->title; - } - $val = strip_tags($contentleft.'|'.$contentright); - $val = preg_replace("/[\r\n\t]/", ' ', $val); - } else { - $contents = questionnaire_choice_values($val); - if ($contents->modname) { - $val = $contents->modname; - } else if ($contents->title) { - $val = $contents->title; - } else if ($contents->text) { - $val = strip_tags($contents->text); - $val = preg_replace("/[\r\n\t]/", ' ', $val); - } - } - } - $newrow[] = $val; - } - } - $values[$qid] = $newrow; - } - - return $values; - } - - /** - * Configure bulk sql - * @return bulk_sql_config - */ - protected function bulk_sql_config() { - return new bulk_sql_config(self::response_table(), 'qrr', true, false, true); - } - -} \ No newline at end of file diff --git a/classes/response/single.php b/classes/response/single.php deleted file mode 100644 index cdc4b647..00000000 --- a/classes/response/single.php +++ /dev/null @@ -1,353 +0,0 @@ -. - -/** - * This file contains the parent class for questionnaire question types. - * - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes - */ - -namespace mod_questionnaire\response; -defined('MOODLE_INTERNAL') || die(); - -/** - * Class for single response types. - * - * @author Mike Churchward - * @package responsetypes - */ - -class single extends base { - static public function response_table() { - return 'questionnaire_resp_single'; - } - - public function insert_response($rid, $val) { - global $DB; - if (!empty($val)) { - foreach ($this->question->choices as $cid => $choice) { - if (strpos($choice->content, '!other') === 0) { - $other = optional_param('q'.$this->question->id.'_'.$cid, null, PARAM_TEXT); - if (!isset($other)) { - continue; - } - if (preg_match("/[^ \t\n]/", $other)) { - $record = new \stdClass(); - $record->response_id = $rid; - $record->question_id = $this->question->id; - $record->choice_id = $cid; - $record->response = $other; - $resid = $DB->insert_record('questionnaire_response_other', $record); - $val = $cid; - break; - } - } - } - } - if (preg_match("/other_q([0-9]+)/", (isset($val) ? $val : ''), $regs)) { - $cid = $regs[1]; - if (!isset($other)) { - $other = optional_param('q'.$this->question->id.'_'.$cid, null, PARAM_TEXT); - } - if (preg_match("/[^ \t\n]/", $other)) { - $record = new \stdClass(); - $record->response_id = $rid; - $record->question_id = $this->question->id; - $record->choice_id = $cid; - $record->response = $other; - $resid = $DB->insert_record('questionnaire_response_other', $record); - $val = $cid; - } - } - $record = new \stdClass(); - $record->response_id = $rid; - $record->question_id = $this->question->id; - $record->choice_id = isset($val) ? $val : 0; - if ($record->choice_id) {// If "no answer" then choice_id is empty (CONTRIB-846). - try { - return $DB->insert_record(static::response_table(), $record); - } catch (\dml_write_exception $ex) { - return false; - } - } else { - return false; - } - } - - public function get_results($rids=false, $anonymous=false) { - global $DB; - - $rsql = ''; - $params = array($this->question->id); - if (!empty($rids)) { - list($rsql, $rparams) = $DB->get_in_or_equal($rids); - $params = array_merge($params, $rparams); - $rsql = ' AND response_id ' . $rsql; - } - - // Added qc.id to preserve original choices ordering. - $sql = 'SELECT rt.id, qc.id as cid, qc.content ' . - 'FROM {questionnaire_quest_choice} qc, ' . - '{'.static::response_table().'} rt ' . - 'WHERE qc.question_id= ? AND qc.content NOT LIKE \'!other%\' AND ' . - 'rt.question_id=qc.question_id AND rt.choice_id=qc.id' . $rsql . ' ' . - 'ORDER BY qc.id'; - - $rows = $DB->get_records_sql($sql, $params); - - // Handle 'other...'. - $sql = 'SELECT rt.id, rt.response, qc.content ' . - 'FROM {questionnaire_response_other} rt, ' . - '{questionnaire_quest_choice} qc ' . - 'WHERE rt.question_id= ? AND rt.choice_id=qc.id' . $rsql . ' ' . - 'ORDER BY qc.id'; - - if ($recs = $DB->get_records_sql($sql, $params)) { - $i = 1; - foreach ($recs as $rec) { - $rows['other'.$i] = new \stdClass(); - $rows['other'.$i]->content = $rec->content; - $rows['other'.$i]->response = $rec->response; - $i++; - } - } - - return $rows; - } - - /** - * Provide the feedback scores for all requested response id's. This should be provided only by questions that provide feedback. - * @param array $rids - * @return array | boolean - */ - public function get_feedback_scores(array $rids) { - global $DB; - - $rsql = ''; - $params = [$this->question->id]; - if (!empty($rids)) { - list($rsql, $rparams) = $DB->get_in_or_equal($rids); - $params = array_merge($params, $rparams); - $rsql = ' AND response_id ' . $rsql; - } - $params[] = 'y'; - - $sql = 'SELECT response_id as rid, c.value AS score ' . - 'FROM {'.$this->response_table().'} r ' . - 'INNER JOIN {questionnaire_quest_choice} c ON r.choice_id = c.id ' . - 'WHERE r.question_id= ? ' . $rsql . ' ' . - 'ORDER BY response_id ASC'; - return $DB->get_records_sql($sql, $params); - } - - /** - * Provide a template for results screen if defined. - * @return mixed The template string or false/ - */ - public function results_template() { - return 'mod_questionnaire/results_choice'; - } - - /** - * Return the JSON structure required for the template. - * - * @param bool $rids - * @param string $sort - * @param bool $anonymous - * @return string - */ - public function display_results($rids=false, $sort='', $anonymous=false) { - global $DB; - - $rows = $this->get_results($rids, $anonymous); - if (is_array($rids)) { - $prtotal = 1; - } else if (is_int($rids)) { - $prtotal = 0; - } - $numresps = count($rids); - - $responsecountsql = 'SELECT COUNT(DISTINCT r.response_id) ' . - 'FROM {' . $this->response_table() . '} r ' . - 'WHERE r.question_id = ? '; - $numrespondents = $DB->count_records_sql($responsecountsql, [$this->question->id]); - - if ($rows) { - foreach ($rows as $idx => $row) { - if (strpos($idx, 'other') === 0) { - $answer = $row->response; - $ccontent = $row->content; - $content = preg_replace(array('/^!other=/', '/^!other/'), - array('', get_string('other', 'questionnaire')), $ccontent); - $content .= ' ' . clean_text($answer); - $textidx = $content; - $this->counts[$textidx] = !empty($this->counts[$textidx]) ? ($this->counts[$textidx] + 1) : 1; - } else { - $contents = questionnaire_choice_values($row->content); - $this->choice = $contents->text.$contents->image; - $textidx = $this->choice; - $this->counts[$textidx] = !empty($this->counts[$textidx]) ? ($this->counts[$textidx] + 1) : 1; - } - } - $pagetags = $this->get_results_tags($this->counts, $numresps, $numrespondents, $prtotal, $sort); - } else { - $pagetags = new \stdClass(); - } - return $pagetags; - } - - /** - * Return an array of answers by question/choice for the given response. Must be implemented by the subclass. - * - * @param int $rid The response id. - * @param null $col Other data columns to return. - * @param bool $csvexport Using for CSV export. - * @param int $choicecodes CSV choicecodes are required. - * @param int $choicetext CSV choicetext is required. - * @return array - */ - static public function response_select($rid, $col = null, $csvexport = false, $choicecodes = 0, $choicetext = 1) { - global $DB; - - $values = []; - $sql = 'SELECT q.id '.$col.', q.type_id as q_type, c.content as ccontent,c.id as cid '. - 'FROM {'.static::response_table().'} a, {questionnaire_question} q, {questionnaire_quest_choice} c '. - 'WHERE a.response_id = ? AND a.question_id=q.id AND a.choice_id=c.id '; - $records = $DB->get_records_sql($sql, [$rid]); - foreach ($records as $qid => $row) { - $cid = $row->cid; - if ($csvexport) { - static $i = 1; - $qrecords = $DB->get_records('questionnaire_quest_choice', ['question_id' => $qid]); - foreach ($qrecords as $value) { - if ($value->id == $cid) { - $contents = questionnaire_choice_values($value->content); - if ($contents->modname) { - $row->ccontent = $contents->modname; - } else { - $content = $contents->text; - if (preg_match('/^!other/', $content)) { - $row->ccontent = get_string('other', 'questionnaire'); - } else if (($choicecodes == 1) && ($choicetext == 1)) { - $row->ccontent = "$i : $content"; - } else if ($choicecodes == 1) { - $row->ccontent = "$i"; - } else { - $row->ccontent = $content; - } - } - $i = 1; - break; - } - $i++; - } - } - unset($row->id); - unset($row->cid); - unset($row->q_type); - $arow = get_object_vars($row); - $newrow = []; - foreach ($arow as $key => $val) { - if (!is_numeric($key)) { - $newrow[] = $val; - } - } - if (preg_match('/^!other/', $row->ccontent)) { - $newrow[] = 'other_' . $cid; - } else { - $newrow[] = (int)$cid; - } - $values[$qid] = $newrow; - } - - return $values; - } - - /** - * Return sql and params for getting responses in bulk. - * @author Guy Thomas - * @param int|array $questionnaireids One id, or an array of ids. - * @param bool|int $responseid - * @param bool|int $userid - * @return array - */ - public function get_bulk_sql($questionnaireids, $responseid = false, $userid = false, $groupid = false, $showincompletes = 0) { - global $DB; - - $sql = $this->bulk_sql(); - if (($groupid !== false) && ($groupid > 0)) { - $groupsql = ' INNER JOIN {groups_members} gm ON gm.groupid = ? AND gm.userid = qr.userid '; - $gparams = [$groupid]; - } else { - $groupsql = ''; - $gparams = []; - } - - if (is_array($questionnaireids)) { - list($qsql, $params) = $DB->get_in_or_equal($questionnaireids); - } else { - $qsql = ' = ? '; - $params = [$questionnaireids]; - } - if ($showincompletes == 1) { - $showcompleteonly = ''; - } else { - $showcompleteonly = 'AND qr.complete = ? '; - $params[] = 'y'; - } - - $sql .= " - AND qr.questionnaireid $qsql $showcompleteonly - LEFT JOIN {questionnaire_response_other} qro ON qro.response_id = qr.id AND qro.choice_id = qrs.choice_id - LEFT JOIN {user} u ON u.id = qr.userid - $groupsql - "; - $params = array_merge($params, $gparams); - - if ($responseid) { - $sql .= " WHERE qr.id = ?"; - $params[] = $responseid; - } else if ($userid) { - $sql .= " WHERE qr.userid = ?"; - $params[] = $userid; - } - - return [$sql, $params]; - } - - /** - * Return sql for getting responses in bulk. - * @author Guy Thomas - * @return string - */ - protected function bulk_sql() { - global $DB; - - $userfields = $this->user_fields_sql(); - $alias = 'qrs'; - $extraselect = 'qrs.choice_id, ' . $DB->sql_order_by_text('qro.response', 1000) . ' AS response, 0 AS rankvalue'; - - return " - SELECT " . $DB->sql_concat_join("'_'", ['qr.id', "'".$this->question->helpname()."'", $alias.'.id']) . " AS id, - qr.submitted, qr.complete, qr.grade, qr.userid, $userfields, qr.id AS rid, $alias.question_id, - $extraselect - FROM {questionnaire_response} qr - JOIN {".static::response_table()."} $alias ON $alias.response_id = qr.id - "; - } -} diff --git a/classes/responsetype/answer/answer.php b/classes/responsetype/answer/answer.php new file mode 100644 index 00000000..7f9bab53 --- /dev/null +++ b/classes/responsetype/answer/answer.php @@ -0,0 +1,83 @@ +. + +namespace mod_questionnaire\responsetype\answer; + +/** + * This defines a structured class to hold question answers. + * + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire + * @copyright 2019, onwards Poet + */ +class answer { + + // Class properties. + + /** @var int $id The id of the question response data record this applies to. */ + public $id; + + /** @var int $responseid This id of the response record this applies to. */ + public $responseid; + + /** @var int $questionid The id of the question this response applies to. */ + public $questionid; + + /** @var string $content The choiceid of this response (if applicable). */ + public $choiceid; + + /** @var string $value The value of this response (if applicable). */ + public $value; + + /** + * Answer constructor. + * @param null $id + * @param null $responseid + * @param null $questionid + * @param null $choiceid + * @param null $value + */ + public function __construct($id = null, $responseid = null, $questionid = null, $choiceid = null, $value = null) { + $this->id = $id; + $this->responseid = $responseid; + $this->questionid = $questionid; + $this->choiceid = $choiceid; + $this->value = $value; + } + + /** + * Create and return an answer object from data. + * + * @param \stdClass|array $answerdata The data to load. + * @return answer + */ + public static function create_from_data($answerdata) { + if (!is_array($answerdata)) { + $answerdata = (array)$answerdata; + } + + $properties = array_keys(get_class_vars(__CLASS__)); + foreach ($properties as $property) { + if (!isset($answerdata[$property])) { + $answerdata[$property] = null; + } + } + + return new answer($answerdata['id'], $answerdata['responseid'], $answerdata['questionid'], $answerdata['choiceid'], + $answerdata['value']); + } +} diff --git a/classes/response/boolean.php b/classes/responsetype/boolean.php similarity index 52% rename from classes/response/boolean.php rename to classes/responsetype/boolean.php index dddcfcba..6dbe39de 100644 --- a/classes/response/boolean.php +++ b/classes/responsetype/boolean.php @@ -14,45 +14,100 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * This file contains the parent class for questionnaire question types. - * - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes - */ - -namespace mod_questionnaire\response; -defined('MOODLE_INTERNAL') || die(); +namespace mod_questionnaire\responsetype; +use coding_exception; +use dml_exception; use mod_questionnaire\db\bulk_sql_config; +use stdClass; /** * Class for boolean response types. * * @author Mike Churchward - * @package response + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire */ +class boolean extends responsetype { -class boolean extends base { - - static public function response_table() { + /** + * Provide the necessary response data table name. Should probably always be used with late static binding 'static::' form + * rather than 'self::' form to allow for class extending. + * + * @return string response table name. + */ + public static function response_table() { return 'questionnaire_response_bool'; } - public function insert_response($rid, $val) { + /** + * Provide an array of answer objects from web form data for the question. + * + * @param \stdClass $responsedata All of the responsedata as an object. + * @param \mod_questionnaire\question\question $question + * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects. + */ + public static function answers_from_webform($responsedata, $question) { + $answers = []; + if (isset($responsedata->{'q'.$question->id}) && !empty($responsedata->{'q'.$question->id})) { + $record = new \stdClass(); + $record->responseid = $responsedata->rid; + $record->questionid = $question->id; + $record->choiceid = $responsedata->{'q' . $question->id}; + $record->value = $responsedata->{'q' . $question->id}; + $answers[] = answer\answer::create_from_data($record); + } + return $answers; + } + + /** + * Provide an array of answer objects from mobile data for the question. + * + * @param \stdClass $responsedata All of the responsedata as an object. + * @param \mod_questionnaire\question\question $question + * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects. + */ + public static function answers_from_appdata($responsedata, $question) { + if (isset($responsedata->{'q'.$question->id}) && !empty($responsedata->{'q'.$question->id})) { + $responsedata->{'q'.$question->id} = ($responsedata->{'q'.$question->id}[0] == 1) ? 'y' : 'n'; + } + return static::answers_from_webform($responsedata, $question); + } + + /** + * Insert a provided response to the question. + * + * @param object $responsedata All of the responsedata as an object. + * @return int|bool - on error the subtype should call set_error and return false. + */ + public function insert_response($responsedata) { global $DB; - if (!empty($val)) { // If "no answer" then choice is empty (CONTRIB-846). + + if (!$responsedata instanceof \mod_questionnaire\responsetype\response\response) { + $response = \mod_questionnaire\responsetype\response\response::response_from_webform($responsedata, [$this->question]); + } else { + $response = $responsedata; + } + + if (!empty($response) && isset($response->answers[$this->question->id][0])) { $record = new \stdClass(); - $record->response_id = $rid; + $record->response_id = $response->id; $record->question_id = $this->question->id; - $record->choice_id = $val; - return $DB->insert_record(self::response_table(), $record); + $record->choice_id = $response->answers[$this->question->id][0]->choiceid; + return $DB->insert_record(static::response_table(), $record); } else { return false; } } + /** + * Provide the result information for the specified result records. + * + * @param int|array $rids - A single response id, or array. + * @param boolean $anonymous - Whether or not responses are anonymous. + * @return array - Array of data records. + */ public function get_results($rids=false, $anonymous=false) { global $DB; @@ -66,7 +121,7 @@ public function get_results($rids=false, $anonymous=false) { $params[] = ''; $sql = 'SELECT choice_id, COUNT(response_id) AS num ' . - 'FROM {'.self::response_table().'} ' . + 'FROM {'.static::response_table().'} ' . 'WHERE question_id= ? ' . $rsql . ' AND choice_id != ? ' . 'GROUP BY choice_id'; return $DB->get_records_sql($sql, $params); @@ -74,8 +129,8 @@ public function get_results($rids=false, $anonymous=false) { /** * If the choice id needs to be transformed into a different value, override this in the child class. - * @param $choiceid - * @return mixed + * @param int $choiceid + * @return string */ public function transform_choiceid($choiceid) { if ($choiceid == 0) { @@ -111,7 +166,7 @@ public function get_feedback_scores(array $rids) { if ($responses = $DB->get_recordset_sql($sql, $params)) { $feedbackscores = []; foreach ($responses as $rid => $response) { - $feedbackscores[$rid] = new \stdClass(); + $feedbackscores[$rid] = new stdClass(); $feedbackscores[$rid]->rid = $rid; $feedbackscores[$rid]->score = ($response->choice_id == 'y') ? 1 : 0; } @@ -121,10 +176,15 @@ public function get_feedback_scores(array $rids) { /** * Provide a template for results screen if defined. + * @param bool $pdf * @return mixed The template string or false/ */ - public function results_template() { - return 'mod_questionnaire/results_choice'; + public function results_template($pdf = false) { + if ($pdf) { + return 'mod_questionnaire/resultspdf_choice'; + } else { + return 'mod_questionnaire/results_choice'; + } } /** @@ -136,10 +196,8 @@ public function results_template() { * @return string */ public function display_results($rids=false, $sort='', $anonymous=false) { - if (empty($this->stryes)) { - $this->stryes = get_string('yes'); - $this->strno = get_string('no'); - } + $stryes = get_string('yes'); + $strno = get_string('no'); if (is_array($rids)) { $prtotal = 1; @@ -148,23 +206,23 @@ public function display_results($rids=false, $sort='', $anonymous=false) { } $numresps = count($rids); - $this->counts = [$this->stryes => 0, $this->strno => 0]; + $counts = [$stryes => 0, $strno => 0]; $numrespondents = 0; if ($rows = $this->get_results($rids, $anonymous)) { foreach ($rows as $row) { - $this->choice = $row->choice_id; + $choice = $row->choice_id; $count = $row->num; - if ($this->choice == 'y') { - $this->choice = $this->stryes; + if ($choice == 'y') { + $choice = $stryes; } else { - $this->choice = $this->strno; + $choice = $strno; } - $this->counts[$this->choice] = intval($count); - $numrespondents += $this->counts[$this->choice]; + $counts[$choice] = intval($count); + $numrespondents += $counts[$choice]; } - $pagetags = $this->get_results_tags($this->counts, $numresps, $numrespondents, $prtotal, ''); + $pagetags = $this->get_results_tags($counts, $numresps, $numrespondents, $prtotal, ''); } else { - $pagetags = new \stdClass(); + $pagetags = new stdClass(); } return $pagetags; } @@ -173,18 +231,14 @@ public function display_results($rids=false, $sort='', $anonymous=false) { * Return an array of answers by question/choice for the given response. Must be implemented by the subclass. * * @param int $rid The response id. - * @param null $col Other data columns to return. - * @param bool $csvexport Using for CSV export. - * @param int $choicecodes CSV choicecodes are required. - * @param int $choicetext CSV choicetext is required. * @return array */ - static public function response_select($rid, $col = null, $csvexport = false, $choicecodes = 0, $choicetext = 1) { + public static function response_select($rid) { global $DB; $values = []; - $sql = 'SELECT q.id '.$col.', a.choice_id '. - 'FROM {'.self::response_table().'} a, {questionnaire_question} q '. + $sql = 'SELECT q.id, q.content, a.choice_id '. + 'FROM {'.static::response_table().'} a, {questionnaire_question} q '. 'WHERE a.response_id= ? AND a.question_id=q.id '; $records = $DB->get_records_sql($sql, [$rid]); foreach ($records as $qid => $row) { @@ -200,20 +254,42 @@ static public function response_select($rid, $col = null, $csvexport = false, $c } $values[$qid] = $newrow; array_push($values[$qid], ($choice == 'y') ? '1' : '0'); - if (!$csvexport) { - array_push($values[$qid], $choice); // DEV still needed for responses display. - } + array_push($values[$qid], $choice); // DEV still needed for responses display. } return $values; } + /** + * Return an array of answer objects by question for the given response id. + * THIS SHOULD REPLACE response_select. + * + * @param int $rid The response id. + * @return array array answer + * @throws dml_exception + */ + public static function response_answers_by_question($rid) { + global $DB; + + $answers = []; + $sql = 'SELECT id, response_id as responseid, question_id as questionid, choice_id as choiceid, choice_id as value ' . + 'FROM {' . static::response_table() .'} ' . + 'WHERE response_id = ? '; + $records = $DB->get_records_sql($sql, [$rid]); + foreach ($records as $record) { + $record->choiceid = ($record->choiceid == 'y') ? 1 : 0; + $answers[$record->questionid][] = answer\answer::create_from_data($record); + } + + return $answers; + } + /** * Configure bulk sql * @return bulk_sql_config */ protected function bulk_sql_config() { - return new bulk_sql_config(self::response_table(), 'qrb', true, false, false); + return new bulk_sql_config(static::response_table(), 'qrb', true, false, false); } /** @@ -238,7 +314,7 @@ protected function bulk_sql() { qr.submitted, qr.complete, qr.grade, qr.userid, $userfields, qr.id AS rid, $alias.question_id, $extraselect FROM {questionnaire_response} qr - JOIN {".self::response_table()."} $alias ON $alias.response_id = qr.id + JOIN {".static::response_table()."} $alias ON $alias.response_id = qr.id "; } } diff --git a/classes/responsetype/date.php b/classes/responsetype/date.php new file mode 100644 index 00000000..befc5cba --- /dev/null +++ b/classes/responsetype/date.php @@ -0,0 +1,286 @@ +. + +namespace mod_questionnaire\responsetype; + +use mod_questionnaire\db\bulk_sql_config; + +/** + * Class for date response types. + * + * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire + */ +class date extends responsetype { + /** + * Provide the necessary response data table name. Should probably always be used with late static binding 'static::' form + * rather than 'self::' form to allow for class extending. + * + * @return string response table name. + */ + public static function response_table() { + return 'questionnaire_response_date'; + } + + /** + * Provide an array of answer objects from web form data for the question. + * + * @param \stdClass $responsedata All of the responsedata as an object. + * @param \mod_questionnaire\question\question $question + * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects. + */ + public static function answers_from_webform($responsedata, $question) { + $answers = []; + if (isset($responsedata->{'q'.$question->id}) && !empty($responsedata->{'q'.$question->id})) { + $record = new \stdClass(); + $record->responseid = $responsedata->rid; + $record->questionid = $question->id; + $record->value = $responsedata->{'q' . $question->id}; + $answers[] = answer\answer::create_from_data($record); + } + return $answers; + } + + /** + * Provide an array of answer objects from mobile data for the question. + * + * @param \stdClass $responsedata All of the responsedata as an object. + * @param \mod_questionnaire\question\question $question + * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects. + */ + public static function answers_from_appdata($responsedata, $question) { + if (isset($responsedata->{'q'.$question->id}) && !empty($responsedata->{'q'.$question->id})) { + // The app can send the date including time (e.g. 2021-06-28T09:03:46.613+02:00), get only the date. + $responsedata->{'q'.$question->id} = substr($responsedata->{'q'.$question->id}[0], 0, 10); + } + return static::answers_from_webform($responsedata, $question); + } + + /** + * Insert a provided response to the question. + * + * @param object $responsedata All of the responsedata as an object. + * @return int|bool - on error the subtype should call set_error and return false. + */ + public function insert_response($responsedata) { + global $DB; + + if (!$responsedata instanceof \mod_questionnaire\responsetype\response\response) { + $response = \mod_questionnaire\responsetype\response\response::response_from_webform($responsedata, [$this->question]); + } else { + $response = $responsedata; + } + + if (!empty($response) && isset($response->answers[$this->question->id][0])) { + $thisdate = $response->answers[$this->question->id][0]->value; + if (!$this->question->check_date_format($thisdate)) { + return false; + } + // Now use ISO date formatting. + $record = new \stdClass(); + $record->response_id = $response->id; + $record->question_id = $this->question->id; + $record->response = $thisdate; + return $DB->insert_record(self::response_table(), $record); + } else { + return false; + } + } + + /** + * Provide the result information for the specified result records. + * + * @param int|array $rids - A single response id, or array. + * @param boolean $anonymous - Whether or not responses are anonymous. + * @return array - Array of data records. + */ + public function get_results($rids=false, $anonymous=false) { + global $DB; + + $rsql = ''; + $params = array($this->question->id); + if (!empty($rids)) { + list($rsql, $rparams) = $DB->get_in_or_equal($rids); + $params = array_merge($params, $rparams); + $rsql = ' AND response_id ' . $rsql; + } + + $sql = 'SELECT id, response ' . + 'FROM {'.static::response_table().'} ' . + 'WHERE question_id= ? ' . $rsql; + + return $DB->get_records_sql($sql, $params); + } + + /** + * Provide a template for results screen if defined. + * @param bool $pdf + * @return mixed The template string or false/ + */ + public function results_template($pdf = false) { + if ($pdf) { + return 'mod_questionnaire/resultspdf_date'; + } else { + return 'mod_questionnaire/results_date'; + } + } + + /** + * Provide the result information for the specified result records. + * + * @param int|array $rids - A single response id, or array. + * @param string $sort - Optional display sort. + * @param boolean $anonymous - Whether or not responses are anonymous. + * @return string - Display output. + */ + public function display_results($rids=false, $sort='', $anonymous=false) { + $numresps = count($rids); + if ($rows = $this->get_results($rids, $anonymous)) { + $numrespondents = count($rows); + $counts = []; + foreach ($rows as $row) { + // Count identical answers (case insensitive). + if (!empty($row->response)) { + $dateparts = preg_split('/-/', $row->response); + $text = make_timestamp($dateparts[0], $dateparts[1], $dateparts[2]); // Unix timestamp. + $textidx = clean_text($text); + $counts[$textidx] = !empty($counts[$textidx]) ? ($counts[$textidx] + 1) : 1; + } + } + $pagetags = $this->get_results_tags($counts, $numresps, $numrespondents); + } else { + $pagetags = new \stdClass(); + } + return $pagetags; + } + + /** + * Gets the results tags for templates for questions with defined choices (single, multiple, boolean). + * + * @param arrays $weights + * @param int $participants Number of questionnaire participants. + * @param int $respondents Number of question respondents. + * @param int $showtotals + * @param string $sort + * @return \stdClass + */ + public function get_results_tags($weights, $participants, $respondents, $showtotals = 1, $sort = '') { + $dateformat = get_string('strfdate', 'questionnaire'); + + $pagetags = new \stdClass(); + if ($respondents == 0) { + return $pagetags; + } + + if (!empty($weights) && is_array($weights)) { + $pagetags->responses = []; + $numresps = 0; + ksort ($weights); // Sort dates into chronological order. + $evencolor = false; + foreach ($weights as $content => $num) { + $response = new \stdClass(); + $response->text = userdate($content, $dateformat, '', false); // Change timestamp into readable dates. + $numresps += $num; + $response->total = $num; + // The 'evencolor' attribute is used by the PDF template. + $response->evencolor = $evencolor; + $pagetags->responses[] = (object)['response' => $response]; + $evencolor = !$evencolor; + } + + if ($showtotals == 1) { + $pagetags->total = new \stdClass(); + $pagetags->total->total = "$numresps/$participants"; + } + } + + return $pagetags; + } + + /** + * Return an array of answers by question/choice for the given response. Must be implemented by the subclass. + * + * @param int $rid The response id. + * @return array + */ + public static function response_select($rid) { + global $DB; + + $values = []; + $sql = 'SELECT q.id, q.content, a.response as aresponse '. + 'FROM {'.static::response_table().'} a, {questionnaire_question} q '. + 'WHERE a.response_id=? AND a.question_id=q.id '; + $records = $DB->get_records_sql($sql, [$rid]); + $dateformat = get_string('strfdate', 'questionnaire'); + foreach ($records as $qid => $row) { + unset ($row->id); + $row = (array)$row; + $newrow = array(); + foreach ($row as $key => $val) { + if (!is_numeric($key)) { + $newrow[] = $val; + // Convert date from yyyy-mm-dd database format to actual questionnaire dateformat. + // does not work with dates prior to 1900 under Windows. + if (preg_match('/\d\d\d\d-\d\d-\d\d/', $val)) { + $dateparts = preg_split('/-/', $val); + $val = make_timestamp($dateparts[0], $dateparts[1], $dateparts[2]); // Unix timestamp. + $val = userdate ( $val, $dateformat); + $newrow[] = $val; + } + } + } + $values["$qid"] = $newrow; + $val = array_pop($values["$qid"]); + array_push($values["$qid"], '', '', $val); + } + + return $values; + } + + /** + * Return an array of answer objects by question for the given response id. + * THIS SHOULD REPLACE response_select. + * + * @param int $rid The response id. + * @return array array answer + * @throws \dml_exception + */ + public static function response_answers_by_question($rid) { + global $DB; + + $answers = []; + $sql = 'SELECT id, response_id as responseid, question_id as questionid, 0 as choiceid, response as value ' . + 'FROM {' . static::response_table() .'} ' . + 'WHERE response_id = ? '; + $records = $DB->get_records_sql($sql, [$rid]); + foreach ($records as $record) { + // Leave the date format in data storage format. + $answers[$record->questionid][] = answer\answer::create_from_data($record); + } + + return $answers; + } + + /** + * Configure bulk sql + * @return bulk_sql_config + */ + protected function bulk_sql_config() { + return new bulk_sql_config(static::response_table(), 'qrd', false, true, false); + } +} diff --git a/classes/responsetype/multiple.php b/classes/responsetype/multiple.php new file mode 100644 index 00000000..d5d943d4 --- /dev/null +++ b/classes/responsetype/multiple.php @@ -0,0 +1,222 @@ +. + +namespace mod_questionnaire\responsetype; + +/** + * Class for multiple response types. + * + * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire + */ +class multiple extends single { + /** + * The only differences between multuple and single responses are the + * response table and the insert logic. + */ + public static function response_table() { + return 'questionnaire_resp_multiple'; + } + + /** + * Provide an array of answer objects from web form data for the question. + * + * @param \stdClass $responsedata All of the responsedata as an object. + * @param \mod_questionnaire\question\question $question + * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects. + * @throws \coding_exception + */ + public static function answers_from_webform($responsedata, $question) { + $answers = []; + if (isset($responsedata->{'q'.$question->id})) { + foreach ($responsedata->{'q' . $question->id} as $cid => $cvalue) { + $cid = clean_param($cid, PARAM_CLEAN); + if (isset($question->choices[$cid])) { + $record = new \stdClass(); + $record->responseid = $responsedata->rid; + $record->questionid = $question->id; + $record->choiceid = $cid; + // If this choice is an "other" choice, look for the added input. + if ($question->choices[$cid]->is_other_choice()) { + $cname = \mod_questionnaire\question\choice::id_other_choice_name($cid); + $record->value = isset($responsedata->{'q' . $question->id}[$cname]) ? + $responsedata->{'q' . $question->id}[$cname] : ''; + } + $answers[$cid] = answer\answer::create_from_data($record); + } + } + } + return $answers; + } + + /** + * Provide an array of answer objects from mobile data for the question. + * + * @param \stdClass $responsedata All of the responsedata as an object. + * @param \mod_questionnaire\question\question $question + * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects. + */ + public static function answers_from_appdata($responsedata, $question) { + // Need to override "single" class' implementation. + $answers = []; + $qname = 'q'.$question->id; + if (isset($responsedata->{$qname}) && !empty($responsedata->{$qname})) { + foreach ($responsedata->{$qname} as $choiceid => $choicevalue) { + if ($choicevalue) { + $record = new \stdClass(); + $record->responseid = $responsedata->rid; + $record->questionid = $question->id; + $record->choiceid = $choiceid; + // If this choice is an "other" choice, look for the added input. + if (isset($question->choices[$choiceid]) && $question->choices[$choiceid]->is_other_choice()) { + $cname = \mod_questionnaire\question\choice::id_other_choice_name($choiceid); + $record->value = + isset($responsedata->{$qname}[$cname]) ? $responsedata->{$qname}[$cname] : ''; + } else { + $record->value = $choicevalue; + } + $answers[] = answer\answer::create_from_data($record); + } + } + } + return $answers; + } + + /** + * Return an array of answers by question/choice for the given response. Must be implemented by the subclass. + * Array is indexed by question, and contains an array by choice code of selected choices. + * + * @param int $rid The response id. + * @return array + */ + public static function response_select($rid) { + global $DB; + + $values = []; + $sql = 'SELECT a.id, q.id as qid, q.content, c.content as ccontent, c.id as cid, o.response ' . + 'FROM {'.static::response_table().'} a ' . + 'INNER JOIN {questionnaire_question} q ON a.question_id = q.id ' . + 'INNER JOIN {questionnaire_quest_choice} c ON a.choice_id = c.id ' . + 'LEFT JOIN {questionnaire_response_other} o ON a.response_id = o.response_id AND c.id = o.choice_id ' . + 'WHERE a.response_id = ? '; + $records = $DB->get_records_sql($sql, [$rid]); + if (!empty($records)) { + $qid = 0; + $newrow = []; + foreach ($records as $row) { + if ($qid == 0) { + $qid = $row->qid; + $newrow['content'] = $row->content; + $newrow['ccontent'] = $row->ccontent; + $newrow['responses'] = []; + } else if ($qid != $row->qid) { + $values[$qid] = $newrow; + $qid = $row->qid; + $newrow = []; + $newrow['content'] = $row->content; + $newrow['ccontent'] = $row->ccontent; + $newrow['responses'] = []; + } + $newrow['responses'][$row->cid] = $row->cid; + if (\mod_questionnaire\question\choice::content_is_other_choice($row->ccontent)) { + $newrow['responses'][\mod_questionnaire\question\choice::id_other_choice_name($row->cid)] = + $row->response; + } + } + $values[$qid] = $newrow; + } + + return $values; + } + + /** + * Return sql and params for getting responses in bulk. + * @param int|array $questionnaireids One id, or an array of ids. + * @param bool|int $responseid + * @param bool|int $userid + * @param bool $groupid + * @param int $showincompletes + * @return array + * author Guy Thomas + */ + public function get_bulk_sql($questionnaireids, $responseid = false, $userid = false, $groupid = false, $showincompletes = 0) { + global $DB; + + $sql = $this->bulk_sql(); + + if (($groupid !== false) && ($groupid > 0)) { + $groupsql = ' INNER JOIN {groups_members} gm ON gm.groupid = ? AND gm.userid = qr.userid '; + $gparams = [$groupid]; + } else { + $groupsql = ''; + $gparams = []; + } + + if (is_array($questionnaireids)) { + list($qsql, $params) = $DB->get_in_or_equal($questionnaireids); + } else { + $qsql = ' = ? '; + $params = [$questionnaireids]; + } + if ($showincompletes == 1) { + $showcompleteonly = ''; + } else { + $showcompleteonly = 'AND qr.complete = ? '; + $params[] = 'y'; + } + + $sql .= " + AND qr.questionnaireid $qsql $showcompleteonly + LEFT JOIN {questionnaire_response_other} qro ON qro.response_id = qr.id AND qro.choice_id = qrm.choice_id + LEFT JOIN {user} u ON u.id = qr.userid + $groupsql + "; + $params = array_merge($params, $gparams); + + if ($responseid) { + $sql .= " WHERE qr.id = ?"; + $params[] = $responseid; + } else if ($userid) { + $sql .= " WHERE qr.userid = ?"; + $params[] = $userid; + } + return [$sql, $params]; + } + + /** + * Return sql for getting responses in bulk. + * @return string + * author Guy Thomas + */ + protected function bulk_sql() { + global $DB; + + $userfields = $this->user_fields_sql(); + $alias = 'qrm'; + $extraselect = ''; + $extraselect .= 'qrm.choice_id, ' . $DB->sql_order_by_text('qro.response', 1000) . ' AS response, 0 AS rankvalue'; + + return " + SELECT " . $DB->sql_concat_join("'_'", ['qr.id', "'".$this->question->helpname()."'", $alias.'.id']) . " AS id, + qr.submitted, qr.complete, qr.grade, qr.userid, $userfields, qr.id AS rid, $alias.question_id, + $extraselect + FROM {questionnaire_response} qr + JOIN {".static::response_table()."} $alias ON $alias.response_id = qr.id + "; + } +} diff --git a/classes/responsetype/numericaltext.php b/classes/responsetype/numericaltext.php new file mode 100644 index 00000000..be14d4e3 --- /dev/null +++ b/classes/responsetype/numericaltext.php @@ -0,0 +1,82 @@ +. + +namespace mod_questionnaire\responsetype; + +/** + * Class for numerical text response types. + * + * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire + */ +class numericaltext extends text { + /** + * Provide an array of answer objects from web form data for the question. + * + * @param \stdClass $responsedata All of the responsedata as an object. + * @param \mod_questionnaire\question\question $question + * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects. + */ + public static function answers_from_webform($responsedata, $question) { + $answers = []; + if (isset($responsedata->{'q'.$question->id}) && is_numeric($responsedata->{'q'.$question->id})) { + $val = $responsedata->{'q' . $question->id}; + // Allow commas as well as points in decimal numbers. + $val = str_replace(",", ".", $responsedata->{'q' . $question->id}); + $val = preg_replace("/[^0-9.\-]*(-?[0-9]*\.?[0-9]*).*/", '\1', $val); + $record = new \stdClass(); + $record->responseid = $responsedata->rid; + $record->questionid = $question->id; + $record->value = $val; + $answers[] = answer\answer::create_from_data($record); + } + return $answers; + } + + /** + * Provide the result information for the specified result records. + * + * @param int|array $rids - A single response id, or array. + * @param string $sort - Optional display sort. + * @param boolean $anonymous - Whether or not responses are anonymous. + * @return string - Display output. + */ + public function display_results($rids=false, $sort='', $anonymous=false) { + if (is_array($rids)) { + $prtotal = 1; + } else if (is_int($rids)) { + $prtotal = 0; + } + if ($rows = $this->get_results($rids, $anonymous)) { + $numrespondents = count($rids); + $numresponses = count($rows); + // Count identical answers (numeric questions only). + $counts = []; + foreach ($rows as $row) { + if (!empty($row->response) || is_numeric($row->response)) { + $textidx = clean_text($row->response); + $counts[$textidx] = !empty($counts[$textidx]) ? ($counts[$textidx] + 1) : 1; + } + } + $pagetags = $this->get_results_tags($counts, $numrespondents, $numresponses, $prtotal); + } else { + $pagetags = new \stdClass(); + } + return $pagetags; + } +} diff --git a/classes/responsetype/rank.php b/classes/responsetype/rank.php new file mode 100644 index 00000000..3d5552bc --- /dev/null +++ b/classes/responsetype/rank.php @@ -0,0 +1,985 @@ +. + +namespace mod_questionnaire\responsetype; + +use Composer\Package\Package; +use mod_questionnaire\db\bulk_sql_config; + +/** + * Class for rank responses. + * + * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire + */ +class rank extends responsetype { + /** + * @var \stdClass $counts Range counts. + */ + public $counts; + + /** + * Provide the necessary response data table name. Should probably always be used with late static binding 'static::' form + * rather than 'self::' form to allow for class extending. + * + * @return string response table name. + */ + public static function response_table() { + return 'questionnaire_response_rank'; + } + + /** + * Provide an array of answer objects from web form data for the question. + * + * @param \stdClass $responsedata All of the responsedata as an object. + * @param \mod_questionnaire\question\question $question + * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects. + * @throws \coding_exception + */ + public static function answers_from_webform($responsedata, $question) { + $answers = []; + foreach ($question->choices as $cid => $choice) { + $other = isset($responsedata->{'q' . $question->id . '_' . $cid}) ? + $responsedata->{'q' . $question->id . '_' . $cid} : null; + // Choice not set or not answered. + if (!isset($other) || $other == '') { + continue; + } + if ($other == get_string('notapplicable', 'questionnaire')) { + $rank = -1; + } else { + $rank = intval($other); + } + $record = new \stdClass(); + $record->responseid = $responsedata->rid; + $record->questionid = $question->id; + $record->choiceid = $cid; + $record->value = $rank; + $answers[$cid] = answer\answer::create_from_data($record); + } + return $answers; + } + + /** + * Provide an array of answer objects from mobile data for the question. + * + * @param \stdClass $responsedata All of the responsedata as an object. + * @param \mod_questionnaire\question\question $question + * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects. + */ + public static function answers_from_appdata($responsedata, $question) { + $answers = []; + if (isset($responsedata->{'q'.$question->id}) && !empty($responsedata->{'q'.$question->id})) { + foreach ($responsedata->{'q' . $question->id} as $choiceid => $choicevalue) { + if (isset($question->choices[$choiceid])) { + $record = new \stdClass(); + $record->responseid = $responsedata->rid; + $record->questionid = $question->id; + $record->choiceid = $choiceid; + if (!empty($question->nameddegrees)) { + // If using named degrees, the app returns the label string. Find the value. + $nameddegreevalue = array_search($choicevalue, $question->nameddegrees); + if ($nameddegreevalue !== false) { + $choicevalue = $nameddegreevalue; + } + } + $record->value = $choicevalue; + $answers[] = answer\answer::create_from_data($record); + } + } + } + return $answers; + } + + /** + * Insert a provided response to the question. + * + * @param object $responsedata All of the responsedata as an object. + * @return int|bool - on error the subtype should call set_error and return false. + */ + public function insert_response($responsedata) { + global $DB; + + if (!$responsedata instanceof \mod_questionnaire\responsetype\response\response) { + $response = \mod_questionnaire\responsetype\response\response::response_from_webform($responsedata, [$this->question]); + } else { + $response = $responsedata; + } + + $resid = false; + + if (isset($response->answers[$this->question->id])) { + foreach ($response->answers[$this->question->id] as $answer) { + // Record the choice selection. + $record = new \stdClass(); + $record->response_id = $response->id; + $record->question_id = $this->question->id; + $record->choice_id = $answer->choiceid; + $record->rankvalue = $answer->value; + $resid = $DB->insert_record(static::response_table(), $record); + } + } + return $resid; + } + + /** + * @param bool $rids + * @param bool $anonymous + * @return array + * + * TODO - This works differently than all other get_results methods. This needs to be refactored. + */ + public function get_results($rids=false, $anonymous=false) { + global $DB; + + $rsql = ''; + if (!empty($rids)) { + list($rsql, $params) = $DB->get_in_or_equal($rids); + $rsql = ' AND response_id ' . $rsql; + } + + $select = 'question_id=' . $this->question->id . ' AND content NOT LIKE \'!other%\' ORDER BY id ASC'; + if ($rows = $DB->get_records_select('questionnaire_quest_choice', $select)) { + foreach ($rows as $row) { + $this->counts[$row->content] = new \stdClass(); + $nbna = $DB->count_records(static::response_table(), array('question_id' => $this->question->id, + 'choice_id' => $row->id, 'rankvalue' => '-1')); + $this->counts[$row->content]->nbna = $nbna; + } + } + + // For nameddegrees, need an array by degree value of positions (zero indexed). + $rankvalue = []; + if (!empty($this->question->nameddegrees)) { + $rankvalue = array_flip(array_keys($this->question->nameddegrees)); + } + + $isrestricted = ($this->question->length < count($this->question->choices)) && $this->question->no_duplicate_choices(); + // Usual case. + if (!$isrestricted) { + if (!empty ($rankvalue)) { + $sql = "SELECT r.id, c.content, r.rankvalue, c.id AS choiceid + FROM {questionnaire_quest_choice} c, {".static::response_table()."} r + WHERE r.choice_id = c.id + AND c.question_id = " . $this->question->id . " + AND r.rankvalue >= 0{$rsql} + ORDER BY choiceid"; + $results = $DB->get_records_sql($sql, $params); + $value = []; + foreach ($results as $result) { + if (isset($rankvalue[$result->rankvalue])) { + if (isset ($value[$result->choiceid])) { + $value[$result->choiceid] += $rankvalue[$result->rankvalue] + 1; + } else { + $value[$result->choiceid] = $rankvalue[$result->rankvalue] + 1; + } + } + } + } + + $sql = "SELECT c.id, c.content, a.average, a.num + FROM {questionnaire_quest_choice} c + INNER JOIN + (SELECT c2.id, AVG(a2.rankvalue) AS average, COUNT(a2.response_id) AS num + FROM {questionnaire_quest_choice} c2, {".static::response_table()."} a2 + WHERE c2.question_id = ? AND a2.question_id = ? AND a2.choice_id = c2.id AND a2.rankvalue >= 0{$rsql} + GROUP BY c2.id) a ON a.id = c.id + order by c.id"; + $results = $DB->get_records_sql($sql, array_merge(array($this->question->id, $this->question->id), $params)); + if (!empty ($rankvalue)) { + foreach ($results as $key => $result) { + if (isset($value[$key])) { + $result->averagevalue = $value[$key] / $result->num; + } + } + } + // Reindex by 'content'. Can't do this from the query as it won't work with MS-SQL. + foreach ($results as $key => $result) { + $results[$result->content] = $result; + unset($results[$key]); + } + return $results; + // Case where scaleitems is less than possible choices. + } else { + $sql = "SELECT c.id, c.content, a.sum, a.num + FROM {questionnaire_quest_choice} c + INNER JOIN + (SELECT c2.id, SUM(a2.rankvalue) AS sum, COUNT(a2.response_id) AS num + FROM {questionnaire_quest_choice} c2, {".static::response_table()."} a2 + WHERE c2.question_id = ? AND a2.question_id = ? AND a2.choice_id = c2.id AND a2.rankvalue >= 0{$rsql} + GROUP BY c2.id) a ON a.id = c.id"; + $results = $DB->get_records_sql($sql, array_merge(array($this->question->id, $this->question->id), $params)); + // Formula to calculate the best ranking order. + $nbresponses = count($rids); + foreach ($results as $key => $result) { + $result->average = ($result->sum + ($nbresponses - $result->num) * ($this->length + 1)) / $nbresponses; + $results[$result->content] = $result; + unset($results[$key]); + } + return $results; + } + } + + /** + * Provide the feedback scores for all requested response id's. This should be provided only by questions that provide feedback. + * @param array $rids + * @return array | boolean + */ + public function get_feedback_scores(array $rids) { + global $DB; + + $rsql = ''; + $params = [$this->question->id]; + if (!empty($rids)) { + list($rsql, $rparams) = $DB->get_in_or_equal($rids); + $params = array_merge($params, $rparams); + $rsql = ' AND response_id ' . $rsql; + } + $params[] = 'y'; + + $sql = 'SELECT r.id, r.response_id as rid, r.question_id AS qid, r.choice_id AS cid, r.rankvalue ' . + 'FROM {'.$this->response_table().'} r ' . + 'INNER JOIN {questionnaire_quest_choice} c ON r.choice_id = c.id ' . + 'WHERE r.question_id= ? ' . $rsql . ' ' . + 'ORDER BY rid,cid ASC'; + $responses = $DB->get_recordset_sql($sql, $params); + + $rid = 0; + $feedbackscores = []; + foreach ($responses as $response) { + if ($rid != $response->rid) { + $rid = $response->rid; + $feedbackscores[$rid] = new \stdClass(); + $feedbackscores[$rid]->rid = $rid; + $feedbackscores[$rid]->score = 0; + } + // Only count scores that are currently defined (in case old responses are using older data). + $feedbackscores[$rid]->score += isset($this->question->nameddegrees[$response->rankvalue]) ? $response->rankvalue : 0; + } + + return (!empty($feedbackscores) ? $feedbackscores : false); + } + + /** + * Provide a template for results screen if defined. + * @param bool $pdf + * @return mixed The template string or false/ + */ + public function results_template($pdf = false) { + if ($pdf) { + return 'mod_questionnaire/resultspdf_rate'; + } else { + return 'mod_questionnaire/results_rate'; + } + } + + /** + * Provide the result information for the specified result records. + * + * @param int|array $rids - A single response id, or array. + * @param string $sort - Optional display sort. + * @param boolean $anonymous - Whether or not responses are anonymous. + * @return string - Display output. + */ + public function display_results($rids=false, $sort='', $anonymous=false) { + $output = ''; + + if (is_array($rids)) { + $prtotal = 1; + } else if (is_int($rids)) { + $prtotal = 0; + } + + if ($rows = $this->get_results($rids, $sort, $anonymous)) { + $stravgvalue = ''; // For printing table heading. + foreach ($this->counts as $key => $value) { + $ccontent = $key; + $avgvalue = ''; + if (array_key_exists($ccontent, $rows)) { + $avg = $rows[$ccontent]->average; + $this->counts[$ccontent]->num = $rows[$ccontent]->num; + if (isset($rows[$ccontent]->averagevalue)) { + $avgvalue = $rows[$ccontent]->averagevalue; + $osgood = false; + if ($this->question->osgood_rate_scale()) { // Osgood's semantic differential. + $osgood = true; + } + if ($stravgvalue == '' && !$osgood) { + $stravgvalue = ' ('.get_string('andaveragevalues', 'questionnaire').')'; + } + } else { + $avgvalue = null; + } + } else { + $avg = 0; + } + $this->counts[$ccontent]->avg = $avg; + $this->counts[$ccontent]->avgvalue = $avgvalue; + } + $output1 = $this->mkresavg($sort, $stravgvalue); + $output2 = $this->mkrescount($rids, $rows, $sort); + $output = (object)array_merge((array)$output1, (array)$output2); + } else { + $output = (object)['noresponses' => true]; + } + return $output; + } + + /** + * Return an array of answers by question/choice for the given response. Must be implemented by the subclass. + * + * @param int $rid The response id. + * @return array + */ + public static function response_select($rid) { + global $DB; + + $values = []; + $sql = 'SELECT a.id as aid, q.id AS qid, q.precise AS precise, c.id AS cid, q.content, c.content as ccontent, + a.rankvalue as arank '. + 'FROM {'.static::response_table().'} a, {questionnaire_question} q, {questionnaire_quest_choice} c '. + 'WHERE a.response_id= ? AND a.question_id=q.id AND a.choice_id=c.id '. + 'ORDER BY aid, a.question_id, c.id'; + $records = $DB->get_records_sql($sql, [$rid]); + foreach ($records as $row) { + // Next two are 'qid' and 'cid', each with numeric and hash keys. + $osgood = false; + if (\mod_questionnaire\question\rate::type_is_osgood_rate_scale($row->precise)) { + $osgood = true; + } + $qid = $row->qid.'_'.$row->cid; + unset($row->aid); // Get rid of the answer id. + unset($row->qid); + unset($row->cid); + unset($row->precise); + $row = (array)$row; + $newrow = []; + foreach ($row as $key => $val) { + if ($key != 'content') { // No need to keep question text - ony keep choice text and rank. + if ($key == 'ccontent') { + if ($osgood) { + list($contentleft, $contentright) = array_merge(preg_split('/[|]/', $val), [' ']); + $contents = questionnaire_choice_values($contentleft); + if ($contents->title) { + $contentleft = $contents->title; + } + $contents = questionnaire_choice_values($contentright); + if ($contents->title) { + $contentright = $contents->title; + } + $val = strip_tags($contentleft.'|'.$contentright); + $val = preg_replace("/[\r\n\t]/", ' ', $val); + } else { + $contents = questionnaire_choice_values($val); + if ($contents->modname) { + $val = $contents->modname; + } else if ($contents->title) { + $val = $contents->title; + } else if ($contents->text) { + $val = strip_tags($contents->text); + $val = preg_replace("/[\r\n\t]/", ' ', $val); + } + } + } + $newrow[] = $val; + } + } + $values[$qid] = $newrow; + } + + return $values; + } + + /** + * Return an array of answer objects by question for the given response id. + * THIS SHOULD REPLACE response_select. + * + * @param int $rid The response id. + * @return array array answer + * @throws \dml_exception + */ + public static function response_answers_by_question($rid) { + global $DB; + + $answers = []; + $sql = 'SELECT id, response_id as responseid, question_id as questionid, choice_id as choiceid, rankvalue as value ' . + 'FROM {' . static::response_table() .'} ' . + 'WHERE response_id = ? '; + $records = $DB->get_records_sql($sql, [$rid]); + foreach ($records as $record) { + $answers[$record->questionid][$record->choiceid] = answer\answer::create_from_data($record); + } + + return $answers; + } + + /** + * Configure bulk sql + * @return bulk_sql_config + */ + protected function bulk_sql_config() { + return new bulk_sql_config(static::response_table(), 'qrr', true, false, true); + } + + /** + * Return a structure for averages. + * @param string $sort + * @param string $stravgvalue + * @return \stdClass + */ + private function mkresavg($sort, $stravgvalue='') { + global $CFG; + + $stravgrank = get_string('averagerank', 'questionnaire'); + $osgood = false; + if ($this->question->precise == 3) { // Osgood's semantic differential. + $osgood = true; + $stravgrank = get_string('averageposition', 'questionnaire'); + } + $stravg = '
'.$stravgrank.$stravgvalue.'
'; + + $isna = $this->question->precise == 1; + $isnahead = ''; + $nbchoices = count($this->counts); + $isrestricted = ($this->question->length < $nbchoices) && $this->question->precise == 2; + + if ($isna) { + $isnahead = get_string('notapplicable', 'questionnaire'); + } + $pagetags = new \stdClass(); + $pagetags->averages = new \stdClass(); + + if ($isna) { + $header1 = new \stdClass(); + $header1->text = ''; + $header1->align = ''; + $header2 = new \stdClass(); + $header2->text = $stravg; + $header2->align = ''; + $header3 = new \stdClass(); + $header3->text = '⇓'; + $header3->align = 'center'; + $header4 = new \stdClass(); + $header4->text = $isnahead; + $header4->align = 'right'; + } else { + if ($osgood) { + $stravg = '
'.$stravgrank.'
'; + $header1 = new \stdClass(); + $header1->text = ''; + $header1->align = ''; + $header2 = new \stdClass(); + $header2->text = $stravg; + $header2->align = ''; + $header3 = new \stdClass(); + $header3->text = ''; + $header3->align = 'center'; + } else { + $header1 = new \stdClass(); + $header1->text = ''; + $header1->align = ''; + $header2 = new \stdClass(); + $header2->text = $stravg; + $header2->align = ''; + $header3 = new \stdClass(); + $header3->text = '⇓'; + $header3->align = 'center'; + } + } + // PDF columns are based on a 11.69in x 8.27in page. Margins are 15mm each side, or 1.1811 in total. + $pdfwidth = 11.69 - 1.1811; + if ($isna) { + $header1->width = '55%'; + $header2->width = '35%'; + $header3->width = '5%'; + $header4->width = '5%'; + $header1->pdfwidth = $pdfwidth * .55; + $header2->pdfwidth = $pdfwidth * .35; + $header3->pdfwidth = $pdfwidth * .05; + $header4->pdfwidth = $pdfwidth * .05; + } else if ($osgood) { + $header1->width = '25%'; + $header2->width = '50%'; + $header3->width = '25%'; + $header1->pdfwidth = $pdfwidth * .25; + $header2->pdfwidth = $pdfwidth * .5; + $header3->pdfwidth = $pdfwidth * .25; + } else { + $header1->width = '60%'; + $header2->width = '35%'; + $header3->width = '5%'; + $header1->pdfwidth = $pdfwidth * .6; + $header2->pdfwidth = $pdfwidth * .35; + $header3->pdfwidth = $pdfwidth * .05; + } + $pagetags->averages->headers = [$header1, $header2, $header3]; + if (isset($header4)) { + $pagetags->averages->headers[] = $header4; + } + + $imageurl = $CFG->wwwroot.'/mod/questionnaire/images/hbar.gif'; + $spacerimage = $CFG->wwwroot . '/mod/questionnaire/images/hbartransp.gif'; + $llength = $this->question->length; + if (!$llength) { + $llength = 5; + } + // Add an extra column to accomodate lower ranks in this case. + $llength += $isrestricted; + $width = 100 / $llength; + $n = array(); + $nameddegrees = 0; + foreach ($this->question->nameddegrees as $degree) { + // To take into account languages filter. + $content = (format_text($degree, FORMAT_HTML, ['noclean' => true])); + $n[$nameddegrees] = $degree; + $nameddegrees++; + } + for ($j = 0; $j < $this->question->length; $j++) { + if (isset($n[$j])) { + $str = $n[$j]; + } else { + $str = $j + 1; + } + } + $rankcols = []; + $pdfwidth = $header2->pdfwidth / (100 / $width); + for ($i = 0; $i <= $llength - 1; $i++) { + if ($isrestricted && $i == $llength - 1) { + $str = "..."; + $rankcols[] = (object)['width' => $width . '%', 'text' => '...', 'pdfwidth' => $pdfwidth]; + } else if (isset($n[$i])) { + $str = $n[$i]; + $rankcols[] = (object)['width' => $width . '%', 'text' => $n[$i], 'pdfwidth' => $pdfwidth]; + } else { + $str = $i + 1; + $rankcols[] = (object)['width' => $width . '%', 'text' => $i + 1, 'pdfwidth' => $pdfwidth]; + } + } + $pagetags->averages->choicelabelrow = new \stdClass(); + $pagetags->averages->choicelabelrow->innertablewidth = $header2->pdfwidth; + $pagetags->averages->choicelabelrow->column1 = (object)['width' => $header1->width, 'align' => $header1->align, + 'text' => '', 'pdfwidth' => $header1->pdfwidth]; + $pagetags->averages->choicelabelrow->column2 = (object)['width' => $header2->width, 'align' => $header2->align, + 'ranks' => $rankcols, 'pdfwidth' => $header2->pdfwidth]; + $pagetags->averages->choicelabelrow->column3 = (object)['width' => $header3->width, 'align' => $header3->align, + 'text' => '', 'pdfwidth' => $header3->pdfwidth]; + if ($isna) { + $pagetags->averages->choicelabelrow->column4 = (object)['width' => $header4->width, 'align' => $header4->align, + 'text' => '', 'pdfwidth' => $header4->pdfwidth]; + } + + switch ($sort) { + case 'ascending': + uasort($this->counts, self::class . '::sortavgasc'); + break; + case 'descending': + uasort($this->counts, self::class . '::sortavgdesc'); + break; + } + reset ($this->counts); + + if (!empty($this->counts) && is_array($this->counts)) { + $pagetags->averages->choiceaverages = []; + foreach ($this->counts as $content => $contentobj) { + // Eliminate potential named degrees on Likert scale. + if (!preg_match("/^[0-9]{1,3}=/", $content)) { + if (isset($contentobj->avg)) { + $avg = $contentobj->avg; + // If named degrees were used, swap averages for display. + if (isset($contentobj->avgvalue)) { + $avg = $contentobj->avgvalue; + $avgvalue = $contentobj->avg; + } else { + $avgvalue = ''; + } + } else { + $avg = ''; + } + $nbna = $contentobj->nbna; + + if ($avg) { + if (($j = $avg * $width) > 0) { + $marginposition = ($avg - 0.5 ) / ($this->question->length + $isrestricted); + } + if (!right_to_left()) { + $margin = 'margin-left:' . $marginposition * 100 . '%'; + $marginpdf = $marginposition * $pagetags->averages->choicelabelrow->innertablewidth; + } else { + $margin = 'margin-right:' . $marginposition * 100 . '%'; + $marginpdf = $pagetags->averages->choicelabelrow->innertablewidth - + ($marginposition * $pagetags->averages->choicelabelrow->innertablewidth); + } + } else { + $margin = ''; + } + + if ($osgood) { + // Ensure there are two bits of content. + list($content, $contentright) = array_merge(preg_split('/[|]/', $content), array(' ')); + } else { + $contents = questionnaire_choice_values($content); + if ($contents->modname) { + $content = $contents->text; + } + } + if ($osgood) { + $choicecol1 = new \stdClass(); + $choicecol1->width = $header1->width; + $choicecol1->pdfwidth = $header1->pdfwidth; + $choicecol1->align = $header1->align; + $choicecol1->text = '
' . + format_text($content, FORMAT_HTML, ['noclean' => true]) . '
'; + $choicecol2 = new \stdClass(); + $choicecol2->width = $header2->width; + $choicecol2->pdfwidth = $header2->pdfwidth; + $choicecol2->align = $header2->align; + $choicecol2->imageurl = $imageurl; + $choicecol2->spacerimage = $spacerimage; + $choicecol2->margin = $margin; + $choicecol2->marginpdf = $marginpdf; + $choicecol3 = new \stdClass(); + $choicecol3->width = $header3->width; + $choicecol3->pdfwidth = $header3->pdfwidth; + $choicecol3->align = $header3->align; + $choicecol3->text = '
' . + format_text($contentright, FORMAT_HTML, ['noclean' => true]) . '
'; + $pagetags->averages->choiceaverages[] = (object)['column1' => $choicecol1, 'column2' => $choicecol2, + 'column3' => $choicecol3]; + // JR JUNE 2012 do not display meaningless average rank values for Osgood. + } else if ($avg || ($nbna != 0)) { + $stravgval = ''; + if ($avg) { + if ($stravgvalue) { + $stravgval = '('.sprintf('%.1f', $avgvalue).')'; + } + $stravgval = sprintf('%.1f', $avg).' '.$stravgval; + if ($isna) { + $choicecol4 = new \stdClass(); + $choicecol4->width = $header4->width; + $choicecol4->pdfwidth = $header4->pdfwidth; + $choicecol4->align = $header4->align; + $choicecol4->text = $nbna; + } + } + $choicecol1 = new \stdClass(); + $choicecol1->width = $header1->width; + $choicecol1->pdfwidth = $header1->pdfwidth; + $choicecol1->align = $header1->align; + $choicecol1->text = format_text($content, FORMAT_HTML, ['noclean' => true]); + $choicecol2 = new \stdClass(); + $choicecol2->width = $header2->width; + $choicecol2->pdfwidth = $header2->pdfwidth; + $choicecol2->align = $header2->align; + $choicecol2->imageurl = $imageurl; + $choicecol2->spacerimage = $spacerimage; + $choicecol2->margin = $margin; + $choicecol2->marginpdf = $marginpdf; + $choicecol3 = new \stdClass(); + $choicecol3->width = $header3->width; + $choicecol3->pdfwidth = $header3->pdfwidth; + $choicecol3->align = $header3->align; + $choicecol3->text = $stravgval; + if ($avg) { + if (isset($choicecol4)) { + $pagetags->averages->choiceaverages[] = (object)['column1' => $choicecol1, + 'column2' => $choicecol2, 'column3' => $choicecol3, 'column4' => $choicecol4]; + } else { + $pagetags->averages->choiceaverages[] = (object)['column1' => $choicecol1, + 'column2' => $choicecol2, 'column3' => $choicecol3]; + } + } else { + $choicecol4 = new \stdClass(); + $choicecol4->width = $header4->width; + $choicecol4->pdfwidth = $header4->pdfwidth; + $choicecol4->align = $header4->align; + $choicecol4->text = $nbna; + $pagetags->averages->choiceaverages[] = (object)['column1' => $choicecol1, 'column2' => $choicecol2, + 'column3' => $choicecol3]; + } + } + } // End if named degrees. + } // End foreach. + } else { + $nodata1 = new \stdClass(); + $nodata1->width = $header1->width; + $nodata1->align = $header1->align; + $nodata1->text = ''; + $nodata2 = new \stdClass(); + $nodata2->width = $header2->width; + $nodata2->align = $header2->align; + $nodata2->text = get_string('noresponsedata', 'mod_questionnaire'); + $nodata3 = new \stdClass(); + $nodata3->width = $header3->width; + $nodata3->align = $header3->align; + $nodata3->text = ''; + if (isset($header4)) { + $nodata4 = new \stdClass(); + $nodata4->width = $header4->width; + $nodata4->align = $header4->align; + $nodata4->text = ''; + $pagetags->averages->nodata = [$nodata1, $nodata2, $nodata3, $nodata4]; + } else { + $pagetags->averages->nodata = [$nodata1, $nodata2, $nodata3]; + } + } + return $pagetags; + } + + /** + * Return a structure for counts. + * @param array $rids + * @param array $rows + * @param string $sort + * @return \stdClass + */ + private function mkrescount($rids, $rows, $sort) { + // Display number of responses to Rate questions - see http://moodle.org/mod/forum/discuss.php?d=185106. + global $DB; + + $nbresponses = count($rids); + // Prepare data to be displayed. + $isrestricted = ($this->question->length < count($this->question->choices)) && $this->question->precise == 2; + + $rsql = ''; + if (!empty($rids)) { + list($rsql, $params) = $DB->get_in_or_equal($rids); + $rsql = ' AND response_id ' . $rsql; + } + + array_unshift($params, $this->question->id); // This is question_id. + $sql = 'SELECT r.id, c.content, r.rankvalue, c.id AS choiceid ' . + 'FROM {questionnaire_quest_choice} c , ' . + '{questionnaire_response_rank} r ' . + 'WHERE c.question_id = ?' . + ' AND r.question_id = c.question_id' . + ' AND r.choice_id = c.id ' . + $rsql . + ' ORDER BY choiceid, rankvalue ASC'; + $choices = $DB->get_records_sql($sql, $params); + + // Sort rows (results) by average value. + if ($sort != 'default') { + $sortarray = array(); + foreach ($rows as $row) { + foreach ($row as $key => $value) { + if (!isset($sortarray[$key])) { + $sortarray[$key] = array(); + } + $sortarray[$key][] = $value; + } + } + $orderby = "average"; + switch ($sort) { + case 'ascending': + array_multisort($sortarray[$orderby], SORT_ASC, $rows); + break; + case 'descending': + array_multisort($sortarray[$orderby], SORT_DESC, $rows); + break; + } + } + $nbranks = $this->question->length; + $ranks = []; + $rankvalue = []; + if (!empty($this->question->nameddegrees)) { + $rankvalue = array_flip(array_keys($this->question->nameddegrees)); + } + foreach ($rows as $row) { + $choiceid = $row->id; + foreach ($choices as $choice) { + if ($choice->choiceid == $choiceid) { + $n = 0; + for ($i = 1; $i <= $nbranks; $i++) { + if ((isset($rankvalue[$choice->rankvalue]) && ($rankvalue[$choice->rankvalue] == ($i - 1))) || + (empty($rankvalue) && ($choice->rankvalue == $i))) { + $n++; + if (!isset($ranks[$choice->content][$i])) { + $ranks[$choice->content][$i] = 0; + } + $ranks[$choice->content][$i] += $n; + } else if (!isset($ranks[$choice->content][$i])) { + $ranks[$choice->content][$i] = 0; + } + } + } + } + } + + // Psettings for display. + $strtotal = ''.get_string('total', 'questionnaire').''; + $isna = $this->question->precise == 1; + $osgood = false; + if ($this->question->precise == 3) { // Osgood's semantic differential. + $osgood = true; + } + if ($this->question->precise == 1) { + $na = get_string('notapplicable', 'questionnaire'); + } else { + $na = ''; + } + $nameddegrees = 0; + $n = array(); + foreach ($this->question->nameddegrees as $degree) { + $content = $degree; + $n[$nameddegrees] = format_text($content, FORMAT_HTML, ['noclean' => true]); + $nameddegrees++; + } + foreach ($this->question->choices as $choice) { + $contents = questionnaire_choice_values($choice->content); + if ($contents->modname) { + $choice->content = $contents->text; + } + } + + $pagetags = new \stdClass(); + $pagetags->totals = new \stdClass(); + $pagetags->totals->headers = []; + if ($osgood) { + $align = 'right'; + } else { + $align = 'left'; + } + $pagetags->totals->headers[] = (object)['align' => $align, + 'text' => ''.get_string('responses', 'questionnaire').'']; + + // Display the column titles. + for ($j = 0; $j < $this->question->length; $j++) { + if (isset($n[$j])) { + $str = $n[$j]; + } else { + $str = $j + 1; + } + $pagetags->totals->headers[] = (object)['align' => 'center', 'text' => ''.$str.'']; + } + if ($osgood) { + $pagetags->totals->headers[] = (object)['align' => 'left', 'text' => '']; + } + $pagetags->totals->headers[] = (object)['align' => 'center', 'text' => $strtotal]; + if ($isrestricted) { + $pagetags->totals->headers[] = (object)['align' => 'center', 'text' => get_string('notapplicable', 'questionnaire')]; + } + if ($na) { + $pagetags->totals->headers[] = (object)['align' => 'center', 'text' => $na]; + } + + // Now display the responses. + $pagetags->totals->choices = []; + foreach ($ranks as $content => $rank) { + $totalcols = []; + // Eliminate potential named degrees on Likert scale. + if (!preg_match("/^[0-9]{1,3}=/", $content)) { + // First display the list of degrees (named or un-named) + // number of NOT AVAILABLE responses for this possible answer. + $nbna = $this->counts[$content]->nbna; + // TOTAL number of responses for this possible answer. + $total = $this->counts[$content]->num; + $nbresp = ''.$total.''; + if ($osgood) { + // Ensure there are two bits of content. + list($content, $contentright) = array_merge(preg_split('/[|]/', $content), array(' ')); + $header = reset($pagetags->totals->headers); + $totalcols[] = (object)['align' => $header->align, + 'text' => format_text($content, FORMAT_HTML, ['noclean' => true])]; + } else { + // Eliminate potentially short-named choices. + $contents = questionnaire_choice_values($content); + if ($contents->modname) { + $content = $contents->text; + } + $header = reset($pagetags->totals->headers); + $totalcols[] = (object)['align' => $header->align, + 'text' => format_text($content, FORMAT_HTML, ['noclean' => true])]; + } + // Display ranks/rates numbers. + $maxrank = max($rank); + for ($i = 1; $i <= $this->question->length; $i++) { + $percent = ''; + if (isset($rank[$i])) { + $str = $rank[$i]; + if ($total !== 0 && $str !== 0) { + $percent = ' ('.number_format(($str * 100) / $total).'%)'; + } + // Emphasize responses with max rank value. + if ($str == $maxrank) { + $str = ''.$str.''; + } + } else { + $str = 0; + } + $header = next($pagetags->totals->headers); + $totalcols[] = (object)['align' => $header->align, 'text' => $str.$percent]; + } + if ($osgood) { + $header = next($pagetags->totals->headers); + $totalcols[] = (object)['align' => $header->align, + 'text' => format_text($contentright, FORMAT_HTML, ['noclean' => true])]; + } + $header = next($pagetags->totals->headers); + $totalcols[] = (object)['align' => $header->align, 'text' => $nbresp]; + if ($isrestricted) { + $header = next($pagetags->totals->headers); + $totalcols[] = (object)['align' => $header->align, 'text' => $nbresponses - $total]; + } + if (!$osgood) { + if ($na) { + $header = next($pagetags->totals->headers); + $totalcols[] = (object)['align' => $header->align, 'text' => $nbna]; + } + } + } // End named degrees. + $pagetags->totals->choices[] = (object)['totalcols' => $totalcols]; + } + return $pagetags; + } + + /** + * Sorting function for ascending. + * @param \stdClass $a + * @param \stdClass $b + * @return int + */ + private static function sortavgasc($a, $b) { + if (isset($a->avg) && isset($b->avg)) { + if ( $a->avg < $b->avg ) { + return -1; + } else if ($a->avg > $b->avg ) { + return 1; + } else { + return 0; + } + } + } + + /** + * Sorting function for descending. + * @param \stdClass $a + * @param \stdClass $b + * @return int + */ + private static function sortavgdesc($a, $b) { + if (isset($a->avg) && isset($b->avg)) { + if ( $a->avg > $b->avg ) { + return -1; + } else if ($a->avg < $b->avg) { + return 1; + } else { + return 0; + } + } + } +} diff --git a/classes/responsetype/response/response.php b/classes/responsetype/response/response.php new file mode 100644 index 00000000..070160d2 --- /dev/null +++ b/classes/responsetype/response/response.php @@ -0,0 +1,173 @@ +. + +namespace mod_questionnaire\responsetype\response; + +/** + * This defines a structured class to hold responses. + * + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire + * @copyright 2019, onwards Poet + */ +class response { + + // Class properties. + + /** @var int $id The id of the response this applies to. */ + public $id; + + /** @var int $questionnaireid The id of the questionnaire this response applies to. */ + public $questionnaireid; + + /** @var int $userid The id of the user for this response. */ + public $userid; + + /** @var int $submitted The most recent submission date of this response. */ + public $submitted; + + /** @var boolean $complete Flag for final submission of this response. */ + public $complete; + + /** @var int $grade Numeric grade for this response (if applicable). */ + public $grade; + + /** @var array $answers Array by question of array of answer objects. */ + public $answers; + + /** + * Choice constructor. + * @param null $id + * @param null $questionnaireid + * @param null $userid + * @param null $submitted + * @param null $complete + * @param null $grade + * @param bool $addanswers + */ + public function __construct($id = null, $questionnaireid = null, $userid = null, $submitted = null, $complete = null, + $grade = null, $addanswers = true) { + $this->id = $id; + $this->questionnaireid = $questionnaireid; + $this->userid = $userid; + $this->submitted = $submitted; + $this->complete = $complete; + $this->grade = $grade; + + // Add answers by questions that exist. + if ($addanswers) { + $this->add_questions_answers(); + } + } + + /** + * Create and return a response object from data. + * + * @param \stdClass|array $responsedata The data to load. + * @return response + */ + public static function create_from_data($responsedata) { + if (!is_array($responsedata)) { + $responsedata = (array)$responsedata; + } + + $properties = array_keys(get_class_vars(__CLASS__)); + foreach ($properties as $property) { + if (!isset($responsedata[$property])) { + $responsedata[$property] = null; + } + } + + return new response($responsedata['id'], $responsedata['questionnaireid'], $responsedata['userid'], + $responsedata['submitted'], $responsedata['complete'], $responsedata['grade']); + } + + /** + * Provide a response object from web form data to the question. + * + * @param \stdClass $responsedata All of the responsedata as an object. + * @param array $questions + * @return bool|response A response object. + */ + public static function response_from_webform($responsedata, $questions) { + global $USER; + + $questionnaireid = isset($responsedata->questionnaire_id) ? $responsedata->questionnaire_id : + (isset($responsedata->a) ? $responsedata->a : 0); + $response = new response($responsedata->rid, $questionnaireid, $USER->id, null, null, null, false); + foreach ($questions as $question) { + if ($question->supports_responses()) { + $response->answers[$question->id] = $question->responsetype::answers_from_webform($responsedata, $question); + } + } + return $response; + } + + /** + * Provide a response object from mobile app data to the question. + * + * @param id $questionnaireid + * @param id $responseid + * @param \stdClass $responsedata All of the responsedata as an object. + * @param array $questions Array of question objects. + * @return bool|response A response object. + */ + public static function response_from_appdata($questionnaireid, $responseid, $responsedata, $questions) { + global $USER; + + $response = new response($responseid, $questionnaireid, $USER->id, null, null, null, false); + + // Process app data by question and choice and create a webform structure. + $processedresponses = new \stdClass(); + $processedresponses->rid = $responseid; + foreach ($responsedata as $answerid => $value) { + $parts = explode('_', $answerid); + if ($parts[0] == 'response') { + $qid = 'q' . $parts[2]; + if (!isset($processedresponses->{$qid})) { + $processedresponses->{$qid} = []; + } + if (isset($parts[3])) { + $cid = $parts[3]; + } else { + $cid = 0; + } + $processedresponses->{$qid}[$cid] = $value; + } + } + + foreach ($questions as $question) { + if ($question->supports_responses() && isset($processedresponses->{'q'.$question->id})) { + $response->answers[$question->id] = $question->responsetype::answers_from_appdata($processedresponses, $question); + } + } + return $response; + } + + /** + * Add the answers to each question in a question array of answers structure. + */ + public function add_questions_answers() { + $this->answers = []; + $this->answers += \mod_questionnaire\responsetype\multiple::response_answers_by_question($this->id); + $this->answers += \mod_questionnaire\responsetype\single::response_answers_by_question($this->id); + $this->answers += \mod_questionnaire\responsetype\rank::response_answers_by_question($this->id); + $this->answers += \mod_questionnaire\responsetype\boolean::response_answers_by_question($this->id); + $this->answers += \mod_questionnaire\responsetype\date::response_answers_by_question($this->id); + $this->answers += \mod_questionnaire\responsetype\text::response_answers_by_question($this->id); + } +} diff --git a/classes/response/base.php b/classes/responsetype/responsetype.php similarity index 70% rename from classes/response/base.php rename to classes/responsetype/responsetype.php index ce857ad2..59e3715d 100644 --- a/classes/response/base.php +++ b/classes/responsetype/responsetype.php @@ -14,51 +14,81 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * This file contains the parent class for questionnaire question types. - * - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes - */ +namespace mod_questionnaire\responsetype; -namespace mod_questionnaire\response; -defined('MOODLE_INTERNAL') || die(); use \html_writer; use \html_table; use mod_questionnaire\db\bulk_sql_config; /** - * Class for describing a response. + * This file contains the parent class for questionnaire response types. * * @author Mike Churchward - * @package response + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire */ +abstract class responsetype { + + // Class properties. + /** @var \mod_questionnaire\question\question $question The question for this response. */ + public $question; -abstract class base { + /** @var int $responseid The id of the response this is for. */ + public $responseid; - public function __construct($question) { + /** @var array $choices An array of \mod_questionnaire\responsetype\choice objects. */ + public $choices; + + /** + * responsetype constructor. + * @param \mod_questionnaire\question\question $question + * @param int|null $responseid + * @param array $choices + */ + public function __construct(\mod_questionnaire\question\question $question, int $responseid = null, array $choices = []) { $this->question = $question; + $this->responseid = $responseid; + $this->choices = $choices; } /** - * Provide the necessary response data table name. + * Provide the necessary response data table name. Should probably always be used with late static binding 'static::' form + * rather than 'self::' form to allow for class extending. * * @return string response table name. */ - static public function response_table() { + public static function response_table() { return 'Must be implemented!'; } + /** + * Return the known response tables. Should be replaced by a better management system eventually. + * @return array + */ + public static function all_response_tables() { + return ['questionnaire_response_bool', 'questionnaire_response_date', 'questionnaire_response_other', + 'questionnaire_response_rank', 'questionnaire_response_text', 'questionnaire_resp_multiple', + 'questionnaire_resp_single']; + } + + /** + * Provide an array of answer objects from web form data for the question. + * + * @param \stdClass $responsedata All of the responsedata as an object. + * @param \mod_questionnaire\question\question $question + * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects. + */ + abstract public static function answers_from_webform($responsedata, $question); + /** * Insert a provided response to the question. * - * @param integer $rid - The data id of the response table id. - * @param mixed $val - The response data provided. + * @param object $responsedata All of the responsedata as an object. * @return int|bool - on error the subtype should call set_error and return false. */ - abstract public function insert_response($rid, $val); + abstract public function insert_response($responsedata); /** * Provide the result information for the specified result records. @@ -81,7 +111,7 @@ abstract public function display_results($rids=false, $sort='', $anonymous=false /** * If the choice id needs to be transformed into a different value, override this in the child class. - * @param $choiceid + * @param mixed $choiceid * @return mixed */ public function transform_choiceid($choiceid) { @@ -90,22 +120,22 @@ public function transform_choiceid($choiceid) { /** * Provide a template for results screen if defined. - * @return mixed The template string or false/ + * @param bool $pdf + * @return mixed The template string or false. */ - public function results_template() { + public function results_template($pdf = false) { return false; } /** * Gets the results tags for templates for questions with defined choices (single, multiple, boolean). * - * @param $weights - * @param $participants Number of questionnaire participants. - * @param $respondents Number of question respondents. - * @param $showtotals + * @param array $weights + * @param int $participants Number of questionnaire participants. + * @param int $respondents Number of question respondents. + * @param int $showtotals * @param string $sort * @return \stdClass - * @throws \coding_exception */ public function get_results_tags($weights, $participants, $respondents, $showtotals = 1, $sort = '') { global $CFG; @@ -128,6 +158,7 @@ public function get_results_tags($weights, $participants, $respondents, $showtot reset ($weights); $pagetags->responses = []; + $evencolor = false; foreach ($weights as $content => $num) { $response = new \stdClass(); $response->text = format_text($content, FORMAT_HTML, ['noclean' => true]); @@ -157,8 +188,11 @@ public function get_results_tags($weights, $participants, $respondents, $showtot $response->percent = sprintf(' %.'.$precision.'f%%', $percent); } $response->total = $num; + // The 'evencolor' attribute is used by the PDF template. + $response->evencolor = $evencolor; $pagetags->responses[] = (object)['response' => $response]; $pos++; + $evencolor = !$evencolor; } // End while. if ($showtotals) { @@ -187,6 +221,7 @@ public function get_results_tags($weights, $participants, $respondents, $showtot $pagetags->total->image2 = $imageurl . 'thbar.gif'; $pagetags->total->percent = sprintf(' %.'.$precision.'f%%', $percent); $pagetags->total->total = "$respondents/$participants"; + $pagetags->total->evencolor = $evencolor; } } @@ -206,24 +241,51 @@ public function get_feedback_scores(array $rids) { * Return an array of answers by question/choice for the given response. Must be implemented by the subclass. * * @param int $rid The response id. - * @param null $col Other data columns to return. - * @param bool $csvexport Using for CSV export. - * @param int $choicecodes CSV choicecodes are required. - * @param int $choicetext CSV choicetext is required. * @return array */ - static public function response_select($rid, $col = null, $csvexport = false, $choicecodes = 0, $choicetext = 1) { + public static function response_select($rid) { return []; } + /** + * Return an array of answer objects by question for the given response id. + * THIS SHOULD REPLACE response_select. + * + * @param int $rid The response id. + * @return array array answer + */ + public static function response_answers_by_question($rid) { + return []; + } + + /** + * Provide an array of answer objects from mobile data for the question. + * + * @param \stdClass $responsedata All of the responsedata as an object. + * @param \mod_questionnaire\question\question $question + * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects. + */ + public static function answers_from_appdata($responsedata, $question) { + // In most cases this can be a direct call to answers_from_webform with the one modification below. Override when this will + // not work. + if (isset($responsedata->{'q'.$question->id}) && !empty($responsedata->{'q'.$question->id})) { + $responsedata->{'q'.$question->id} = $responsedata->{'q'.$question->id}[0]; + } + return static::answers_from_webform($responsedata, $question); + } + /** * Return all the fields to be used for users in bulk questionnaire sql. * - * @author: Guy Thomas * @return string + * author: Guy Thomas */ protected function user_fields_sql() { - $userfieldsarr = get_all_user_name_fields(); + if (class_exists('\core_user\fields')) { + $userfieldsarr = \core_user\fields::get_name_fields(); + } else { + $userfieldsarr = get_all_user_name_fields(); + } $userfieldsarr = array_merge($userfieldsarr, ['username', 'department', 'institution']); $userfields = ''; foreach ($userfieldsarr as $field) { @@ -236,12 +298,13 @@ protected function user_fields_sql() { /** * Return sql and params for getting responses in bulk. - * @author Guy Thomas * @param int|array $questionnaireids One id, or an array of ids. * @param bool|int $responseid * @param bool|int $userid * @param bool|int $groupid + * @param int $showincompletes * @return array + * author Guy Thomas */ public function get_bulk_sql($questionnaireids, $responseid = false, $userid = false, $groupid = false, $showincompletes = 0) { global $DB; diff --git a/classes/responsetype/single.php b/classes/responsetype/single.php new file mode 100644 index 00000000..d8dc2f7c --- /dev/null +++ b/classes/responsetype/single.php @@ -0,0 +1,400 @@ +. + +namespace mod_questionnaire\responsetype; + +/** + * Class for single response types. + * + * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire + */ +class single extends responsetype { + /** + * Provide the necessary response data table name. Should probably always be used with late static binding 'static::' form + * rather than 'self::' form to allow for class extending. + * + * @return string response table name. + */ + public static function response_table() { + return 'questionnaire_resp_single'; + } + + /** + * Provide an array of answer objects from web form data for the question. + * + * @param \stdClass $responsedata All of the responsedata as an object. + * @param \mod_questionnaire\question\question $question + * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects. + * @throws \coding_exception + */ + public static function answers_from_webform($responsedata, $question) { + $answers = []; + if (isset($responsedata->{'q'.$question->id}) && isset($question->choices[$responsedata->{'q'.$question->id}])) { + $record = new \stdClass(); + $record->responseid = $responsedata->rid; + $record->questionid = $question->id; + $record->choiceid = $responsedata->{'q'.$question->id}; + // If this choice is an "other" choice, look for the added input. + if ($question->choices[$responsedata->{'q'.$question->id}]->is_other_choice()) { + $cname = 'q' . $question->id . + \mod_questionnaire\question\choice::id_other_choice_name($responsedata->{'q'.$question->id}); + $record->value = isset($responsedata->{$cname}) ? $responsedata->{$cname} : ''; + } + $answers[$responsedata->{'q'.$question->id}] = answer\answer::create_from_data($record); + } + return $answers; + } + + /** + * Provide an array of answer objects from mobile data for the question. + * + * @param \stdClass $responsedata All of the responsedata as an object. + * @param \mod_questionnaire\question\question $question + * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects. + */ + public static function answers_from_appdata($responsedata, $question) { + $answers = []; + $qname = 'q'.$question->id; + if (isset($responsedata->{$qname}[0]) && !empty($responsedata->{$qname}[0])) { + $record = new \stdClass(); + $record->responseid = $responsedata->rid; + $record->questionid = $question->id; + $record->choiceid = $responsedata->{$qname}[0]; + // If this choice is an "other" choice, look for the added input. + if ($question->choices[$record->choiceid]->is_other_choice()) { + $cname = \mod_questionnaire\question\choice::id_other_choice_name($record->choiceid); + $record->value = + isset($responsedata->{$qname}[$cname]) ? $responsedata->{$qname}[$cname] : ''; + } else { + $record->value = ''; + } + $answers[] = answer\answer::create_from_data($record); + } + return $answers; + } + + /** + * Insert a provided response to the question. + * + * @param object $responsedata All of the responsedata as an object. + * @return int|bool - on error the subtype should call set_error and return false. + */ + public function insert_response($responsedata) { + global $DB; + + if (!$responsedata instanceof \mod_questionnaire\responsetype\response\response) { + $response = \mod_questionnaire\responsetype\response\response::response_from_webform($responsedata, [$this->question]); + } else { + $response = $responsedata; + } + + $resid = false; + if (!empty($response) && isset($response->answers[$this->question->id])) { + foreach ($response->answers[$this->question->id] as $answer) { + if (isset($this->question->choices[$answer->choiceid])) { + if ($this->question->choices[$answer->choiceid]->is_other_choice()) { + // If no input specified, ignore this choice. + if (empty($answer->value) || preg_match("/^[\s]*$/", $answer->value)) { + continue; + } + $record = new \stdClass(); + $record->response_id = $response->id; + $record->question_id = $this->question->id; + $record->choice_id = $answer->choiceid; + $record->response = clean_text($answer->value); + $DB->insert_record('questionnaire_response_other', $record); + } + // Record the choice selection. + $record = new \stdClass(); + $record->response_id = $response->id; + $record->question_id = $this->question->id; + $record->choice_id = $answer->choiceid; + $resid = $DB->insert_record(static::response_table(), $record); + } + } + } + return $resid; + } + + /** + * Provide the result information for the specified result records. + * + * @param int|array $rids - A single response id, or array. + * @param boolean $anonymous - Whether or not responses are anonymous. + * @return array - Array of data records. + */ + public function get_results($rids=false, $anonymous=false) { + global $DB; + + $rsql = ''; + $params = array($this->question->id); + if (!empty($rids)) { + list($rsql, $rparams) = $DB->get_in_or_equal($rids); + $params = array_merge($params, $rparams); + $rsql = ' AND response_id ' . $rsql; + } + + // Added qc.id to preserve original choices ordering. + $sql = 'SELECT rt.id, qc.id as cid, qc.content ' . + 'FROM {questionnaire_quest_choice} qc, ' . + '{'.static::response_table().'} rt ' . + 'WHERE qc.question_id= ? AND qc.content NOT LIKE \'!other%\' AND ' . + 'rt.question_id=qc.question_id AND rt.choice_id=qc.id' . $rsql . ' ' . + 'ORDER BY qc.id'; + + $rows = $DB->get_records_sql($sql, $params); + + // Handle 'other...'. + $sql = 'SELECT rt.id, rt.response, qc.content ' . + 'FROM {questionnaire_response_other} rt, ' . + '{questionnaire_quest_choice} qc ' . + 'WHERE rt.question_id= ? AND rt.choice_id=qc.id' . $rsql . ' ' . + 'ORDER BY qc.id'; + + if ($recs = $DB->get_records_sql($sql, $params)) { + $i = 1; + foreach ($recs as $rec) { + $rows['other'.$i] = new \stdClass(); + $rows['other'.$i]->content = $rec->content; + $rows['other'.$i]->response = $rec->response; + $i++; + } + } + + return $rows; + } + + /** + * Provide the feedback scores for all requested response id's. This should be provided only by questions that provide feedback. + * @param array $rids + * @return array | boolean + */ + public function get_feedback_scores(array $rids) { + global $DB; + + $rsql = ''; + $params = [$this->question->id]; + if (!empty($rids)) { + list($rsql, $rparams) = $DB->get_in_or_equal($rids); + $params = array_merge($params, $rparams); + $rsql = ' AND response_id ' . $rsql; + } + $params[] = 'y'; + + $sql = 'SELECT response_id as rid, c.value AS score ' . + 'FROM {'.$this->response_table().'} r ' . + 'INNER JOIN {questionnaire_quest_choice} c ON r.choice_id = c.id ' . + 'WHERE r.question_id= ? ' . $rsql . ' ' . + 'ORDER BY response_id ASC'; + return $DB->get_records_sql($sql, $params); + } + + /** + * Provide a template for results screen if defined. + * @param bool $pdf + * @return mixed The template string or false/ + */ + public function results_template($pdf = false) { + if ($pdf) { + return 'mod_questionnaire/resultspdf_choice'; + } else { + return 'mod_questionnaire/results_choice'; + } + } + + /** + * Return the JSON structure required for the template. + * + * @param bool $rids + * @param string $sort + * @param bool $anonymous + * @return string + */ + public function display_results($rids=false, $sort='', $anonymous=false) { + global $DB; + + $rows = $this->get_results($rids, $anonymous); + if (is_array($rids)) { + $prtotal = 1; + } else if (is_int($rids)) { + $prtotal = 0; + } + $numresps = count($rids); + + $responsecountsql = 'SELECT COUNT(DISTINCT r.response_id) ' . + 'FROM {' . $this->response_table() . '} r ' . + 'WHERE r.question_id = ? '; + $numrespondents = $DB->count_records_sql($responsecountsql, [$this->question->id]); + + if ($rows) { + $counts = []; + foreach ($rows as $idx => $row) { + if (strpos($idx, 'other') === 0) { + $answer = $row->response; + $ccontent = $row->content; + $content = \mod_questionnaire\question\choice::content_other_choice_display($ccontent); + $content .= ' ' . clean_text($answer); + $textidx = $content; + $counts[$textidx] = !empty($counts[$textidx]) ? ($counts[$textidx] + 1) : 1; + } else { + $contents = questionnaire_choice_values($row->content); + $textidx = $contents->text.$contents->image; + $counts[$textidx] = !empty($counts[$textidx]) ? ($counts[$textidx] + 1) : 1; + } + } + $pagetags = $this->get_results_tags($counts, $numresps, $numrespondents, $prtotal, $sort); + } else { + $pagetags = new \stdClass(); + } + return $pagetags; + } + + /** + * Return an array of answers by question/choice for the given response. Must be implemented by the subclass. + * Array is indexed by question, and contains an array by choice code of selected choices. + * + * @param int $rid The response id. + * @return array + */ + public static function response_select($rid) { + global $DB; + + $values = []; + $sql = 'SELECT a.id, q.id as qid, q.content, c.content as ccontent, c.id as cid, o.response ' . + 'FROM {'.static::response_table().'} a ' . + 'INNER JOIN {questionnaire_question} q ON a.question_id = q.id ' . + 'INNER JOIN {questionnaire_quest_choice} c ON a.choice_id = c.id ' . + 'LEFT JOIN {questionnaire_response_other} o ON a.response_id = o.response_id AND c.id = o.choice_id ' . + 'WHERE a.response_id = ? '; + $records = $DB->get_records_sql($sql, [$rid]); + foreach ($records as $row) { + $newrow['content'] = $row->content; + $newrow['ccontent'] = $row->ccontent; + $newrow['responses'] = []; + $newrow['responses'][$row->cid] = $row->cid; + if (\mod_questionnaire\question\choice::content_is_other_choice($row->ccontent)) { + $newrow['responses'][\mod_questionnaire\question\choice::id_other_choice_name($row->cid)] = $row->response; + } + $values[$row->qid] = $newrow; + } + + return $values; + } + + /** + * Return an array of answer objects by question for the given response id. + * THIS SHOULD REPLACE response_select. + * + * @param int $rid The response id. + * @return array array answer + * @throws \dml_exception + */ + public static function response_answers_by_question($rid) { + global $DB; + + $answers = []; + $sql = 'SELECT r.id as id, r.response_id as responseid, r.question_id as questionid, r.choice_id as choiceid, ' . + 'o.response as value ' . + 'FROM {' . static::response_table() .'} r ' . + 'LEFT JOIN {questionnaire_response_other} o ON r.response_id = o.response_id AND r.question_id = o.question_id AND ' . + 'r.choice_id = o.choice_id ' . + 'WHERE r.response_id = ? '; + $records = $DB->get_records_sql($sql, [$rid]); + foreach ($records as $record) { + $answers[$record->questionid][$record->choiceid] = answer\answer::create_from_data($record); + } + + return $answers; + } + + /** + * Return sql and params for getting responses in bulk. + * @param int|array $questionnaireids One id, or an array of ids. + * @param bool|int $responseid + * @param bool|int $userid + * @param bool|int $groupid + * @param int $showincompletes + * @return array + * author Guy Thomas + */ + public function get_bulk_sql($questionnaireids, $responseid = false, $userid = false, $groupid = false, $showincompletes = 0) { + global $DB; + + $sql = $this->bulk_sql(); + if (($groupid !== false) && ($groupid > 0)) { + $groupsql = ' INNER JOIN {groups_members} gm ON gm.groupid = ? AND gm.userid = qr.userid '; + $gparams = [$groupid]; + } else { + $groupsql = ''; + $gparams = []; + } + + if (is_array($questionnaireids)) { + list($qsql, $params) = $DB->get_in_or_equal($questionnaireids); + } else { + $qsql = ' = ? '; + $params = [$questionnaireids]; + } + if ($showincompletes == 1) { + $showcompleteonly = ''; + } else { + $showcompleteonly = 'AND qr.complete = ? '; + $params[] = 'y'; + } + + $sql .= " + AND qr.questionnaireid $qsql $showcompleteonly + LEFT JOIN {questionnaire_response_other} qro ON qro.response_id = qr.id AND qro.choice_id = qrs.choice_id + LEFT JOIN {user} u ON u.id = qr.userid + $groupsql + "; + $params = array_merge($params, $gparams); + + if ($responseid) { + $sql .= " WHERE qr.id = ?"; + $params[] = $responseid; + } else if ($userid) { + $sql .= " WHERE qr.userid = ?"; + $params[] = $userid; + } + + return [$sql, $params]; + } + + /** + * Return sql for getting responses in bulk. + * @author Guy Thomas + * @return string + */ + protected function bulk_sql() { + global $DB; + + $userfields = $this->user_fields_sql(); + $alias = 'qrs'; + $extraselect = 'qrs.choice_id, ' . $DB->sql_order_by_text('qro.response', 1000) . ' AS response, 0 AS rankvalue'; + + return " + SELECT " . $DB->sql_concat_join("'_'", ['qr.id', "'".$this->question->helpname()."'", $alias.'.id']) . " AS id, + qr.submitted, qr.complete, qr.grade, qr.userid, $userfields, qr.id AS rid, $alias.question_id, + $extraselect + FROM {questionnaire_response} qr + JOIN {".static::response_table()."} $alias ON $alias.response_id = qr.id + "; + } +} diff --git a/classes/responsetype/slider.php b/classes/responsetype/slider.php new file mode 100644 index 00000000..d0b160e8 --- /dev/null +++ b/classes/responsetype/slider.php @@ -0,0 +1,76 @@ +. + +namespace mod_questionnaire\responsetype; + +/** + * Class for slider text response types. + * + * @author Hieu Vu Van + * @copyright 2022 The Open University. + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire + */ +class slider extends numericaltext { + /** + * Return an array of answer objects by question for the given response id. + * THIS SHOULD REPLACE response_select. + * + * @param int $rid The response id. + * @return array array answer + * @throws \dml_exception + */ + public static function response_answers_by_question($rid) { + global $DB; + + $answers = []; + $sql = 'SELECT qs.id, qs.response_id as responseid, qs.question_id as questionid, + 0 as choiceid, qs.response as value, qq.extradata ' . + 'FROM {' . static::response_table() . '} qs ' . + 'INNER JOIN {questionnaire_question} qq ON qq.id = qs.question_id ' . + 'WHERE response_id = ? '; + $records = $DB->get_records_sql($sql, [$rid]); + foreach ($records as $record) { + $answers[$record->questionid][] = answer\answer::create_from_data($record); + if (!empty($record->extradata)) { + $answers[$record->questionid]['extradata'] = json_decode($record->extradata); + } + } + return $answers; + } + + /** + * Provide the feedback scores for all requested response id's. This should be provided only by questions that provide feedback. + * @param array $rids + * @return array | boolean + */ + public function get_feedback_scores(array $rids) { + global $DB; + $rsql = ''; + $params = [$this->question->id]; + if (!empty($rids)) { + list($rsql, $rparams) = $DB->get_in_or_equal($rids); + $params = array_merge($params, $rparams); + $rsql = ' AND response_id ' . $rsql; + } + $sql = 'SELECT response_id as rid, response AS score ' . + 'FROM {'.$this->response_table().'} r ' . + 'WHERE r.question_id= ? ' . $rsql . ' ' . + 'ORDER BY response_id ASC'; + return $DB->get_records_sql($sql, $params); + } + +} diff --git a/classes/response/text.php b/classes/responsetype/text.php similarity index 58% rename from classes/response/text.php rename to classes/responsetype/text.php index 8a04ec13..b03b7d47 100644 --- a/classes/response/text.php +++ b/classes/responsetype/text.php @@ -14,50 +14,81 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * This file contains the parent class for questionnaire question types. - * - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questiontypes - */ - -namespace mod_questionnaire\response; -defined('MOODLE_INTERNAL') || die(); +namespace mod_questionnaire\responsetype; use mod_questionnaire\db\bulk_sql_config; /** * Class for text response types. - * * @author Mike Churchward - * @package responsetypes + * @copyright 2016 onward Mike Churchward (mike.churchward@poetopensource.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + * @package mod_questionnaire */ - -class text extends base { - static public function response_table() { +class text extends responsetype { + /** + * Provide the necessary response data table name. Should probably always be used with late static binding 'static::' form + * rather than 'self::' form to allow for class extending. + * + * @return string response table name. + */ + public static function response_table() { return 'questionnaire_response_text'; } - public function insert_response($rid, $val) { + /** + * Provide an array of answer objects from web form data for the question. + * + * @param \stdClass $responsedata All of the responsedata as an object. + * @param \mod_questionnaire\question\question $question + * @return array \mod_questionnaire\responsetype\answer\answer An array of answer objects. + */ + public static function answers_from_webform($responsedata, $question) { + $answers = []; + if (isset($responsedata->{'q'.$question->id}) && (strlen($responsedata->{'q'.$question->id}) > 0)) { + $val = $responsedata->{'q' . $question->id}; + $record = new \stdClass(); + $record->responseid = $responsedata->rid; + $record->questionid = $question->id; + $record->value = $val; + $answers[] = answer\answer::create_from_data($record); + } + return $answers; + } + + /** + * Insert a provided response to the question. + * + * @param object $responsedata All of the responsedata as an object. + * @return int|bool - on error the subtype should call set_error and return false. + */ + public function insert_response($responsedata) { global $DB; - // Only insert if non-empty content. - if ($this->question->type_id == QUESNUMERIC) { - $val = str_replace(",", ".", $val); // Allow commas as well as points in decimal numbers. - $val = preg_replace("/[^0-9.\-]*(-?[0-9]*\.?[0-9]*).*/", '\1', $val); + + if (!$responsedata instanceof \mod_questionnaire\responsetype\response\response) { + $response = \mod_questionnaire\responsetype\response\response::response_from_webform($responsedata, [$this->question]); + } else { + $response = $responsedata; } - if (preg_match("/[^ \t\n]/", $val)) { + if (!empty($response) && isset($response->answers[$this->question->id][0])) { $record = new \stdClass(); - $record->response_id = $rid; + $record->response_id = $response->id; $record->question_id = $this->question->id; - $record->response = $val; - return $DB->insert_record(self::response_table(), $record); + $record->response = clean_text($response->answers[$this->question->id][0]->value); + return $DB->insert_record(static::response_table(), $record); } else { return false; } } + /** + * Provide the result information for the specified result records. + * + * @param int|array $rids - A single response id, or array. + * @param boolean $anonymous - Whether or not responses are anonymous. + * @return array - Array of data records. + */ public function get_results($rids=false, $anonymous=false) { global $DB; @@ -70,7 +101,7 @@ public function get_results($rids=false, $anonymous=false) { if ($anonymous) { $sql = 'SELECT t.id, t.response, r.submitted AS submitted, ' . 'r.questionnaireid, r.id AS rid ' . - 'FROM {'.self::response_table().'} t, ' . + 'FROM {'.static::response_table().'} t, ' . '{questionnaire_response} r ' . 'WHERE question_id=' . $this->question->id . $rsql . ' AND t.response_id = r.id ' . @@ -79,7 +110,7 @@ public function get_results($rids=false, $anonymous=false) { $sql = 'SELECT t.id, t.response, r.submitted AS submitted, r.userid, u.username AS username, ' . 'u.id as usrid, ' . 'r.questionnaireid, r.id AS rid ' . - 'FROM {'.self::response_table().'} t, ' . + 'FROM {'.static::response_table().'} t, ' . '{questionnaire_response} r, ' . '{user} u ' . 'WHERE question_id=' . $this->question->id . $rsql . @@ -92,18 +123,24 @@ public function get_results($rids=false, $anonymous=false) { /** * Provide a template for results screen if defined. + * @param bool $pdf * @return mixed The template string or false/ */ - public function results_template() { - return 'mod_questionnaire/results_text'; + public function results_template($pdf = false) { + if ($pdf) { + return 'mod_questionnaire/resultspdf_text'; + } else { + return 'mod_questionnaire/results_text'; + } } /** - * @param bool $rids - * @param string $sort - * @param bool $anonymous - * @return string - * @throws \coding_exception + * Provide the result information for the specified result records. + * + * @param int|array $rids - A single response id, or array. + * @param string $sort - Optional display sort. + * @param boolean $anonymous - Whether or not responses are anonymous. + * @return string - Display output. */ public function display_results($rids=false, $sort='', $anonymous=false) { if (is_array($rids)) { @@ -114,21 +151,7 @@ public function display_results($rids=false, $sort='', $anonymous=false) { if ($rows = $this->get_results($rids, $anonymous)) { $numrespondents = count($rids); $numresponses = count($rows); - $isnumeric = $this->question->type_id == QUESNUMERIC; - // Count identical answers (numeric questions only). - if ($isnumeric) { - foreach ($rows as $row) { - if (!empty($row->response) || $row->response === "0") { - $this->text = $row->response; - $textidx = clean_text($this->text); - $this->counts[$textidx] = !empty($this->counts[$textidx]) ? ($this->counts[$textidx] + 1) : 1; - $this->userid[$textidx] = !empty($this->counts[$textidx]) ? ($this->counts[$textidx] + 1) : 1; - } - } - $pagetags = $this->get_results_tags($this->counts, $numrespondents, $numresponses, $prtotal); - } else { - $pagetags = $this->get_results_tags($rows, $numrespondents, $numresponses, $prtotal); - } + $pagetags = $this->get_results_tags($rows, $numrespondents, $numresponses, $prtotal); } else { $pagetags = new \stdClass(); } @@ -136,15 +159,14 @@ public function display_results($rids=false, $sort='', $anonymous=false) { } /** - * Override the results tags function for templates for questions with dates. + * Gets the results tags for templates for questions with defined choices (single, multiple, boolean). * - * @param $weights - * @param $participants Number of questionnaire participants. - * @param $respondents Number of question respondents. - * @param $showtotals + * @param array $weights + * @param int $participants Number of questionnaire participants. + * @param int $respondents Number of question respondents. + * @param int $showtotals * @param string $sort * @return \stdClass - * @throws \coding_exception */ public function get_results_tags($weights, $participants, $respondents, $showtotals = 1, $sort = '') { $pagetags = new \stdClass(); @@ -166,9 +188,10 @@ public function get_results_tags($weights, $participants, $respondents, $showtot '¤tgroupid='.$currentgroupid; } $users = []; + $evencolor = false; foreach ($weights as $row) { $response = new \stdClass(); - $response->text = format_text($row->response, FORMAT_HTML, ['noclean' => true]); + $response->text = format_text($row->response, FORMAT_HTML); if ($viewsingleresponse && $nonanonymous) { $rurl = $url.'&rid='.$row->rid.'&individualresponse=1'; $title = userdate($row->submitted); @@ -179,7 +202,10 @@ public function get_results_tags($weights, $participants, $respondents, $showtot } else { $response->respondent = ''; } + // The 'evencolor' attribute is used by the PDF template. + $response->evencolor = $evencolor; $pagetags->responses[] = (object)['response' => $response]; + $evencolor = !$evencolor; } if ($showtotals == 1) { @@ -194,29 +220,38 @@ public function get_results_tags($weights, $participants, $respondents, $showtot if (!empty($weights) && is_array($weights)) { ksort($weights); + $evencolor = false; foreach ($weights as $text => $num) { $response = new \stdClass(); $response->text = $text; $response->respondent = $num; + // The 'evencolor' attribute is used by the PDF template. + $response->evencolor = $evencolor; $nbresponses += $num; $sum += $text * $num; + $evencolor = !$evencolor; $pagetags->responses[] = (object)['response' => $response]; } $response = new \stdClass(); $response->text = $sum; $response->respondent = $strtotal; + $response->evencolor = $evencolor; $pagetags->responses[] = (object)['response' => $response]; + $evencolor = !$evencolor; $response = new \stdClass(); $response->respondent = $straverage; $avg = $sum / $nbresponses; $response->text = sprintf('%.' . $this->question->precise . 'f', $avg); + $response->evencolor = $evencolor; $pagetags->responses[] = (object)['response' => $response]; + $evencolor = !$evencolor; if ($showtotals == 1) { $pagetags->total = new \stdClass(); $pagetags->total->total = "$respondents/$participants"; + $pagetags->total->evencolor = $evencolor; } } } @@ -228,18 +263,14 @@ public function get_results_tags($weights, $participants, $respondents, $showtot * Return an array of answers by question/choice for the given response. Must be implemented by the subclass. * * @param int $rid The response id. - * @param null $col Other data columns to return. - * @param bool $csvexport Using for CSV export. - * @param int $choicecodes CSV choicecodes are required. - * @param int $choicetext CSV choicetext is required. * @return array */ - static public function response_select($rid, $col = null, $csvexport = false, $choicecodes = 0, $choicetext = 1) { + public static function response_select($rid) { global $DB; $values = []; - $sql = 'SELECT q.id '.$col.', a.response as aresponse '. - 'FROM {'.self::response_table().'} a, {questionnaire_question} q '. + $sql = 'SELECT q.id, q.content, a.response as aresponse '. + 'FROM {'.static::response_table().'} a, {questionnaire_question} q '. 'WHERE a.response_id=? AND a.question_id=q.id '; $records = $DB->get_records_sql($sql, [$rid]); foreach ($records as $qid => $row) { @@ -259,12 +290,35 @@ static public function response_select($rid, $col = null, $csvexport = false, $c return $values; } + /** + * Return an array of answer objects by question for the given response id. + * THIS SHOULD REPLACE response_select. + * + * @param int $rid The response id. + * @return array array answer + * @throws \dml_exception + */ + public static function response_answers_by_question($rid) { + global $DB; + + $answers = []; + $sql = 'SELECT id, response_id as responseid, question_id as questionid, 0 as choiceid, response as value ' . + 'FROM {' . static::response_table() .'} ' . + 'WHERE response_id = ? '; + $records = $DB->get_records_sql($sql, [$rid]); + foreach ($records as $record) { + $answers[$record->questionid][] = answer\answer::create_from_data($record); + } + + return $answers; + } + /** * Configure bulk sql * @return bulk_sql_config */ protected function bulk_sql_config() { - return new bulk_sql_config(self::response_table(), 'qrt', false, true, false); + return new bulk_sql_config(static::response_table(), 'qrt', false, true, false); } } diff --git a/classes/search/activity.php b/classes/search/activity.php index 631c020f..3c775a1e 100644 --- a/classes/search/activity.php +++ b/classes/search/activity.php @@ -25,8 +25,6 @@ namespace mod_questionnaire\search; -defined('MOODLE_INTERNAL') || die(); - /** * Search area for mod_questionnaire activities. * @@ -77,4 +75,4 @@ public function get_document($record, $options = []) { return $doc; } -} \ No newline at end of file +} diff --git a/classes/search/question.php b/classes/search/question.php index 2ac1af8f..0d5d4b60 100644 --- a/classes/search/question.php +++ b/classes/search/question.php @@ -13,21 +13,17 @@ // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Contains class mod_questionnaire\search\question - * - * @package mod_questionnaire - * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ + namespace mod_questionnaire\search; -defined('MOODLE_INTERNAL') || die(); + /** + * Contains the question class definition for search. + * * Search area for mod_questionnaire questions. Separated from the activity search so that admins can choose whether or not they * want this part enabled. * * @package mod_questionnaire + * @author Mike Churchward * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -157,4 +153,4 @@ public function get_context_url(\core_search\document $doc) { $context = \context::instance_by_id($doc->get('contextid')); return new \moodle_url('/mod/questionnaire/view.php', ['id' => $context->instanceid]); } -} \ No newline at end of file +} diff --git a/classes/settings_form.php b/classes/settings_form.php index aab1f412..c810f0d6 100644 --- a/classes/settings_form.php +++ b/classes/settings_form.php @@ -14,21 +14,24 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * @package mod_questionnaire - * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - namespace mod_questionnaire; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/formslib.php'); +/** + * The questionnaire settings form. + * @package mod_questionnaire + * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class settings_form extends \moodleform { + /** + * Defines the form. + */ public function definition() { global $questionnaire, $questionnairerealms; @@ -88,7 +91,13 @@ public function definition() { $mform->setType('thank_body', PARAM_RAW); $mform->setDefault('thank_body', $questionnaire->survey->thank_body); - $mform->addElement('text', 'email', get_string('email', 'questionnaire'), array('size' => '75')); + $allowemailreporting = get_config('questionnaire', 'allowemailreporting'); + if (!$allowemailreporting) { + $attributes = ['size' => '75', 'disabled' => 'disabled']; + } else { + $attributes = ['size' => '75']; + } + $mform->addElement('text', 'email', get_string('email', 'questionnaire'), $attributes); $mform->setType('email', PARAM_TEXT); $mform->setDefault('email', $questionnaire->survey->email); $mform->addHelpButton('email', 'sendemail', 'questionnaire'); @@ -121,8 +130,15 @@ public function definition() { } + /** + * Validation rules for form. + * @param array $data array of ("fieldname"=>value) of submitted data + * @param array $files array of uploaded files "element_name"=>tmp_file_path + * @return array of "element_name"=>"error_description" if there are errors, + * or an empty array if everything is OK (true allowed for backwards compatibility too). + */ public function validation($data, $files) { $errors = parent::validation($data, $files); return $errors; } -} \ No newline at end of file +} diff --git a/classes/task/cleanup.php b/classes/task/cleanup.php index 1dc8396e..7cd40678 100644 --- a/classes/task/cleanup.php +++ b/classes/task/cleanup.php @@ -14,17 +14,16 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +namespace mod_questionnaire\task; + /** * A scheduled task for Questionnaire. * * @package mod_questionnaire * @copyright 2015 The Open University + * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -namespace mod_questionnaire\task; - -defined('MOODLE_INTERNAL') || die(); - class cleanup extends \core\task\scheduled_task { /** @@ -36,6 +35,9 @@ public function get_name() { return get_string('crontask', 'mod_questionnaire'); } + /** + * Execute method. + */ public function execute() { global $CFG; require_once($CFG->dirroot . '/mod/questionnaire/locallib.php'); diff --git a/complete.php b/complete.php index 074ff29c..9950770f 100644 --- a/complete.php +++ b/complete.php @@ -14,8 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -// This page prints a particular instance of questionnaire. - +/** + * This page prints a particular instance of questionnaire. + * + * @package mod_questionnaire + * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ require_once("../../config.php"); require_once($CFG->libdir . '/completionlib.php'); require_once($CFG->dirroot.'/mod/questionnaire/questionnaire.class.php'); @@ -26,7 +33,7 @@ $SESSION->questionnaire->current_tab = 'view'; $id = optional_param('id', null, PARAM_INT); // Course Module ID. -$a = optional_param('a', null, PARAM_INT); // questionnaire ID. +$a = optional_param('a', null, PARAM_INT); // Questionnaire ID. $sid = optional_param('sid', null, PARAM_INT); // Survey id. $resume = optional_param('resume', null, PARAM_INT); // Is this attempt a resume of a saved attempt? @@ -47,13 +54,13 @@ $PAGE->set_url($url); $PAGE->set_context($context); -$questionnaire = new questionnaire(0, $questionnaire, $course, $cm); +$questionnaire = new questionnaire( $course, $cm, 0, $questionnaire); // Add renderer and page objects to the questionnaire object for display use. $questionnaire->add_renderer($PAGE->get_renderer('mod_questionnaire')); $questionnaire->add_page(new \mod_questionnaire\output\completepage()); $questionnaire->strquestionnaires = get_string("modulenameplural", "questionnaire"); -$questionnaire->strquestionnaire = get_string("modulename", "questionnaire"); +$questionnaire->strquestionnaire = get_string("modulename", "questionnaire"); // Mark as viewed. $completion = new completion_info($course); @@ -77,4 +84,4 @@ // Output the page. echo $questionnaire->renderer->header(); echo $questionnaire->renderer->render($questionnaire->page); -echo $questionnaire->renderer->footer($course); \ No newline at end of file +echo $questionnaire->renderer->footer($course); diff --git a/composer.json b/composer.json index 014357e7..728c346a 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "remotelearner/moodle-mod_questionnaire", + "name": "poetos/moodle-mod_questionnaire", "type": "moodle-mod", "require": { "composer/installers": "~1.0" diff --git a/db/access.php b/db/access.php index 1fc8a9b0..bc95e281 100644 --- a/db/access.php +++ b/db/access.php @@ -238,4 +238,4 @@ ) ) -); \ No newline at end of file +); diff --git a/db/install.php b/db/install.php index 0a459a11..55b0eda4 100644 --- a/db/install.php +++ b/db/install.php @@ -14,18 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/* - * - * @package mod - * @subpackage questionnaire +/** + * The file containing the install functions. + * @package mod_questionnaire * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * This file is executed right after the install.xml - * @copyright 2010 Remote Learner (http://www.remote-learner.net) + * @copyright 2016 Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); - +/** + * The install function. + */ function xmldb_questionnaire_install() { global $DB; @@ -93,6 +93,13 @@ function xmldb_questionnaire_install() { $questiontype->response_table = 'response_text'; $id = $DB->insert_record('questionnaire_question_type', $questiontype); + $questiontype = new stdClass(); + $questiontype->typeid = 11; + $questiontype->type = 'Slider'; + $questiontype->has_choices = 'n'; + $questiontype->response_table = 'response_text'; + $id = $DB->insert_record('questionnaire_question_type', $questiontype); + $questiontype = new stdClass(); $questiontype->typeid = 99; $questiontype->type = 'Page Break'; @@ -107,4 +114,4 @@ function xmldb_questionnaire_install() { $questiontype->response_table = ''; $id = $DB->insert_record('questionnaire_question_type', $questiontype); -} \ No newline at end of file +} diff --git a/db/install.xml b/db/install.xml index bc2fcb74..a1b7af0b 100644 --- a/db/install.xml +++ b/db/install.xml @@ -7,7 +7,7 @@ - + @@ -21,15 +21,20 @@ - + + + + + +
@@ -53,10 +58,10 @@ + -
@@ -66,16 +71,20 @@ - - + + + + + +
@@ -86,10 +95,8 @@ + - - -
@@ -115,14 +122,12 @@ + - - -
- + @@ -136,7 +141,7 @@
- + @@ -164,7 +169,7 @@
- + @@ -179,7 +184,7 @@
- + @@ -194,7 +199,7 @@
- + @@ -208,7 +213,7 @@
- + @@ -222,8 +227,8 @@
- - + + @@ -232,12 +237,13 @@ +
- - + + @@ -246,6 +252,7 @@ +
@@ -260,10 +267,9 @@ + + - - -
\ No newline at end of file diff --git a/db/log.php b/db/log.php index af423063..968e5dc0 100644 --- a/db/log.php +++ b/db/log.php @@ -17,9 +17,8 @@ /** * Capability definitions for the quiz module. * - * @package mod - * @subpackage questionnaire - * @copyright 2010 Remote-Learner.net (http://www.remote-learner.net) + * @package mod_questionnaire + * @copyright 2016 Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -29,4 +28,4 @@ array('module' => 'questionnaire', 'action' => 'view all', 'mtable' => 'questionnaire', 'field' => 'name'), array('module' => 'questionnaire', 'action' => 'submit', 'mtable' => 'questionnaire_response', 'field' => 'id'), array('module' => 'questionnaire', 'action' => 'view', 'mtable' => 'questionnaire', 'field' => 'name'), -); \ No newline at end of file +); diff --git a/db/mobile.php b/db/mobile.php index 4f6d5f0d..01bec8fb 100644 --- a/db/mobile.php +++ b/db/mobile.php @@ -29,17 +29,23 @@ 'handlers' => [ 'questionsview' => [ 'displaydata' => [ - 'icon' => $CFG->wwwroot . '/mod/questionnaire/pix/icon.gif', - 'class' => '', + 'icon' => $CFG->wwwroot . '/mod/questionnaire/pix/icon.svg', + 'class' => 'core-course-module-questionnaire-handler', ], 'delegate' => 'CoreCourseModuleDelegate', - 'method' => 'mobile_view_activity' + 'method' => 'mobile_view_activity', + 'styles' => [ + 'url' => $CFG->wwwroot . '/mod/questionnaire/styles_app.css', + 'version' => '1.5' + ] ] ], 'lang' => [ ['yourresponse', 'questionnaire'], ['submitted', 'questionnaire'], + ['answerquestions', 'questionnaire'], ['areyousure', 'moodle'], + ['resumesurvey', 'questionnaire'], ['success', 'moodle'], ['savechanges', 'moodle'], ['nextpage', 'questionnaire'], @@ -48,4 +54,4 @@ ['required', 'moodle'] ], ] -]; \ No newline at end of file +]; diff --git a/db/services.php b/db/services.php index e7c529be..ba55f345 100644 --- a/db/services.php +++ b/db/services.php @@ -26,14 +26,22 @@ defined('MOODLE_INTERNAL') || die; +$services = [ + 'mod_questionnaire_ws' => [ + 'functions' => ['mod_questionnaire_submit_questionnaire_response'], + 'requiredcapability' => '', + 'enabled' => 1 + ] +]; + $functions = [ - 'mod_questionnaire_submit_questionnaire_branching' => [ - 'classname' => 'mod_questionnaire_external', - 'methodname' => 'submit_questionnaire_branching', + 'mod_questionnaire_submit_questionnaire_response' => [ + 'classname' => 'mod_questionnaire\external', + 'methodname' => 'submit_questionnaire_response', 'classpath' => 'mod/questionnaire/externallib.php', - 'description' => 'Questionnaire Branching submit', + 'description' => 'Questionnaire submit', 'type' => 'write', 'capabilities' => 'mod/questionnaire:submit', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE] ] -]; \ No newline at end of file +]; diff --git a/db/tasks.php b/db/tasks.php index c36110b1..40d241ab 100644 --- a/db/tasks.php +++ b/db/tasks.php @@ -16,6 +16,7 @@ /** * Definition of Questionnaire scheduled tasks. + * * Default is to run once every 12 hours. * * @package mod_questionnaire @@ -23,9 +24,9 @@ * @copyright 2015 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - defined('MOODLE_INTERNAL') || die(); +/** @var array $tasks */ $tasks = array( array( 'classname' => 'mod_questionnaire\task\cleanup', diff --git a/db/upgrade.php b/db/upgrade.php index 270b1758..dd5fd989 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -15,14 +15,18 @@ // along with Moodle. If not, see . /** + * The file containing the upgrade functions. * @package mod_questionnaire - * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) + * @copyright 2016 Mike Churchward (mike.churchward@poetopensource.org) * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); - +/** + * The module upgrade function. + * @param int $oldversion + * @return bool + */ function xmldb_questionnaire_upgrade($oldversion=0) { global $CFG, $DB; @@ -724,11 +728,10 @@ function xmldb_questionnaire_upgrade($oldversion=0) { } // Get all of the attempts records, and add the questionnaire id to the corresponding response record. - $rs = $DB->get_recordset('questionnaire_attempts'); - foreach ($rs as $attempt) { - $DB->set_field('questionnaire_response', 'questionnaireid', $attempt->qid, ['id' => $attempt->rid]); - } - $rs->close(); + $sql = 'UPDATE {questionnaire_response} qr ' . + 'INNER JOIN {questionnaire_attempts} qa ON qr.id = qa.rid ' . + 'SET qr.questionnaireid = qa.qid'; + $DB->execute($sql, []); // Get all of the response records with a '0' questionnaireid, and extract the questionnaireid from the survey_id field. $rs = $DB->get_recordset('questionnaire_response', ['questionnaireid' => 0]); @@ -808,10 +811,204 @@ function xmldb_questionnaire_upgrade($oldversion=0) { upgrade_mod_savepoint(true, 2018050106, 'questionnaire'); } - return $result; + if ($oldversion < 2018110103) { + + // Define field id to be added to questionnaire_question. + $table = new xmldb_table('questionnaire_question'); + $field = new xmldb_field('extradata', XMLDB_TYPE_TEXT, null, null, null, null, null, 'deleted'); + + // Conditionally launch add field id. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Need to move rank named degree choices to the new field. + \mod_questionnaire\question\rate::move_all_nameddegree_choices(); + + // Questionnaire savepoint reached. + upgrade_mod_savepoint(true, 2018110103, 'questionnaire'); + } + + if ($oldversion < 2020011507) { + // This operation might take a while. Cancel PHP timeouts for this. + \core_php_time_limit::raise(); + + // Making the database tables standard across the board. + $table = new xmldb_table('questionnaire'); + $field1 = new xmldb_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $field2 = new xmldb_field('sid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + + // Changing fields that are used in indexes and keys generates errors (sometimes). Drop all foreign keys and indexes first; + // recreate them after. And, the might be a key or an index, so drop both and fix after. + $key1 = new xmldb_key('course', XMLDB_KEY_FOREIGN, ['course'], 'course', ['id']); + $dbman->drop_key($table, $key1); + $index1 = new xmldb_index('course', XMLDB_INDEX_NOTUNIQUE, ['course']); + if ($dbman->index_exists($table, $index1)) { + $dbman->drop_index($table, $index1); + } + $key2 = new xmldb_key('sid', XMLDB_KEY_FOREIGN, ['sid'], 'questionnaire_survey', ['id']); + $dbman->drop_key($table, $key2); + $index2 = new xmldb_index('sid', XMLDB_INDEX_NOTUNIQUE, ['sid']); + if ($dbman->index_exists($table, $index2)) { + $dbman->drop_index($table, $index2); + } + $index3 = new xmldb_index('respview', XMLDB_INDEX_NOTUNIQUE, ['resp_view']); + if ($dbman->index_exists($table, $index3)) { + $dbman->drop_index($table, $index3); + } + $dbman->change_field_type($table, $field1); + $dbman->change_field_type($table, $field2); + $dbman->add_key($table, $key1); + $dbman->add_key($table, $key2); + $dbman->add_index($table, $index3); + + $table = new xmldb_table('questionnaire_survey'); + $index = new xmldb_index('courseid', XMLDB_INDEX_NOTUNIQUE, ['courseid']); + if ($dbman->index_exists($table, $index)) { + $dbman->drop_index($table, $index); + } + $key = new xmldb_key('courseid', XMLDB_KEY_FOREIGN, ['courseid'], 'course', ['id']); + $dbman->drop_key($table, $key); + $dbman->add_key($table, $key); + + $table = new xmldb_table('questionnaire_question'); + $field = new xmldb_field('surveyid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $index = new xmldb_index('quest_question_sididx', XMLDB_INDEX_NOTUNIQUE, ['surveyid', 'deleted']); + if ($dbman->index_exists($table, $index)) { + $dbman->drop_index($table, $index); + } + $dbman->change_field_type($table, $field); + $dbman->add_index($table, $index); + $field = new xmldb_field('length', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $dbman->change_field_type($table, $field); + $field = new xmldb_field('precise', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $dbman->change_field_type($table, $field); + + $table = new xmldb_table('questionnaire_quest_choice'); + $index = new xmldb_index('questionid', XMLDB_INDEX_NOTUNIQUE, ['question_id']); + if ($dbman->index_exists($table, $index)) { + $dbman->drop_index($table, $index); + } + $key = new xmldb_key('questionid', XMLDB_KEY_FOREIGN, ['question_id'], 'questionnaire_question', ['id']); + $dbman->drop_key($table, $key); + $dbman->add_key($table, $key); + + $table = new xmldb_table('questionnaire_response'); + $index = new xmldb_index('questionnaireid', XMLDB_INDEX_NOTUNIQUE, ['questionnaireid']); + if ($dbman->index_exists($table, $index)) { + $dbman->drop_index($table, $index); + } + $key = new xmldb_key('questionnaireid', XMLDB_KEY_FOREIGN, ['questionnaireid'], 'questionnaire', ['id']); + $dbman->drop_key($table, $key); + $dbman->add_key($table, $key); + + // Postgres and MSSQL have a bug that impacts changing fields with a sequence defined (see bug MDL-68799), so don't change + // this for Postgres or MSSQL. + if (($DB->get_dbfamily() !== 'postgres') && ($DB->get_dbfamily() !== 'mssql')) { + $idfield = new xmldb_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE); + + $table = new xmldb_table('questionnaire_response_bool'); + $dbman->change_field_type($table, $idfield); + + $table = new xmldb_table('questionnaire_response_date'); + $dbman->change_field_type($table, $idfield); + + $table = new xmldb_table('questionnaire_response_other'); + $dbman->change_field_type($table, $idfield); + + $table = new xmldb_table('questionnaire_response_rank'); + $dbman->change_field_type($table, $idfield); + + $table = new xmldb_table('questionnaire_resp_single'); + $dbman->change_field_type($table, $idfield); + + $table = new xmldb_table('questionnaire_response_text'); + $dbman->change_field_type($table, $idfield); + + $table = new xmldb_table('questionnaire_fb_sections'); + $dbman->change_field_type($table, $idfield); + + $table = new xmldb_table('questionnaire_feedback'); + $dbman->change_field_type($table, $idfield); + } + + $table = new xmldb_table('questionnaire_response_rank'); + $field = new xmldb_field('rankvalue', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $dbman->change_field_type($table, $field); + + $table = new xmldb_table('questionnaire_fb_sections'); + $field = new xmldb_field('surveyid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $key = new xmldb_key('surveyid', XMLDB_KEY_FOREIGN, ['surveyid'], 'questionnaire_survey', ['id']); + $dbman->drop_key($table, $key); + $dbman->change_field_type($table, $field); + $dbman->add_key($table, $key); + + $table = new xmldb_table('questionnaire_feedback'); + $field = new xmldb_field('sectionid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $key = new xmldb_key('sectionid', XMLDB_KEY_FOREIGN, ['sectionid'], 'questionnaire_fb_sections', ['id']); + $dbman->drop_key($table, $key); + $dbman->change_field_type($table, $field); + $dbman->add_key($table, $key); + + $table = new xmldb_table('questionnaire_survey'); + $field = new xmldb_field('feedbacksections', XMLDB_TYPE_INTEGER, '2', null, null, null, '0'); + $dbman->change_field_type($table, $field); + + $table = new xmldb_table('questionnaire_dependency'); + $index = new xmldb_index('questionid', XMLDB_INDEX_NOTUNIQUE, ['questionid']); + if ($dbman->index_exists($table, $index)) { + $dbman->drop_index($table, $index); + } + $key = new xmldb_key('questionid', XMLDB_KEY_FOREIGN, ['questionid'], 'questionnaire_question', ['id']); + $dbman->drop_key($table, $key); + $dbman->add_key($table, $key); + $key = new xmldb_key('surveyid', XMLDB_KEY_FOREIGN, ['surveyid'], 'questionnaire_survey', ['id']); + $dbman->add_key($table, $key); + + // Questionnaire savepoint reached. + upgrade_mod_savepoint(true, 2020011507, 'questionnaire'); + } + + if ($oldversion < 2020062301) { + // Add show progress bar setting. + $table = new xmldb_table('questionnaire'); + $field = new xmldb_field('progressbar', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, 0, 'autonum'); + + // Conditionally launch add field. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Questionnaire savepoint reached. + upgrade_mod_savepoint(true, 2020062301, 'questionnaire'); + } + + if ($oldversion < 2022092200) { + // Add new slider question type. + $exist = $DB->record_exists('questionnaire_question_type', ['typeid' => 11]); + if (!$exist) { + $questiontype = new stdClass(); + $questiontype->typeid = 11; + $questiontype->type = 'Slider'; + $questiontype->has_choices = 'n'; + $questiontype->response_table = 'response_text'; + $DB->insert_record('questionnaire_question_type', $questiontype); + } + upgrade_mod_savepoint(true, 2022092200, 'questionnaire'); + } + + if ($oldversion < 2022121600.02) { + // Upgrade for downloadoptions - useridentityfields setting. + upgrade_mod_savepoint(true, 2022121600.02, 'questionnaire'); + } + + return true; } -// Supporting functions used once. +/** + * Supporting functions used once. + * @return bool + */ function questionnaire_upgrade_2007120101() { global $DB; diff --git a/drawchart.php b/drawchart.php index de9a7f8d..ab50fe83 100644 --- a/drawchart.php +++ b/drawchart.php @@ -15,16 +15,27 @@ // along with Moodle. If not, see . /** + * Library draw chart function. * @package mod_questionnaire * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); - -function draw_chart($feedbacktype, $charttype=null, $labels, - $score=null, $allscore=null, $globallabel=null, $groupname, $allresponses) { +/** + * This is the function. + * @param string $feedbacktype + * @param array $labels + * @param string $groupname + * @param bool $allresponses + * @param null|string $charttype + * @param null|array $score + * @param null|array $allscore + * @param null|string $globallabel + * @return string + */ +function draw_chart($feedbacktype, $labels, $groupname, + $allresponses, $charttype=null, $score=null, $allscore=null, $globallabel=null) { global $PAGE; $pageoutput = ''; @@ -564,4 +575,4 @@ function draw_chart($feedbacktype, $charttype=null, $labels, } return $pageoutput; -} \ No newline at end of file +} diff --git a/externallib.php b/externallib.php index 9470baa0..7715fc1e 100644 --- a/externallib.php +++ b/externallib.php @@ -24,9 +24,21 @@ * @since Moodle 3.0 */ -defined('MOODLE_INTERNAL') || die; +namespace mod_questionnaire; + +defined('MOODLE_INTERNAL') || die(); + require_once($CFG->libdir . '/externallib.php'); + +use external_api; +use external_function_parameters; +use external_single_structure; +use external_multiple_structure; +use external_value; +use external_warnings; + require_once($CFG->dirroot . '/mod/questionnaire/lib.php'); +require_once($CFG->dirroot . '/mod/questionnaire/questionnaire.class.php'); /** * Questionnaire module external functions @@ -37,29 +49,31 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.0 */ -class mod_questionnaire_external extends \external_api { +class external extends external_api { /** - * Describes the parameters for submit_questionnaire_branching_parameters. + * Describes the parameters for submit_questionnaire_response. * * @return external_function_parameters * @since Moodle 3.0 */ - public static function submit_questionnaire_branching_parameters() { - return new \external_function_parameters( + public static function submit_questionnaire_response_parameters() { + return new external_function_parameters( [ - 'questionnaireid' => new \external_value(PARAM_INT, 'Questionnaire instance id'), - 'surveyid' => new \external_value(PARAM_INT, 'Survey id'), - 'userid' => new \external_value(PARAM_INT, 'User id'), - 'cmid' => new \external_value(PARAM_INT, 'Course module id'), - 'sec' => new \external_value(PARAM_INT, 'Section number'), - 'completed' => new \external_value(PARAM_INT, 'Completed survey or not'), - 'submit' => new \external_value(PARAM_INT, 'Submit survey or not'), - 'responses' => new \external_multiple_structure( - new \external_single_structure( + 'questionnaireid' => new external_value(PARAM_INT, 'Questionnaire instance id'), + 'surveyid' => new external_value(PARAM_INT, 'Survey id'), + 'userid' => new external_value(PARAM_INT, 'User id'), + 'cmid' => new external_value(PARAM_INT, 'Course module id'), + 'sec' => new external_value(PARAM_INT, 'Section number'), + 'completed' => new external_value(PARAM_INT, 'Completed survey or not'), + 'rid' => new external_value(PARAM_INT, 'Existing response id'), + 'submit' => new external_value(PARAM_INT, 'Submit survey or not'), + 'action' => new external_value(PARAM_ALPHA, 'Page action'), + 'responses' => new external_multiple_structure( + new external_single_structure( [ - 'name' => new \external_value(PARAM_RAW, 'data key'), - 'value' => new \external_value(PARAM_RAW, 'data value') + 'name' => new external_value(PARAM_RAW, 'data key'), + 'value' => new external_value(PARAM_RAW, 'data value') ] ), 'The data to be saved', VALUE_DEFAULT, [] @@ -68,7 +82,7 @@ public static function submit_questionnaire_branching_parameters() { ); } - /** + /** * Submit questionnaire responses * * @param int $questionnaireid the questionnaire instance id @@ -77,15 +91,15 @@ public static function submit_questionnaire_branching_parameters() { * @param int $cmid Course module id * @param int $sec Section number * @param int $completed Completed survey 1/0 + * @param int $rid Already in progress response id. * @param int $submit Submit survey? + * @param string $action * @param array $responses the response ids * @return array answers information and warnings - * @since Moodle 3.0 */ - public static function submit_questionnaire_branching($questionnaireid, $surveyid, $userid, - $cmid, $sec, $completed, $submit, $responses) { - - $params = self::validate_parameters(self::submit_questionnaire_branching_parameters(), + public static function submit_questionnaire_response($questionnaireid, $surveyid, $userid, $cmid, $sec, $completed, $rid, + $submit, $action, $responses) { + self::validate_parameters(self::submit_questionnaire_response_parameters(), [ 'questionnaireid' => $questionnaireid, 'surveyid' => $surveyid, @@ -93,24 +107,22 @@ public static function submit_questionnaire_branching($questionnaireid, $surveyi 'cmid' => $cmid, 'sec' => $sec, 'completed' => $completed, + 'rid' => $rid, 'submit' => $submit, + 'action' => $action, 'responses' => $responses ] ); - if (!$questionnaire = get_questionnaire($params['questionnaireid'])) { - throw new \moodle_exception("invalidcoursemodule", "error"); - } - list($course, $cm) = get_course_and_cm_from_instance($questionnaire, 'questionnaire'); + list($cm, $course, $questionnaire) = questionnaire_get_standard_page_items($cmid); + $questionnaire = new \questionnaire($course, $cm, 0, $questionnaire); $context = \context_module::instance($cm->id); self::validate_context($context); require_capability('mod/questionnaire:submit', $context); - $result = save_questionnaire_data_branching($questionnaireid, $surveyid, $userid, $cmid, - $sec, $completed, $submit, $responses); - + $result = $questionnaire->save_mobile_data($userid, $sec, $completed, $rid, $submit, $action, $responses); $result['submitted'] = true; if (isset($result['warnings']) && !empty($result['warnings'])) { unset($result['responses']); @@ -121,18 +133,17 @@ public static function submit_questionnaire_branching($questionnaireid, $surveyi } /** - * Describes the submit_questionnaire_branching return value. + * Describes the submit_questionnaire_response return value. * - * @return external_multiple_structure + * @return external_single_structure * @since Moodle 3.0 */ - public static function submit_questionnaire_branching_returns() { - return new \external_single_structure( + public static function submit_questionnaire_response_returns() { + return new external_single_structure( [ - 'submitted' => new \external_value(PARAM_BOOL, 'submitted', true, false, false), - 'warnings' => new \external_warnings(), - 'params' => new \external_warnings(), + 'submitted' => new external_value(PARAM_BOOL, 'submitted', VALUE_REQUIRED, false, false), + 'warnings' => new external_warnings() ] ); } -} \ No newline at end of file +} diff --git a/fbsections.php b/fbsections.php index e35273f4..13f3a8e7 100644 --- a/fbsections.php +++ b/fbsections.php @@ -36,15 +36,15 @@ $sectionid = optional_param('sectionid', 0, PARAM_INT); if (! $cm = get_coursemodule_from_id('questionnaire', $id)) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } if (! $course = $DB->get_record("course", ["id" => $cm->course])) { - print_error('coursemisconf'); + throw new \moodle_exception('coursemisconf', 'mod_questionnaire'); } if (! $questionnaire = $DB->get_record("questionnaire", ["id" => $cm->instance])) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } // Needed here for forced language courses. @@ -58,11 +58,11 @@ $SESSION->questionnaire = new stdClass(); } -$questionnaire = new questionnaire(0, $questionnaire, $course, $cm); +$questionnaire = new questionnaire($course, $cm, 0, $questionnaire); if ($sectionid) { // Get the specified section by its id. - $feedbacksection = new mod_questionnaire\feedback\section(['id' => $sectionid], $questionnaire->questions); + $feedbacksection = new mod_questionnaire\feedback\section($questionnaire->questions, ['id' => $sectionid]); } else if (!$DB->count_records('questionnaire_fb_sections', ['surveyid' => $questionnaire->sid])) { // There are no sections currently, so create one. @@ -75,8 +75,8 @@ } else { // Get the specified section by section number. - $feedbacksection = new mod_questionnaire\feedback\section(['surveyid' => $questionnaire->survey->id, 'sectionnum' => $section], - $questionnaire->questions); + $feedbacksection = new mod_questionnaire\feedback\section($questionnaire->questions, + ['surveyid' => $questionnaire->survey->id, 'sectionnum' => $section]); } // Get all questions that are valid feedback questions. @@ -94,7 +94,7 @@ $SESSION->questionnaire->current_tab = 'feedback'; if (!$questionnaire->capabilities->editquestions) { - print_error('nopermissions', 'error', 'mod:questionnaire:editquestions'); + throw new \moodle_exception('nopermissions', 'mod_questionnaire'); } // Handle confirmed actions that impact display immediately. diff --git a/feedback.php b/feedback.php index 575fe455..cf6c2f50 100644 --- a/feedback.php +++ b/feedback.php @@ -33,15 +33,15 @@ $action = optional_param('action', '', PARAM_ALPHA); if (! $cm = get_coursemodule_from_id('questionnaire', $id)) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } if (! $course = $DB->get_record("course", ["id" => $cm->course])) { - print_error('coursemisconf'); + throw new \moodle_exception('coursemisconf', 'mod_questionnaire'); } if (! $questionnaire = $DB->get_record("questionnaire", ["id" => $cm->instance])) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } // Needed here for forced language courses. @@ -53,7 +53,7 @@ if (!isset($SESSION->questionnaire)) { $SESSION->questionnaire = new stdClass(); } -$questionnaire = new questionnaire(0, $questionnaire, $course, $cm); +$questionnaire = new questionnaire($course, $cm, 0, $questionnaire); // Add renderer and page objects to the questionnaire object for display use. $questionnaire->add_renderer($PAGE->get_renderer('mod_questionnaire')); @@ -62,7 +62,7 @@ $SESSION->questionnaire->current_tab = 'feedback'; if (!$questionnaire->capabilities->editquestions) { - print_error('nopermissions', 'error', 'mod:questionnaire:editquestions'); + throw new \moodle_exception('nopermissions', 'mod_questionnaire'); } $feedbackform = new \mod_questionnaire\feedback_form('feedback.php'); @@ -91,7 +91,7 @@ } if ($settings = $feedbackform->get_data()) { - if (isset($settings->feedbacksettingsbutton1) || isset($settings->buttongroup)) { + if (isset($settings->feedbacksettingsbutton1) || isset($settings->feedbacksettingsbutton2) || isset($settings->buttongroup)) { if (isset ($settings->feedbackscores)) { $sdata->feedbackscores = $settings->feedbackscores; } else { @@ -125,7 +125,7 @@ } $sdata->courseid = $settings->courseid; if (!($sid = $questionnaire->survey_update($sdata))) { - print_error('couldnotcreatenewsurvey', 'questionnaire'); + throw new \moodle_exception('couldnotcreatenewsurvey', 'mod_questionnaire'); } } diff --git a/grade.php b/grade.php index 4ea4a2b7..43780fde 100644 --- a/grade.php +++ b/grade.php @@ -31,7 +31,7 @@ $id = required_param('id', PARAM_INT); $cm = get_coursemodule_from_id('questionnaire', $id, 0, false, MUST_EXIST); if (! $questionnaire = $DB->get_record("questionnaire", array("id" => $cm->instance))) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); require_login($course, false, $cm); diff --git a/images/hbartransp.gif b/images/hbartransp.gif new file mode 100644 index 00000000..31838a05 Binary files /dev/null and b/images/hbartransp.gif differ diff --git a/index.php b/index.php index ce65d281..05bce900 100644 --- a/index.php +++ b/index.php @@ -17,20 +17,18 @@ /** * This script lists all the instances of questionnaire in a particular course * - * @package mod - * @subpackage questionnaire - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @package mod_questionnaire + * @copyright 2016 Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - require_once("../../config.php"); require_once($CFG->dirroot.'/mod/questionnaire/locallib.php'); $id = required_param('id', PARAM_INT); $PAGE->set_url('/mod/questionnaire/index.php', array('id' => $id)); if (! $course = $DB->get_record('course', array('id' => $id))) { - print_error('incorrectcourseid', 'questionnaire'); + throw new \moodle_exception('Filter has not been set.', 'mod_questionnaire'); } $coursecontext = context_course::instance($id); require_login($course->id); @@ -212,4 +210,4 @@ echo html_writer::table($table); // Finish the page. -echo $OUTPUT->footer(); \ No newline at end of file +echo $OUTPUT->footer(); diff --git a/javascript/mobile.js b/javascript/mobile.js index 1291f57a..e46928da 100644 --- a/javascript/mobile.js +++ b/javascript/mobile.js @@ -23,8 +23,11 @@ this.questionsFormErrors = {}; for (const fieldkey in this.CONTENT_OTHERDATA.fields) { questionsFormFields[fieldkey] = []; questionsFormFields[fieldkey][0] = ''; - for (const itemid in this.CONTENT_OTHERDATA.questions[this.CONTENT_OTHERDATA.pagenum][this.CONTENT_OTHERDATA.fields[fieldkey].id]) { - questionsFormFields[fieldkey][0] = this.CONTENT_OTHERDATA.questions[this.CONTENT_OTHERDATA.pagenum][this.CONTENT_OTHERDATA.fields[fieldkey].id][itemid].value; + for (const itemid in + this.CONTENT_OTHERDATA.questions[this.CONTENT_OTHERDATA.pagenum][this.CONTENT_OTHERDATA.fields[fieldkey].id]) { + questionsFormFields[fieldkey][0] = + this.CONTENT_OTHERDATA.questions[this.CONTENT_OTHERDATA.pagenum][this.CONTENT_OTHERDATA.fields[fieldkey].id] + [itemid].value; } if (this.CONTENT_OTHERDATA.fields[fieldkey].required === 'y') { questionsFormFields[fieldkey][1] = this.Validators.required; diff --git a/lang/en/questionnaire.php b/lang/en/questionnaire.php index cb920781..7de8d9f7 100644 --- a/lang/en/questionnaire.php +++ b/lang/en/questionnaire.php @@ -23,6 +23,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['accessibility:rate:choice'] = 'Row {$a->rowposition}, {$a->choicetitle}: Column {$a->colposition}, {$a->choiceanswer}.'; $string['action'] = 'Action'; $string['activityoverview'] = 'You have questionnaires that are due'; $string['additionalinfo'] = 'Additional Info'; @@ -37,6 +38,8 @@ $string['alignment_help'] = 'Select buttons alignment: vertical (default) or horizontal.'; $string['alignment_link'] = 'mod/questionnaire/questions#Radio_Buttons'; $string['all'] = 'All'; +$string['allnameddegrees'] = 'Named degrees'; +$string['allnameddegrees_help'] = 'Specify text to display for rate values instead of the number. Leave a value blank to not use.'; $string['alreadyfilled'] = 'You have already filled out this questionnaire for us{$a}. Thank you.'; $string['andaveragevalues'] = 'and average values'; $string['anonymous'] = 'Anonymous'; @@ -55,8 +58,9 @@ $string['autonumberpages'] = 'Auto number pages'; $string['autonumberpagesandquestions'] = 'Auto number pages and questions'; $string['average'] = 'Average'; -$string['averagerank'] = 'Average rank'; $string['averageposition'] = 'Average position'; +$string['averagerank'] = 'Average rank'; +$string['averagesrow'] = 'Averages (where applicable):'; $string['bodytext'] = 'Body text'; $string['boxesnbexact'] = 'exactly {$a} box(es).'; $string['boxesnbmax'] = 'a maximum of {$a} box(es).'; @@ -84,14 +88,15 @@ $string['checknotstarted'] = 'Select not started'; $string['checkstarted'] = 'Select started'; $string['clicktoswitch'] = '(click to switch)'; +$string['closebeforeopen'] = 'You have specified a close date before the open date.'; $string['closed'] = 'The questionnaire was closed on {$a}. Thanks.'; -$string['closedate'] = 'Use Close Date'; +$string['closedate'] = 'Allow responses until'; $string['closeson'] = 'Questionnaire closes on {$a}'; -$string['closedate_help'] = 'You can specify a date to close the questionnaire here. Check the check box, and select the date and time you want. - Users will not be able to fill out the questionnaire after that date. If this is not selected, it will never be closed.'; $string['completionsubmit'] = 'Student must submit this questionnaire to complete it'; $string['condition'] = 'Condition'; $string['confalts'] = '- OR -
Confirmation page'; +$string['configemailreporting'] = 'Allow reports by email'; +$string['configemailreportinglong'] = 'Enables options for some reports to be emailed directly to specified email addresses.'; $string['configusergraph'] = 'Display charts for "Personality Test" feedback'; $string['configusergraphlong'] = 'Use the Rgraph library to display "Personality Test" feedback charts.'; $string['configmaxsections'] = 'Maximum feedback sections'; @@ -113,10 +118,14 @@ $string['createcontent_help'] = 'Select one of the radio button options. \'Create new\' is the default.'; $string['createcontent_link'] = 'mod/questionnaire/mod#Content_Options'; $string['createnew'] = 'Create new'; +$string['centerlabel'] = 'Centre label'; $string['date'] = 'Date'; $string['date_help'] = 'Use this question type if you expect the response to be a correctly formatted date.'; $string['date_link'] = 'mod/questionnaire/questions#Date'; -$string['dateformatting'] = 'Use the day/month/year format, e.g. for March 14th, 1945:  14/3/1945'; +$string['dateformatting'] = 'Use the year-month-day format, e.g. for March 4th, 1945:  1945-03-04'; +// Prior to release 3.6.0, you could specify an input date format in the above string. Now, the format must be as below. This +// string is used now in case sites modified the above string. +$string['strictdateformatting'] = 'Enter the date using the date picker below.'; $string['deleteallresponses'] = 'Delete ALL Responses'; $string['deletecurrentquestion'] = 'Delete question {$a}'; $string['deletedallgroupresp'] = 'Deleted ALL Responses in group {$a}'; @@ -135,10 +144,12 @@ $string['directwarnings'] = 'Direct dependencies to this question will be removed. This will affect:'; $string['displaymethod'] = 'Display method not defined for question.'; $string['download'] = 'Download'; -$string['downloadtextformat'] = 'Download as CSV'; -$string['downloadtextformat_help'] = 'This feature enables you to save all the responses of a questionnaire to a text file (CSV). - This file can then be imported into a spreadsheet (e.g. MS Excel or Open Office Calc) or a statistical package for further processing the data.'; +$string['downloadpdf'] = 'Download PDF'; +$string['downloadtextformat'] = 'Download'; +$string['downloadtextformat_help'] = 'This feature enables you to download questionnaire responses in a file format of your choice. + The file can then be opened in a spreadsheet program (e.g. MS Excel or Open Office Calc) or a statistical package for further processing.'; $string['downloadtextformat_link'] = 'mod/questionnaire/report#Download_in_text_format'; +$string['downloadtypes'] = 'Report type'; $string['dropdown'] = 'Dropdown Box'; $string['dropdown_help'] = 'There is no real advantage to using the Dropdown Box over using the Radio Buttons except perhaps for longish lists of options, to save screen space.'; @@ -148,6 +159,15 @@ $string['editingquestionnaire'] = 'Editing Questionnaire Settings'; $string['editquestion'] = 'Editing {$a} question'; $string['email'] = 'Email'; +$string['emailextra'] = 'Send download to emails'; +$string['emailextra_help'] = 'Will send the download file to the listed email addresses, separated by commas. Note that NO security or privacy checking is done. + \'allowemailreporting\' must be enabled in module settings to access this.'; +$string['emailsnotspecified'] = 'No email(s) were specified.'; +$string['emailroles'] = 'Send download to roles'; +$string['emailroles_help'] = 'Will send the download file to all roles with "mod/questionnaire:submissionnotification" capability via email. + \'allowemailreporting\' must be enabled in module settings to access this.'; +$string['emailsend'] = 'Send reports'; +$string['emailssent'] = 'Downloads sent to specified email(s).'; $string['errnewname'] = 'Sorry, name already in use. Pick a new name.'; $string['erroropening'] = 'Error opening questionnaire.'; $string['errortable'] = 'Error system table corrupt.'; @@ -196,7 +216,8 @@ $string['feedbacknotes_help'] = 'Text entered here will be displayed to the respondents at the end of their Feedback Report'; $string['feedbackoptions'] = 'Feedback options'; $string['feedbackoptions_help'] = 'Feedback options are available if your questionnaire contains the following question types and question settings: -Radio buttons; Dropdown box; Yes/No; or Rate (normal or Osgood scale). Those questions must be set as Required, their Question Name field must NOT be empty and the Possible answers choices must contain a value.'; +Radio buttons; Dropdown box; Yes/No; Rate (normal or Osgood scale) or Slider. Those questions must be set as Required, their Question Name field must NOT be empty and the Possible answers choices must contain a value. +Slider questions must NOT use a negative value for the Minimum slider range.'; $string['feedbackoptions_link'] = 'mod/questionnaire/personality_test'; $string['feedbackremovequestionfromsection'] = 'This question is part of feedback section [{$a}]'; $string['feedbackremovesection'] = 'Removing this question will completely remove feedback section [{$a}]'; @@ -234,6 +255,7 @@ $string['headingtext'] = 'Heading text'; $string['horizontal'] = 'Horizontal'; $string['id'] = 'ID'; +$string['includerankaverages'] = 'Include rank question averages'; $string['includechoicecodes'] = 'Include choice codes'; $string['includechoicetext'] = 'Include choice text'; $string['includeincomplete'] = "Include incomplete responses"; @@ -244,12 +266,19 @@ $string['invalidresponserecord'] = 'Invalid response record specified.'; $string['invalidsurveyid'] = 'Invalid questionnaire ID.'; $string['invalidsectionid'] = 'Invalid feedback section specified.'; +$string['invalidrange'] = 'The maximum slider value must be greater than the minimum slider value.'; +$string['invalidstartingvalue'] = 'The starting value must be equal to or between the minimum and maximum values. For example, if using a scale of 1-10, the starting value could be 5.'; +$string['invalidminmaxrange'] = 'This question type supports an absolute maximum range of -100 to +100. We expect the vast majority of questionnaire designs to use a range of 1-10 or -10 to +10.'; +$string['invalidincrement'] = 'Note that the value increments must be lower than the maximum value. For example, if a scale of 1-10, the increment value would probably be 1.'; $string['indirectwarnings'] = 'This list shows the indirect dependent questions and the remaining dependencies for direct dependent questions:'; $string['kindofratescale'] = 'Type of rate scale'; $string['kindofratescale_help'] = 'Right-click on the More Help link below.'; $string['kindofratescale_link'] = 'mod/questionnaire/questions#Type_of_rate_scale'; $string['lastrespondent'] = 'Last Respondent'; $string['length'] = 'Length'; +$string['leftlabel'] = 'Left label'; +$string['leftpart'] = '{$a->min} is {$a->leftlabel}'; +$string['leftpartdefault'] = '{$a->min} is minimum slider range'; $string['managequestions'] = 'Manage questions'; $string['managequestions_help'] = 'In the Manage questions section of the Edit Questions page, you can conduct a number of operations on a Questionnaire\'s questions.'; $string['managequestions_link'] = 'mod/questionnaire/questions#Manage_questions'; @@ -269,6 +298,10 @@ Default values are 20 characters for the Input Box width and 25 characters for the maximum length of text entered.'; $string['messageprovider:message'] = 'Questionnaire reminder'; $string['messageprovider:notification'] = 'Questionnaire submission'; +$string['middlepart'] = ', {$a->centreval} is {$a->middlelabel}'; +$string['middlepartdefault'] = ', {$a->centreval} is average'; +$string['middlepartwithtwovalues'] = ', {$a->centreval1} and {$a->centreval2} are {$a->middlelabel}'; +$string['middlepartwithtwovaluesdefault'] = ', {$a->centreval1} and {$a->centreval2} are average'; $string['minforcedresponses'] = 'Min. forced responses'; $string['minforcedresponses_help'] = 'Use these parameters to force respondent to tick a minimum of **Min.** boxes and a maximum of **Max.** check boxes. To force an exact number of check boxes to be ticked, set **Min.** and **Max.** to the same value. If only a min or a max value is desired, just leave the other @@ -276,15 +309,20 @@ respondent does not comply with your requirements. Obviously you should make any requirements clear to the respondent either in the general instructions of your Questionnaire or in the text of relevant questions.'; $string['misconfigured'] = 'Course is misconfigured'; -$string['missingquestion'] = 'Please answer Required question '; -$string['missingquestions'] = 'Please answer Required questions: '; +$string['missingquestion'] = 'Please answer required question '; +$string['missingquestions'] = 'Please answer required questions: '; $string['modulename'] = 'Questionnaire'; $string['modulename_help'] = 'The questionnaire module allows you to construct surveys using a variety of question types, for the purpose of gathering data from users.'; +$string['modulename_link'] = 'mod/questionnaire/view'; $string['modulenameplural'] = 'Questionnaires'; $string['movedisabled'] = 'This item cannot be moved'; $string['myresponses'] = 'All your responses'; $string['myresponsetitle'] = 'Your {$a} response(s)'; $string['myresults'] = 'Your Results'; +$string['minrange'] = 'Minimum slider range (left)'; +$string['minrange_help'] = 'Set the minimum value of the range on the left-hand side. It defaults to 1, but can set as low as -100. If you use a negative number (-100 to -1), the right-hand maximum will be expressed with a positive (+) sign.'; +$string['maxrange'] = 'Maximum slider range (right)'; +$string['maxrange_help'] = 'Set the maximum value of the range on the right-hand side. It defaults to 100, but it could be any number between 1-100. If the minimum value for the left-hand is a negative value, the maximum range will be expressed with a positive (+) sign.'; $string['name'] = 'Name'; $string['navigate'] = 'Allow branching questions'; $string['navigate_help'] = 'Enable Yes/No and Radio Buttons questions to have Child questions dependent on their choices in your questionnaire.'; @@ -332,9 +370,8 @@ $string['numeric'] = 'Numeric'; $string['numeric_help'] = 'Use this question type if you expect the response to be a correctly formatted number.'; $string['of'] = 'of'; -$string['opendate'] = 'Use Open Date'; -$string['opendate_help'] = 'You can specify a date to open the questionnaire here. Check the check box, and select the date and time you want. - Users will not be able to fill out the questionnaire before that date. If this is not selected, it will be open immediately.'; +$string['openafterclose'] = 'You have specified an open date after the close date'; +$string['opendate'] = 'Allow responses from'; $string['option'] = 'option {$a}'; $string['optional'] = 'Optional - At least one of this dependencies has to be fulfilled.'; $string['optionalname'] = 'Question Name'; @@ -431,6 +468,10 @@ $string['privacy:metadata:questionnaire_resp_single:choice_id'] = 'The ID of the choice record for this response.'; $string['private'] = 'Private'; +$string['progressbar'] = 'Show progress bar'; +$string['progresshelp'] = 'Progress Bar'; +$string['progresshelp_help'] = 'Move on to the next page to fill up the progress bar'; +$string['progressbar_info'] = 'Questionnaire {$a} complete.'; $string['public'] = 'Public'; $string['publiccopy'] = 'Copy:'; $string['publicoriginal'] = 'Original:'; @@ -525,7 +566,11 @@ $string['resume_link'] = 'mod/questionnaire/mod#Save/Resume_answers'; $string['resumesurvey'] = 'Resume questionnaire'; $string['return'] = 'Return'; +$string['rightlabel'] = 'Right label'; +$string['rightpart'] = ' and {$a->max} is {$a->rightlabel}'; +$string['rightpartdefault'] = ' and {$a->max} is maximum slider range'; $string['save'] = 'Save'; +$string['save_and_exit'] = 'Save and exit'; $string['saveasnew'] = 'Save as New Question'; $string['savedbutnotsubmitted'] = 'This questionnaire has been saved but not yet submitted.'; $string['savedprogress'] = 'Your progress has been saved. You may return at any time to complete this questionnaire.'; @@ -546,7 +591,7 @@ $string['send_message_to'] = 'Send message to:'; $string['sendemail_help'] = 'Sends a copy of each submission to the specified address or addresses. You can provide more than one address by separating them with commas. -Leave blank for no email backup.'; +Leave blank for no email backup. \'allowemailreporting\' must be enabled in module settings to access this.'; $string['set'] = 'set'; $string['settings'] = 'Settings'; $string['settingssaved'] = 'Settings saved'; @@ -568,9 +613,16 @@ $string['subtitle_help'] = 'Subtitle of this questionnaire. Appears below the title on the first page only.'; $string['subject'] = 'Subject'; $string['summary'] = 'Summary'; +$string['summaryreportattached'] = 'Questionnaire summary report attached'; $string['surveynotexists'] = 'questionnaire does not exist.'; $string['surveyowner'] = 'You must be a questionnaire owner to perform this operation.'; $string['surveyresponse'] = 'Response from questionnaire'; +$string['slider'] = 'Slider'; +$string['slider_help'] = 'The slider question allows respondents to select a value from a continuous range by dragging a slider between two extremes. A centre value can also be set.'; +$string['startingvalue'] = 'Slider starting value'; +$string['startingvalue_help'] = 'The slider starting value specifies where the slider should first appear for respondents. It defaults to 1 because the range is unknown. You may wish to start it in the centre of the range by giving a central value (a range of 1-100 has a centre value of 50).'; +$string['stepvalue'] = 'Slider increment value'; +$string['stepvalue_help'] = 'The slider increment value specifies how finely you wish respondents to indicate their response in the range. The question defaults to a range of 1-100 with an increment of one, allowing respondents to give values of 70, 71, 72, 73, 74 etc. But you could instead set increments of five, allowing respondents to give values of 60, 65, 70, 75, 80 etc., or even just a range of 1-10 with increments of 1.'; $string['template'] = 'Template'; $string['templatenotviewable'] = 'Template questionnaires are not viewable.'; $string['text'] = 'Question Text'; @@ -607,7 +659,7 @@ $string['usetemplate'] = 'Use template'; $string['vertical'] = 'Vertical'; $string['view'] = 'View'; -$string['viewallresponses'] = 'View All Responses'; +$string['viewallresponses'] = 'View all responses'; $string['viewallresponses_help'] = 'If the questionnaire is set to **Group Mode**: *Visible groups*, or is set to *Separate groups* and the current user has the *moodle/site:accessallgroups* capability (in the current context), and groups have been defined in the current course, then the user has access to a dropdown list of groups. This dropdown list enables the user to "filter" the questionnaire responses by groups. @@ -618,8 +670,9 @@ $string['viewindividualresponse'] = 'Individual responses'; $string['viewindividualresponse_help'] = 'Click on the respondents\' names in the list below to view their individual responses.'; $string['viewresponses'] = 'All responses ({$a})'; -$string['viewyourresponses'] = 'Your responses- view {$a}'; +$string['viewyourresponses'] = 'View your response(s)'; $string['warning'] = 'Warning, error encountered.'; +$string['where'] = 'where '; $string['wronganswers'] = 'There is something wrong with your answers (see below)'; $string['wrongdateformat'] = 'The date entered: {$a} does not correspond to the format shown in the example.'; $string['wrongdaterange'] = 'ERROR! The year must be set in the 1902 to 2037 range.'; @@ -627,7 +680,8 @@ $string['wrongformats'] = 'There is something wrong with your answer to questions: '; $string['yesno'] = 'Yes/No'; $string['yesno_help'] = 'Simple Yes/No question.'; -$string['yourresponse'] = 'Your response'; -$string['yourresponses'] = 'Your responses'; +$string['yourresponse'] = 'View your response(s)'; +$string['yourresponses'] = 'View your response(s)'; $string['crontask'] = 'Questionnaire cleanup job'; -$string['selectdropdowntext'] = 'Select One'; \ No newline at end of file +$string['nopermissions'] = 'Sorry, but you do not currently have permissions to view this page or perform this action.'; +$string['unanswered'] = 'Unanswered'; diff --git a/lib.php b/lib.php index efdb8af6..dd7f2ea0 100644 --- a/lib.php +++ b/lib.php @@ -14,20 +14,25 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -// Library of functions and constants for module questionnaire. - /** + * Library of functions and constants for module questionnaire. * @package mod_questionnaire - * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) + * @copyright 2016 Mike Churchward (mike.churchward@poetopensource.org) * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); - +/** This may no longer be needed. */ define('QUESTIONNAIRE_RESETFORM_RESET', 'questionnaire_reset_data_'); + +/** This may no longer be needed. */ define('QUESTIONNAIRE_RESETFORM_DROP', 'questionnaire_drop_questionnaire_'); +/** + * Library supports implementation. + * @param string $feature + * @return bool|null + */ function questionnaire_supports($feature) { switch($feature) { case FEATURE_BACKUP_MOODLE2: @@ -48,24 +53,36 @@ function questionnaire_supports($feature) { return true; case FEATURE_SHOW_DESCRIPTION: return true; - + case FEATURE_MOD_PURPOSE: + return MOD_PURPOSE_COMMUNICATION; default: return null; } } /** + * Return any extra capabilities. * @return array all other caps used in module */ function questionnaire_get_extra_capabilities() { return array('moodle/site:accessallgroups'); } -function get_questionnaire($questionnaireid) { +/** + * Implementation of get_instance. + * @param int $questionnaireid + * @return false|mixed|stdClass + */ +function questionnaire_get_instance($questionnaireid) { global $DB; return $DB->get_record('questionnaire', array('id' => $questionnaireid)); } +/** + * Implementation of add_instance. + * @param stdClass $questionnaire + * @return bool|int + */ function questionnaire_add_instance($questionnaire) { // Given an object containing all the necessary data, // (defined by the form in mod.html) this function @@ -75,13 +92,14 @@ function questionnaire_add_instance($questionnaire) { require_once($CFG->dirroot.'/mod/questionnaire/questionnaire.class.php'); require_once($CFG->dirroot.'/mod/questionnaire/locallib.php'); - // Check the realm and set it to the survey if it's set. + $copyfiles = false; + // Check the realm and set it to the survey if it's set. if (empty($questionnaire->sid)) { // Create a new survey. $course = get_course($questionnaire->course); $cm = new stdClass(); - $qobject = new questionnaire(0, $questionnaire, $course, $cm); + $qobject = new questionnaire($course, $cm, 0, $questionnaire); if ($questionnaire->create == 'new-0') { $sdata = new stdClass(); @@ -98,7 +116,7 @@ function questionnaire_add_instance($questionnaire) { $sdata->feedbacknotes = ''; $sdata->courseid = $course->id; if (!($sid = $qobject->survey_update($sdata))) { - print_error('couldnotcreatenewsurvey', 'questionnaire'); + throw new \moodle_exception('couldnotcreatenewsurvey', 'mod_questionnaire'); } } else { $copyid = explode('-', $questionnaire->create); @@ -116,6 +134,9 @@ function questionnaire_add_instance($questionnaire) { // All new questionnaires should be created as "private". // Even if they are *copies* of public or template questionnaires. $DB->set_field('questionnaire_survey', 'realm', 'private', array('id' => $sid)); + + // Need to copy any files from the old questionnaire instance to the new one. + $questionnaire->copyid = $copyid; } // If the survey has dependency data, need to set the questionnaire to allow dependencies. if ($DB->count_records('questionnaire_dependency', ['surveyid' => $sid]) > 0) { @@ -127,14 +148,6 @@ function questionnaire_add_instance($questionnaire) { $questionnaire->timemodified = time(); - // May have to add extra stuff in here. - if (empty($questionnaire->useopendate)) { - $questionnaire->opendate = 0; - } - if (empty($questionnaire->useclosedate)) { - $questionnaire->closedate = 0; - } - if ($questionnaire->resume == '1') { $questionnaire->resume = 1; } else { @@ -154,9 +167,12 @@ function questionnaire_add_instance($questionnaire) { return $questionnaire->id; } -// Given an object containing all the necessary data, -// (defined by the form in mod.html) this function -// will update an existing instance with new data. +/** + * Given an object containing all the necessary data, (defined by the form in mod.html) this function will update an existing + * instance with new data. + * @param stdClass $questionnaire + * @return bool + */ function questionnaire_update_instance($questionnaire) { global $DB, $CFG; require_once($CFG->dirroot.'/mod/questionnaire/locallib.php'); @@ -169,14 +185,6 @@ function questionnaire_update_instance($questionnaire) { $questionnaire->timemodified = time(); $questionnaire->id = $questionnaire->instance; - // May have to add extra stuff in here. - if (empty($questionnaire->useopendate)) { - $questionnaire->opendate = 0; - } - if (empty($questionnaire->useclosedate)) { - $questionnaire->closedate = 0; - } - if ($questionnaire->resume == '1') { $questionnaire->resume = 1; } else { @@ -195,9 +203,11 @@ function questionnaire_update_instance($questionnaire) { return $DB->update_record("questionnaire", $questionnaire); } -// Given an ID of an instance of this module, -// this function will permanently delete the instance -// and any data that depends on it. +/** + * Given an ID of an instance of this module, this function will permanently delete the instance and any data that depends on it. + * @param int $id + * @return bool + */ function questionnaire_delete_instance($id) { global $DB, $CFG; require_once($CFG->dirroot.'/mod/questionnaire/locallib.php'); @@ -229,15 +239,54 @@ function questionnaire_delete_instance($id) { return $result; } -// Return a small object with summary information about what a -// user has done with a given particular instance of this module -// Used for user activity reports. -// $return->time = the time they did it -// $return->info = a short text description. /** - * $course and $mod are unused, but API requires them. Suppress PHPMD warning. + * Add a get_coursemodule_info function in case any questionnaire type wants to add 'extra' information + * for the course (see resource). + * + * Given a course_module object, this function returns any "extra" information that may be needed + * when printing this activity in a course listing. See get_array_of_activities() in course/lib.php. * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param stdClass $coursemodule The coursemodule object (record). + * @return cached_cm_info An object on information that the courses + * will know about (most noticeably, an icon). + */ +function questionnaire_get_coursemodule_info($coursemodule) { + global $DB; + + $questionnaire = $DB->get_record('questionnaire', + array('id' => $coursemodule->instance), 'id, name, intro, introformat, + completionsubmit'); + if (!$questionnaire) { + return null; + } + + $info = new cached_cm_info(); + $info->customdata = (object)[]; + + if ($coursemodule->showdescription) { + // Convert intro to html. Do not filter cached version, filters run at display time. + // Based on the function quiz_get_coursemodule_info() in the quiz module. + $info->content = format_module_intro('questionnaire', $questionnaire, $coursemodule->id, false); + } + + // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'. + if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) { + $info->customdata->customcompletionrules['completionsubmit'] = $questionnaire->completionsubmit; + } + return $info; +} + +/** + * Return a small object with summary information about what a user has done with a given particular instance of this module. + * Used for user activity reports. + * $return->time = the time they did it + * $return->info = a short text description. + * $course and $mod are unused, but API requires them. Suppress PHPMD warning. + * @param stdClass $course + * @param stdClass $user + * @param stdClass $mod + * @param stdClass $questionnaire + * @return stdClass */ function questionnaire_user_outline($course, $user, $mod, $questionnaire) { global $CFG; @@ -259,12 +308,15 @@ function questionnaire_user_outline($course, $user, $mod, $questionnaire) { return $result; } -// Print a detailed representation of what a user has done with -// a given particular instance of this module, for user activity reports. /** + * Print a detailed representation of what a user has done with a given particular instance of this module, for user + * activity reports. * $course and $mod are unused, but API requires them. Suppress PHPMD warning. - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param stdClass $course + * @param stdClass $user + * @param stdClass $mod + * @param stdClass $questionnaire + * @return bool */ function questionnaire_user_complete($course, $user, $mod, $questionnaire) { global $CFG; @@ -285,24 +337,25 @@ function questionnaire_user_complete($course, $user, $mod, $questionnaire) { return true; } -// Given a course and a time, this module should find recent activity -// that has occurred in questionnaire activities and print it out. -// Return true if there was output, or false is there was none. /** + * Given a course and a time, this module should find recent activity that has occurred in questionnaire activities and print it + * out. + * Return true if there was output, or false is there was none. * $course, $isteacher and $timestart are unused, but API requires them. Suppress PHPMD warning. - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param stdClass $course + * @param bool $isteacher + * @param int $timestart + * @return false */ function questionnaire_print_recent_activity($course, $isteacher, $timestart) { return false; // True if anything was printed, otherwise false. } -// Must return an array of grades for a given instance of this module, -// indexed by user. It also returns a maximum allowed grade. /** + * Must return an array of grades for a given instance of this module, indexed by user. It also returns a maximum allowed grade. * $questionnaireid is unused, but API requires it. Suppress PHPMD warning. - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param int $questionnaireid + * @return null */ function questionnaire_grades($questionnaireid) { return null; @@ -311,7 +364,7 @@ function questionnaire_grades($questionnaireid) { /** * Return grade for given user or all users. * - * @param int $questionnaireid id of assignment + * @param stdClass $questionnaire * @param int $userid optional user id, 0 means all users * @return array array of grades, false if none */ @@ -327,18 +380,15 @@ function questionnaire_get_user_grades($questionnaire, $userid=0) { $sql = "SELECT r.id, u.id AS userid, r.grade AS rawgrade, r.submitted AS dategraded, r.submitted AS datesubmitted FROM {user} u, {questionnaire_response} r WHERE u.id = r.userid AND r.questionnaireid = $questionnaire->id AND r.complete = 'y' $usersql"; - return $DB->get_records_sql($sql, $params); + return $DB->get_records_sql($sql, $params) ?? []; } /** - * Update grades by firing grade_updated event - * - * @param object $assignment null means all assignments - * @param int $userid specific user only, 0 mean all - * + * Update grades by firing grade_updated event. * $nullifnone is unused, but API requires it. Suppress PHPMD warning. - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param stdClass $questionnaire + * @param int $userid + * @param bool $nullifnone */ function questionnaire_update_grades($questionnaire=null, $userid=0, $nullifnone=true) { global $CFG, $DB; @@ -388,8 +438,8 @@ function questionnaire_update_grades($questionnaire=null, $userid=0, $nullifnone /** * Create grade item for given questionnaire * - * @param object $questionnaire object with extra cmidnumber - * @param mixed optional array/object of grade(s); 'reset' means reset grades in gradebook + * @param stdClass $questionnaire object with extra cmidnumber + * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook * @return int 0 if ok, error code otherwise */ function questionnaire_grade_item_update($questionnaire, $grades = null) { @@ -410,12 +460,12 @@ function questionnaire_grade_item_update($questionnaire, $grades = null) { if ($questionnaire->grade > 0) { $params['gradetype'] = GRADE_TYPE_VALUE; - $params['grademax'] = $questionnaire->grade; - $params['grademin'] = 0; + $params['grademax'] = $questionnaire->grade; + $params['grademin'] = 0; } else if ($questionnaire->grade < 0) { $params['gradetype'] = GRADE_TYPE_SCALE; - $params['scaleid'] = -$questionnaire->grade; + $params['scaleid'] = -$questionnaire->grade; } else if ($questionnaire->grade == 0) { // No Grade..be sure to delete the grade item if it exists. $grades = null; @@ -439,13 +489,11 @@ function questionnaire_grade_item_update($questionnaire, $grades = null) { * it it has support for grading and scales. Commented code should be * modified if necessary. See forum, glossary or journal modules * as reference. - * @param $questionnaireid int - * @param $scaleid int + * @param int $questionnaireid + * @param int $scaleid * @return boolean True if the scale is used by any questionnaire * * Function parameters are unused, but API requires them. Suppress PHPMD warning. - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ function questionnaire_scale_used ($questionnaireid, $scaleid) { return false; @@ -455,672 +503,27 @@ function questionnaire_scale_used ($questionnaireid, $scaleid) { * Checks if scale is being used by any instance of questionnaire * * This is used to find out if scale used anywhere - * @param $scaleid int + * @param int $scaleid * @return boolean True if the scale is used by any questionnaire * * Function parameters are unused, but API requires them. Suppress PHPMD warning. - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ function questionnaire_scale_used_anywhere($scaleid) { return false; } -/** - * Get questionnaire data - * for mobile only kurvin hendricks - * - * @global object $DB - * @param int $cmid - * @param int|bool $userid - * @return array - * @throws moodle_exception - */ -function get_questionnaire_data($cmid, $userid = false) { - global $DB, $USER; - if ($q = get_coursemodule_from_id('questionnaire', $cmid)) { - if (!$questionnaire = get_questionnaire($q->instance)) { - throw new \moodle_exception("invalidcoursemodule", "error"); - } - } - - $resumedsql = 'SELECT id FROM ' - . '{questionnaire_response} ' - . ' WHERE questionnaireid = ? AND userid = ? AND complete = ? AND submitted <= ?'; - $params = ['userid' => $userid, 'questionnaireid' => $q->instance, 'complete' => 'y']; - $time = time(); - $ret = [ - 'questionnaire' => [ - 'id' => $questionnaire->id, - 'name' => format_string($questionnaire->name), - 'intro' => $questionnaire->intro, - 'userid' => intval($userid ? $userid : $USER->id), - 'questionnaireid' => intval($questionnaire->sid), - 'autonumpages' => in_array($questionnaire->autonum, [1, 2]), - 'autonumquestions' => in_array($questionnaire->autonum, [1, 3]) - ], - 'response' => [ - 'id' => 0, - 'questionnaireid' => 0, - 'submitted' => 0, - 'complete' => 'n', - 'grade' => 0, - 'userid' => 0, - 'fullname' => '', - 'userdate' => '', - ], - 'answered' => [], - 'fields' => [], - 'responses' => [], - 'questionscount' => 0, - 'pagescount' => 1, - 'resumed' => $DB->get_records_sql($resumedsql, - [$q->instance, $USER->id, 'n', ($time - (60 * 10))]), - 'completed' => $DB->record_exists('questionnaire_response', $params), - ]; - $sql = 'SELECT qq.*,qqt.response_table FROM ' - . '{questionnaire_question} qq LEFT JOIN {questionnaire_question_type} qqt ' - . 'ON qq.type_id = qqt.typeid WHERE qq.surveyid = ? AND qq.deleted = ? ' - . 'ORDER BY qq.position'; - - // Building dataset here, will build uncomplete questions here. - if ($questions = $DB->get_records_sql($sql, [$questionnaire->sid, 'n'])) { - require_once('classes/question/base.php'); - $pagenum = 1; - $context = \context_module::instance($cmid); - $qnum = 0; - foreach ($questions as $question) { - $ret['questionscount']++; - $qnum++; - $fieldkey = 'response_'.$question->type_id.'_'.$question->id; - $options = ['noclean' => true, 'para' => false, 'filter' => true, - 'context' => $context, 'overflowdiv' => true]; - if ($question->type_id != QUESPAGEBREAK) { - $ret['questionsinfo'][$pagenum][$question->id] = - $ret['fields'][$fieldkey] = [ - 'id' => $question->id, - 'surveyid' => $question->surveyid, - 'name' => $question->name, - 'type_id' => $question->type_id, - 'length' => $question->length, - 'content' => ($ret['questionnaire']['autonumquestions'] ? '' : '') . format_text(file_rewrite_pluginfile_urls( - $question->content, 'pluginfile.php', $context->id, - 'mod_questionnaire', 'question', $question->id), - FORMAT_HTML, $options), - 'content_stripped' => strip_tags($question->content), - 'required' => $question->required, - 'deleted' => $question->deleted, - 'response_table' => $question->response_table, - 'fieldkey' => $fieldkey, - 'precise' => $question->precise, - 'qnum' => $qnum, - 'errormessage' => get_string('required') . ': ' . $question->name, - ]; - } - $std = new \stdClass(); - $std->id = $std->choice_id = 0; - $std->question_id = $question->id; - $std->content = ''; - $std->value = null; - switch ($question->type_id) { - case QUESYESNO: // Yes/No bool. - $stdyes = new \stdClass(); - $stdyes->id = 1; - $stdyes->choice_id = 'y'; - $stdyes->question_id = $question->id; - $stdyes->value = null; - $stdyes->content = get_string('yes'); - $stdyes->isbool = true; - if ($ret['questionsinfo'][$pagenum][$question->id]['required']) { - $stdyes->value = 'y'; - $stdyes->firstone = true; - } - $ret['questions'][$pagenum][$question->id][1] = $stdyes; - $stdno = new \stdClass(); - $stdno->id = 0; - $stdno->choice_id = 'n'; - $stdno->question_id = $question->id; - $stdno->value = null; - $stdno->content = get_string('no'); - $stdno->isbool = true; - $ret['questions'][$pagenum][$question->id][0] = $stdno; - $ret['questionsinfo'][$pagenum][$question->id]['isbool'] = true; - break; - case QUESTEXT: // Text. - case QUESESSAY: // Essay. - $ret['questions'][$pagenum][$question->id][0] = $std; - $ret['questionsinfo'][$pagenum][$question->id]['istextessay'] = true; - break; - case QUESRADIO: // Radiobutton. - $ret['questionsinfo'][$pagenum][$question->id]['isradiobutton'] = true; - $excludes = []; - if ($items = $DB->get_records('questionnaire_quest_choice', - ['question_id' => $question->id])) { - - foreach ($items as $item) { - if (!in_array($item->id, $excludes)) { - $item->choice_id = $item->id; - if ($item->value == null) { - $item->value = ''; - } - $ret['questions'][$pagenum][$question->id][$item->id] = $item; - if ($question->type_id != 8) { - if ($ret['questionsinfo'][$pagenum][$question->id]['required']) { - if (!isset($ret['questionsinfo'][$pagenum][$question->id]['firstone'])) { - $ret['questionsinfo'][$pagenum][$question->id]['firstone'] = true; - $ret['questions'][$pagenum][$question->id][$item->id]->value = intval($item->choice_id); - $ret['questions'][$pagenum][$question->id][$item->id]->firstone = true; - } - } - } - } - } - } - break; - case QUESCHECK: // Checkbox. - $ret['questionsinfo'][$pagenum][$question->id]['ischeckbox'] = true; - $excludes = []; - if ($items = $DB->get_records('questionnaire_quest_choice', - ['question_id' => $question->id])) { - - foreach ($items as $item) { - if (!in_array($item->id, $excludes)) { - $item->choice_id = $item->id; - if ($item->value == null) { - $item->value = ''; - } - $ret['questions'][$pagenum][$question->id][$item->id] = $item; - if ($question->type_id != QUESRATE) { - if ($ret['questionsinfo'][$pagenum][$question->id]['required']) { - if (!isset($ret['questionsinfo'][$pagenum][$question->id]['firstone'])) { - $ret['questionsinfo'][$pagenum][$question->id]['firstone'] = true; - $ret['questions'][$pagenum][$question->id][$item->id]->firstone = true; - } - } - } - } - } - } - break; - case QUESDROP: // Select. - $ret['questionsinfo'][$pagenum][$question->id]['isselect'] = true; - $excludes = []; - if ($items = $DB->get_records('questionnaire_quest_choice', - ['question_id' => $question->id])) { - - foreach ($items as $item) { - if (!in_array($item->id, $excludes)) { - $item->choice_id = $item->id; - if ($item->value == null) { - $item->value = ''; - } - $ret['questions'][$pagenum][$question->id][$item->id] = $item; - if ($question->type_id != 9) { - if ($ret['questionsinfo'][$pagenum][$question->id]['required']) { - if (!isset($ret['questionsinfo'][$pagenum][$question->id]['firstone'])) { - $ret['questionsinfo'][$pagenum][$question->id]['firstone'] = true; - $ret['questions'][$pagenum][$question->id][$item->id]->value = intval($item->choice_id); - $ret['questions'][$pagenum][$question->id][$item->id]->firstone = true; - } - } - } - } - } - } - break; - case QUESRATE: // Rate 1-NN. - $excludes = []; - if ($items = $DB->get_records('questionnaire_quest_choice', - ['question_id' => $question->id])) { - $ret['questionsinfo'][$pagenum][$question->id]['israte'] = true; - $vals = $extracontents = []; - foreach ($items as $item) { - $item->na = false; - if ($question->precise == 0) { - $ret['questions'][$pagenum][$question->id][$item->id] = $item; - if ($ret['questionsinfo'][$pagenum][$question->id]['required'] == 'y') { - $ret['questions'][$pagenum][$question->id][$item->id]->min - = $ret['questions'][$pagenum][$question->id][$item->id]->minstr = 1; - } else { - $ret['questions'][$pagenum][$question->id][$item->id]->min - = $ret['questions'][$pagenum][$question->id][$item->id]->minstr = 0; - } - $ret['questions'][$pagenum][$question->id][$item->id]->max - = $ret['questions'][$pagenum][$question->id][$item->id]->maxstr - = intval($question->length); - } else if ($question->precise == 1) { - $ret['questions'][$pagenum][$question->id][$item->id] = $item; - if ($ret['questionsinfo'][$pagenum][$question->id]['required'] == 'y') { - $ret['questions'][$pagenum][$question->id][$item->id]->min - = $ret['questions'][$pagenum][$question->id][$item->id]->minstr = 1; - } else { - $ret['questions'][$pagenum][$question->id][$item->id]->min - = $ret['questions'][$pagenum][$question->id][$item->id]->minstr = 0; - } - $ret['questions'][$pagenum][$question->id][$item->id]->max = intval($question->length) + 1; - $ret['questions'][$pagenum][$question->id][$item->id]->na = true; - } else if ($question->precise > 1) { - $excludes[$item->id] = $item->id; - if ($item->value == null) { - if ($arr = explode('|', $item->content)) { - if (count($arr) == 2) { - $ret['questions'][$pagenum][$question->id][$item->id] = $item; - $ret['questions'][$pagenum][$question->id][$item->id]->content = ''; - $ret['questions'][$pagenum][$question->id][$item->id]->minstr = $arr[0]; - $ret['questions'][$pagenum][$question->id][$item->id]->maxstr = $arr[1]; - } - } - } else { - $val = intval($item->value); - $vals[$val] = $val; - $extracontents[] = $item->content; - } - } - } - if ($vals) { - if ($q = $ret['questions'][$pagenum][$question->id]) { - foreach (array_keys($q) as $itemid) { - $ret['questions'][$pagenum][$question->id][$itemid]->min = min($vals); - $ret['questions'][$pagenum][$question->id][$itemid]->max = max($vals); - } - } - } - if ($extracontents) { - $extracontents = array_unique($extracontents); - $extrahtml = '
    '; - foreach ($extracontents as $extracontent) { - $extrahtml .= '
  • '.$extracontent.'
  • '; - } - $extrahtml .= '
'; - $ret['questionsinfo'][$pagenum][$question->id]['content'] - .= format_text($extrahtml, FORMAT_HTML, $options); - } - foreach ($items as $item) { - if (!in_array($item->id, $excludes)) { - $item->choice_id = $item->id; - if ($item->value == null) { - $item->value = ''; - } - $ret['questions'][$pagenum][$question->id][$item->id] = $item; - if ($question->type_id != QUESRATE) { - if ($ret['questionsinfo'][$pagenum][$question->id]['required']) { - if (!isset($ret['questionsinfo'][$pagenum][$question->id]['firstone'])) { - $ret['questionsinfo'][$pagenum][$question->id]['firstone'] = true; - $ret['questions'][$pagenum][$question->id][$item->id]->value = intval($item->choice_id); - $ret['questions'][$pagenum][$question->id][$item->id]->firstone = true; - } - } - } - } - } - } - break; - case QUESDATE: // Date 12/12/12. - $ret['questionsinfo'][$pagenum][$question->id]['isdate'] = true; - $excludes = []; - - if ($item = $DB->get_records('questionnaire_question', - ['id' => $question->id])) { - $ret['questions'][$pagenum][$question->id][$item->id] = $item; - } - break; - case QUESNUMERIC: // Numeric 1 - 9. - $ret['questionsinfo'][$pagenum][$question->id]['isnumeric'] = true; - $excludes = []; - - if ($item = $DB->get_records('questionnaire_question', - ['id' => $question->id])) { - $ret['questions'][$pagenum][$question->id][$item->id] = $item; - } - break; - case QUESPAGEBREAK: - $ret['questionscount']--; - $ret['pagescount']++; - $pagenum++; - $qnum--; - break; - } - $ret['questionsinfo'][$pagenum][$question->id]['qnum'] = $qnum; - if ($ret['questionnaire']['autonumquestions']) { - $ret['questionsinfo'][$pagenum][$question->id]['content'] = - $qnum.'. '.$ret['questionsinfo'][$pagenum][$question->id]['content']; - $ret['questionsinfo'][$pagenum][$question->id]['content_stripped'] = - $qnum.'. '.$ret['questionsinfo'][$pagenum][$question->id]['content_stripped']; - } - } - - if ($userid) { - if ($response = $DB->get_record_sql('SELECT qr.* FROM {questionnaire_response} qr ' - . 'LEFT JOIN {user} u ON qr.userid = u.id WHERE qr.questionnaireid = ? ' - . 'AND qr.userid = ?', [$questionnaire->id, $userid])) { - $ret['response'] = (array) $response; - $ret['response']['submitted_userdate'] = ''; - if (isset($ret['response']['submitted']) && !empty($ret['response']['submitted'])) { - $ret['response']['submitted_userdate'] = userdate($ret['response']['submitted']); - } - $ret['response']['fullname'] = fullname($DB->get_record('user', ['id' => $userid])); - $ret['response']['userdate'] = userdate($ret['response']['submitted']); - - foreach ($ret['questionsinfo'] as $pagenum => $data1) { - foreach ($data1 as $questionid => $data2) { - $ret['answered'][$questionid] = false; - if (isset($data2['response_table']) && !empty($data2['response_table'])) { - if ($values = $DB->get_records_sql('SELECT * FROM {questionnaire_' - . $data2['response_table'] . '} WHERE response_id = ? AND question_id = ?', - [$response->id, $questionid])) { - foreach ($values as $value) { - switch($data2['type_id']) { - case QUESYESNO: // Yes/No bool. - if (isset($ret['questions'][$pagenum][$questionid])) { - if (isset($value->choice_id) && !empty($value->choice_id)) { - $ret['answered'][$questionid] = true; - if ($value->choice_id == 'y') { - $ret['questions'][$pagenum][$questionid][1]->value = 'y'; - $ret['responses']['response_'.$data2['type_id'].'_'.$questionid] = 'y'; - } else { - $ret['questions'][$pagenum][$questionid][0]->value = 'n'; - $ret['responses']['response_'.$data2['type_id'].'_'.$questionid] = 'n'; - } - } - } - break; - case QUESTEXT: // Text. - if (isset($value->response) && !empty($value->response)) { - $ret['answered'][$questionid] = true; - $ret['questions'][$pagenum][$questionid][0]->value = $value->response; - $ret['responses']['response_'.$data2['type_id'].'_'.$questionid] = $value->response; - } - break; - case QUESESSAY: // Essay. - if (isset($value->response) && !empty($value->response)) { - $ret['answered'][$questionid] = true; - $ret['questions'][$pagenum][$questionid][0]->value = $value->response; - $ret['responses']['response_'.$data2['type_id'].'_'.$questionid] = $value->response; - } - break; - case QUESRADIO: // Radiobutton. - if ($value = $DB->get_records_sql('SELECT * FROM {questionnaire_' - . $data2['response_table'] . '} WHERE response_id = ? AND question_id = ?', - [$response->id, $questionid])) { - foreach ($value as $row) { - foreach ($ret['questions'][$pagenum][$questionid] as $k => $item) { - if ($item->id == $row->choice_id) { - $ret['answered'][$questionid] = true; - $ret['questions'][$pagenum][$questionid][$k]->value = intval($item->id); - $ret['responses']['response_'.$data2['type_id'] - .'_'.$questionid] = intval($item->id); - } - } - } - } - break; - case QUESCHECK: // Checkbox. - if ($value = $DB->get_records_sql('SELECT * FROM {questionnaire_' - . $data2['response_table'] . '} WHERE response_id = ? AND question_id = ?', - [$response->id, $questionid])) { - foreach ($value as $row) { - foreach ($ret['questions'][$pagenum][$questionid] as $k => $item) { - if ($item->id == $row->choice_id) { - $ret['answered'][$questionid] = true; - $ret['questions'][$pagenum][$questionid][$k]->value = intval($item->id); - $ret['responses']['response_'.$data2['type_id'] - .'_'.$questionid] = intval($item->id); - } - } - } - } - case QUESDROP: // Select. - if ($value = $DB->get_records_sql('SELECT * FROM {questionnaire_' - . $data2['response_table'] . '} WHERE response_id = ? AND question_id = ?', - [$response->id, $questionid])) { - foreach ($value as $row) { - foreach ($ret['questions'][$pagenum][$questionid] as $k => $item) { - if ($item->id == $row->choice_id) { - $ret['answered'][$questionid] = true; - $ret['questions'][$pagenum][$questionid][$k]->value = intval($item->id); - $ret['responses']['response_'.$data2['type_id'] - .'_'.$questionid] = intval($item->id); - } - } - } - } - break; - case QUESRATE: // Rate 1-NN. - if ($value = $DB->get_records_sql('SELECT * FROM {questionnaire_' - . $data2['response_table'] . '} WHERE response_id = ? AND question_id = ?', - [$response->id, $questionid])) { - foreach ($value as $row) { - if ($questionid == $row->question_id) { - $ret['answered'][$questionid] = true; - $v = $row->rankvalue + 1; - if ($ret['questionsinfo'][$pagenum][$questionid]['precise'] == 1) { - if ($row->rankvalue == -1) { - $v = $ret['questions'][$pagenum][$questionid][$row->choice_id]->max; - } - } - $ret['questions'][$pagenum][$questionid][$row->choice_id]->value - = $ret['responses']['response_'.$data2['type_id'] - .'_'.$questionid.'_'.$row->choice_id] = $v; - $ret['questions'][$pagenum][$questionid][$row->choice_id]->choice_id - = $row->choice_id; - } - } - } - break; - case QUESDATE: // Date 12/12/12. - if (isset($value->response) && !empty($value->response)) { - $ret['answered'][$questionid] = true; - $ret['questions'][$pagenum][$questionid][0]->value = $value->response; - $ret['responses']['response_'.$data2['type_id'].'_'.$questionid] = $value->response; - } - break; - case QUESNUMERIC: // Numeric 1 - 9. - if (isset($value->response) && !empty($value->response)) { - $ret['answered'][$questionid] = true; - $ret['questions'][$pagenum][$questionid][0]->value = $value->response; - $ret['responses']['response_'.$data2['type_id'].'_'.$questionid] = $value->response; - } - break; - default: - break; - } - } - } - } - } - } - } - } - } - return $ret; -} - -function save_questionnaire_data_branching($questionnaireid, $surveyid, $userid, - $cmid, $sec, $completed, $submit, array $responses) { - - global $DB, $CFG; - $ret = [ - 'responses' => [], - 'warnings' => [] - ]; - if (!$completed) { - require_once('questionnaire.class.php'); - $cm = get_coursemodule_from_id('questionnaire', $cmid); - $questionnaire = new \questionnaire($questionnaireid, null, - $DB->get_record('course', ['id' => $cm->course]), $cm); - $rid = $questionnaire->delete_insert_response( - $DB->get_field('questionnaire_response', 'id', - ['questionnaireid' => $surveyid, 'complete' => 'n', - 'userid' => $userid]), $sec, $userid); - $questionnairedata = get_questionnaire_data($cmid, $userid); - $pagequestions = isset($questionnairedata['questions'][$sec]) ? $questionnairedata['questions'][$sec] : []; - if (!empty($pagequestions)) { - $pagequestionsids = array_keys($pagequestions); - $missingquestions = $warningmessages = []; - foreach ($pagequestionsids as $questionid) { - $missingquestions[$questionid] = $questionid; - } - foreach ($pagequestionsids as $questionid) { - foreach ($responses as $response) { - $args = explode('_', $response['name']); - if (count($args) >= 3) { - $typeid = intval($args[1]); - $rquestionid = intval($args[2]); - unset($missingquestions[$rquestionid]); - if ($typeid == $questionnairedata['questionsinfo'][$sec][$rquestionid]['type_id']) { - if ($rquestionid > 0 && !in_array($response['value'], array(-9999, 'undefined'))) { - - if ($typeid == QUESCHECK) { - if(!empty($response['value'])) { - // If checkbox handle differently because we need to check if question value is set to true. - if (isset($args[3]) && !empty($args[3])) { - $choiceid = intval($args[3]); - $rec = new \stdClass(); - $rec->response_id = $rid; - $rec->question_id = intval($rquestionid); - $rec->choice_id = $choiceid; - - $dupecheck = $DB->get_record('questionnaire_resp_multiple', - ['response_id' => $rec->response_id, - 'question_id' => $rec->question_id, - 'choice_id' => $rec->choice_id] - ); - - if (empty($dupecheck)) { - $DB->insert_record('questionnaire_resp_multiple', $rec); - } - } - } - } else if ($typeid == QUESRATE) { // Questionranking saving. - if (isset($args[3]) && !empty($args[3])) { - $choiceid = intval($args[3]); - $value = intval($response['value']) - 1; - $rec = new \stdClass(); - $rec->response_id = $rid; - $rec->question_id = intval($rquestionid); - $rec->choice_id = $choiceid; - $rec->rankvalue = $value; - if ($questionnairedata['questionsinfo'][$sec][$rquestionid]['precise'] == 1) { - if ($value == $questionnairedata['questions'][$sec][$rquestionid][$choiceid]->max - 1) { - $rec->rankvalue = -1; - } - } - - $dupecheck = $DB->get_record('questionnaire_response_rank', - ['response_id' => $rec->response_id, - 'question_id' => $rec->question_id, - 'choice_id' => $rec->choice_id] - ); - - if (empty($dupecheck)) { - $DB->insert_record('questionnaire_response_rank', $rec); - } - } - } else if ($typeid == QUESRADIO) { - if (isset($args[2]) && !empty($args[2])) { - $choiceid = intval($args[2]); - $rec = new \stdClass(); - $rec->response_id = $rid; - $rec->question_id = intval($rquestionid); - $rec->choice_id = $response['value']; - - $dupecheck = $DB->get_record('questionnaire_resp_single', - ['response_id' => $rec->response_id, - 'question_id' => $rec->question_id, - 'choice_id' => $rec->choice_id] - ); - - if (empty($dupecheck)) { - $DB->insert_record('questionnaire_resp_single', $rec); - } - } - } else if ($typeid == QUESDATE) { - if (isset($args[2]) && !empty($args[2])) { - $choiceid = intval($args[2]); - $rec = new \stdClass(); - $rec->response_id = $rid; - $rec->question_id = intval($rquestionid); - $rec->response = $response['value']; - - $responsetable = 'questionnaire_'; - $responsetable = $responsetable . - $questionnairedata['questionsinfo'][$sec][$rquestionid]['response_table']; - - $dupecheck = $DB->get_record($responsetable, - ['response_id' => $rid, - 'question_id' => $rquestionid] - ); - - if (empty($dupecheck)) { - $DB->insert_record($responsetable, $rec); - } - } - } else { - $questionobj = \mod_questionnaire\question\base::question_builder( - $questionnairedata['questionsinfo'][$sec][$rquestionid]['type_id'], - $questionnairedata['questionsinfo'][$sec][$rquestionid]); - - $responsetable = 'questionnaire_'; - $responsetable = $responsetable . - $questionnairedata['questionsinfo'][$sec][$rquestionid]['response_table']; - - $dupecheck = $DB->get_record($responsetable, - ['response_id' => $rid, - 'question_id' => $rquestionid] - ); - - if (empty($dupecheck)) { - if ($questionobj->insert_response($rid, $response['value'])) { - $ret['responses'][$rid][$questionid] = $response['value']; - } - } - } - } else { - $missingquestions[$rquestionid] = $rquestionid; - } - } - } - } - } - if ($missingquestions) { - foreach ($missingquestions as $questionid) { - if ($questionnairedata['questionsinfo'][$sec][$questionid]['required'] == 'y') { - $ret['warnings'][] = [ - 'item' => 'mod_questionnaire_question', - 'itemid' => $questionid, - 'warningcode' => 'required', - 'message' => s(get_string('required') . ': ' - . $questionnairedata['questionsinfo'][$sec][$questionid]['name']) - ]; - } - } - } - } - } - - if ($submit && (!isset($ret['warnings']) || empty($ret['warnings']))) { - $questionnaire->commit_submission_response( - $DB->get_field('questionnaire_response', 'id', - ['questionnaireid' => $surveyid, 'complete' => 'n', - 'userid' => $userid]), $userid); - } - return $ret; -} - /** * Serves the questionnaire attachments. Implements needed access control ;-) * - * @param object $course - * @param object $cm - * @param object $context + * @param stdClass $course + * @param stdClass $cm + * @param stdClass $context * @param string $filearea * @param array $args * @param bool $forcedownload * @return bool false if file not found, does not return if found - justsend the file * * $forcedownload is unused, but API requires it. Suppress PHPMD warning. - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ function questionnaire_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload) { global $DB; @@ -1177,30 +580,27 @@ function questionnaire_pluginfile($course, $cm, $context, $filearea, $args, $for * @param navigation_node $questionnairenode The node to add module settings to * * $settings is unused, but API requires it. Suppress PHPMD warning. - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ -function questionnaire_extend_settings_navigation(settings_navigation $settings, - navigation_node $questionnairenode) { +function questionnaire_extend_settings_navigation(settings_navigation $settings, navigation_node $questionnairenode) { + global $DB, $USER, $CFG; - global $PAGE, $DB, $USER, $CFG; $individualresponse = optional_param('individualresponse', false, PARAM_INT); $rid = optional_param('rid', false, PARAM_INT); // Response id. $currentgroupid = optional_param('group', 0, PARAM_INT); // Group id. require_once($CFG->dirroot.'/mod/questionnaire/questionnaire.class.php'); - $context = $PAGE->cm->context; - $cmid = $PAGE->cm->id; - $cm = $PAGE->cm; - $course = $PAGE->course; + $cm = $settings->get_page()->cm; + $context = $cm->context; + $cmid = $cm->id; + $course = $settings->get_page()->course; if (! $questionnaire = $DB->get_record("questionnaire", array("id" => $cm->instance))) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } $courseid = $course->id; - $questionnaire = new questionnaire(0, $questionnaire, $course, $cm); + $questionnaire = new questionnaire($course, $cm, 0, $questionnaire); if ($owner = $DB->get_field('questionnaire_survey', 'courseid', ['id' => $questionnaire->sid])) { $owner = (trim($owner) == trim($courseid)); @@ -1391,16 +791,35 @@ function questionnaire_extend_settings_navigation(settings_navigation $settings, // Any other questionnaire functions go here. Each of them must have a name that // starts with questionnaire_. +/** + * Return the view actions. + * @return string[] + */ function questionnaire_get_view_actions() { return array('view', 'view all'); } +/** + * Return the post actions. + * @return string[] + */ function questionnaire_get_post_actions() { return array('submit', 'update'); } +/** + * Return the recent activity. + * @param array $activities + * @param int $index + * @param int $timestart + * @param int $courseid + * @param int $cmid + * @param int $userid + * @param int $groupid + * @return mixed|void + */ function questionnaire_get_recent_mod_activity(&$activities, &$index, $timestart, - $courseid, $cmid, $userid = 0, $groupid = 0) { + $courseid, $cmid, $userid = 0, $groupid = 0) { global $CFG, $COURSE, $USER, $DB; require_once($CFG->dirroot . '/mod/questionnaire/locallib.php'); @@ -1416,7 +835,7 @@ function questionnaire_get_recent_mod_activity(&$activities, &$index, $timestart $cm = $modinfo->cms[$cmid]; $questionnaire = $DB->get_record('questionnaire', ['id' => $cm->instance]); - $questionnaire = new questionnaire(0, $questionnaire, $course, $cm); + $questionnaire = new questionnaire($course, $cm, 0, $questionnaire); $context = context_module::instance($cm->id); $grader = has_capability('mod/questionnaire:viewsingleresponse', $context); @@ -1453,11 +872,11 @@ function questionnaire_get_recent_mod_activity(&$activities, &$index, $timestart if ($groupid) { $groupselect = 'AND gm.groupid = :groupid'; - $groupjoin = 'JOIN {groups_members} gm ON gm.userid=u.id'; + $groupjoin = 'JOIN {groups_members} gm ON gm.userid=u.id'; $params['groupid'] = $groupid; } else { $groupselect = ''; - $groupjoin = ''; + $groupjoin = ''; } $params['timestart'] = $timestart; @@ -1479,8 +898,8 @@ function questionnaire_get_recent_mod_activity(&$activities, &$index, $timestart } $accessallgroups = has_capability('moodle/site:accessallgroups', $context); - $viewfullnames = has_capability('moodle/site:viewfullnames', $context); - $groupmode = groups_get_activity_groupmode($cm, $course); + $viewfullnames = has_capability('moodle/site:viewfullnames', $context); + $groupmode = groups_get_activity_groupmode($cm, $course); $usersgroups = null; $aname = format_string($cm->name, true); @@ -1517,19 +936,19 @@ function questionnaire_get_recent_mod_activity(&$activities, &$index, $timestart $tmpactivity = new stdClass(); - $tmpactivity->type = 'questionnaire'; - $tmpactivity->cmid = $cm->id; + $tmpactivity->type = 'questionnaire'; + $tmpactivity->cmid = $cm->id; $tmpactivity->cminstance = $cm->instance; // Current user is admin - or teacher enrolled in original public course. if (isset($cmoriginal)) { $tmpactivity->cminstance = $cmoriginal->instance; } $tmpactivity->cannotview = false; - $tmpactivity->anonymous = false; - $tmpactivity->name = $aname; + $tmpactivity->anonymous = false; + $tmpactivity->name = $aname; $tmpactivity->sectionnum = $cm->sectionnum; - $tmpactivity->timestamp = $attempt->submitted; - $tmpactivity->groupid = $groupid; + $tmpactivity->timestamp = $attempt->submitted; + $tmpactivity->groupid = $groupid; if (isset($userattempts[$attempt->lastname])) { $tmpactivity->nbattempts = $userattempts[$attempt->lastname]; } @@ -1551,7 +970,7 @@ function questionnaire_get_recent_mod_activity(&$activities, &$index, $timestart } } if ($questionnaire->respondenttype != 'anonymous') { - $tmpactivity->user->fullname = fullname($attempt, $viewfullnames); + $tmpactivity->user->fullname = fullname($attempt, $viewfullnames); } else { $tmpactivity->user = ''; unset ($tmpactivity->user); @@ -1564,16 +983,13 @@ function questionnaire_get_recent_mod_activity(&$activities, &$index, $timestart /** * Prints all users who have completed a specified questionnaire since a given time * - * @global object - * @param object $activity + * @param stdClass $activity * @param int $courseid * @param string $detail not used but needed for compability * @param array $modnames * @return void Output is echo'd * * $details and $modenames are unused, but API requires them. Suppress PHPMD warning. - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ function questionnaire_print_recent_mod_activity($activity, $courseid, $detail, $modnames) { global $OUTPUT; @@ -1642,10 +1058,6 @@ function questionnaire_print_recent_mod_activity($activity, $courseid, $detail, * questionnaires that have a deadline that has not already passed * and it is available for taking. * - * @global object - * @global stdClass - * @global object - * @uses CONTEXT_MODULE * @param array $courses An array of course objects to get questionnaire instances from * @param array $htmlarray Store overview output array( course ID => 'questionnaire' => HTML output ) * @return void @@ -1660,9 +1072,9 @@ function questionnaire_print_overview($courses, &$htmlarray) { } // Get Necessary Strings. - $strquestionnaire = get_string('modulename', 'questionnaire'); + $strquestionnaire = get_string('modulename', 'questionnaire'); $strnotattempted = get_string('noattempts', 'questionnaire'); - $strattempted = get_string('attempted', 'questionnaire'); + $strattempted = get_string('attempted', 'questionnaire'); $strsavedbutnotsubmitted = get_string('savedbutnotsubmitted', 'questionnaire'); $now = time(); @@ -1686,7 +1098,7 @@ function questionnaire_print_overview($courses, &$htmlarray) { // Deadline. $str .= $OUTPUT->box(get_string('closeson', 'questionnaire', userdate($questionnaire->closedate)), 'info'); $attempts = $DB->get_records('questionnaire_response', - ['questionnaireid' => $questionnaire->id, 'userid' => $USER->id, 'complete' => 'y']); + ['questionnaireid' => $questionnaire->id, 'userid' => $USER->id, 'complete' => 'y']) ?? []; $nbattempts = count($attempts); // Do not display a questionnaire as due if it can only be sumbitted once and it has already been submitted! @@ -1730,7 +1142,7 @@ function questionnaire_print_overview($courses, &$htmlarray) { * Implementation of the function for printing the form elements that control * whether the course reset functionality affects the questionnaire. * - * @param $mform the course reset form that is being built. + * @param stdClass $mform the course reset form that is being built. */ function questionnaire_reset_course_form_definition($mform) { $mform->addElement('header', 'questionnaireheader', get_string('modulenameplural', 'questionnaire')); @@ -1740,11 +1152,10 @@ function questionnaire_reset_course_form_definition($mform) { /** * Course reset form defaults. + * @param stdClass $course * @return array the defaults. * * Function parameters are unused, but API requires them. Suppress PHPMD warning. - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ function questionnaire_reset_course_form_defaults($course) { return array('reset_questionnaire' => 1); @@ -1754,7 +1165,7 @@ function questionnaire_reset_course_form_defaults($course) { * Actual implementation of the reset course functionality, delete all the * questionnaire responses for course $data->courseid. * - * @param object $data the data submitted from the reset course. + * @param stdClass $data the data submitted from the reset course. * @return array status array */ function questionnaire_reset_userdata($data) { @@ -1810,17 +1221,13 @@ function questionnaire_reset_userdata($data) { * Obtains the automatic completion state for this questionnaire based on the condition * in questionnaire settings. * - * @param object $course Course * @param object $cm Course-module * @param int $userid User ID * @param bool $type Type of comparison (or/and; can be used as return value if no conditions) * @return bool True if completed, false if not, $type if conditions not set. * - * $course is unused, but API requires it. Suppress PHPMD warning. - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ -function questionnaire_get_completion_state($course, $cm, $userid, $type) { +function questionnaire_get_completion_state($cm, $userid, $type) { global $DB; // Get questionnaire details. @@ -1867,155 +1274,40 @@ function mod_questionnaire_core_calendar_provide_event_action(calendar_event $ev } /** - * custom version of functions required for mobile + * Called after the activity and module have been created. Use this to copy any images if the questionnaire was created from another + * questionnaire survey. + * + * @param stdClass $data + * @param stdClass $course + * @throws coding_exception */ -function get_mobile_response($userid, $rid = 0, $qid = 0) { - global $DB; - - $rid = intval($rid); - if ($rid != 0) { - // Check for valid rid. - $fields = 'id, userid'; - $params = ['id' => $rid, 'questionnaireid' => $qid, 'userid' => $userid, 'complete' => 'n']; - return ($DB->get_record('questionnaire_response', $params, $fields) !== false) ? $rid : ''; - - } else { - // Find latest in progress rid. - $params = ['questionnaireid' => $qid, 'userid' => $userid, 'complete' => 'n']; - if ($records = $DB->get_records('questionnaire_response', $params, 'submitted DESC', 'id,questionnaireid', 0, 1)) { - $rec = reset($records); - return $rec->id; - } else { - return ''; - } - } -} - -function get_mobile_questionnaire($questionnaire, $pagenum, $branching = 0) { +function mod_questionnaire_coursemodule_edit_post_actions($data, $course) { global $DB; - // Need to change the page num based on. - // The check for required questions. - // That's the logic I am thinking about. - // Eg page num is 3 if you have never done a course. - - if (!empty($questionnaire['questionsinfo'][1])) { - $surveyinfo = $questionnaire['questionsinfo'][1]; - $surveyinfo = array_shift($surveyinfo); - $sid = $surveyinfo['surveyid']; - - } - // Logic for resuming questionnaire for mobile. - $prevpage = 1; - $responses = $questionnaire['responses']; - foreach ($responses as $key => $response) { - $args = explode('_', $key); - if ($args[1] == 1 || $args[1] != $pagenum) { - $prevpage = (int)$args[1]; - } - } - - $questionnairedependency = $DB->get_records('questionnaire_dependency', ['surveyid' => $sid]); - $nondependentquestions = array(); - - foreach ($questionnaire['fields'] as $question) { - $nondependentquestions[$question['id']] = array( - 'id' => $question['id'], - 'qnum' => $question['qnum'] - ); - } - - foreach ($questionnairedependency as $dependency) { - if ($dependency->dependchoiceid == '0') { - unset($nondependentquestions[$dependency->questionid]); - } - - foreach ($nondependentquestions as $nondependent) { - if ($questionnaire['answered'][$nondependent['id']] === true - && !empty($questionnaire['resumed'])) { // Resuming questionnaire here. - unset($nondependentquestions[$nondependent['id']]); - } else { - array_shift($nondependentquestions); - break; - } - } - } - - if (count($questionnairedependency) > 0) { - foreach ($questionnaire['fields'] as $question) { - if ($question['qnum'] == $pagenum) { - foreach ($questionnairedependency as $dependency) { - if ($dependency->questionid == $question['id']) { - $answereddependency = (array_shift($questionnaire['responses']) == 'n' ? 1 : 0); //yes = 0, no = 1 - - if ($answereddependency == $dependency->dependchoiceid) { - // Find next question that does not have dependency. - $pagenums = array( - 'prevpage' => $pagenum - 1, - 'pagenum' => $pagenum, - 'nextpage' => $pagenum + 1 - ); - return $pagenums; - } else { - $nextpage = array_shift(array_slice($nondependentquestions, 1, 1, true)); - $pagenum = array_shift($nondependentquestions); - - if ($pagenum['qnum'] == 1) { - $prevpage = null; - $pagenum = 1; - $nextpage = $nextpage['qnum'] - 1; - } else { - $pagenum = $pagenum['qnum']; - $nextpage = $pagenum + 1; - $prevpage = $pagenum - 1; - } - - $pagenums = array( - 'prevpage' => $prevpage, - 'pagenum' => $pagenum, - 'nextpage' => $nextpage, - ); - - return $pagenums; - // Need to get page next page num without any dependencies. - } - } else { - $pagenums = array( - 'prevpage' => $prevpage, - 'pagenum' => $pagenum, - 'nextpage' => $pagenum + 1, - ); - return $pagenums; - } + if (!empty($data->copyid)) { + $cm = (object)['id' => $data->coursemodule]; + $questionnaire = new questionnaire($course, $cm, 0, $data); + $oldquestionnaireid = $DB->get_field('questionnaire', 'id', ['sid' => $data->copyid]); + $oldcm = get_coursemodule_from_instance('questionnaire', $oldquestionnaireid); + $oldquestionnaire = new questionnaire($course, $oldcm, $oldquestionnaireid, null); + $oldcontext = context_module::instance($oldcm->id); + $newcontext = context_module::instance($data->coursemodule); + $areas = $questionnaire->get_all_file_areas(); + $oldareas = $oldquestionnaire->get_all_file_areas(); + $fs = new \mod_questionnaire\file_storage(); + foreach ($areas as $area => $ids) { + if (is_array($ids)) { + $oldid = current($oldareas[$area]); + foreach ($ids as $id) { + $fs->copy_area_files_to_new_context($oldcontext->id, $newcontext->id, 'mod_questionnaire', $area, $oldid, $id); + $oldid = next($oldareas[$area]); } + } else { + $fs->copy_area_files_to_new_context($oldcontext->id, $newcontext->id, 'mod_questionnaire', $area, + $oldareas[$area], $ids); } } - } else { - $pagenums = array( - 'prevpage' => $pagenum - 1, - 'pagenum' => $pagenum, - 'nextpage' => $pagenum + 1, - ); - return $pagenums; } -} - -function check_mobile_branching_logic($questionnaire) { - global $DB; - $surveyinfo = []; - $sid = 0; - - if (!empty($questionnaire['questionsinfo'][1])) { - $surveyinfo = $questionnaire['questionsinfo'][1]; - $surveyinfo = array_shift($surveyinfo); - $sid = $surveyinfo['surveyid']; - } - - $questionnairedependency = $DB->get_records('questionnaire_dependency', ['surveyid' => $sid]); - - if (!empty($questionnairedependency)) { - return true; - } - return false; + return $data; } diff --git a/locallib.php b/locallib.php index be92b0ba..4fce8590 100644 --- a/locallib.php +++ b/locallib.php @@ -15,27 +15,16 @@ // along with Moodle. If not, see . /** + * Updates the contents of the survey with the provided data. If no data is provided, it checks for posted data. + * * This library replaces the phpESP application with Moodle specific code. It will eventually * replace all of the phpESP application, removing the dependency on that. - */ - -/** - * Updates the contents of the survey with the provided data. If no data is provided, - * it checks for posted data. - * - * @param int $surveyid The id of the survey to update. - * @param string $old_tab The function that was being executed. - * @param object $sdata The data to update the survey with. * - * @return string|boolean The function to go to, or false on error. - * - */ - -/** * @package mod_questionnaire * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * */ defined('MOODLE_INTERNAL') || die(); @@ -87,115 +76,11 @@ 2 => get_string('autonumberpages', 'questionnaire'), 3 => get_string('autonumberpagesandquestions', 'questionnaire')); -function questionnaire_check_date ($thisdate, $insert=false) { - $dateformat = get_string('strfdate', 'questionnaire'); - if (preg_match('/(%[mdyY])(.+)(%[mdyY])(.+)(%[mdyY])/', $dateformat, $matches)) { - $datepieces = explode($matches[2], $thisdate); - foreach ($datepieces as $datepiece) { - if (!is_numeric($datepiece)) { - return 'wrongdateformat'; - } - } - $pattern = "/[^dmy]/i"; - $dateorder = strtolower(preg_replace($pattern, '', $dateformat)); - $countpieces = count($datepieces); - if ($countpieces == 1) { // Assume only year entered. - switch ($dateorder) { - case 'dmy': // Most countries. - case 'mdy': // USA. - $datepieces[2] = $datepieces[0]; // year - $datepieces[0] = '1'; // Assumed 1st month of year. - $datepieces[1] = '1'; // Assumed 1st day of month. - break; - case 'ymd': // ISO 8601 standard - $datepieces[1] = '1'; // Assumed 1st month of year. - $datepieces[2] = '1'; // Assumed 1st day of month. - break; - } - } - if ($countpieces == 2) { // Assume only month and year entered. - switch ($dateorder) { - case 'dmy': // Most countries. - $datepieces[2] = $datepieces[1]; // Year. - $datepieces[1] = $datepieces[0]; // Month. - $datepieces[0] = '1'; // Assumed 1st day of month. - break; - case 'mdy': // USA - $datepieces[2] = $datepieces[1]; // Year. - $datepieces[0] = $datepieces[0]; // Month. - $datepieces[1] = '1'; // Assumed 1st day of month. - break; - case 'ymd': // ISO 8601 standard - $datepieces[2] = '1'; // Assumed 1st day of month. - break; - } - } - if (count($datepieces) > 1) { - if ($matches[1] == '%m') { - $month = $datepieces[0]; - } - if ($matches[1] == '%d') { - $day = $datepieces[0]; - } - if ($matches[1] == '%y') { - $year = strftime('%C').$datepieces[0]; - } - if ($matches[1] == '%Y') { - $year = $datepieces[0]; - } - - if ($matches[3] == '%m') { - $month = $datepieces[1]; - } - if ($matches[3] == '%d') { - $day = $datepieces[1]; - } - if ($matches[3] == '%y') { - $year = strftime('%C').$datepieces[1]; - } - if ($matches[3] == '%Y') { - $year = $datepieces[1]; - } - - if ($matches[5] == '%m') { - $month = $datepieces[2]; - } - if ($matches[5] == '%d') { - $day = $datepieces[2]; - } - if ($matches[5] == '%y') { - $year = strftime('%C').$datepieces[2]; - } - if ($matches[5] == '%Y') { - $year = $datepieces[2]; - } - - $month = min(12, $month); - $month = max(1, $month); - if ($month == 2) { - $day = min(29, $day); - } else if ($month == 4 || $month == 6 || $month == 9 || $month == 11) { - $day = min(30, $day); - } else { - $day = min(31, $day); - } - $day = max(1, $day); - if (!$thisdate = gmmktime(0, 0, 0, $month, $day, $year)) { - return 'wrongdaterange'; - } else { - if ($insert) { - $thisdate = trim(userdate ($thisdate, '%Y-%m-%d', '1', false)); - } else { - $thisdate = trim(userdate ($thisdate, $dateformat, '1', false)); - } - } - return $thisdate; - } - } else { - return ('wrongdateformat'); - } -} - +/** + * Return the choice values for the content. + * @param string $content + * @return stdClass + */ function questionnaire_choice_values($content) { // If we run the content through format_text first, any filters we want to use (e.g. multilanguage) should work. @@ -249,24 +134,37 @@ function questionnaire_choice_values($content) { * @return array a standard jsmodule structure. */ function questionnaire_get_js_module() { - return array( + return [ 'name' => 'mod_questionnaire', 'fullpath' => '/mod/questionnaire/module.js', - 'requires' => array('base', 'dom', 'event-delegate', 'event-key', - 'core_question_engine', 'moodle-core-formchangechecker'), - 'strings' => array( - array('cancel', 'moodle'), - array('flagged', 'question'), - array('functiondisabledbysecuremode', 'quiz'), - array('startattempt', 'quiz'), - array('timesup', 'quiz'), - array('changesmadereallygoaway', 'moodle'), - ), - ); + 'requires' => ['base', 'dom', 'event-delegate', 'event-key', + 'core_question_engine', 'moodle-core-formchangechecker'], + 'strings' => [ + ['cancel', 'moodle'], + ['flagged', 'question'], + ['functiondisabledbysecuremode', 'quiz'], + ['startattempt', 'quiz'], + ['timesup', 'quiz'], + ['changesmadereallygoaway', 'moodle'], + ['leftpart', 'questionnaire'], + ['leftpartdefault', 'questionnaire'], + ['middlepart', 'questionnaire'], + ['middlepartdefault', 'questionnaire'], + ['middlepartwithtwovalues', 'questionnaire'], + ['middlepartwithtwovaluesdefault', 'questionnaire'], + ['rightpart', 'questionnaire'], + ['rightpartdefault', 'questionnaire'], + ['where', 'questionnaire'], + ], + ]; } /** - * Get all the questionnaire responses for a user + * Get all the questionnaire responses for a user. + * @param int $questionnaireid + * @param int $userid + * @param bool $complete + * @return array */ function questionnaire_get_user_responses($questionnaireid, $userid, $complete=true) { global $DB; @@ -279,7 +177,7 @@ function questionnaire_get_user_responses($questionnaireid, $userid, $complete=t WHERE questionnaireid = ? AND userid = ? ".$andcomplete." - ORDER BY submitted ASC ", array($questionnaireid, $userid)); + ORDER BY submitted ASC ", array($questionnaireid, $userid)) ?? []; } /** @@ -297,23 +195,23 @@ function questionnaire_load_capabilities($cmid) { $context = questionnaire_get_context($cmid); $cb = new stdClass(); - $cb->view = has_capability('mod/questionnaire:view', $context); - $cb->submit = has_capability('mod/questionnaire:submit', $context); - $cb->viewsingleresponse = has_capability('mod/questionnaire:viewsingleresponse', $context); + $cb->view = has_capability('mod/questionnaire:view', $context); + $cb->submit = has_capability('mod/questionnaire:submit', $context); + $cb->viewsingleresponse = has_capability('mod/questionnaire:viewsingleresponse', $context); $cb->submissionnotification = has_capability('mod/questionnaire:submissionnotification', $context); - $cb->downloadresponses = has_capability('mod/questionnaire:downloadresponses', $context); - $cb->deleteresponses = has_capability('mod/questionnaire:deleteresponses', $context); - $cb->manage = has_capability('mod/questionnaire:manage', $context); - $cb->editquestions = has_capability('mod/questionnaire:editquestions', $context); - $cb->createtemplates = has_capability('mod/questionnaire:createtemplates', $context); - $cb->createpublic = has_capability('mod/questionnaire:createpublic', $context); - $cb->readownresponses = has_capability('mod/questionnaire:readownresponses', $context); - $cb->readallresponses = has_capability('mod/questionnaire:readallresponses', $context); + $cb->downloadresponses = has_capability('mod/questionnaire:downloadresponses', $context); + $cb->deleteresponses = has_capability('mod/questionnaire:deleteresponses', $context); + $cb->manage = has_capability('mod/questionnaire:manage', $context); + $cb->editquestions = has_capability('mod/questionnaire:editquestions', $context); + $cb->createtemplates = has_capability('mod/questionnaire:createtemplates', $context); + $cb->createpublic = has_capability('mod/questionnaire:createpublic', $context); + $cb->readownresponses = has_capability('mod/questionnaire:readownresponses', $context); + $cb->readallresponses = has_capability('mod/questionnaire:readallresponses', $context); $cb->readallresponseanytime = has_capability('mod/questionnaire:readallresponseanytime', $context); - $cb->printblank = has_capability('mod/questionnaire:printblank', $context); - $cb->preview = has_capability('mod/questionnaire:preview', $context); + $cb->printblank = has_capability('mod/questionnaire:printblank', $context); + $cb->preview = has_capability('mod/questionnaire:preview', $context); - $cb->viewhiddenactivities = has_capability('moodle/course:viewhiddenactivities', $context, null, false); + $cb->viewhiddenactivities = has_capability('moodle/course:viewhiddenactivities', $context, null, false); return $cb; } @@ -331,13 +229,17 @@ function questionnaire_get_context($cmid) { } if (!$context = context_module::instance($cmid)) { - print_error('badcontext'); + throw new \moodle_exception('badcontext', 'mod_questionnaire'); } return $context; } -// This function *really* shouldn't be needed, but since sometimes we can end up with -// orphaned surveys, this will clean them up. +/** + * This function *really* shouldn't be needed, but since sometimes we can end up with + * orphaned surveys, this will clean them up. + * @return bool + * @throws dml_exception + */ function questionnaire_cleanup() { global $DB; @@ -355,6 +257,12 @@ function questionnaire_cleanup() { return true; } +/** + * Delete the survey. + * @param int $sid + * @param int $questionnaireid + * @return bool + */ function questionnaire_delete_survey($sid, $questionnaireid) { global $DB; $status = true; @@ -392,6 +300,12 @@ function questionnaire_delete_survey($sid, $questionnaireid) { return $status; } +/** + * Delete the response. + * @param stdClass $response + * @param string $questionnaire + * @return bool + */ function questionnaire_delete_response($response, $questionnaire='') { global $DB; $status = true; @@ -424,6 +338,11 @@ function questionnaire_delete_response($response, $questionnaire='') { return $status; } +/** + * Delete all responses for the questionnaire. + * @param int $qid + * @return bool + */ function questionnaire_delete_responses($qid) { global $DB; @@ -439,6 +358,11 @@ function questionnaire_delete_responses($qid) { return true; } +/** + * Delete all dependencies for the questionnaire. + * @param int $qid + * @return bool + */ function questionnaire_delete_dependencies($qid) { global $DB; @@ -449,6 +373,12 @@ function questionnaire_delete_dependencies($qid) { return true; } +/** + * Get a survey selection records. + * @param int $courseid + * @param string $type + * @return array|false + */ function questionnaire_get_survey_list($courseid=0, $type='') { global $DB; @@ -495,9 +425,15 @@ function questionnaire_get_survey_list($courseid=0, $type='') { $params = [$courseid]; } } - return $DB->get_records_sql($sql, $params); + return $DB->get_records_sql($sql, $params) ?? []; } +/** + * Get survey selection list. + * @param int $courseid + * @param string $type + * @return array + */ function questionnaire_get_survey_select($courseid=0, $type='') { global $OUTPUT, $DB; @@ -534,6 +470,12 @@ function questionnaire_get_survey_select($courseid=0, $type='') { return $surveylist; } +/** + * Return the language string for the specified question type. + * @param int $id + * @return lang_string|mixed|string + * @throws coding_exception + */ function questionnaire_get_type ($id) { switch ($id) { case 1: @@ -554,6 +496,8 @@ function questionnaire_get_type ($id) { return get_string('date', 'questionnaire'); case 10: return get_string('numeric', 'questionnaire'); + case 11: + return get_string('slider', 'questionnaire'); case 100: return get_string('sectiontext', 'questionnaire'); case 99: @@ -568,8 +512,6 @@ function questionnaire_get_type ($id) { * @param object $questionnaire * @return void */ - /* added by JR 16 march 2009 based on lesson_process_post_save script */ - function questionnaire_set_events($questionnaire) { // Adding the questionnaire to the eventtable. global $DB; @@ -589,6 +531,7 @@ function questionnaire_set_events($questionnaire) { $event->modulename = 'questionnaire'; $event->instance = $questionnaire->id; $event->eventtype = 'open'; + $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->timestart = $questionnaire->opendate; $event->visible = instance_is_visible('questionnaire', $questionnaire); $event->timeduration = ($questionnaire->closedate - $questionnaire->opendate); @@ -596,18 +539,21 @@ function questionnaire_set_events($questionnaire) { if ($questionnaire->closedate && $questionnaire->opendate && ($event->timeduration <= QUESTIONNAIRE_MAX_EVENT_LENGTH)) { // Single event for the whole questionnaire. $event->name = $questionnaire->name; + $event->timesort = $questionnaire->opendate; calendar_event::create($event); } else { // Separate start and end events. - $event->timeduration = 0; + $event->timeduration = 0; if ($questionnaire->opendate) { $event->name = $questionnaire->name.' ('.get_string('questionnaireopens', 'questionnaire').')'; + $event->timesort = $questionnaire->opendate; calendar_event::create($event); unset($event->id); // So we can use the same object for the close event. } if ($questionnaire->closedate) { $event->name = $questionnaire->name.' ('.get_string('questionnairecloses', 'questionnaire').')'; $event->timestart = $questionnaire->closedate; + $event->timesort = $questionnaire->closedate; $event->eventtype = 'close'; calendar_event::create($event); } @@ -617,14 +563,15 @@ function questionnaire_set_events($questionnaire) { /** * Get users who have not completed the questionnaire * - * @global object - * @uses CONTEXT_MODULE * @param object $cm - * @param int $group single groupid + * @param int $sid + * @param bool $group single groupid * @param string $sort - * @param int $startpage - * @param int $pagecount + * @param bool $startpage + * @param bool $pagecount * @return object the userrecords + * @throws coding_exception + * @throws dml_exception */ function questionnaire_get_incomplete_users($cm, $sid, $group = false, @@ -639,15 +586,7 @@ function questionnaire_get_incomplete_users($cm, $sid, // First get all users who can complete this questionnaire. $cap = 'mod/questionnaire:submit'; $fields = 'u.id, u.username'; - if (!$allusers = get_users_by_capability($context, - $cap, - $fields, - $sort, - '', - '', - $group, - '', - true)) { + if (!$allusers = get_enrolled_users($context, $cap, $group, $fields, $sort)) { return false; } $allusers = array_keys($allusers); @@ -674,6 +613,8 @@ function questionnaire_get_incomplete_users($cm, $sid, /** * Called by HTML editor in showrespondents and Essay question. Based on question/essay/renderer. * Pending general solution to using the HTML editor outside of moodleforms in Moodle pages. + * @param int $context + * @return array */ function questionnaire_get_editor_options($context) { return array( @@ -686,8 +627,11 @@ function questionnaire_get_editor_options($context) { ); } -// Get the parent of a child question. -// TODO - This needs to be refactored or removed. +/** + * Get the parent of a child question. + * @param stdClass $question + * @return array + */ function questionnaire_get_parent ($question) { global $DB; $qid = $question->id; @@ -723,15 +667,15 @@ function questionnaire_get_parent ($question) { break; } // Qdependquestion, parenttype and qdependchoice fields to be used in preview mode. - $parent [$qid]['qdependquestion'] = 'q'.$dependquestion->id; - $parent [$qid]['qdependchoice'] = $qdependchoice; - $parent [$qid]['parenttype'] = $dependquestion->type_id; + $parent[$qid]['qdependquestion'] = 'q'.$dependquestion->id; + $parent[$qid]['qdependchoice'] = $qdependchoice; + $parent[$qid]['parenttype'] = $dependquestion->type_id; // Other fields to be used in Questions edit mode. - $parent [$qid]['position'] = $question->position; - $parent [$qid]['name'] = $question->name; - $parent [$qid]['content'] = $question->content; - $parent [$qid]['parentposition'] = $dependquestion->position; - $parent [$qid]['parent'] = $dependquestion->name.'->'.$dependchoice; + $parent[$qid]['position'] = $question->position; + $parent[$qid]['name'] = $question->name; + $parent[$qid]['content'] = $question->content; + $parent[$qid]['parentposition'] = $dependquestion->position; + $parent[$qid]['parent'] = format_string($dependquestion->name) . '->' . format_string ($dependchoice); } return $parent; } @@ -793,7 +737,11 @@ function questionnaire_get_child_positions ($questions) { return $childpositions; } -// Check that the needed page breaks are present to separate child questions. +/** + * Check that the needed page breaks are present to separate child questions. + * @param stdClass $questionnaire + * @return false|lang_string|string + */ function questionnaire_check_page_breaks($questionnaire) { global $DB; $msg = ''; @@ -801,70 +749,86 @@ function questionnaire_check_page_breaks($questionnaire) { $newpbids = array(); $delpb = 0; $sid = $questionnaire->survey->id; - $questions = $DB->get_records('questionnaire_question', ['surveyid' => $sid, 'deleted' => 'n'], 'id'); $positions = array(); - foreach ($questions as $key => $qu) { - $positions[$qu->position]['question_id'] = $key; - $positions[$qu->position]['type_id'] = $qu->type_id; - $positions[$qu->position]['qname'] = $qu->name; - $positions[$qu->position]['qpos'] = $qu->position; - - $dependencies = $DB->get_records('questionnaire_dependency', ['questionid' => $key , 'surveyid' => $sid], - 'id ASC', 'id, dependquestionid, dependchoiceid, dependlogic'); - $positions[$qu->position]['dependencies'] = $dependencies; + if ($questions = $DB->get_records('questionnaire_question', ['surveyid' => $sid, 'deleted' => 'n'], 'position')) { + foreach ($questions as $key => $qu) { + $newqu = new stdClass(); + $newqu->question_id = $key; + $newqu->type_id = $qu->type_id; + $newqu->qname = $qu->name; + $newqu->qpos = $qu->position; + + $dependencies = $DB->get_records('questionnaire_dependency', ['questionid' => $key, 'surveyid' => $sid], + 'id ASC', 'id, dependquestionid, dependchoiceid, dependlogic'); + $newqu->dependencies = $dependencies ?? []; + $positions[] = (array)$newqu; + } } $count = count($positions); - for ($i = $count; $i > 0; $i--) { + for ($i = $count - 1; $i >= 0; $i--) { $qu = $positions[$i]; $questionnb = $i; + $prevqu = null; + $prevtypeid = null; + if ($i > 0) { + $prevqu = $positions[$i - 1]; + $prevtypeid = $prevqu['type_id']; + } if ($qu['type_id'] == QUESPAGEBREAK) { $questionnb--; // If more than one consecutive page breaks, remove extra one(s). - $prevqu = null; - $prevtypeid = null; - if ($i > 1) { - $prevqu = $positions[$i - 1]; - $prevtypeid = $prevqu['type_id']; - } - // If $i == $count then remove that extra page break in last position. - if ($prevtypeid == QUESPAGEBREAK || $i == $count || $qu['qpos'] == 1) { + // Remove that extra page break in 1st position. + if ($prevtypeid == QUESPAGEBREAK || $i == $count - 1 || $qu['qpos'] == 1) { $qid = $qu['question_id']; $delpb ++; $msg .= get_string("checkbreaksremoved", "questionnaire", $delpb).'
'; // Need to reload questions. - $questions = $DB->get_records('questionnaire_question', ['surveyid' => $sid, 'deleted' => 'n'], 'id'); - $DB->set_field('questionnaire_question', 'deleted', 'y', ['id' => $qid, 'surveyid' => $sid]); - $select = 'surveyid = '.$sid.' AND deleted = \'n\' AND position > '. - $questions[$qid]->position; - if ($records = $DB->get_records_select('questionnaire_question', $select, null, 'position ASC')) { - foreach ($records as $record) { - $DB->set_field('questionnaire_question', 'position', $record->position - 1, ['id' => $record->id]); + if ($questions = $DB->get_records('questionnaire_question', ['surveyid' => $sid, 'deleted' => 'n'], 'id')) { + $DB->set_field('questionnaire_question', 'deleted', 'y', ['id' => $qid, 'surveyid' => $sid]); + $select = 'surveyid = ' . $sid . ' AND deleted = \'n\' AND position > ' . + $questions[$qid]->position; + if ($records = $DB->get_records_select('questionnaire_question', $select, null, 'position ASC')) { + foreach ($records as $record) { + $DB->set_field('questionnaire_question', 'position', $record->position - 1, ['id' => $record->id]); + } } } } } // Add pagebreak between question child and not dependent question that follows. if ($qu['type_id'] != QUESPAGEBREAK) { - $j = $i - 1; - if ($j != 0) { - $prevtypeid = $positions[$j]['type_id']; - $prevdependencies = $positions[$j]['dependencies']; - - $outerdependencies = count($qu['dependencies']) >= count($prevdependencies) ? $qu['dependencies'] : $prevdependencies; - $innerdependencies = count($qu['dependencies']) < count($prevdependencies) ? $qu['dependencies'] : $prevdependencies; - + if ($prevqu) { + $prevdependencies = $prevqu['dependencies']; + $outerdependencies = count($qu['dependencies']) >= count($prevdependencies) ? + $qu['dependencies'] : $prevdependencies; + $innerdependencies = count($qu['dependencies']) < count($prevdependencies) ? + $qu['dependencies'] : $prevdependencies; + + $okeys = []; + $ikeys = []; foreach ($outerdependencies as $okey => $outerdependency) { foreach ($innerdependencies as $ikey => $innerdependency) { if ($outerdependency->dependquestionid === $innerdependency->dependquestionid && - $outerdependency->dependchoiceid === $innerdependency->dependchoiceid && - $outerdependency->dependlogic === $innerdependency->dependlogic) { - unset($outerdependencies[$okey]); - unset($innerdependencies[$ikey]); + $outerdependency->dependchoiceid === $innerdependency->dependchoiceid && + $outerdependency->dependlogic === $innerdependency->dependlogic) { + $okeys[] = $okey; + $ikeys[] = $ikey; } } } + foreach ($okeys as $key) { + if (key_exists($key, $outerdependencies)) { + unset($outerdependencies[$key]); + } + } + foreach ($ikeys as $key) { + if (key_exists($key, $innerdependencies)) { + unset($innerdependencies[$key]); + } + } + $diffdependencies = count($outerdependencies) + count($innerdependencies); if (($prevtypeid != QUESPAGEBREAK && $diffdependencies != 0) @@ -886,9 +850,8 @@ function questionnaire_check_page_breaks($questionnaire) { return (false); } $newpbids[] = $newqid; - $movetopos = $i; - $questionnaire = new questionnaire($questionnaire->id, null, $course, $cm); - $questionnaire->move_question($newqid, $movetopos); + $questionnaire = new questionnaire($course, $cm, $questionnaire->id, null); + $questionnaire->move_question($newqid, $qu['qpos']); } } } @@ -898,7 +861,7 @@ function questionnaire_check_page_breaks($questionnaire) { } else if ($newpbids) { $msg .= get_string('checkbreaksadded', 'questionnaire').' '; $newpbids = array_reverse ($newpbids); - $questionnaire = new questionnaire($questionnaire->id, null, $course, $cm); + $questionnaire = new questionnaire($course, $cm, $questionnaire->id, null); foreach ($newpbids as $newpbid) { $msg .= $questionnaire->questions[$newpbid]->position.' '; } @@ -908,6 +871,10 @@ function questionnaire_check_page_breaks($questionnaire) { /** * Code snippet used to set up the questionform. + * @param stdClass $questionnaire + * @param int $qid + * @param int $qtype + * @return mixed|\mod_questionnaire\question\question */ function questionnaire_prep_for_questionform($questionnaire, $qid, $qtype) { $context = context_module::instance($questionnaire->cm->id); @@ -933,7 +900,7 @@ function questionnaire_prep_for_questionform($questionnaire, $qid, $qtype) { } } } else { - $question = \mod_questionnaire\question\base::question_builder($qtype); + $question = \mod_questionnaire\question\question::question_builder($qtype); $question->sid = $questionnaire->survey->id; $question->id = $questionnaire->cm->id; $question->type_id = $qtype; @@ -957,28 +924,28 @@ function questionnaire_get_standard_page_items($id = null, $a = null) { if ($id) { if (! $cm = get_coursemodule_from_id('questionnaire', $id)) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } if (! $course = $DB->get_record("course", array("id" => $cm->course))) { - print_error('coursemisconf'); + throw new \moodle_exception('coursemisconf', 'mod_questionnaire'); } if (! $questionnaire = $DB->get_record("questionnaire", array("id" => $cm->instance))) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } } else { if (! $questionnaire = $DB->get_record("questionnaire", array("id" => $a))) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } if (! $course = $DB->get_record("course", array("id" => $questionnaire->course))) { - print_error('coursemisconf'); + throw new \moodle_exception('coursemisconf', 'mod_questionnaire'); } if (! $cm = get_coursemodule_from_instance("questionnaire", $questionnaire->id, $course->id)) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } } return (array($cm, $course, $questionnaire)); -} \ No newline at end of file +} diff --git a/mobiledump.txt b/mobiledump.txt new file mode 100644 index 00000000..e8fe347c --- /dev/null +++ b/mobiledump.txt @@ -0,0 +1,54 @@ + + <%#choices%> + + + + + + checked="true"<%/value%> + value="<%id%>" + <%#completed%> disabled="true"<%/completed%>> + + + + + <%/choices%> + + + + + + +=========== error_log =============== + +[Thu Aug 01 10:17:34.671596 2019] [php7:notice] [pid 67268] [client ::1:61305] stdClass Object +( + [action] => nextpage + [appcustomurlscheme] => moodlemobile + [appid] => com.moodle.moodlemobile + [appisdesktop] => 1 + [appismobile] => 0 + [appiswide] => 1 + [applang] => en-us + [appplatform] => mac + [appversioncode] => 3700 + [appversionname] => 3.7.0 + [cmid] => 153 + [completeq] => 1 + [pagenum] => 2 + [rid] => 0 + [userid] => 3 +) + diff --git a/mod_form.php b/mod_form.php index 23fa7e58..26fcd227 100644 --- a/mod_form.php +++ b/mod_form.php @@ -14,27 +14,30 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * print the form to add or edit a questionnaire-instance - * - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package questionnaire - */ - defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/course/moodleform_mod.php'); require_once($CFG->dirroot.'/mod/questionnaire/questionnaire.class.php'); require_once($CFG->dirroot.'/mod/questionnaire/locallib.php'); +/** + * print the form to add or edit a questionnaire-instance + * + * @package mod_questionnaire + * @author Mike Churchward + * @copyright 2016 onward Mike Churchward (mike.churchward@poetgroup.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ class mod_questionnaire_mod_form extends moodleform_mod { + /** + * Form definition. + */ protected function definition() { global $COURSE; global $questionnairetypes, $questionnairerespondents, $questionnaireresponseviewers, $autonumbering; - $questionnaire = new questionnaire($this->_instance, null, $COURSE, $this->_cm); + $questionnaire = new questionnaire($COURSE, $this->_cm, $this->_instance, null); $mform =& $this->_form; @@ -46,21 +49,9 @@ protected function definition() { $this->standard_intro_elements(get_string('description')); - $mform->addElement('header', 'timinghdr', get_string('timing', 'form')); - - $enableopengroup = array(); - $enableopengroup[] =& $mform->createElement('checkbox', 'useopendate', get_string('opendate', 'questionnaire')); - $enableopengroup[] =& $mform->createElement('date_time_selector', 'opendate', ''); - $mform->addGroup($enableopengroup, 'enableopengroup', get_string('opendate', 'questionnaire'), ' ', false); - $mform->addHelpButton('enableopengroup', 'opendate', 'questionnaire'); - $mform->disabledIf('enableopengroup', 'useopendate', 'notchecked'); - - $enableclosegroup = array(); - $enableclosegroup[] =& $mform->createElement('checkbox', 'useclosedate', get_string('closedate', 'questionnaire')); - $enableclosegroup[] =& $mform->createElement('date_time_selector', 'closedate', ''); - $mform->addGroup($enableclosegroup, 'enableclosegroup', get_string('closedate', 'questionnaire'), ' ', false); - $mform->addHelpButton('enableclosegroup', 'closedate', 'questionnaire'); - $mform->disabledIf('enableclosegroup', 'useclosedate', 'notchecked'); + $mform->addElement('header', 'availabilityhdr', get_string('availability')); + $mform->addElement('date_time_selector', 'opendate', get_string('opendate', 'questionnaire'), ['optional' => true]); + $mform->addElement('date_time_selector', 'closedate', get_string('closedate', 'questionnaire'), ['optional' => true]); $mform->addElement('header', 'questionnairehdr', get_string('responseoptions', 'questionnaire')); @@ -94,6 +85,8 @@ protected function definition() { // Default = autonumber both questions and pages. $mform->setDefault('autonum', 3); + $mform->addElement('advcheckbox', 'progressbar', get_string('progressbar', 'questionnaire')); + // Removed potential scales from list of grades. CONTRIB-3167. $grades[0] = get_string('nograde'); for ($i = 100; $i >= 1; $i--) { @@ -155,6 +148,10 @@ protected function definition() { $this->add_action_buttons(); } + /** + * Pre-process form data. + * @param array $defaultvalues + */ public function data_preprocessing(&$defaultvalues) { global $DB; if (empty($defaultvalues['opendate'])) { @@ -179,19 +176,51 @@ public function data_preprocessing(&$defaultvalues) { } } + /** + * Enforce validation rules here + * @param array $data array of ("fieldname"=>value) of submitted data + * @param array $files array of uploaded files "element_name"=>tmp_file_path + * @return array + **/ public function validation($data, $files) { $errors = parent::validation($data, $files); + + // Check open and close times are consistent. + if ($data['opendate'] && $data['closedate'] && + $data['closedate'] < $data['opendate']) { + $errors['closedate'] = get_string('closebeforeopen', 'questionnaire'); + } + return $errors; } + /** + * Add any completion rules for the form. + * @return string[] + */ public function add_completion_rules() { + global $CFG; + + // Changes for Moodle 4.3 - MDL-78516. + if ($CFG->branch < 403) { + $suffix = ''; + } else { + $suffix = $this->get_suffix(); + } + $mform =& $this->_form; - $mform->addElement('checkbox', 'completionsubmit', '', get_string('completionsubmit', 'questionnaire')); - return array('completionsubmit'); + $mform->addElement('checkbox', 'completionsubmit' . $suffix, '', + get_string('completionsubmit', 'questionnaire')); + return ['completionsubmit' . $suffix]; } + /** + * True if the completion rule is enabled. + * @param array $data + * @return bool + */ public function completion_rule_enabled($data) { return !empty($data['completionsubmit']); } -} \ No newline at end of file +} diff --git a/module.js b/module.js index cd1647db..030dec53 100644 --- a/module.js +++ b/module.js @@ -27,13 +27,21 @@ * http://stackoverflow.com/questions/6787383/what-is-the-solution-to-remove-add-a-class-in-pure-javascript. * */ -function addClass(el, aclass){ +/** + * @param {HTMLElement} el + * @param {string} aclass + */ +function addClass(el, aclass) { el.className += ' ' + aclass; } -function removeClass(el, aclass){ +/** + * @param {HTMLElement} el + * @param {string} aclass + */ +function removeClass(el, aclass) { var elClass = ' ' + el.className + ' '; - while(elClass.indexOf(' ' + aclass + ' ') != - 1) { + while (elClass.indexOf(' ' + aclass + ' ') != -1) { elClass = elClass.replace(' ' + aclass + ' ', ''); } el.className = elClass; @@ -44,7 +52,6 @@ function removeClass(el, aclass){ * Javascript for hiding/displaying children questions on preview page of * questionnaire with conditional branching. */ - function depend(children, choices) { children = children.split(','); choices = choices.split(','); @@ -82,7 +89,7 @@ function depend(children, choices) { droplist.disabled = false; } delete children[i]; - } else if (children[i]){ + } else if (children[i]) { if (typeof document !== "undefined" && ("classList" in document.createElement("a"))) { q.classList.remove('qn-container'); q.classList.add('hidedependquestion'); @@ -114,6 +121,10 @@ function depend(children, choices) { /* exported dependdrop */ +/** + * @param {string} qId + * @param {*} children + */ function dependdrop(qId, children) { var e = document.getElementById(qId); var choice = e.options[e.selectedIndex].value; @@ -124,11 +135,18 @@ function dependdrop(qId, children) { // When respondent enters text in !other field, corresponding // radio button OR check box is automatically checked. /* exported other_check */ +/** + * @param {string | void} name + */ function other_check(name) { var other = name.split("_"); + var other = name.slice(name.indexOf("o") + 1); + if (other.indexOf("]") != -1) { + other = other.slice(0, other.indexOf("]")); + } var f = document.getElementById("phpesp_response"); for (var i = 0; i <= f.elements.length; i++) { - if (f.elements[i].value == "other_" + other[1]) { + if (f.elements[i].value == other) { f.elements[i].checked = true; break; } @@ -137,6 +155,9 @@ function other_check(name) { // Automatically empty an !other text input field if another Radio button is clicked. /* exported other_check_empty */ +/** + * @param {string} name + */ function other_check_empty(name, value) { var f = document.getElementById("phpesp_response"); var i; @@ -145,7 +166,7 @@ function other_check_empty(name, value) { f.elements[i].checked = true; var otherid = f.elements[i].name + "_" + f.elements[i].value.substring(6); var other = document.getElementsByName(otherid); - if (value.substr(0,6) != "other_") { + if (value.substr(0, 6) != "other_") { other[0].value = ""; } else { other[0].focus(); @@ -165,6 +186,10 @@ function other_check_empty(name, value) { // In a Rate question type of sub-type Order : automatically uncheck a Radio button // when another radio button in the same column is clicked. /* exported other_rate_uncheck */ +/** + * @param {string} name + * @param {string} value + */ function other_rate_uncheck(name, value) { var col_name = name.substr(0, name.indexOf("_")); var inputbuttons = document.getElementsByTagName("input"); @@ -179,6 +204,9 @@ function other_rate_uncheck(name, value) { // Empty an !other text input when corresponding Check Box is clicked (supposedly to empty it). /* exported checkbox_empty */ +/** + * @param {string} name + */ function checkbox_empty(name) { var actualbuttons = document.getElementsByName(name); for (var i = 0; i <= actualbuttons.length; i++) { @@ -200,24 +228,26 @@ M.mod_questionnaire = M.mod_questionnaire || {}; /* exported Y */ /* exported e */ -M.mod_questionnaire.init_attempt_form = function(Y) { - M.core_formchangechecker.init({formid: 'phpesp_response'}); +M.mod_questionnaire.init_attempt_form = function() { + require(['core_form/changechecker'], function(FormChangeChecker) { + FormChangeChecker.watchFormById('phpesp_response'); + }); }; M.mod_questionnaire.init_sendmessage = function(Y) { - Y.on('click', function(e) { + Y.on('click', function() { Y.all('input.usercheckbox').each(function() { this.set('checked', 'checked'); }); }, '#checkall'); - Y.on('click', function(e) { + Y.on('click', function() { Y.all('input.usercheckbox').each(function() { this.set('checked', ''); }); }, '#checknone'); - Y.on('click', function(e) { + Y.on('click', function() { Y.all('input.usercheckbox').each(function() { if (this.get('alt') == 0) { this.set('checked', 'checked'); @@ -227,7 +257,7 @@ M.mod_questionnaire.init_sendmessage = function(Y) { }); }, '#checknotstarted'); - Y.on('click', function(e) { + Y.on('click', function() { Y.all('input.usercheckbox').each(function() { if (this.get('alt') == 1) { this.set('checked', 'checked'); @@ -237,4 +267,123 @@ M.mod_questionnaire.init_sendmessage = function(Y) { }); }, '#checkstarted'); -}; \ No newline at end of file +}; +M.mod_questionnaire.init_slider = function() { + const allRanges = document.querySelectorAll(".question-slider"); + allRanges.forEach(wrap => { + const range = wrap.querySelector("input.questionnaire-slider"); + const bubble = wrap.querySelector(".bubble"); + const labels = { + leftlabel: wrap.querySelector(".left-side-label"), + middlelabel: wrap.querySelector(".middle-side-label"), + rightlabel: wrap.querySelector(".right-side-label") + }; + + range.addEventListener("input", () => { + setBubble(range, bubble); + createAccessibilityHeading(range, bubble, labels); + }); + setBubble(range, bubble); + createAccessibilityHeading(range, bubble, labels); + }); + + function setBubble(range, bubble) { + const val = range.value; + const min = range.min ? range.min : 0; + const max = range.max ? range.max : 100; + var newVal = Number(((val - min) * 100) / (max - min)); + var positiveVal = ''; + if (range.min && range.min < 0) { + if (range.max && range.max > 0) { + if (val > 0) { + positiveVal = '+'; + } + } + } + bubble.innerHTML = positiveVal + val; + + // Sorta magic numbers based on size of the native UI thumb + bubble.style.left = `calc(${newVal}% + (${8 - newVal * 0.15}px))`; + } + + /** + * Adds accessibility support by generating an h2 element with relevant text. + * @param {HTMLElement} range - The range input element. + * @param {HTMLElement} bubble - The bubble element. + * @param {Object} labels - The object containing label elements. + */ + function createAccessibilityHeading(range, bubble, labels) { + const min = range.min ? range.min : 0; + const max = range.max ? range.max : 100; + const step = range.step ? range.step : 1; + const centerValues = calculateCenterValues(range); + + const accesshideElement = document.createElement('h2'); + accesshideElement.classList.add('accesshide'); + + const a = { + min, + max, + leftlabel: labels.leftlabel.innerHTML, + rightlabel: labels.rightlabel.innerHTML, + middlelabel: labels.middlelabel.innerHTML, + centreval: centerValues[0], + centreval1: centerValues[0], + centreval2: centerValues[1], + }; + + const rangeNum = max - min; + const numSteps = rangeNum / step; + + const middleLabel = (numSteps % 2 !== 0) + ? (labels.middlelabel.innerHTML + ? M.util.get_string('middlepartwithtwovalues', 'questionnaire', a) + : M.util.get_string('middlepartwithtwovaluesdefault', 'questionnaire', a)) + : (labels.middlelabel.innerHTML + ? M.util.get_string('middlepart', 'questionnaire', a) + : M.util.get_string('middlepartdefault', 'questionnaire', a)); + + const leftPart = labels.leftlabel.innerHTML ? M.util.get_string('leftpart', 'questionnaire', a) + : M.util.get_string('leftpartdefault', 'questionnaire', a); + + const middlePart = middleLabel ? middleLabel : ''; + + const rightPart = labels.rightlabel.innerHTML + ? M.util.get_string('rightpart', 'questionnaire', a) : M.util.get_string('rightpartdefault', 'questionnaire', a); + + // Include a whitespace to accommodate a setting of the NVDA screen reader. + const whitespace = '\xa0'; + accesshideElement.textContent = whitespace + M.util.get_string('where', 'questionnaire', a) + leftPart + middlePart + rightPart; + bubble.appendChild(accesshideElement); + } + + /** + * Calculates the center value(s) based on the given range. + * @param {Object} range - The range object containing properties: min, max, step. + * @returns {Array} - An array containing the center value(s). + */ + function calculateCenterValues(range) { + const min = parseInt(range.min); + const max = parseInt(range.max); + const step = parseInt(range.step); + const rangeNum = max - min; + + // Calculate the number of steps. + const numSteps = rangeNum / step; + + // Calculate the center value(s). + const centerValues = []; + if (numSteps % 2 === 0) { + // Even number of steps, return single center value. + const center = (min + max) / 2; + centerValues.push(center); + } else { + // Odd number of steps, calculate lower and upper center values. + const lowerCenter = min + Math.floor(numSteps / 2) * step; + const upperCenter = lowerCenter + step; + centerValues.push(lowerCenter, upperCenter); + } + + return centerValues; + } +}; diff --git a/myreport.php b/myreport.php index 9b269cf0..ea8dea75 100644 --- a/myreport.php +++ b/myreport.php @@ -14,8 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -// This page shows results of a questionnaire to a student. - +/** + * This page shows results of a questionnaire to a student. + * + * @package mod_questionnaire + * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ require_once("../../config.php"); require_once($CFG->dirroot.'/mod/questionnaire/questionnaire.class.php'); @@ -27,13 +34,13 @@ $currentgroupid = optional_param('group', 0, PARAM_INT); // Groupid. if (! $questionnaire = $DB->get_record("questionnaire", array("id" => $instance))) { - print_error('incorrectquestionnaire', 'questionnaire'); + throw new \moodle_exception('incorrectquestionnaire', 'mod_questionnaire'); } if (! $course = $DB->get_record("course", array("id" => $questionnaire->course))) { - print_error('coursemisconf'); + throw new \moodle_exception('coursemisconf', 'mod_questionnaire'); } if (! $cm = get_coursemodule_from_instance("questionnaire", $questionnaire->id, $course->id)) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } require_course_login($course, true, $cm); @@ -42,7 +49,7 @@ // Should never happen, unless called directly by a snoop... if ( !has_capability('mod/questionnaire:readownresponses', $context) || $userid != $USER->id) { - print_error('Permission denied'); + throw new \moodle_exception('nopermissions', 'mod_questionnaire'); } $url = new moodle_url($CFG->wwwroot.'/mod/questionnaire/myreport.php', array('instance' => $instance)); if (isset($userid)) { @@ -65,7 +72,7 @@ $PAGE->set_title(get_string('questionnairereport', 'questionnaire')); $PAGE->set_heading(format_string($course->fullname)); -$questionnaire = new questionnaire(0, $questionnaire, $course, $cm); +$questionnaire = new questionnaire($course, $cm, 0, $questionnaire); // Add renderer and page objects to the questionnaire object for display use. $questionnaire->add_renderer($PAGE->get_renderer('mod_questionnaire')); $questionnaire->add_page(new \mod_questionnaire\output\reportpage()); @@ -82,7 +89,7 @@ switch ($action) { case 'summary': if (empty($questionnaire->survey)) { - print_error('surveynotexists', 'questionnaire'); + throw new \moodle_exception('surveynotexists', 'mod_questionnaire'); } $SESSION->questionnaire->current_tab = 'mysummary'; $resps = $questionnaire->get_responses($userid); @@ -100,7 +107,7 @@ include('tabs.php'); $questionnaire->page->add_to_page('myheaders', $titletext); - $questionnaire->survey_results(1, 1, '', '', $rids, $USER->id); + $questionnaire->survey_results($rids, $USER->id); echo $questionnaire->renderer->render($questionnaire->page); @@ -110,10 +117,10 @@ case 'vall': if (empty($questionnaire->survey)) { - print_error('surveynotexists', 'questionnaire'); + throw new \moodle_exception('surveynotexists', 'mod_questionnaire'); } $SESSION->questionnaire->current_tab = 'myvall'; - $resps = $questionnaire->get_responses($userid); + $questionnaire->add_user_responses($userid); $titletext = get_string('myresponses', 'questionnaire'); // Print the page header. @@ -123,7 +130,7 @@ include('tabs.php'); $questionnaire->page->add_to_page('myheaders', $titletext); - $questionnaire->view_all_responses($resps); + $questionnaire->view_all_responses(); echo $questionnaire->renderer->render($questionnaire->page); // Finish the page. echo $questionnaire->renderer->footer($course); @@ -131,7 +138,7 @@ case 'vresp': if (empty($questionnaire->survey)) { - print_error('surveynotexists', 'questionnaire'); + throw new \moodle_exception('surveynotexists', 'mod_questionnaire'); } $SESSION->questionnaire->current_tab = 'mybyresponse'; $usergraph = get_config('questionnaire', 'usergraph'); @@ -261,7 +268,7 @@ $resps = $respsallparticipants; } $compare = true; - $questionnaire->view_response($rid, null, null, $resps, $compare, $iscurrentgroupmember, false, $currentgroupid); + $questionnaire->view_response($rid, null, $resps, $compare, $iscurrentgroupmember, false, $currentgroupid); // Finish the page. echo $questionnaire->renderer->render($questionnaire->page); echo $questionnaire->renderer->footer($course); diff --git a/pix/b/pdfdown.svg b/pix/b/pdfdown.svg new file mode 100644 index 00000000..3313da6c --- /dev/null +++ b/pix/b/pdfdown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pix/monologo.png b/pix/monologo.png new file mode 100644 index 00000000..15e1f3e8 Binary files /dev/null and b/pix/monologo.png differ diff --git a/pix/monologo.svg b/pix/monologo.svg new file mode 100644 index 00000000..2f736ed5 --- /dev/null +++ b/pix/monologo.svg @@ -0,0 +1 @@ + diff --git a/preview.php b/preview.php index 7df8eab9..49245e3e 100644 --- a/preview.php +++ b/preview.php @@ -14,35 +14,42 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -// This page displays a non-completable instance of questionnaire. +/** + * This page displays a non-completable instance of questionnaire. + * + * @package mod_questionnaire + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 onward Mike Churchward (mike.churchward@poetgroup.org) + * @author Mike Churchward + */ require_once("../../config.php"); require_once($CFG->dirroot.'/mod/questionnaire/questionnaire.class.php'); -$id = optional_param('id', 0, PARAM_INT); -$sid = optional_param('sid', 0, PARAM_INT); -$popup = optional_param('popup', 0, PARAM_INT); -$qid = optional_param('qid', 0, PARAM_INT); +$id = optional_param('id', 0, PARAM_INT); +$sid = optional_param('sid', 0, PARAM_INT); +$popup = optional_param('popup', 0, PARAM_INT); +$qid = optional_param('qid', 0, PARAM_INT); $currentgroupid = optional_param('group', 0, PARAM_INT); // Groupid. if ($id) { if (! $cm = get_coursemodule_from_id('questionnaire', $id)) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } if (! $course = $DB->get_record("course", array("id" => $cm->course))) { - print_error('coursemisconf'); + throw new \moodle_exception('coursemisconf', 'mod_questionnaire'); } if (! $questionnaire = $DB->get_record("questionnaire", array("id" => $cm->instance))) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } } else { if (! $survey = $DB->get_record("questionnaire_survey", array("id" => $sid))) { - print_error('surveynotexists', 'questionnaire'); + throw new \moodle_exception('surveynotexists', 'mod_questionnaire'); } if (! $course = $DB->get_record("course", ["id" => $survey->courseid])) { - print_error('coursemisconf'); + throw new \moodle_exception('coursemisconf', 'mod_questionnaire'); } // Dummy questionnaire object. $questionnaire = new stdClass(); @@ -79,7 +86,7 @@ $PAGE->set_context($context); $PAGE->set_cm($cm); // CONTRIB-5872 - I don't know why this is needed. -$questionnaire = new questionnaire($qid, $questionnaire, $course, $cm); +$questionnaire = new questionnaire($course, $cm, $qid, $questionnaire); // Add renderer and page objects to the questionnaire object for display use. $questionnaire->add_renderer($PAGE->get_renderer('mod_questionnaire')); @@ -90,7 +97,7 @@ (isset($questionnaire->capabilities) && $questionnaire->capabilities->preview); if (!$canpreview && !$popup) { // Should never happen, unless called directly by a snoop... - print_error('nopermissions', 'questionnaire', $CFG->wwwroot.'/mod/questionnaire/view.php?id='.$cm->id); + throw new \moodle_exception('nopermissions', 'mod_questionnaire'); } if (!isset($SESSION->questionnaire)) { @@ -141,7 +148,7 @@ $questionnaire->renderer->action_link($link, $linkname, $action, array('class' => $class, 'title' => $title), new pix_icon('t/print', $title))); } -$questionnaire->survey_print_render('', 'preview', $course->id, $rid = 0, $popup); +$questionnaire->survey_print_render($course->id, '', 'preview', $rid = 0, $popup); if ($popup) { $questionnaire->page->add_to_page('closebutton', $questionnaire->renderer->close_window_button()); } diff --git a/print.php b/print.php index d903aba3..c5b49a49 100644 --- a/print.php +++ b/print.php @@ -14,6 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * The main page to print a questionnaire. + * + * @package mod_questionnaire + * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ require_once("../../config.php"); require_once($CFG->dirroot.'/mod/questionnaire/questionnaire.class.php'); @@ -25,19 +34,19 @@ $referer = $CFG->wwwroot.'/mod/questionnaire/report.php'; if (! $questionnaire = $DB->get_record("questionnaire", array("id" => $qid))) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } if (! $course = $DB->get_record("course", array("id" => $questionnaire->course))) { - print_error('coursemisconf'); + throw new \moodle_exception('coursemisconf', 'mod_questionnaire'); } if (! $cm = get_coursemodule_from_instance("questionnaire", $questionnaire->id, $course->id)) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } // Check login and get context. require_login($courseid); -$questionnaire = new questionnaire(0, $questionnaire, $course, $cm); +$questionnaire = new questionnaire($course, $cm, 0, $questionnaire); // Add renderer and page objects to the questionnaire object for display use. $questionnaire->add_renderer($PAGE->get_renderer('mod_questionnaire')); @@ -50,7 +59,7 @@ // If you can't view the questionnaire, or can't view a specified response, error out. if (!($questionnaire->capabilities->view && (($rid == 0) || $questionnaire->can_view_response($rid)))) { // Should never happen, unless called directly by a snoop... - print_error('nopermissions', 'moodle', $CFG->wwwroot.'/mod/questionnaire/view.php?id='.$cm->id); + throw new \moodle_exception('nopermissions', 'mod_questionnaire'); } $blankquestionnaire = true; if ($rid != 0) { @@ -66,6 +75,6 @@ $PAGE->set_pagelayout('popup'); echo $questionnaire->renderer->header(); $questionnaire->page->add_to_page('closebutton', $questionnaire->renderer->close_window_button()); -$questionnaire->survey_print_render('', 'print', $courseid, $rid, $blankquestionnaire); +$questionnaire->survey_print_render($courseid, '', 'print', $rid, $blankquestionnaire); echo $questionnaire->renderer->render($questionnaire->page); echo $questionnaire->renderer->footer(); diff --git a/qsettings.php b/qsettings.php index a681539c..16179ab0 100644 --- a/qsettings.php +++ b/qsettings.php @@ -14,7 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -// This page prints a particular instance of questionnaire. +/** + * This page handles the question settings. + * + * @package mod_questionnaire + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 Mike Churchward (mike.churchward@poetopensource.org) + */ require_once("../../config.php"); require_once($CFG->dirroot.'/mod/questionnaire/questionnaire.class.php'); @@ -25,15 +31,15 @@ $submitbutton2 = optional_param('submitbutton2', '', PARAM_ALPHA); if (! $cm = get_coursemodule_from_id('questionnaire', $id)) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } if (! $course = $DB->get_record("course", array("id" => $cm->course))) { - print_error('coursemisconf'); + throw new \moodle_exception('coursemisconf', 'mod_questionnaire'); } if (! $questionnaire = $DB->get_record("questionnaire", array("id" => $cm->instance))) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } // Needed here for forced language courses. @@ -46,7 +52,7 @@ if (!isset($SESSION->questionnaire)) { $SESSION->questionnaire = new stdClass(); } -$questionnaire = new questionnaire(0, $questionnaire, $course, $cm); +$questionnaire = new questionnaire($course, $cm, 0, $questionnaire); // Add renderer and page objects to the questionnaire object for display use. $questionnaire->add_renderer($PAGE->get_renderer('mod_questionnaire')); @@ -55,7 +61,7 @@ $SESSION->questionnaire->current_tab = 'settings'; if (!$questionnaire->capabilities->manage) { - print_error('nopermissions', 'error', 'mod:questionnaire:manage'); + throw new \moodle_exception('nopermissions', 'mod_questionnaire'); } $settingsform = new \mod_questionnaire\settings_form('qsettings.php'); @@ -89,9 +95,9 @@ $sdata->infoitemid = $settings->info['itemid']; $sdata->infoformat = $settings->info['format']; - $sdata->info = $settings->info['text']; - $sdata->info = file_save_draft_area_files($sdata->infoitemid, $context->id, 'mod_questionnaire', 'info', - $sdata->id, array('subdirs' => true), $sdata->info); + $sdata->info = $settings->info['text']; + $sdata->info = file_save_draft_area_files($sdata->infoitemid, $context->id, 'mod_questionnaire', 'info', + $sdata->id, array('subdirs' => true), $sdata->info); $sdata->theme = ''; // Deprecated theme field. $sdata->thanks_page = $settings->thanks_page; @@ -99,14 +105,14 @@ $sdata->thankitemid = $settings->thank_body['itemid']; $sdata->thankformat = $settings->thank_body['format']; - $sdata->thank_body = $settings->thank_body['text']; - $sdata->thank_body = file_save_draft_area_files($sdata->thankitemid, $context->id, 'mod_questionnaire', 'thankbody', - $sdata->id, array('subdirs' => true), $sdata->thank_body); + $sdata->thank_body = $settings->thank_body['text']; + $sdata->thank_body = file_save_draft_area_files($sdata->thankitemid, $context->id, 'mod_questionnaire', 'thankbody', + $sdata->id, array('subdirs' => true), $sdata->thank_body); $sdata->email = $settings->email; $sdata->courseid = $settings->courseid; if (!($sid = $questionnaire->survey_update($sdata))) { - print_error('couldnotcreatenewsurvey', 'questionnaire'); + throw new \moodle_exception('couldnotcreatenewsurvey', 'mod_questionnaire'); } else { if ($submitbutton2) { $redirecturl = course_get_url($cm->course); diff --git a/questionnaire.class.php b/questionnaire.class.php index 269ab6bf..004d4609 100644 --- a/questionnaire.class.php +++ b/questionnaire.class.php @@ -14,23 +14,27 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +use mod_questionnaire\feedback\section; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/mod/questionnaire/locallib.php'); + +#[\AllowDynamicProperties] /** + * Provided the main API functions for questionnaire. + * * @package mod_questionnaire * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) * @author Mike Churchward * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -defined('MOODLE_INTERNAL') || die(); - -require_once($CFG->dirroot.'/mod/questionnaire/locallib.php'); - class questionnaire { // Class Properties. /** - * @var \mod_questionnaire\question\base[] $quesitons + * @var \mod_questionnaire\question\question[] $quesitons */ public $questions = []; @@ -52,11 +56,16 @@ class questionnaire { // Class Methods. - /* - * The class constructor - * + /** + * The constructor. + * @param stdClass $course + * @param stdClass $cm + * @param int $id + * @param null|stdClass $questionnaire + * @param bool $addquestions + * @throws dml_exception */ - public function __construct($id = 0, $questionnaire = null, &$course, &$cm, $addquestions = true) { + public function __construct(&$course, &$cm, $id = 0, $questionnaire = null, $addquestions = true) { global $DB; if ($id) { @@ -91,11 +100,15 @@ public function __construct($id = 0, $questionnaire = null, &$course, &$cm, $add if (!empty($this->cm->id)) { $this->capabilities = questionnaire_load_capabilities($this->cm->id); } + + // Don't automatically add responses. + $this->responses = []; } /** * Adding a survey record to the object. - * + * @param int $sid + * @param null $survey */ public function add_survey($sid = 0, $survey = null) { global $DB; @@ -109,6 +122,7 @@ public function add_survey($sid = 0, $survey = null) { /** * Adding questions to the object. + * @param bool $sid */ public function add_questions($sid = false) { global $DB; @@ -129,11 +143,11 @@ public function add_questions($sid = false) { $isbreak = false; foreach ($records as $record) { - $this->questions[$record->id] = \mod_questionnaire\question\base::question_builder($record->type_id, + $this->questions[$record->id] = \mod_questionnaire\question\question::question_builder($record->type_id, $record, $this->context); if ($record->type_id != QUESPAGEBREAK) { - $this->questionsbysec[$sec][$record->id] = &$this->questions[$record->id]; + $this->questionsbysec[$sec][] = $record->id; $isbreak = false; } else { // Sanity check: no section break allowed as first position, no 2 consecutive section breaks. @@ -146,112 +160,141 @@ public function add_questions($sid = false) { } } + /** + * Load all response information for this user. + * + * @param int $userid + */ + public function add_user_responses($userid = null) { + global $USER, $DB; + + // Empty questionnaires cannot have responses. + if (empty($this->id)) { + return; + } + + if ($userid === null) { + $userid = $USER->id; + } + + $responses = $this->get_responses($userid); + foreach ($responses as $response) { + $this->responses[$response->id] = mod_questionnaire\responsetype\response\response::create_from_data($response); + } + } + + /** + * Load the specified response information. + * + * @param int $responseid + */ + public function add_response(int $responseid) { + global $DB; + + // Empty questionnaires cannot have responses. + if (empty($this->id)) { + return; + } + + $response = $DB->get_record('questionnaire_response', ['id' => $responseid]); + $this->responses[$response->id] = mod_questionnaire\responsetype\response\response::create_from_data($response); + } + + /** + * Load the response information from a submitted web form. + * + * @param stdClass $formdata + */ + public function add_response_from_formdata(stdClass $formdata) { + $this->responses[0] = mod_questionnaire\responsetype\response\response::response_from_webform($formdata, $this->questions); + } + + /** + * Return a response object from a submitted mobile app form. + * + * @param stdClass $appdata + * @param int $sec + * @return bool|\mod_questionnaire\responsetype\response\response + */ + public function build_response_from_appdata(stdClass $appdata, $sec=0) { + $questions = []; + if ($sec == 0) { + $questions = $this->questions; + } else { + foreach ($this->questionsbysec[$sec] as $questionid) { + $questions[$questionid] = $this->questions[$questionid]; + } + } + return mod_questionnaire\responsetype\response\response::response_from_appdata($this->id, 0, $appdata, $questions); + } + /** * Add the renderer to the questionnaire object. - * @param \plugin_renderer_base $renderer The module renderer, extended from core renderer. + * @param plugin_renderer_base $renderer The module renderer, extended from core renderer. */ - public function add_renderer($renderer) { + public function add_renderer(plugin_renderer_base $renderer) { $this->renderer = $renderer; } /** * Add the templatable page to the questionnaire object. - * @param \renderable, \templatable $page The page to rendere, implementing core classes. + * @param templatable $page The page to render, implementing core classes. */ public function add_page($page) { $this->page = $page; } + /** + * Return true if questions should be automatically numbered. + * @return bool + */ + public function questions_autonumbered() { + // Value of 1 if questions should be numbered. Value of 3 if both questions and pages should be numbered. + return (!empty($this->autonum) && (($this->autonum == 1) || ($this->autonum == 3))); + } + + /** + * Return true if pages should be automatically numbered. + * @return bool + */ + public function pages_autonumbered() { + // Value of 2 if pages should be numbered. Value of 3 if both questions and pages should be numbered. + return (!empty($this->autonum) && (($this->autonum == 2) || ($this->autonum == 3))); + } + + /** + * The main module view function. + */ public function view() { global $CFG, $USER, $PAGE; $PAGE->set_title(format_string($this->name)); $PAGE->set_heading(format_string($this->course->fullname)); - - // Initialise the JavaScript. - $PAGE->requires->js_init_call('M.mod_questionnaire.init_attempt_form', null, false, questionnaire_get_js_module()); - - $questionnaire = $this; - - if (!$this->capabilities->view) { - $this->page->add_to_page('notifications', - $this->renderer->notification(get_string('noteligible', 'questionnaire', $this->name), - \core\output\notification::NOTIFY_ERROR)); - } else if (!$this->is_active()) { - $this->page->add_to_page('notifications', - $this->renderer->notification(get_string('notavail', 'questionnaire'), \core\output\notification::NOTIFY_ERROR)); - } else if (!$this->is_open()) { - $this->page->add_to_page('notifications', - $this->renderer->notification(get_string('notopen', 'questionnaire', userdate($this->opendate)), - \core\output\notification::NOTIFY_ERROR)); - } else if ($this->is_closed()) { - $this->page->add_to_page('notifications', - $this->renderer->notification(get_string('closed', 'questionnaire', userdate($this->closedate)), - \core\output\notification::NOTIFY_ERROR)); - } else if (!$this->user_is_eligible($USER->id)) { - $this->page->add_to_page('notifications', - $this->renderer->notification(get_string('noteligible', 'questionnaire'), \core\output\notification::NOTIFY_ERROR)); - } else if ($this->survey->realm == 'template') { - $this->page->add_to_page('notifications', - $this->renderer->notification(get_string('templatenotviewable', 'questionnaire'), - \core\output\notification::NOTIFY_ERROR)); - } else if (!$this->user_can_take($USER->id)) { - switch ($this->qtype) { - case QUESTIONNAIREDAILY: - $msgstring = ' '.get_string('today', 'questionnaire'); - break; - case QUESTIONNAIREWEEKLY: - $msgstring = ' '.get_string('thisweek', 'questionnaire'); - break; - case QUESTIONNAIREMONTHLY: - $msgstring = ' '.get_string('thismonth', 'questionnaire'); - break; - default: - $msgstring = ''; - break; - } - $this->page->add_to_page('notifications', - $this->renderer->notification(get_string('alreadyfilled', 'questionnaire', $msgstring), - \core\output\notification::NOTIFY_ERROR)); + $message = $this->user_access_messages($USER->id, true); + if ($message !== false) { + $this->page->add_to_page('notifications', $message); } else { // Handle the main questionnaire completion page. $quser = $USER->id; - $msg = $this->print_survey($USER->id, $quser); + $msg = $this->print_survey($quser, $USER->id); // If Questionnaire was submitted with all required fields completed ($msg is empty), // then record the submittal. $viewform = data_submitted($CFG->wwwroot."/mod/questionnaire/complete.php"); - if (!empty($viewform->rid)) { - $viewform->rid = (int)$viewform->rid; - } - if (!empty($viewform->sec)) { - $viewform->sec = (int)$viewform->sec; - } - if (data_submitted() && confirm_sesskey() && isset($viewform->submit) && isset($viewform->submittype) && + if ($viewform && confirm_sesskey() && isset($viewform->submit) && isset($viewform->submittype) && ($viewform->submittype == "Submit Survey") && empty($msg)) { + if (!empty($viewform->rid)) { + $viewform->rid = (int)$viewform->rid; + } + if (!empty($viewform->sec)) { + $viewform->sec = (int)$viewform->sec; + } $this->response_delete($viewform->rid, $viewform->sec); - $this->rid = $this->response_insert($viewform->sec, $viewform->rid, $quser); + $this->rid = $this->response_insert($viewform, $quser); $this->response_commit($this->rid); - // If it was a previous save, rid is in the form... - if (!empty($viewform->rid) && is_numeric($viewform->rid)) { - $rid = $viewform->rid; - - // Otherwise its in this object. - } else { - $rid = $this->rid; - } - - if ($this->grade != 0) { - $questionnaire = new stdClass(); - $questionnaire->id = $this->id; - $questionnaire->name = $this->name; - $questionnaire->grade = $this->grade; - $questionnaire->cmidnumber = $this->cm->idnumber; - $questionnaire->courseid = $this->course->id; - questionnaire_update_grades($questionnaire, $quser); - } + $this->update_grades($quser); // Update completion state. $completion = new completion_info($this->course); @@ -267,7 +310,7 @@ public function view() { 'courseid' => $this->course->id, 'relateduserid' => $USER->id, 'anonymous' => $anonymous, - 'other' => array('questionnaireid' => $questionnaire->id) + 'other' => array('questionnaireid' => $this->id) ); $event = \mod_questionnaire\event\attempt_submitted::create($params); $event->trigger(); @@ -278,12 +321,24 @@ public function view() { } } + /** + * Delete the specified response, and insert a new one. + * @param int $rid + * @param int $sec + * @param int $quser + * @return bool|int + */ public function delete_insert_response($rid, $sec, $quser) { $this->response_delete($rid, $sec); - $this->rid = $this->response_insert($sec, $rid, $quser); + $this->rid = $this->response_insert((object)['sec' => $sec, 'rid' => $rid], $quser); return $this->rid; } + /** + * Commit the response. + * @param int $rid + * @param int $quser + */ public function commit_submission_response($rid, $quser) { $this->response_commit($rid); // If it was a previous save, rid is in the form... @@ -293,15 +348,9 @@ public function commit_submission_response($rid, $quser) { } else { $rid = $this->rid; } - if ($this->grade != 0) { - $questionnaire = new \stdClass(); - $questionnaire->id = $this->id; - $questionnaire->name = $this->name; - $questionnaire->grade = $this->grade; - $questionnaire->cmidnumber = $this->cm->idnumber; - $questionnaire->courseid = $this->course->id; - questionnaire_update_grades($questionnaire, $quser); - } + + $this->update_grades($quser); + // Update completion state. $completion = new \completion_info($this->course); if ($completion->is_enabled($this->cm) && $this->completionsubmit) { @@ -321,17 +370,40 @@ public function commit_submission_response($rid, $quser) { $event->trigger(); } - /* - * Function to view an entire responses data. - * - */ - public function view_response($rid, $referer= '', $blankquestionnaire = false, $resps = '', $compare = false, - $isgroupmember = false, $allresponses = false, $currentgroupid = 0) { - $this->print_survey_start('', 1, 1, 0, $rid, false); + /** + * Update the grade for this questionnaire and user. + * + * @param int $userid + */ + private function update_grades($userid) { + if ($this->grade != 0) { + $questionnaire = new \stdClass(); + $questionnaire->id = $this->id; + $questionnaire->name = $this->name; + $questionnaire->grade = $this->grade; + $questionnaire->cmidnumber = $this->cm->idnumber; + $questionnaire->courseid = $this->course->id; + questionnaire_update_grades($questionnaire, $userid); + } + } + + /** + * Function to view an entire responses data. + * @param int $rid + * @param string $referer + * @param string $resps + * @param bool $compare + * @param bool $isgroupmember + * @param bool $allresponses + * @param int $currentgroupid + * @param string $outputtarget + */ + public function view_response($rid, $referer= '', $resps = '', $compare = false, $isgroupmember = false, $allresponses = false, + $currentgroupid = 0, $outputtarget = 'html') { + $this->print_survey_start('', 1, 1, 0, $rid, false, $outputtarget); - $data = new stdClass(); $i = 0; - $this->response_import_all($rid, $data); + $this->add_response($rid); if ($referer != 'print') { $feedbackmessages = $this->response_analysis($rid, $resps, $compare, $isgroupmember, $allresponses, $currentgroupid); @@ -349,63 +421,29 @@ public function view_response($rid, $referer= '', $blankquestionnaire = false, $ $this->page->add_to_page('feedbacknotes', $this->renderer->box(format_text($text, FORMAT_HTML))); } } + $pdf = ($outputtarget == 'pdf') ? true : false; foreach ($this->questions as $question) { if ($question->type_id < QUESPAGEBREAK) { $i++; } if ($question->type_id != QUESPAGEBREAK) { - $this->page->add_to_page('responses', $this->renderer->response_output($question, $data, $i)); + $this->page->add_to_page('responses', + $this->renderer->response_output($question, $this->responses[$rid], $i, $pdf)); } } } - /* - * Function to view an entire responses data. - * - * $value is unused, but is needed in order to get the $key elements of the array. Suppress PHPMD warning. - * - * @SuppressWarnings(PHPMD.UnusedLocalVariable) - */ - public function view_all_responses($resps) { + /** + * Function to view all loaded responses. + */ + public function view_all_responses() { $this->print_survey_start('', 1, 1, 0); // If a student's responses have been deleted by teacher while student was viewing the report, // then responses may have become empty, hence this test is necessary. - if ($resps) { - foreach ($resps as $resp) { - $data[$resp->id] = new stdClass(); - $this->response_import_all($resp->id, $data[$resp->id]); - } - $i = 0; - - $allrespdata = []; - foreach ($this->questions as $question) { - if ($question->type_id < QUESPAGEBREAK) { - $i++; - } - $qid = preg_quote('q'.$question->id, '/'); - if ($question->type_id != QUESPAGEBREAK) { - $allrespdata[$i] = []; - $allrespdata[$i]['question'] = $question; - foreach ($data as $respid => $respdata) { - $hasresp = false; - foreach ($respdata as $key => $value) { - if ($hasresp = preg_match("/$qid(_|$)/", $key)) { - break; - } - } - // Do not display empty responses. - if ($hasresp) { - $allrespdata[$i][] = [ - 'respdate' => userdate($resps[$respid]->submitted), - 'respdata' => $respdata - ]; - } - } - } - } - $this->page->add_to_page('responses', $this->renderer->all_response_output($allrespdata)); + if (!empty($this->responses)) { + $this->page->add_to_page('responses', $this->renderer->all_response_output($this->responses, $this->questions)); } else { $this->page->add_to_page('responses', $this->renderer->all_response_output(get_string('noresponses', 'questionnaire'))); } @@ -414,18 +452,36 @@ public function view_all_responses($resps) { } // Access Methods. + + /** + * True if the questionnaire is active. + * @return bool + */ public function is_active() { return (!empty($this->survey)); } + /** + * True if the questionnaire is open. + * @return bool + */ public function is_open() { return ($this->opendate > 0) ? ($this->opendate < time()) : true; } + /** + * True if the questionnaire is closed. + * @return bool + */ public function is_closed() { return ($this->closedate > 0) ? ($this->closedate < time()) : false; } + /** + * True if the specified user can complete this questionnaire. + * @param int $userid + * @return bool + */ public function user_can_take($userid) { if (!$this->is_active() || !$this->user_is_eligible($userid)) { @@ -439,10 +495,79 @@ public function user_can_take($userid) { } } + /** + * True if the specified user is eligible to complete this questionnaire. + * @param int $userid + * @return bool + */ public function user_is_eligible($userid) { return ($this->capabilities->view && $this->capabilities->submit); } + /** + * Return any message if the user cannot complete this questionnaire, explaining why. + * @param int $userid + * @param bool $asnotification Return as a rendered notification. + * @return bool|string + */ + public function user_access_messages($userid = 0, $asnotification = false) { + global $USER; + + if ($userid == 0) { + $userid = $USER->id; + } + $message = false; + + if (!$this->is_active()) { + if ($this->capabilities->manage) { + $msg = 'removenotinuse'; + } else { + $msg = 'notavail'; + } + $message = get_string($msg, 'questionnaire'); + + } else if ($this->survey->realm == 'template') { + $message = get_string('templatenotviewable', 'questionnaire'); + + } else if (!$this->is_open()) { + $message = get_string('notopen', 'questionnaire', userdate($this->opendate)); + + } else if ($this->is_closed()) { + $message = get_string('closed', 'questionnaire', userdate($this->closedate)); + + } else if (!$this->user_is_eligible($userid)) { + $message = get_string('noteligible', 'questionnaire'); + + } else if (!$this->user_can_take($userid)) { + switch ($this->qtype) { + case QUESTIONNAIREDAILY: + $msgstring = ' ' . get_string('today', 'questionnaire'); + break; + case QUESTIONNAIREWEEKLY: + $msgstring = ' ' . get_string('thisweek', 'questionnaire'); + break; + case QUESTIONNAIREMONTHLY: + $msgstring = ' ' . get_string('thismonth', 'questionnaire'); + break; + default: + $msgstring = ''; + break; + } + $message = get_string("alreadyfilled", "questionnaire", $msgstring); + } + + if (($message !== false) && $asnotification) { + $message = $this->renderer->notification($message, \core\output\notification::NOTIFY_ERROR); + } + + return $message; + } + + /** + * True if the specified user has a saved response for this questionnaire. + * @param int $userid + * @return bool + */ public function user_has_saved_response($userid) { global $DB; @@ -450,6 +575,11 @@ public function user_has_saved_response($userid) { ['questionnaireid' => $this->id, 'userid' => $userid, 'complete' => 'n']); } + /** + * True if the specified user can complete this questionnaire at this time. + * @param int $userid + * @return bool + */ public function user_time_for_new_attempt($userid) { global $DB; @@ -506,10 +636,19 @@ public function user_time_for_new_attempt($userid) { return $cantake; } + /** + * True if the accessing course contains the actual questionnaire, as opposed to an instance of a public questionnaire. + * @return bool + */ public function is_survey_owner() { return (!empty($this->survey->courseid) && ($this->course->id == $this->survey->courseid)); } + /** + * True if the user can view the specified response. + * @param int $rid + * @return bool|void + */ public function can_view_response($rid) { global $USER, $DB; @@ -568,14 +707,15 @@ public function can_view_response($rid) { } } + /** + * True if the user can view the responses to this questionnaire, and there are valid responses. + * @param null|int $usernumresp + * @return bool + */ public function can_view_all_responses($usernumresp = null) { - global $USER, $DB, $SESSION; + global $USER, $SESSION; - if ($owner = $DB->get_field('questionnaire_survey', 'courseid', ['id' => $this->sid])) { - $owner = ($owner == $this->course->id); - } else { - $owner = true; - } + $owner = $this->is_survey_owner(); $numresp = $this->count_submissions(); if ($usernumresp === null) { $usernumresp = $this->count_submissions($USER->id); @@ -591,25 +731,54 @@ public function can_view_all_responses($usernumresp = null) { // If questionnaire is set to separate groups, prevent user who is not member of any group // to view All responses. $canviewgroups = true; + $canviewallgroups = has_capability('moodle/site:accessallgroups', $this->context); $groupmode = groups_get_activity_groupmode($this->cm, $this->course); if ($groupmode == 1) { $canviewgroups = groups_has_membership($this->cm, $USER->id); } - $canviewallgroups = has_capability('moodle/site:accessallgroups', $this->context); - return (( // Teacher or non-editing teacher (if can view all groups). - ($canviewallgroups || - // Non-editing teacher (with canviewallgroups capability removed), if member of a group. - ($canviewgroups && $this->capabilities->readallresponseanytime)) && - ($numresp > 0) && $owner && ($numselectedresps > 0)) || - ($this->capabilities->readallresponses && ($numresp > 0) && $canviewgroups && - // If resp_view is set to QUESTIONNAIRE_STUDENTVIEWRESPONSES_NEVER, then this will always be false. + $grouplogic = $canviewgroups || $canviewallgroups; + $respslogic = ($numresp > 0) && ($numselectedresps > 0); + return $this->can_view_all_responses_anytime($grouplogic, $respslogic) || + $this->can_view_all_responses_with_restrictions($usernumresp, $grouplogic, $respslogic); + } + + /** + * True if the user can view all of the responses to this questionnaire any time, and there are valid responses. + * @param bool $grouplogic + * @param bool $respslogic + * @return bool + */ + public function can_view_all_responses_anytime($grouplogic = true, $respslogic = true) { + // Can view if you are a valid group user, this is the owning course, and there are responses, and you have no + // response view restrictions. + return $grouplogic && $respslogic && $this->is_survey_owner() && $this->capabilities->readallresponseanytime; + } + + /** + * True if the user can view all of the responses to this questionnaire any time, and there are valid responses. + * @param null|int $usernumresp + * @param bool $grouplogic + * @param bool $respslogic + * @return bool + */ + public function can_view_all_responses_with_restrictions($usernumresp, $grouplogic = true, $respslogic = true) { + // Can view if you are a valid group user, this is the owning course, and there are responses, and you can view + // subject to viewing settings.. + return $grouplogic && $respslogic && $this->is_survey_owner() && + ($this->capabilities->readallresponses && ($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_ALWAYS || - ($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENCLOSED && $this->is_closed()) || - ($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENANSWERED && ($usernumresp > 0))) && - $this->is_survey_owner())); + ($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENCLOSED && $this->is_closed()) || + ($this->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENANSWERED && $usernumresp))); + } + /** + * Return the number of submissions for this questionnaire. + * @param bool $userid + * @param int $groupid + * @return int + */ public function count_submissions($userid=false, $groupid=0) { global $DB; @@ -654,7 +823,6 @@ public function count_submissions($userid=false, $groupid=0) { * @param int|bool $userid * @param int $groupid * @return array - * @throws dml_exception */ public function get_responses($userid=false, $groupid=0) { global $DB; @@ -693,9 +861,14 @@ public function get_responses($userid=false, $groupid=0) { } $sql .= ' ORDER BY r.id'; - return $DB->get_records_sql($sql, $params); + return $DB->get_records_sql($sql, $params) ?? []; } + /** + * True if any of the questions are required. + * @param int $section + * @return bool + */ private function has_required($section = 0) { if (empty($this->questions)) { return false; @@ -706,8 +879,8 @@ private function has_required($section = 0) { } } } else { - foreach ($this->questionsbysec[$section] as $question) { - if ($question->required()) { + foreach ($this->questionsbysec[$section] as $questionid) { + if ($this->questions[$questionid]->required()) { return true; } } @@ -734,7 +907,8 @@ public function has_dependencies() { } /** - * @param $questionid + * Get a list of all dependent questions. + * @param int $questionid * @return array */ public function get_all_dependants($questionid) { @@ -765,7 +939,8 @@ public function get_all_dependants($questionid) { } /** - * @param $questionid + * Get a list of all dependent questions. + * @param int $questionid * @return array */ public function get_dependants($questionid) { @@ -785,8 +960,8 @@ public function get_dependants($questionid) { /** * Function to sort descendants array in get_dependants function. - * @param $a - * @param $b + * @param mixed $a + * @param mixed $b * @return int */ private static function cmp($a, $b) { @@ -820,7 +995,7 @@ public function get_dependants_and_choices() { /** * Load needed parent question information into the dependencies structure for the requested question. - * @param $question + * @param \mod_questionnaire\question\question $question * @return bool */ public function load_parents($question) { @@ -861,22 +1036,101 @@ public function load_parents($question) { $question->dependencies[$did]->name = $question->name; $question->dependencies[$did]->content = $question->content; $question->dependencies[$did]->parentposition = $dependquestion->position; - $question->dependencies[$did]->parent = $dependquestion->name.'->'.$dependchoice; + $question->dependencies[$did]->parent = format_string($dependquestion->name) . '->' . format_string($dependchoice); } return true; } + /** + * Determine the next valid page and return it. Return false if no valid next page. + * @param int $secnum + * @param int $rid + * @return int | bool + */ + public function next_page($secnum, $rid) { + $secnum++; + $numsections = isset($this->questionsbysec) ? count($this->questionsbysec) : 0; + if ($this->has_dependencies()) { + while (!$this->eligible_questions_on_page($secnum, $rid)) { + $secnum++; + // We have reached the end of questionnaire on a page without any question left. + if ($secnum > $numsections) { + $secnum = false; + break; + } + } + } + return $secnum; + } + + /** + * Determine the previous valid page and return it. Return false if no valid previous page. + * @param int $secnum + * @param int $rid + * @return int | bool + */ + public function prev_page($secnum, $rid) { + $secnum--; + if ($this->has_dependencies()) { + while (($secnum > 0) && !$this->eligible_questions_on_page($secnum, $rid)) { + $secnum--; + } + } + if ($secnum === 0) { + $secnum = false; + } + return $secnum; + } + + /** + * Return the correct action to a next page request. + * @param mod_questionnaire\responsetype\response\response $response + * @param int $userid + * @return bool|int|string + */ + public function next_page_action($response, $userid) { + $msg = $this->response_check_format($response->sec, $response); + if (empty($msg)) { + $response->rid = $this->existing_response_action($response, $userid); + return $this->next_page($response->sec, $response->rid); + } else { + return $msg; + } + } + + /** + * Return the correct action to a previous page request. + * @param mod_questionnaire\responsetype\response\response $response + * @param int $userid + * @return bool|int + */ + public function previous_page_action($response, $userid) { + $response->rid = $this->existing_response_action($response, $userid); + return $this->prev_page($response->sec, $response->rid); + } + + /** + * Handle updating an existing response. + * @param mod_questionnaire\responsetype\response\response $response + * @param int $userid + * @return bool|int + */ + public function existing_response_action($response, $userid) { + $this->response_delete($response->rid, $response->sec); + return $this->response_insert($response, $userid); + } + /** * Are there any eligible questions to be displayed on the specified page/section. - * @param $secnum The section number to check. - * @param $rid The current response id. + * @param int $secnum The section number to check. + * @param int $rid The current response id. * @return boolean */ public function eligible_questions_on_page($secnum, $rid) { $questionstodisplay = false; - foreach ($this->questionsbysec[$secnum] as $question) { - if ($question->dependency_fulfilled($rid, $this->questions)) { + foreach ($this->questionsbysec[$secnum] as $questionid) { + if ($this->questions[$questionid]->dependency_fulfilled($rid, $this->questions)) { $questionstodisplay = true; break; } @@ -886,17 +1140,23 @@ public function eligible_questions_on_page($secnum, $rid) { // Display Methods. - public function print_survey($userid=false, $quser) { + /** + * The main display method for the survey. Adds HTML to the templates. + * @param int $quser + * @param bool $userid + * @return string|void + */ + public function print_survey($quser, $userid=false) { global $SESSION, $CFG; - $formdata = new stdClass(); - if (data_submitted() && confirm_sesskey()) { - $formdata = data_submitted(); + if (!($formdata = data_submitted()) || !confirm_sesskey()) { + $formdata = new stdClass(); } - $formdata->rid = $this->get_response($quser); + + $formdata->rid = $this->get_latest_responseid($quser); // If student saved a "resume" questionnaire OR left a questionnaire unfinished // and there are more pages than one find the page of the last answered question. - if (!empty($formdata->rid) && (empty($formdata->sec) || intval($formdata->sec) < 1)) { + if (($formdata->rid != 0) && (empty($formdata->sec) || intval($formdata->sec) < 1)) { $formdata->sec = $this->response_select_max_sec($formdata->rid); } if (empty($formdata->sec)) { @@ -926,11 +1186,12 @@ public function print_survey($userid=false, $quser) { if (empty($msg)) { return; } + $formdata->rid = $this->existing_response_action($formdata, $userid); } if (!empty($formdata->resume) && ($this->resume)) { $this->response_delete($formdata->rid, $formdata->sec); - $formdata->rid = $this->response_insert($formdata->sec, $formdata->rid, $quser, true); + $formdata->rid = $this->response_insert($formdata, $quser, true); $this->response_goto_saved($action); return; } @@ -940,21 +1201,14 @@ public function print_survey($userid=false, $quser) { $msg = $this->response_check_format($formdata->sec, $formdata); if ($msg) { $formdata->next = ''; + $formdata->rid = $this->existing_response_action($formdata, $userid); } else { - $this->response_delete($formdata->rid, $formdata->sec); - $formdata->rid = $this->response_insert($formdata->sec, $formdata->rid, $quser); - // Skip logic. - $formdata->sec++; - if ($this->has_dependencies()) { - while (!$this->eligible_questions_on_page($formdata->sec, $formdata->rid)) { - $this->response_delete($formdata->rid, $formdata->sec); - $formdata->sec++; - // We have reached the end of questionnaire on a page without any question left. - if ($formdata->sec > $numsections) { - $SESSION->questionnaire->end = true; // End of questionnaire reached on a no questions page. - break; - } - } + $nextsec = $this->next_page_action($formdata, $userid); + if ($nextsec === false) { + $SESSION->questionnaire->end = true; // End of questionnaire reached on a no questions page. + $formdata->sec = $numsections + 1; + } else { + $formdata->sec = $nextsec; } } } @@ -971,21 +1225,19 @@ public function print_survey($userid=false, $quser) { $msg = $this->response_check_format($formdata->sec, $formdata, false, true); if ($msg) { $formdata->prev = ''; + $formdata->rid = $this->existing_response_action($formdata, $userid); } else { - $this->response_delete($formdata->rid, $formdata->sec); - $formdata->rid = $this->response_insert($formdata->sec, $formdata->rid, $quser); - $formdata->sec--; - // Skip logic. - if ($this->has_dependencies()) { - while (($formdata->sec > 0) && !$this->eligible_questions_on_page($formdata->sec, $formdata->rid)) { - $formdata->sec--; - } + $prevsec = $this->previous_page_action($formdata, $userid); + if ($prevsec === false) { + $formdata->sec = 0; + } else { + $formdata->sec = $prevsec; } } } if (!empty($formdata->rid)) { - $this->response_import_sec($formdata->rid, $formdata->sec, $formdata); + $this->add_response($formdata->rid); } $formdatareferer = !empty($formdata->referer) ? htmlspecialchars($formdata->referer) : ''; @@ -993,22 +1245,26 @@ public function print_survey($userid=false, $quser) { $this->page->add_to_page('formstart', $this->renderer->complete_formstart($action, ['referer' => $formdatareferer, 'a' => $this->id, 'sid' => $this->survey->id, 'rid' => $formdatarid, 'sec' => $formdata->sec, 'sesskey' => sesskey()])); if (isset($this->questions) && $numsections) { // Sanity check. - $this->survey_render($formdata->sec, $msg, $formdata); + $this->survey_render($formdata, $formdata->sec, $msg); $controlbuttons = []; if ($formdata->sec > 1) { - $controlbuttons['prev'] = ['type' => 'submit', 'value' => '<< '.get_string('previouspage', 'questionnaire')]; + $controlbuttons['prev'] = ['type' => 'submit', 'class' => 'btn btn-secondary control-button-prev', + 'value' => '<< '.get_string('previouspage', 'questionnaire')]; } if ($this->resume) { - $controlbuttons['resume'] = ['type' => 'submit', 'value' => get_string('save', 'questionnaire')]; + $controlbuttons['resume'] = ['type' => 'submit', 'class' => 'btn btn-secondary control-button-save', + 'value' => get_string('save_and_exit', 'questionnaire')]; } // Add a 'hidden' variable for the mod's 'view.php', and use a language variable for the submit button. if ($formdata->sec == $numsections) { $controlbuttons['submittype'] = ['type' => 'hidden', 'value' => 'Submit Survey']; - $controlbuttons['submit'] = ['type' => 'submit', 'value' => get_string('submitsurvey', 'questionnaire')]; + $controlbuttons['submit'] = ['type' => 'submit', 'class' => 'btn btn-primary control-button-submit', + 'value' => get_string('submitsurvey', 'questionnaire')]; } else { - $controlbuttons['next'] = ['type' => 'submit', 'value' => get_string('nextpage', 'questionnaire').' >>']; + $controlbuttons['next'] = ['type' => 'submit', 'class' => 'btn btn-secondary control-button-next', + 'value' => get_string('nextpage', 'questionnaire').' >>']; } $this->page->add_to_page('controlbuttons', $this->renderer->complete_controlbuttons($controlbuttons)); } else { @@ -1020,7 +1276,14 @@ public function print_survey($userid=false, $quser) { return $msg; } - private function survey_render($section = 1, $message = '', &$formdata) { + /** + * Print the entire survey page. + * @param stdClass $formdata + * @param int $section + * @param string $message + * @return bool|void + */ + private function survey_render(&$formdata, $section = 1, $message = '') { $this->usehtmleditor = null; @@ -1042,8 +1305,8 @@ private function survey_render($section = 1, $message = '', &$formdata) { $i = 0; if ($section > 1) { for ($j = 2; $j <= $section; $j++) { - foreach ($this->questionsbysec[$j - 1] as $question) { - if ($question->type_id < QUESPAGEBREAK) { + foreach ($this->questionsbysec[$j - 1] as $questionid) { + if ($this->questions[$questionid]->type_id < QUESPAGEBREAK) { $i++; } } @@ -1051,14 +1314,26 @@ private function survey_render($section = 1, $message = '', &$formdata) { } $this->print_survey_start($message, $section, $numsections, $hasrequired, '', 1); - foreach ($this->questionsbysec[$section] as $question) { - if ($question->type_id != QUESSECTIONTEXT) { + // Only show progress bar on questionnaires with more than one page. + if ($this->progressbar && isset($this->questionsbysec) && count($this->questionsbysec) > 1) { + $this->page->add_to_page('progressbar', + $this->renderer->render_progress_bar($section, $this->questionsbysec)); + } + foreach ($this->questionsbysec[$section] as $questionid) { + if ($this->questions[$questionid]->is_numbered()) { $i++; } // Need questionnaire id to get the questionnaire object in sectiontext (Label) question class. $formdata->questionnaire_id = $this->id; + if (isset($formdata->rid) && !empty($formdata->rid)) { + $this->add_response($formdata->rid); + } else { + $this->add_response_from_formdata($formdata); + } $this->page->add_to_page('questions', - $this->renderer->question_output($question, $formdata, [], $i, $this->usehtmleditor)); + $this->renderer->question_output($this->questions[$questionid], + (isset($this->responses[$formdata->rid]) ? $this->responses[$formdata->rid] : []), + $i, $this->usehtmleditor, [])); } $this->print_survey_end($section, $numsections); @@ -1066,7 +1341,18 @@ private function survey_render($section = 1, $message = '', &$formdata) { return; } - private function print_survey_start($message, $section, $numsections, $hasrequired, $rid='', $blankquestionnaire=false) { + /** + * Print the start of the survey page. + * @param string $message + * @param int $section + * @param int $numsections + * @param bool $hasrequired + * @param string $rid + * @param bool $blankquestionnaire + * @param string $outputtarget + */ + private function print_survey_start($message, $section, $numsections, $hasrequired, $rid='', $blankquestionnaire=false, + $outputtarget = 'html') { global $CFG, $DB; require_once($CFG->libdir.'/filelib.php'); @@ -1129,7 +1415,35 @@ private function print_survey_start($message, $section, $numsections, $hasrequir } } if ($ruser) { - $respinfo = get_string('respondent', 'questionnaire').': '.$ruser.''; + $respinfo = ''; + if ($outputtarget == 'html') { + // Disable the pdf function for now, until it looks a lot better. + if (false) { + $linkname = get_string('downloadpdf', 'mod_questionnaire'); + $link = new moodle_url('/mod/questionnaire/report.php', + [ + 'action' => 'vresp', + 'instance' => $this->id, + 'target' => 'pdf', + 'individualresponse' => 1, + 'rid' => $rid + ] + ); + $downpdficon = new pix_icon('b/pdfdown', $linkname, 'mod_questionnaire'); + $respinfo .= $this->renderer->action_link($link, null, null, null, $downpdficon); + } + + $linkname = get_string('print', 'mod_questionnaire'); + $link = new \moodle_url('/mod/questionnaire/report.php', + ['action' => 'vresp', 'instance' => $this->id, 'target' => 'print', 'individualresponse' => 1, 'rid' => $rid]); + $htmlicon = new pix_icon('t/print', $linkname); + $options = ['menubar' => true, 'location' => false, 'scrollbars' => true, 'resizable' => true, + 'height' => 600, 'width' => 800, 'title' => $linkname]; + $name = 'popup'; + $action = new popup_action('click', $link, $name, $options); + $respinfo .= $this->renderer->action_link($link, null, $action, ['title' => $linkname], $htmlicon) . ' '; + } + $respinfo .= get_string('respondent', 'questionnaire').': '.$ruser.''; if ($this->survey_is_public()) { // For a public questionnaire, look for the course that used it. $coursename = ''; @@ -1166,15 +1480,17 @@ private function print_survey_start($message, $section, $numsections, $hasrequir } if ($section == 1) { if (!empty($this->survey->title)) { - $this->page->add_to_page('title', clean_text($this->survey->title, FORMAT_HTML)); + $this->survey->title = format_string($this->survey->title); + $this->page->add_to_page('title', $this->survey->title); } if (!empty($this->survey->subtitle)) { - $this->page->add_to_page('subtitle', clean_text($this->survey->subtitle, FORMAT_HTML)); + $this->survey->subtitle = format_string($this->survey->subtitle); + $this->page->add_to_page('subtitle', $this->survey->subtitle); } if ($this->survey->info) { $infotext = file_rewrite_pluginfile_urls($this->survey->info, 'pluginfile.php', $this->context->id, 'mod_questionnaire', 'info', $this->survey->id); - $this->page->add_to_page('addinfo', $infotext); + $this->page->add_to_page('addinfo', format_text($infotext, FORMAT_HTML, ['noclean' => true])); } } @@ -1183,10 +1499,14 @@ private function print_survey_start($message, $section, $numsections, $hasrequir } } + /** + * Print the end of the survey page. + * @param int $section + * @param int $numsections + */ private function print_survey_end($section, $numsections) { - $autonum = $this->autonum; - // If no questions autonumbering. - if ($autonum < 3) { + // If no pages autonumbering. + if (!$this->pages_autonumbered()) { return; } if ($numsections > 1) { @@ -1198,19 +1518,27 @@ private function print_survey_end($section, $numsections) { } } - // Blankquestionnaire : if we are printing a blank questionnaire. - public function survey_print_render($message = '', $referer='', $courseid, $rid=0, $blankquestionnaire=false) { + /** + * Display a survey suitable for printing. + * @param int $courseid + * @param string $message + * @param string $referer + * @param int $rid + * @param bool $blankquestionnaire If we are printing a blank questionnaire. + * @return false|void + */ + public function survey_print_render($courseid, $message = '', $referer='', $rid=0, $blankquestionnaire=false) { global $DB, $CFG; if (! $course = $DB->get_record("course", array("id" => $courseid))) { - print_error('incorrectcourseid', 'questionnaire'); + throw new \moodle_exception('incorrectcourseid', 'mod_questionnaire'); } $this->course = $course; if (!empty($rid)) { // If we're viewing a response, use this method. - $this->view_response($rid, $referer, $blankquestionnaire); + $this->view_response($rid, $referer); return; } @@ -1244,6 +1572,8 @@ public function survey_print_render($message = '', $referer='', $courseid, $rid= $errors = 1; if (data_submitted()) { $formdata = data_submitted(); + $formdata->rid = $formdata->rid ?? 0; + $this->add_response_from_formdata($formdata); $pageerror = ''; $s = 1; $errors = 0; @@ -1261,7 +1591,7 @@ public function survey_print_render($message = '', $referer='', $courseid, $rid= } } - $this->print_survey_start($message, $section = 1, 1, $hasrequired, $rid = ''); + $this->print_survey_start($message, 1, 1, $hasrequired, ''); if (($referer == 'preview') && $this->has_dependencies()) { $allqdependants = $this->get_dependants_and_choices(); @@ -1281,16 +1611,17 @@ public function survey_print_render($message = '', $referer='', $courseid, $rid= $output .= $this->renderer->print_preview_pagenumber(get_string('page', 'questionnaire').' '.$page); $page++; } - foreach ($section as $question) { - if ($question->type_id == QUESSECTIONTEXT) { + foreach ($section as $questionid) { + if (!$this->questions[$questionid]->is_numbered()) { $i--; } - if (isset($allqdependants[$question->id])) { - $dependants = $allqdependants[$question->id]; + if (isset($allqdependants[$questionid])) { + $dependants = $allqdependants[$questionid]; } else { $dependants = []; } - $output .= $this->renderer->question_output($question, $formdata, $dependants, $i++, null); + $output .= $this->renderer->question_output($this->questions[$questionid], $this->responses[0] ?? [], + $i++, null, $dependants); $this->page->add_to_page('questions', $output); $output = ''; } @@ -1304,6 +1635,11 @@ public function survey_print_render($message = '', $referer='', $courseid, $rid= return; } + /** + * Update an existing survey. + * @param stdClass $sdata + * @return bool|int + */ public function survey_update($sdata) { global $DB; @@ -1345,7 +1681,7 @@ public function survey_update($sdata) { $name = $DB->get_field('questionnaire_survey', 'name', array('id' => $this->survey->id)); // Trying to change survey name. - if (trim($name) != trim(stripslashes($sdata->name))) { // $sdata will already have slashes added to it. + if (trim($name) != trim(stripslashes($sdata->name))) { // Var $sdata will already have slashes added to it. $count = $DB->count_records('questionnaire_survey', array('name' => $sdata->name)); if ($count != 0) { $errstr = get_string('errnewname', 'questionnaire'); // TODO: notused! @@ -1372,7 +1708,11 @@ public function survey_update($sdata) { return($this->survey->id); } - /* Creates an editable copy of a survey. */ + /** + * Creates an editable copy of a survey. + * @param int $owner + * @return bool|int + */ public function survey_copy($owner) { global $DB; @@ -1421,9 +1761,12 @@ public function survey_copy($owner) { $qidarray[$oldid] = $newqid; foreach ($question->choices as $key => $choice) { $oldcid = $key; - unset($choice->id); - $choice->question_id = $newqid; - if (!$newcid = $DB->insert_record('questionnaire_quest_choice', $choice)) { + $newchoice = (object) [ + 'question_id' => $newqid, + 'content' => $choice->content, + 'value' => $choice->value, + ]; + if (!$newcid = $DB->insert_record('questionnaire_quest_choice', $newchoice)) { return(false); } $cidarray[$oldcid] = $newcid; @@ -1431,42 +1774,45 @@ public function survey_copy($owner) { } // Replicate all dependency data. - $dependquestions = $DB->get_records('questionnaire_dependency', ['surveyid' => $this->survey->id], 'questionid'); - foreach ($dependquestions as $dquestion) { - $record = new stdClass(); - $record->questionid = $qidarray[$dquestion->questionid]; - $record->surveyid = $newsid; - $record->dependquestionid = $qidarray[$dquestion->dependquestionid]; - // The response may not use choice id's (example boolean). If not, just copy the value. - $response = $this->questions[$dquestion->dependquestionid]->response; - if ($response->transform_choiceid($dquestion->dependchoiceid) == $dquestion->dependchoiceid) { - $record->dependchoiceid = $cidarray[$dquestion->dependchoiceid]; - } else { - $record->dependchoiceid = $dquestion->dependchoiceid; + if ($dependquestions = $DB->get_records('questionnaire_dependency', ['surveyid' => $this->survey->id], 'questionid')) { + foreach ($dependquestions as $dquestion) { + $record = new stdClass(); + $record->questionid = $qidarray[$dquestion->questionid]; + $record->surveyid = $newsid; + $record->dependquestionid = $qidarray[$dquestion->dependquestionid]; + // The response may not use choice id's (example boolean). If not, just copy the value. + $responsetype = $this->questions[$dquestion->dependquestionid]->responsetype; + if ($responsetype->transform_choiceid($dquestion->dependchoiceid) == $dquestion->dependchoiceid) { + $record->dependchoiceid = $cidarray[$dquestion->dependchoiceid]; + } else { + $record->dependchoiceid = $dquestion->dependchoiceid; + } + $record->dependlogic = $dquestion->dependlogic; + $record->dependandor = $dquestion->dependandor; + $DB->insert_record('questionnaire_dependency', $record); } - $record->dependlogic = $dquestion->dependlogic; - $record->dependandor = $dquestion->dependandor; - $DB->insert_record('questionnaire_dependency', $record); } // Replicate any feedback data. // TODO: Need to handle image attachments (same for other copies above). - $fbsections = $DB->get_records('questionnaire_fb_sections', ['surveyid' => $this->survey->id], 'id'); - foreach ($fbsections as $fbsid => $fbsection) { - $fbsection->surveyid = $newsid; - $scorecalculation = unserialize($fbsection->scorecalculation); - $newscorecalculation = []; - foreach ($scorecalculation as $qid => $val) { - $newscorecalculation[$qidarray[$qid]] = $val; - } - $fbsection->scorecalculation = serialize($newscorecalculation); - unset($fbsection->id); - $newfbsid = $DB->insert_record('questionnaire_fb_sections', $fbsection); - $feedbackrecs = $DB->get_records('questionnaire_feedback', ['sectionid' => $fbsid], 'id'); - foreach ($feedbackrecs as $feedbackrec) { - $feedbackrec->sectionid = $newfbsid; - unset($feedbackrec->id); - $DB->insert_record('questionnaire_feedback', $feedbackrec); + if ($fbsections = $DB->get_records('questionnaire_fb_sections', ['surveyid' => $this->survey->id], 'id')) { + foreach ($fbsections as $fbsid => $fbsection) { + $fbsection->surveyid = $newsid; + $scorecalculation = section::decode_scorecalculation($fbsection->scorecalculation); + $newscorecalculation = []; + foreach ($scorecalculation as $qid => $val) { + $newscorecalculation[$qidarray[$qid]] = $val; + } + $fbsection->scorecalculation = serialize($newscorecalculation); + unset($fbsection->id); + $newfbsid = $DB->insert_record('questionnaire_fb_sections', $fbsection); + if ($feedbackrecs = $DB->get_records('questionnaire_feedback', ['sectionid' => $fbsid], 'id')) { + foreach ($feedbackrecs as $feedbackrec) { + $feedbackrec->sectionid = $newfbsid; + unset($feedbackrec->id); + $DB->insert_record('questionnaire_feedback', $feedbackrec); + } + } } } @@ -1475,6 +1821,14 @@ public function survey_copy($owner) { // RESPONSE LIBRARY. + /** + * Check that all questions have been answered in a suitable way. + * @param int $section + * @param stdClass $formdata + * @param bool $checkmissing + * @param bool $checkwrongformat + * @return string + */ private function response_check_format($section, $formdata, $checkmissing = true, $checkwrongformat = true) { $missing = 0; $strmissing = ''; // Missing questions. @@ -1483,8 +1837,8 @@ private function response_check_format($section, $formdata, $checkmissing = true $i = 1; for ($j = 2; $j <= $section; $j++) { // ADDED A SIMPLE LOOP FOR MAKING SURE PAGE BREAKS (type 99) AND LABELS (type 100) ARE NOT ALLOWED. - foreach ($this->questionsbysec[$j - 1] as $sectionrecord) { - $tid = $sectionrecord->type_id; + foreach ($this->questionsbysec[$j - 1] as $questionid) { + $tid = $this->questions[$questionid]->type_id; if ($tid < QUESPAGEBREAK) { $i++; } @@ -1492,25 +1846,30 @@ private function response_check_format($section, $formdata, $checkmissing = true } $qnum = $i - 1; - foreach ($this->questionsbysec[$section] as $question) { - $tid = $question->type_id; - if ($tid != QUESSECTIONTEXT) { - $qnum++; - } - if (!$question->response_complete($formdata)) { - $missing++; - $strmissing .= get_string('num', 'questionnaire').$qnum.'. '; - } - if (!$question->response_valid($formdata)) { - $wrongformat++; - $strwrongformat .= get_string('num', 'questionnaire').$qnum.'. '; + if (key_exists($section, $this->questionsbysec)) { + foreach ($this->questionsbysec[$section] as $questionid) { + + if ($this->questions[$questionid]->is_numbered()) { + $qnum++; + } + if (!$this->questions[$questionid]->response_complete($formdata)) { + $missing++; + $strnum = get_string('num', 'questionnaire') . $qnum . '. '; + $strmissing .= $strnum; + // Pop-up notification at the point of the error. + $strnoti = get_string('missingquestion', 'questionnaire') . $strnum; + $this->questions[$questionid]->add_notification($strnoti); + } + if (!$this->questions[$questionid]->response_valid($formdata)) { + $wrongformat++; + $strwrongformat .= get_string('num', 'questionnaire') . $qnum . '. '; + } } } $message = ''; $nonumbering = false; - $autonum = $this->autonum; // If no questions autonumbering do not display missing question(s) number(s). - if ($autonum != 1 && $autonum != 3) { + if (!$this->questions_autonumbered()) { $nonumbering = true; } if ($checkmissing && $missing) { @@ -1540,6 +1899,11 @@ private function response_check_format($section, $formdata, $checkmissing = true return ($message); } + /** + * Delete the spcified response. + * @param int $rid + * @param null|int $sec + */ private function response_delete($rid, $sec = null) { global $DB; @@ -1558,8 +1922,8 @@ private function response_delete($rid, $sec = null) { /* get question_id's in this section */ $qids = array(); - foreach ($this->questionsbysec[$sec] as $question) { - $qids[] = $question->id; + foreach ($this->questionsbysec[$sec] as $questionid) { + $qids[] = $questionid; } if (empty($qids)) { return; @@ -1582,39 +1946,11 @@ private function response_delete($rid, $sec = null) { } } - private function response_import_sec($rid, $sec, &$varr) { - if ($sec < 1 || !isset($this->questionsbysec[$sec])) { - return; - } - $vals = $this->response_select($rid, 'content'); - reset($vals); - foreach ($vals as $id => $arr) { - if (isset($arr[0]) && is_array($arr[0])) { - // Multiple. - $varr->{'q'.$id} = array_map('array_pop', $arr); - } else { - $varr->{'q'.$id} = array_pop($arr); - } - } - } - - private function response_import_all($rid, &$varr) { - - $vals = $this->response_select($rid, 'content'); - reset($vals); - foreach ($vals as $id => $arr) { - if (strstr($id, '_') && isset($arr[4])) { // Single OR multiple with !other choice selected. - $varr->{'q'.$id} = $arr[4]; - } else { - if (isset($arr[0]) && is_array($arr[0])) { // Multiple. - $varr->{'q'.$id} = array_map('array_pop', $arr); - } else { // Boolean, rate and other. - $varr->{'q'.$id} = array_pop($arr); - } - } - } - } - + /** + * Commit the specified response. + * @param int $rid + * @return bool + */ private function response_commit($rid) { global $DB; @@ -1631,29 +1967,29 @@ private function response_commit($rid) { return $DB->update_record('questionnaire_response', $record); } - private function get_response($userid, $rid = 0) { + /** + * Get the latest response id for the user, or verify that the given response id is valid. + * @param int $userid + * @return int + */ + public function get_latest_responseid($userid) { global $DB; - $rid = intval($rid); - if ($rid != 0) { - // Check for valid rid. - $fields = 'id, userid'; - $params = ['id' => $rid, 'questionnaireid' => $this->id, 'userid' => $userid, 'complete' => 'n']; - return ($DB->get_record('questionnaire_response', $params, $fields) !== false) ? $rid : ''; - + // Find latest in progress rid. + $params = ['questionnaireid' => $this->id, 'userid' => $userid, 'complete' => 'n']; + if ($records = $DB->get_records('questionnaire_response', $params, 'submitted DESC', 'id,questionnaireid', 0, 1)) { + $rec = reset($records); + return $rec->id; } else { - // Find latest in progress rid. - $params = ['questionnaireid' => $this->id, 'userid' => $userid, 'complete' => 'n']; - if ($records = $DB->get_records('questionnaire_response', $params, 'submitted DESC', 'id,questionnaireid', 0, 1)) { - $rec = reset($records); - return $rec->id; - } else { - return ''; - } + return 0; } } - // Returns the number of the section in which questions have been answered in a response. + /** + * Returns the number of the section in which questions have been answered in a response. + * @param int $rid + * @return int + */ private function response_select_max_sec($rid) { global $DB; @@ -1665,7 +2001,11 @@ private function response_select_max_sec($rid) { return $max; } - // Returns the position of the last answered question in a response. + /** + * Returns the position of the last answered question in a response. + * @param int $rid + * @return int + */ private function response_select_max_pos($rid) { global $DB; @@ -1730,12 +2070,11 @@ private function submission_notify($rid) { private function send_submission_notifications($rid) { global $CFG, $USER; - $answers = new stdClass(); - $this->response_import_all($rid, $answers); + $this->add_response($rid); $message = ''; if ($this->notifications == 2) { - $message .= $this->get_full_submission_for_notifications($answers); + $message .= $this->get_full_submission_for_notifications($rid); } $success = true; @@ -1786,21 +2125,21 @@ private function send_submission_notifications($rid) { */ private function send_message($info, $eventtype) { $eventdata = new \core\message\message(); - $eventdata->courseid = $this->course->id; - $eventdata->modulename = 'questionnaire'; - $eventdata->userfrom = $info->userfrom; - $eventdata->userto = $info->userto; - $eventdata->subject = $info->postsubject; - $eventdata->fullmessage = $info->posttext; + $eventdata->courseid = $this->course->id; + $eventdata->modulename = 'questionnaire'; + $eventdata->userfrom = $info->userfrom; + $eventdata->userto = $info->userto; + $eventdata->subject = $info->postsubject; + $eventdata->fullmessage = $info->posttext; $eventdata->fullmessageformat = FORMAT_PLAIN; - $eventdata->fullmessagehtml = $info->posthtml; - $eventdata->smallmessage = $info->postsubject; + $eventdata->fullmessagehtml = $info->posthtml; + $eventdata->smallmessage = $info->postsubject; - $eventdata->name = $eventtype; - $eventdata->component = 'mod_questionnaire'; - $eventdata->notification = 1; - $eventdata->contexturl = $info->submissionurl; - $eventdata->contexturlname = $info->name; + $eventdata->name = $eventtype; + $eventdata->component = 'mod_questionnaire'; + $eventdata->notification = 1; + $eventdata->contexturl = $info->submissionurl; + $eventdata->contexturlname = $info->name; message_send($eventdata); } @@ -1811,7 +2150,7 @@ private function send_message($info, $eventtype) { * @param int $userid The submission to grade * @return array */ - protected function get_notifiable_users($userid) { + public function get_notifiable_users($userid) { // Potential users should be active users only. $potentialusers = get_enrolled_users($this->context, 'mod/questionnaire:submissionnotification', null, 'u.*', null, null, null, true); @@ -1856,12 +2195,11 @@ protected function get_notifiable_users($userid) { /** * Return a formatted string containing all the questions and answers for a specific submission. - * @param $answers The array of answers from import_all_responses. + * @param int $rid * @return string - * @throws coding_exception */ - private function get_full_submission_for_notifications($answers) { - $responses = $this->get_full_submission_for_export($answers); + private function get_full_submission_for_notifications($rid) { + $responses = $this->get_full_submission_for_export($rid); $message = ''; foreach ($responses as $response) { $message .= html_to_text($response->questionname) . "
\n"; @@ -1878,23 +2216,25 @@ private function get_full_submission_for_notifications($answers) { /** * Construct the response data for a given response and return a structured export. - * @param $rid + * @param int $rid * @return string * @throws coding_exception */ public function get_structured_response($rid) { - $answers = new stdClass(); - $this->response_import_all($rid, $answers); - return $this->get_full_submission_for_export($answers); + $this->add_response($rid); + return $this->get_full_submission_for_export($rid); } /** * Return a JSON structure containing all the questions and answers for a specific submission. - * @param $answers The array of answers from import_all_responses. - * @return string - * @throws coding_exception + * @param int $rid + * @return array */ - private function get_full_submission_for_export($answers) { + private function get_full_submission_for_export($rid) { + if (!isset($this->responses[$rid])) { + $this->add_response($rid); + } + $exportstructure = []; foreach ($this->questions as $question) { $rqid = 'q' . $question->id; @@ -1906,54 +2246,46 @@ private function get_full_submission_for_export($answers) { $choices = []; $cids = []; foreach ($question->choices as $cid => $choice) { - if (!empty($choice->value)) { + if (!empty($choice->value) && (strpos($choice->content, '=') !== false)) { $choices[$choice->value] = substr($choice->content, (strpos($choice->content, '=') + 1)); } else { $cids[$rqid . '_' . $cid] = $choice->content; } } - foreach ($cids as $rqid => $choice) { - if (isset($answers->$rqid)) { + if (isset($this->responses[$rid]->answers[$question->id])) { + foreach ($cids as $rqid => $choice) { $cid = substr($rqid, (strpos($rqid, '_') + 1)); - if (isset($question->choices[$cid]) && isset($choices[$answers->$rqid + 1])) { - $rating = $choices[$answers->$rqid + 1]; - } else { - $rating = $answers->$rqid + 1; + if (isset($this->responses[$rid]->answers[$question->id][$cid])) { + if (isset($question->choices[$cid]) && + isset($choices[$this->responses[$rid]->answers[$question->id][$cid]->value])) { + $rating = $choices[$this->responses[$rid]->answers[$question->id][$cid]->value]; + } else { + $rating = $this->responses[$rid]->answers[$question->id][$cid]->value; + } + $response->answers[] = $question->choices[$cid]->content . ' = ' . $rating; } - $response->answers[] = $question->choices[$cid]->content . ' = ' . $rating; } } } else if ($question->has_choices()) { - // Check for "other". - foreach ($question->choices as $cid => $choice) { - if (strpos($choice->content, '!other=') !== false) { - $other = $cid; - break; - } - } $answertext = ''; - if (isset($answers->$rqid) && is_array($answers->$rqid)) { + if (isset($this->responses[$rid]->answers[$question->id])) { $i = 0; - foreach ($answers->$rqid as $answer) { + foreach ($this->responses[$rid]->answers[$question->id] as $answer) { if ($i > 0) { $answertext .= '; '; } - if (isset($other) && ($answer == ('other_' . $other))) { - $answertext .= $answers->{$rqid . '_' . $other}; + if ($question->choices[$answer->choiceid]->is_other_choice()) { + $answertext .= $answer->value; } else { - $answertext .= $question->choices[$answer]->content; + $answertext .= $question->choices[$answer->choiceid]->content; } $i++; } - } else if (isset($answers->$rqid) && isset($other) && ($answers->$rqid == ('other_' . $other))) { - $answertext .= $answers->{$rqid . '_' . $other}; - } else if (isset($answers->$rqid) && isset($question->choices[$answers->$rqid])) { - $answertext .= $question->choices[$answers->$rqid]->content; } $response->answers[] = $answertext; - } else if (isset($answers->$rqid)) { - $response->answers[] = $answers->$rqid; + } else if (isset($this->responses[$rid]->answers[$question->id])) { + $response->answers[] = $this->responses[$rid]->answers[$question->id][0]->value; } $exportstructure[] = $response; } @@ -2005,13 +2337,11 @@ private function get_formatted_answers_for_emails($answers) { * @param int $rid The id of the response record. * @param string $email The comma separated list of emails to send to. * @return bool - * @throws coding_exception - * @throws dml_exception */ private function response_send_email($rid, $email) { global $CFG; - $submission = $this->generate_csv($rid, '', null, 1, 0); + $submission = $this->generate_csv(0, $rid, '', null, 1); if (!empty($submission)) { $answers = $this->get_formatted_answers_for_emails($submission); } else { @@ -2032,10 +2362,10 @@ private function response_send_email($rid, $email) { '&rid='.$rid.'&instance='.$this->id; // Html and plaintext body. - $bodyhtml = ''.$url.''.$endhtml; - $bodyplaintext = $url.$endplaintext; - $bodyhtml .= get_string('surveyresponse', 'questionnaire') .' "'.$name.'"'.$endhtml; - $bodyplaintext .= get_string('surveyresponse', 'questionnaire') .' "'.$name.'"'.$endplaintext; + $bodyhtml = ''.$url.''.$endhtml; + $bodyplaintext = $url.$endplaintext; + $bodyhtml .= get_string('surveyresponse', 'questionnaire') .' "'.$name.'"'.$endhtml; + $bodyplaintext .= get_string('surveyresponse', 'questionnaire') .' "'.$name.'"'.$endplaintext; $bodyhtml .= $answers['html']; $bodyplaintext .= $answers['plaintext']; @@ -2061,19 +2391,27 @@ private function response_send_email($rid, $email) { return $return; } - public function response_insert($section, $rid, $userid, $resume=false) { + /** + * Insert the provided response. + * @param object $responsedata An object containing all data for the response. + * @param int $userid + * @param bool $resume + * @return bool|int + */ + public function response_insert($responsedata, $userid, $resume=false) { global $DB; $record = new stdClass(); $record->submitted = time(); - if (empty($rid)) { + if (empty($responsedata->rid)) { // Create a uniqe id for this response. $record->questionnaireid = $this->id; $record->userid = $userid; - $rid = $DB->insert_record('questionnaire_response', $record); + $responsedata->rid = $DB->insert_record('questionnaire_response', $record); + $responsedata->id = $responsedata->rid; } else { - $record->id = $rid; + $record->id = $responsedata->rid; $DB->update_record('questionnaire_response', $record); } if ($resume) { @@ -2092,56 +2430,47 @@ public function response_insert($section, $rid, $userid, $resume=false) { $event->trigger(); } - if (!empty($this->questionsbysec[$section])) { - foreach ($this->questionsbysec[$section] as $question) { - // NOTE *** $val really should be a value obtained from the caller or somewhere else. - // Note that "optional_param" accepting arrays is deprecated for optional_param_array. - if ($question->responsetable == 'resp_multiple') { - $val = optional_param_array('q'.$question->id, '', PARAM_RAW); - } else { - $val = optional_param('q'.$question->id, '', PARAM_RAW); - } - $question->insert_response($rid, $val); + if (!isset($responsedata->sec)) { + $responsedata->sec = 1; + } + if (!empty($this->questionsbysec[$responsedata->sec])) { + foreach ($this->questionsbysec[$responsedata->sec] as $questionid) { + $this->questions[$questionid]->insert_response($responsedata); } } - return($rid); + return($responsedata->rid); } - private function response_select($rid, $col = null, $csvexport = false, $choicecodes=0, $choicetext=1) { - if ($col == null) { - $col = ''; - } - if (!is_array($col) && !empty($col)) { - $col = explode(',', preg_replace("/\s/", '', $col)); - } - if (is_array($col) && count($col) > 0) { - $callback = function($a) { - return 'q.'.$a; - }; - $col = ',' . implode(',', array_map($callback, $col)); - } - + /** + * Get the answers for the all response types. + * @param int $rid + * @return array + */ + private function response_select($rid) { // Response_bool (yes/no). - $values = \mod_questionnaire\response\boolean::response_select($rid, $col, $csvexport, $choicecodes, $choicetext); + $values = \mod_questionnaire\responsetype\boolean::response_select($rid); // Response_single (radio button or dropdown). - $values += \mod_questionnaire\response\single::response_select($rid, $col, $csvexport, $choicecodes, $choicetext); + $values += \mod_questionnaire\responsetype\single::response_select($rid); // Response_multiple. - $values += \mod_questionnaire\response\multiple::response_select($rid, $col, $csvexport, $choicecodes, $choicetext); + $values += \mod_questionnaire\responsetype\multiple::response_select($rid); // Response_rank. - $values += \mod_questionnaire\response\rank::response_select($rid, $col, $csvexport, $choicecodes, $choicetext); + $values += \mod_questionnaire\responsetype\rank::response_select($rid); // Response_text. - $values += \mod_questionnaire\response\text::response_select($rid, $col, $csvexport, $choicecodes, $choicetext); + $values += \mod_questionnaire\responsetype\text::response_select($rid); // Response_date. - $values += \mod_questionnaire\response\date::response_select($rid, $col, $csvexport, $choicecodes, $choicetext); + $values += \mod_questionnaire\responsetype\date::response_select($rid); return($values); } + /** + * Redirect to the appropriate finish page. + */ private function response_goto_thankyou() { global $CFG, $USER, $DB; @@ -2178,6 +2507,11 @@ private function response_goto_thankyou() { if (empty($thankhead)) { $thankhead = get_string('thank_head', 'questionnaire'); } + if ($this->progressbar && isset($this->questionsbysec) && count($this->questionsbysec) > 1) { + // Show 100% full progress bar on completion. + $this->page->add_to_page('progressbar', + $this->renderer->render_progress_bar(count($this->questionsbysec) + 1, $this->questionsbysec)); + } $this->page->add_to_page('title', $thankhead); $this->page->add_to_page('addinfo', format_text(file_rewrite_pluginfile_urls($thankbody, 'pluginfile.php', @@ -2190,20 +2524,22 @@ private function response_goto_thankyou() { $currentgroupid = 0; } if ($this->capabilities->readownresponses) { - $this->page->add_to_page('message', - (''. - get_string("continue").'')); + $url = new moodle_url('myreport.php', ['id' => $this->cm->id, 'instance' => $this->cm->instance, 'user' => $USER->id, + 'byresponse' => 0, 'action' => 'vresp']); + $this->page->add_to_page('continue', $this->renderer->single_button($url, get_string('continue'))); } else { - $this->page->add_to_page('message', - (''. - get_string("continue").'')); + $url = new moodle_url('/course/view.php', ['id' => $this->course->id]); + $this->page->add_to_page('continue', $this->renderer->single_button($url, get_string('continue'))); } return; } + /** + * Redirect to the provided url. + * @param string $url + */ private function response_goto_saved($url) { - global $CFG; + global $CFG, $USER; $resumesurvey = get_string('resumesurvey', 'questionnaire'); $savedprogress = get_string('savedprogress', 'questionnaire', ''.$resumesurvey.''); @@ -2212,11 +2548,33 @@ private function response_goto_saved($url) { $this->page->add_to_page('respondentinfo', $this->renderer->homelink($CFG->wwwroot.'/course/view.php?id='.$this->course->id, get_string("backto", "moodle", $this->course->fullname))); + + if ($this->resume) { + $message = $this->user_access_messages($USER->id, true); + if ($message === false) { + if ($this->user_can_take($USER->id)) { + if ($this->questions) { // Sanity check. + if ($this->user_has_saved_response($USER->id)) { + $this->page->add_to_page('respondentinfo', + $this->renderer->homelink($CFG->wwwroot . '/mod/questionnaire/complete.php?' . + 'id=' . $this->cm->id . '&resume=1', $resumesurvey)); + } + } + } + } + } return; } // Survey Results Methods. + /** + * Add the navigation to the responses page. + * @param int $currrid + * @param int $currentgroupid + * @param stdClass $cm + * @param bool $byresponse + */ public function survey_results_navbar_alpha($currrid, $currentgroupid, $cm, $byresponse) { global $CFG, $DB; @@ -2316,7 +2674,7 @@ public function survey_results_navbar_alpha($currrid, $currentgroupid, $cm, $byr $name = 'popup'; $link = new moodle_url($url); $action = new popup_action('click', $link, $name, $options); - $actionlink = $this->renderer->action_link($link, $linkname, $action, ['role' => 'link', 'title' => $title], + $actionlink = $this->renderer->action_link($link, $linkname, $action, ['title' => $title], new pix_icon('t/print', $title)); $navbar->printaction = $actionlink; $this->page->add_to_page('navigationbar', $this->renderer->navigationbar($navbar)); @@ -2358,6 +2716,7 @@ public function survey_results_navbar_alpha($currrid, $currentgroupid, $cm, $byr $respcols = new stdClass(); for ($i = 0; $i < $colnumber; $i++) { $colname = 'respondentscolumn'.$i; + $respcols->$colname = (object)['respondentlink' => []]; for ($j = 0; $j < $lines; $j++) { $respcols->{$colname}->respondentlink[] = $resparr[$a]; $a++; @@ -2374,7 +2733,15 @@ public function survey_results_navbar_alpha($currrid, $currentgroupid, $cm, $byr } } - // Display responses for current user (your responses). + /** + * Display responses for current user (your responses). + * @param int $currrid + * @param int $userid + * @param int $instance + * @param array $resps + * @param string $reporttype + * @param string $sid + */ public function survey_results_navbar_student($currrid, $userid, $instance, $resps, $reporttype='myreport', $sid='') { global $DB; $stranonymous = get_string('anonymous', 'questionnaire'); @@ -2438,33 +2805,21 @@ public function survey_results_navbar_student($currrid, $userid, $instance, $res $this->page->add_to_page('bottomnavigationbar', $this->renderer->usernavigationbar($navbar)); } - /* {{{ proto string survey_results(int surveyid, int precision, bool show_totals, int question_id, - * array choice_ids, int response_id) - Builds HTML for the results for the survey. If a - question id and choice id(s) are given, then the results - are only calculated for respodants who chose from the - choice ids for the given question id. - Returns empty string on sucess, else returns an error - string. */ - - public function survey_results($precision = 1, $showtotals = 1, $qid = '', $cids = '', $rid = '', - $uid=false, $currentgroupid='', $sort='') { + /** + * Builds HTML for the results for the survey. If a question id and choice id(s) are given, then the results are only calculated + * for respodants who chose from the choice ids for the given question id. Returns empty string on success, else returns an + * error string. + * @param string $rid + * @param bool $uid + * @param bool $pdf + * @param string $currentgroupid + * @param string $sort + * @return string|void + */ + public function survey_results($rid = '', $uid=false, $pdf = false, $currentgroupid='', $sort='') { global $SESSION, $DB; $SESSION->questionnaire->noresponses = false; - if (empty($precision)) { - $precision = 1; - } - if ($showtotals === '') { - $showtotals = 1; - } - - if (is_int($cids)) { - $cids = array($cids); - } - if (is_string($cids)) { - $cids = preg_split("/ /", $cids); // Turn space seperated list into array. - } // Build associative array holding whether each question // type has answer choices or not and the table the answers are in @@ -2537,9 +2892,9 @@ public function survey_results($precision = 1, $showtotals = 1, $qid = '', $cids $this->survey_results_navbar($rid); } - $this->page->add_to_page('title', clean_text($this->survey->title)); + $this->page->add_to_page('title', format_string($this->survey->title)); if ($this->survey->subtitle) { - $this->page->add_to_page('subtitle', clean_text($this->survey->subtitle)); + $this->page->add_to_page('subtitle', format_string($this->survey->subtitle)); } if ($this->survey->info) { $infotext = file_rewrite_pluginfile_urls($this->survey->info, 'pluginfile.php', @@ -2555,27 +2910,41 @@ public function survey_results($precision = 1, $showtotals = 1, $qid = '', $cids if ($question->type_id == QUESPAGEBREAK) { continue; } - $this->page->add_to_page('responses', $this->renderer->container_start('qn-container')); - if ($question->type_id != QUESSECTIONTEXT) { + if ($question->is_numbered()) { $qnum++; + } + if (!$pdf) { + $this->page->add_to_page('responses', $this->renderer->container_start('qn-container')); $this->page->add_to_page('responses', $this->renderer->container_start('qn-info')); - if ($question->type_id != QUESSECTIONTEXT) { + if ($question->is_numbered()) { $this->page->add_to_page('responses', $this->renderer->heading($qnum, 2, 'qn-number')); } $this->page->add_to_page('responses', $this->renderer->container_end()); // End qn-info. + $this->page->add_to_page('responses', $this->renderer->container_start('qn-content')); } - $this->page->add_to_page('responses', $this->renderer->container_start('qn-content')); // If question text is "empty", i.e. 2 non-breaking spaces were inserted, do not display any question text. if ($question->content == '

  

') { $question->content = ''; } - $this->page->add_to_page('responses', - $this->renderer->container(format_text(file_rewrite_pluginfile_urls($question->content, 'pluginfile.php', + if ($pdf) { + $response = new stdClass(); + if ($question->is_numbered()) { + $response->qnum = $qnum; + } + $response->qcontent = format_text(file_rewrite_pluginfile_urls($question->content, 'pluginfile.php', $question->context->id, 'mod_questionnaire', 'question', $question->id), - FORMAT_HTML, ['noclean' => true]), 'qn-question')); - $this->page->add_to_page('responses', $this->renderer->results_output($question, $rids, $sort, $anonymous)); - $this->page->add_to_page('responses', $this->renderer->container_end()); // End qn-content. - $this->page->add_to_page('responses', $this->renderer->container_end()); // End qn-container. + FORMAT_HTML, ['noclean' => true]); + $response->results = $this->renderer->results_output($question, $rids, $sort, $anonymous, $pdf); + $this->page->add_to_page('responses', $response); + } else { + $this->page->add_to_page('responses', + $this->renderer->container(format_text(file_rewrite_pluginfile_urls($question->content, 'pluginfile.php', + $question->context->id, 'mod_questionnaire', 'question', $question->id), + FORMAT_HTML, ['noclean' => true]), 'qn-question')); + $this->page->add_to_page('responses', $this->renderer->results_output($question, $rids, $sort, $anonymous)); + $this->page->add_to_page('responses', $this->renderer->container_end()); // End qn-content. + $this->page->add_to_page('responses', $this->renderer->container_end()); // End qn-container. + } } return; @@ -2583,12 +2952,9 @@ public function survey_results($precision = 1, $showtotals = 1, $qid = '', $cids /** * Get unique list of question types used in the current survey. - * - * @author: Guy Thomas - * @param int $surveyid + * author: Guy Thomas * @param bool $uniquebytable * @return array - * @throws moodle_exception */ protected function get_survey_questiontypes($uniquebytable = false) { @@ -2623,22 +2989,26 @@ protected function choice_types() { /** * Return all the fields to be used for users in questionnaire sql. - * - * @author: Guy Thomas + * author: Guy Thomas * @return array|string */ protected function user_fields() { - $userfieldsarr = get_all_user_name_fields(); + if (class_exists('\core_user\fields')) { + $userfieldsarr = \core_user\fields::get_name_fields(); + } else { + $userfieldsarr = get_all_user_name_fields(); + } $userfieldsarr = array_merge($userfieldsarr, ['username', 'department', 'institution']); return $userfieldsarr; } /** * Get all survey responses in one go. - * - * @author: Guy Thomas + * author: Guy Thomas * @param string $rid * @param string $userid + * @param bool $groupid + * @param int $showincompletes * @return array */ protected function get_survey_all_responses($rid = '', $userid = '', $groupid = false, $showincompletes = 0) { @@ -2649,25 +3019,25 @@ protected function get_survey_all_responses($rid = '', $userid = '', $groupid = // If a questionnaire is "public", and this is the master course, need to get responses from all instances. if ($this->survey_is_public_master()) { - $qids = array_keys($DB->get_records('questionnaire', ['sid' => $this->sid], 'id')); + $qids = array_keys($DB->get_records('questionnaire', ['sid' => $this->sid], 'id') ?? []); } else { $qids = $this->id; } foreach ($uniquetypes as $type) { - $question = \mod_questionnaire\question\base::question_builder($type); - if (!isset($question->response)) { + $question = \mod_questionnaire\question\question::question_builder($type); + if (!isset($question->responsetype)) { continue; } $allresponsessql .= $allresponsessql == '' ? '' : ' UNION ALL '; - list ($sql, $params) = $question->response->get_bulk_sql($qids, $rid, $userid, $groupid, $showincompletes); + list ($sql, $params) = $question->responsetype->get_bulk_sql($qids, $rid, $userid, $groupid, $showincompletes); $allresponsesparams = array_merge($allresponsesparams, $params); $allresponsessql .= $sql; } $allresponsessql .= " ORDER BY usrid, id"; $allresponses = $DB->get_recordset_sql($allresponsessql, $allresponsesparams); - return $allresponses; + return $allresponses ?? []; } /** @@ -2690,39 +3060,29 @@ public function survey_is_public_master() { /** * Process individual row for csv output - * @param array $outputrow output row + * @param array $row * @param stdClass $resprow resultset row * @param int $currentgroupid * @param array $questionsbyposition * @param int $nbinfocols * @param int $numrespcols + * @param array $options + * @param array $identityfields * @return array - * @throws Exception - * @throws coding_exception - * @throws dml_exception - * @throws dml_missing_record_exception - * @throws dml_multiple_records_exception */ protected function process_csv_row(array &$row, stdClass $resprow, $currentgroupid, array &$questionsbyposition, $nbinfocols, - $numrespcols, $showincompletes = 0) { + $numrespcols, + $options, + $identityfields) { global $DB; - static $config = null; // If using an anonymous response, map users to unique user numbers so that number of unique anonymous users can be seen. static $anonumap = []; - if ($config === null) { - $config = get_config('questionnaire', 'downloadoptions'); - } - $options = empty($config) ? array() : explode(',', $config); - if ($showincompletes == 1) { - $options[] = 'complete'; - } - $positioned = []; $user = new stdClass(); foreach ($this->user_fields() as $userfield) { @@ -2818,6 +3178,9 @@ protected function process_csv_row(array &$row, if (in_array('complete', $options)) { array_push($positioned, $resprow->complete); } + foreach ($identityfields as $field) { + array_push($positioned, $resprow->$field); + } for ($c = $nbinfocols; $c < $numrespcols; $c++) { if (isset($row[$c])) { @@ -2837,10 +3200,19 @@ protected function process_csv_row(array &$row, return $positioned; } - /* {{{ proto array survey_generate_csv(int surveyid) - Exports the results of a survey to an array. - */ - public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, $currentgroupid, $showincompletes = 0) { + /** + * Exports the results of a survey to an array. + * @param int $currentgroupid + * @param string $rid + * @param string $userid + * @param int $choicecodes + * @param int $choicetext + * @param int $showincompletes + * @param int $rankaverages + * @return array + */ + public function generate_csv($currentgroupid, $rid='', $userid='', $choicecodes=1, $choicetext=0, $showincompletes=0, + $rankaverages=0) { global $DB; raise_memory_limit('1G'); @@ -2859,11 +3231,18 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, if (in_array($option, array('response', 'submitted', 'id'))) { $columns[] = get_string($option, 'questionnaire'); $types[] = 0; + } else if ($option == 'useridentityfields') { + // Ignore option. + continue; } else { $columns[] = get_string($option); $types[] = 1; } } + $identityfields = $this->get_identity_fields($options); + foreach ($identityfields as $field) { + $columns[] = \core_user\fields::get_display_name($field); + } $nbinfocols = count($columns); $idtocsvmap = array( @@ -2877,11 +3256,12 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, '0', // 7: rating -> number '0', // 8: rate -> number '1', // 9: date -> string - '0' // 10: numeric -> number. + '0', // 10: numeric -> number. + '0', // 11: slider -> number. ); if (!$survey = $DB->get_record('questionnaire_survey', array('id' => $this->survey->id))) { - print_error ('surveynotexists', 'questionnaire'); + throw new \moodle_exception('surveynotexists', 'mod_questionnaire'); } // Get all responses for this survey in one go. @@ -2921,7 +3301,7 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, foreach ($this->questions as $question) { // Skip questions that aren't response capable. - if (!isset($question->response)) { + if (!isset($question->responsetype)) { continue; } // Establish the table's field names. @@ -2947,7 +3327,7 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, foreach ($choices as $choice) { $content = $choice->content; // If "Other" add a column for the actual "other" text entered. - if (preg_match('/^!other/', $content)) { + if (\mod_questionnaire\question\choice::content_is_other_choice($content)) { $col = $choice->name.'_'.$stringother; $columns[][$qpos] = $col; $questionidcols[][$qpos] = null; @@ -2975,7 +3355,7 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, array_push($types, '0'); // If "Other" add a column for the "other" checkbox. // Then add a column for the actual "other" text entered. - if (preg_match('/^!other/', $content)) { + if (\mod_questionnaire\question\choice::content_is_other_choice($content)) { $content = $stringother; $col = $choice->name.'->['.$content.']'; $columns[][$qpos] = $col; @@ -2991,7 +3371,7 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, $modality = ''; $content = $choice->content; $osgood = false; - if ($choice->precise == 3) { + if (\mod_questionnaire\question\rate::type_is_osgood_rate_scale($choice->precise)) { $osgood = true; } if (preg_match("/^[0-9]{1,3}=/", $content, $ndd)) { @@ -3076,19 +3456,65 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, $formatoptions = new stdClass(); $formatoptions->filter = false; // To prevent any filtering in CSV output. + if ($rankaverages) { + $averages = []; + $rids = []; + $allresponsesrs2 = $this->get_survey_all_responses($rid, $userid, $currentgroupid); + foreach ($allresponsesrs2 as $responserow) { + if (!isset($rids[$responserow->rid])) { + $rids[$responserow->rid] = $responserow->rid; + } + } + } + // Get textual versions of responses, add them to output at the correct col position. $prevresprow = false; // Previous response row. $row = []; + if ($rankaverages) { + $averagerow = []; + } + $useridentityfields = []; foreach ($allresponsesrs as $responserow) { $rid = $responserow->rid; $qid = $responserow->question_id; + + // It's possible for a response to exist for a deleted question. Ignore these. + if (!isset($this->questions[$qid])) { + continue; + } + + if (!empty($identityfields)) { + // Get identity fields for user. + if (isset($useridentityfields[$responserow->userid])) { + $customfields = $useridentityfields[$responserow->userid]; + } else { + $customfields = self::get_user_identity_fields($this->context, $responserow->userid); + $useridentityfields[$responserow->userid] = $customfields; + } + + // Set profile fields for user in response row. + foreach ($identityfields as $field) { + $responserow->{$field} = $customfields->{$field}; + } + } + $question = $this->questions[$qid]; $qtype = intval($question->type_id); + if ($rankaverages) { + if ($qtype === QUESRATE) { + if (empty($averages[$qid])) { + $results = $this->questions[$qid]->responsetype->get_results($rids); + foreach ($results as $qresult) { + $averages[$qid][$qresult->id] = $qresult->average; + } + } + } + } $questionobj = $this->questions[$qid]; if ($prevresprow !== false && $prevresprow->rid !== $rid) { $output[] = $this->process_csv_row($row, $prevresprow, $currentgroupid, $questionsbyposition, - $nbinfocols, $numrespcols, $showincompletes); + $nbinfocols, $numrespcols, $options, $identityfields); $row = []; } @@ -3096,10 +3522,13 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, $key = $qid.'_'.$responserow->choice_id; $position = $questionpositions[$key]; if ($qtype === QUESRATE) { - $choicetxt = $responserow->rankvalue + 1; + $choicetxt = $responserow->rankvalue; + if ($rankaverages) { + $averagerow[$position] = $averages[$qid][$responserow->choice_id]; + } } else { $content = $choicesbyqid[$qid][$responserow->choice_id]->content; - if (preg_match('/^!other/', $content)) { + if (\mod_questionnaire\question\choice::content_is_other_choice($content)) { // If this is an "other" column, put the text entered in the next position. $row[$position + 1] = $responserow->response; $choicetxt = empty($responserow->choice_id) ? '0' : '1'; @@ -3128,9 +3557,9 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, } $content = $choicesbyqid[$qid][$responserow->choice_id]->content; - if (preg_match('/^!other/', $content)) { + if (\mod_questionnaire\question\choice::content_is_other_choice($content)) { // If this has an "other" text, use it. - $responsetxt = get_string('other', 'questionnaire'); + $responsetxt = \mod_questionnaire\question\choice::content_other_choice_display($content); $responsetxt1 = $responserow->response; } else if (($choicecodes == 1) && ($choicetext == 1)) { $responsetxt = $c.' : '.$content; @@ -3155,6 +3584,7 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, $row[$position] = $responsetxt; // Check for "other" text and set it to the next position if present. if (!empty($responsetxt1)) { + $responsetxt1 = preg_replace("/[\r\n\t]/", ' ', $responsetxt1); $row[$position + 1] = $responsetxt1; unset($responsetxt1); } @@ -3166,7 +3596,22 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, if ($prevresprow !== false) { // Add final row to output. May not exist if no response data was ever present. $output[] = $this->process_csv_row($row, $prevresprow, $currentgroupid, $questionsbyposition, - $nbinfocols, $numrespcols, $showincompletes); + $nbinfocols, $numrespcols, $options, $identityfields); + } + + // Add averages row if appropriate. + if ($rankaverages) { + $summaryrow = []; + $summaryrow[0] = get_string('averagesrow', 'questionnaire'); + $i = 1; + for ($i = 1; $i < $nbinfocols; $i++) { + $summaryrow[$i] = ''; + } + $pos = 0; + for ($i = $nbinfocols; $i < $numrespcols; $i++) { + $summaryrow[$i] = isset($averagerow[$i]) ? $averagerow[$i] : ''; + } + $output[] = $summaryrow; } // Change table headers to incorporate actual question numbers. @@ -3211,7 +3656,6 @@ public function generate_csv($rid='', $userid='', $choicecodes=1, $choicetext=0, * @param int $movetopos The position to move question to. * */ - public function move_question($moveqid, $movetopos) { global $DB; @@ -3238,6 +3682,17 @@ public function move_question($moveqid, $movetopos) { return false; } + /** + * Render the response analysis page. + * @param int $rid + * @param array $resps + * @param bool $compare + * @param bool $isgroupmember + * @param bool $allresponses + * @param int $currentgroupid + * @param array $filteredsections + * @return array|string + */ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allresponses, $currentgroupid, $filteredsections = null) { global $DB, $CFG; @@ -3252,10 +3707,16 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre $action = optional_param('action', 'vall', PARAM_ALPHA); - if ($resp = $DB->get_record('questionnaire_response', ['id' => $rid]) ) { + $resp = $DB->get_record('questionnaire_response', ['id' => $rid]); + if (!empty($resp)) { $userid = $resp->userid; - if ($user = $DB->get_record('user', ['id' => $userid])) { - $ruser = fullname($user); + $user = $DB->get_record('user', ['id' => $userid]); + if (!empty($user)) { + if ($this->respondenttype == 'anonymous') { + $ruser = '- ' . get_string('anonymous', 'questionnaire') . ' -'; + } else { + $ruser = fullname($user); + } } } // Available group modes (0 = no groups; 1 = separate groups; 2 = visible groups). @@ -3350,11 +3811,12 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre $sectionlabel = $fbsections[$sectionid]->sectionlabel; $sectionheading = $fbsections[$sectionid]->sectionheading; - $feedbacks = $DB->get_records('questionnaire_feedback', ['sectionid' => $sectionid]); $labels = array(); - foreach ($feedbacks as $feedback) { - if ($feedback->feedbacklabel != '') { - $labels[] = $feedback->feedbacklabel; + if ($feedbacks = $DB->get_records('questionnaire_feedback', ['sectionid' => $sectionid])) { + foreach ($feedbacks as $feedback) { + if ($feedback->feedbacklabel != '') { + $labels[] = $feedback->feedbacklabel; + } } } $feedback = $DB->get_record_select('questionnaire_feedback', @@ -3392,8 +3854,8 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre $usergraph = get_config('questionnaire', 'usergraph'); if ($usergraph && $this->survey->chart_type) { $this->page->add_to_page('feedbackcharts', - draw_chart ($feedbacktype = 'global', $this->survey->chart_type, $labels, - $score, $allscore, $sectionlabel, $groupname, $allresponses)); + draw_chart ($feedbacktype = 'global', $labels, $groupname, + $allresponses, $this->survey->chart_type, $score, $allscore, $sectionlabel)); } // Display class or group score. Pending chart library decision to display? // Find out if this feedback sectionlabel has a pipe separator. @@ -3406,6 +3868,7 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre $oppositeallscore = ' | '.$allscore[1].'%'; } if ($this->survey->feedbackscores) { + $table = $table ?? new html_table(); if ($compare) { $table->data[] = array($sectionlabel, $score[0].'%'.$oppositescore, $allscore[0].'%'.$oppositeallscore); } else { @@ -3447,13 +3910,13 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre foreach ($fbsections as $key => $fbsection) { if ($fbsection->section == $section) { $feedbacksectionid = $key; - $scorecalculation = unserialize($fbsection->scorecalculation); + $scorecalculation = section::decode_scorecalculation($fbsection->scorecalculation); if (empty($scorecalculation) && !is_array($scorecalculation)) { $scorecalculation = []; } $sectionheading = $fbsection->sectionheading; $imageid = $fbsection->id; - $chartlabels [$section] = $fbsection->sectionlabel; + $chartlabels[$section] = $fbsection->sectionlabel; } } foreach ($scorecalculation as $qid => $key) { @@ -3534,24 +3997,28 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre default: } - foreach ($allscore as $key => $sc) { - if (isset($chartlabels[$key])) { - $lb = explode("|", $chartlabels[$key]); - $oppositescore = ''; - $oppositeallscore = ''; - if (count($lb) > 1) { - $sectionlabel = $lb[0] . ' | ' . $lb[1]; - $oppositescore = ' | ' . $oppositescorepercent[$key] . '%'; - $oppositeallscore = ' | ' . $alloppositescorepercent[$key] . '%'; - } else { - $sectionlabel = $chartlabels[$key]; - } - // If all questions of $section are unseen then don't show feedbackscores for this section. - if ($compare && !is_nan($scorepercent[$key])) { - $table->data[] = array($sectionlabel, $scorepercent[$key] . '%' . $oppositescore, - $allscorepercent[$key] . '%' . $oppositeallscore); - } else if (isset($allscorepercent[$key]) && !is_nan($allscorepercent[$key])) { - $table->data[] = array($sectionlabel, $allscorepercent[$key] . '%' . $oppositeallscore); + if ($this->survey->feedbackscores) { + foreach ($allscore as $key => $sc) { + if (isset($chartlabels[$key])) { + $lb = explode("|", $chartlabels[$key]); + $oppositescore = ''; + $oppositeallscore = ''; + if (count($lb) > 1) { + $sectionlabel = $lb[0] . ' | ' . $lb[1]; + $oppositescore = ' | ' . $oppositescorepercent[$key] . '%'; + $oppositeallscore = ' | ' . $alloppositescorepercent[$key] . '%'; + } else { + $sectionlabel = $chartlabels[$key]; + } + // If all questions of $section are unseen then don't show feedbackscores for this section. + if ($compare && !is_nan($scorepercent[$key])) { + $table = $table ?? new html_table(); + $table->data[] = array($sectionlabel, $scorepercent[$key] . '%' . $oppositescore, + $allscorepercent[$key] . '%' . $oppositeallscore); + } else if (isset($allscorepercent[$key]) && !is_nan($allscorepercent[$key])) { + $table = $table ?? new html_table(); + $table->data[] = array($sectionlabel, $allscorepercent[$key] . '%' . $oppositeallscore); + } } } } @@ -3565,9 +4032,19 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre } if ($usergraph && $this->survey->chart_type) { - $this->page->add_to_page('feedbackcharts', - draw_chart($feedbacktype = 'sections', $this->survey->chart_type, array_values($chartlabels), - array_values($scorepercent), array_values($allscorepercent), $sectionlabel, $groupname, $allresponses)); + $this->page->add_to_page( + 'feedbackcharts', + draw_chart( + 'sections', + array_values($chartlabels), + $groupname, + $allresponses, + $this->survey->chart_type, + array_values($scorepercent), + array_values($allscorepercent), + $sectionlabel + ) + ); } if ($this->survey->feedbackscores) { $this->page->add_to_page('feedbackscores', html_writer::table($table)); @@ -3576,4 +4053,127 @@ public function response_analysis($rid, $resps, $compare, $isgroupmember, $allre return $feedbackmessages; } -} \ No newline at end of file + // Mobile support area. + + /** + * Save the data from the mobile app. + * @param int $userid + * @param int $sec + * @param bool $completed + * @param int $rid + * @param bool $submit + * @param string $action + * @param array $responses + * @return array + */ + public function save_mobile_data($userid, $sec, $completed, $rid, $submit, $action, array $responses) { + global $DB, $CFG; // Do not delete "$CFG". + + $ret = []; + $response = $this->build_response_from_appdata((object)$responses, $sec); + $response->sec = $sec; + $response->rid = $rid; + $response->id = $rid; + + if ($action == 'nextpage') { + $result = $this->next_page_action($response, $userid); + if (is_string($result)) { + $ret['warnings'] = $result; + } else { + $ret['nextpagenum'] = $result; + } + } else if ($action == 'previouspage') { + $ret['nextpagenum'] = $this->previous_page_action($response, $userid); + } else if (!$completed) { + // If reviewing a completed questionnaire, don't insert a response. + $msg = $this->response_check_format($response->sec, $response); + if (empty($msg)) { + $rid = $this->response_insert($response, $userid); + } else { + $ret['warnings'] = $msg; + $ret['response'] = $response; + } + } + + if ($submit && (!isset($ret['warnings']) || empty($ret['warnings']))) { + $this->commit_submission_response($rid, $userid); + } + return $ret; + } + + /** + * Get all of the areas that can have files. + * @return array + * @throws dml_exception + */ + public function get_all_file_areas() { + global $DB; + + $areas = []; + $areas['info'] = $this->sid; + $areas['thankbody'] = $this->sid; + + // Add question areas. + if (empty($this->questions)) { + $this->add_questions(); + } + $areas['question'] = []; + foreach ($this->questions as $question) { + $areas['question'][] = $question->id; + } + + // Add feedback areas. + $areas['feedbacknotes'] = $this->sid; + $fbsections = $DB->get_records('questionnaire_fb_sections', ['surveyid' => $this->sid]); + if (!empty($fbsections)) { + $areas['sectionheading'] = []; + foreach ($fbsections as $section) { + $areas['sectionheading'][] = $section->id; + $feedbacks = $DB->get_records('questionnaire_feedback', ['sectionid' => $section->id]); + if (!empty($feedbacks)) { + $areas['feedback'] = []; + foreach ($feedbacks as $feedback) { + $areas['feedback'][] = $feedback->id; + } + } + } + } + + return $areas; + } + + /** + * Gets the identity fields. + * + * @param array $options + * @return array + */ + protected function get_identity_fields($options) { + $fields = !in_array('useridentityfields', $options) || $this->respondenttype == 'anonymous' ? [] : + \core_user\fields::get_identity_fields($this->context); + return $fields; + } + + /** + * Gets the identity fields values for a user. + * + * @param object $context + * @param int $userid + * @return array + */ + public static function get_user_identity_fields($context, $userid) { + global $DB; + + $fields = \core_user\fields::for_identity($context); + [ + 'selects' => $selects, + 'joins' => $joins, + 'params' => $params + ] = (array)$fields->get_sql('u', false, '', '', false); + $sql = "SELECT $selects + FROM {user} u $joins + WHERE u.id = ?"; + $row = $DB->get_record_sql($sql, array_merge($params, [$userid])); + return $row; + } +} diff --git a/questions.php b/questions.php index 1156aad2..6175512c 100644 --- a/questions.php +++ b/questions.php @@ -14,28 +14,36 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * This page handles the main question editing screen. + * + * @package mod_questionnaire + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 Mike Churchward (mike.churchward@poetopensource.org) + */ + require_once("../../config.php"); require_once($CFG->dirroot.'/mod/questionnaire/questionnaire.class.php'); -require_once($CFG->dirroot.'/mod/questionnaire/classes/question/base.php'); // Needed for question type constants. +require_once($CFG->dirroot.'/mod/questionnaire/classes/question/question.php'); // Needed for question type constants. -$id = required_param('id', PARAM_INT); // Course module ID +$id = required_param('id', PARAM_INT); // Course module ID. $action = optional_param('action', 'main', PARAM_ALPHA); // Screen. -$qid = optional_param('qid', 0, PARAM_INT); // Question id. -$moveq = optional_param('moveq', 0, PARAM_INT); // Question id to move. -$delq = optional_param('delq', 0, PARAM_INT); // Question id to delete -$qtype = optional_param('type_id', 0, PARAM_INT); // Question type. +$qid = optional_param('qid', 0, PARAM_INT); // Question id. +$moveq = optional_param('moveq', 0, PARAM_INT); // Question id to move. +$delq = optional_param('delq', 0, PARAM_INT); // Question id to delete. +$qtype = optional_param('type_id', 0, PARAM_INT); // Question type. $currentgroupid = optional_param('group', 0, PARAM_INT); // Group id. if (! $cm = get_coursemodule_from_id('questionnaire', $id)) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } if (! $course = $DB->get_record("course", array("id" => $cm->course))) { - print_error('coursemisconf'); + throw new \moodle_exception('coursemisconf', 'mod_questionnaire'); } if (! $questionnaire = $DB->get_record("questionnaire", array("id" => $cm->instance))) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } require_course_login($course, true, $cm); @@ -50,18 +58,18 @@ $PAGE->set_url($url); $PAGE->set_context($context); -$questionnaire = new questionnaire(0, $questionnaire, $course, $cm); +$questionnaire = new questionnaire($course, $cm, 0, $questionnaire); // Add renderer and page objects to the questionnaire object for display use. $questionnaire->add_renderer($PAGE->get_renderer('mod_questionnaire')); $questionnaire->add_page(new \mod_questionnaire\output\questionspage()); if (!$questionnaire->capabilities->editquestions) { - print_error('nopermissions', 'error', 'mod:questionnaire:edit'); + throw new \moodle_exception('nopermissions', 'mod_questionnaire'); } $questionnairehasdependencies = $questionnaire->has_dependencies(); -$haschildren = []; +$dependants = null; if (!isset($SESSION->questionnaire)) { $SESSION->questionnaire = new stdClass(); } @@ -77,7 +85,7 @@ $questionnaireid = $questionnaire->id; // Need to reload questions before setting deleted question to 'y'. - $questions = $DB->get_records('questionnaire_question', ['surveyid' => $sid, 'deleted' => 'n'], 'id'); + $questions = $DB->get_records('questionnaire_question', ['surveyid' => $sid, 'deleted' => 'n'], 'id') ?? []; $DB->set_field('questionnaire_question', 'deleted', 'y', ['id' => $qid, 'surveyid' => $sid]); // Delete all dependency records for this question. @@ -112,7 +120,7 @@ // Log question deleted event. $context = context_module::instance($questionnaire->cm->id); - $questiontype = \mod_questionnaire\question\base::qtypename($questionnaire->questions[$qid]->type_id); + $questiontype = \mod_questionnaire\question\question::qtypename($questionnaire->questions[$qid]->type_id); $params = array( 'context' => $context, 'courseid' => $questionnaire->course->id, @@ -174,17 +182,16 @@ if ($qtype == QUESPAGEBREAK) { redirect($CFG->wwwroot.'/mod/questionnaire/questions.php?id='.$questionnaire->cm->id.'&delq='.$qid); } + + $action = "confirmdelquestion"; if ($questionnairehasdependencies) { // Important: due to possibly multiple parents per question // just remove the dependency and inform the user about it. - $haschildren = $questionnaire->get_all_dependants($qid); - } - if (count($haschildren) != 0) { - $action = "confirmdelquestionparent"; - } else { - $action = "confirmdelquestion"; + $dependants = $questionnaire->get_all_dependants($qid); + if (!(empty($dependants->directs) && empty($dependants->indirects))) { + $action = "confirmdelquestionparent"; + } } - } else if (isset($qformdata->editbutton)) { // Switch to edit question screen. $action = 'question'; @@ -213,7 +220,7 @@ $questionrec->surveyid = $qformdata->sid; $questionrec->type_id = QUESPAGEBREAK; $questionrec->content = 'break'; - $question = \mod_questionnaire\question\base::question_builder(QUESPAGEBREAK); + $question = \mod_questionnaire\question\question::question_builder(QUESPAGEBREAK); $question->add($questionrec); $reload = true; } else { @@ -289,7 +296,7 @@ // Log question created event. if (isset($qformdata)) { $context = context_module::instance($questionnaire->cm->id); - $questiontype = \mod_questionnaire\question\base::qtypename($qformdata->type_id); + $questiontype = \mod_questionnaire\question\question::qtypename($qformdata->type_id); $params = array( 'context' => $context, 'courseid' => $questionnaire->course->id, @@ -305,7 +312,7 @@ // Reload the form data if called for... if ($reload) { unset($questionsform); - $questionnaire = new questionnaire($questionnaire->id, null, $course, $cm); + $questionnaire = new questionnaire($course, $cm, $questionnaire->id, null); // Add renderer and page objects to the questionnaire object for display use. $questionnaire->add_renderer($PAGE->get_renderer('mod_questionnaire')); $questionnaire->add_page(new \mod_questionnaire\output\questionspage()); @@ -392,12 +399,14 @@ if ($action == "confirmdelquestionparent") { $strnum = get_string('position', 'questionnaire'); $qid = key($qformdata->removebutton); - // Show the dependencies and inform about the dependencies to be removed. - // Split dependencies in direct and indirect ones to separate for the confirm-dialogue. Only direct ones will be deleted. - // List direct dependencies. - $msg .= $questionnaire->renderer->dependency_warnings($haschildren->directs, 'directwarnings', $strnum); - // List indirect dependencies. - $msg .= $questionnaire->renderer->dependency_warnings($haschildren->indirects, 'indirectwarnings', $strnum); + if ($dependants) { + // Show the dependencies and inform about the dependencies to be removed. + // Split dependencies in direct and indirect ones to separate for the confirm-dialogue. + // Only direct ones will be deleted. List direct dependencies. + $msg .= $questionnaire->renderer->dependency_warnings($dependants->directs, 'directwarnings', $strnum); + // List indirect dependencies. + $msg .= $questionnaire->renderer->dependency_warnings($dependants->indirects, 'indirectwarnings', $strnum); + } } $questionnaire->page->add_to_page('formarea', $questionnaire->renderer->confirm($msg, $buttonyes, $buttonno)); @@ -405,4 +414,4 @@ $questionnaire->page->add_to_page('formarea', $questionsform->render()); } echo $questionnaire->renderer->render($questionnaire->page); -echo $questionnaire->renderer->footer(); \ No newline at end of file +echo $questionnaire->renderer->footer(); diff --git a/report.php b/report.php old mode 100644 new mode 100755 index 0b764b8a..d8d129e1 --- a/report.php +++ b/report.php @@ -14,6 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * The main report page for a questionnaire. + * + * @package mod_questionnaire + * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ require_once("../../config.php"); require_once($CFG->dirroot.'/mod/questionnaire/questionnaire.class.php'); @@ -26,6 +35,8 @@ $individualresponse = optional_param('individualresponse', false, PARAM_INT); $currentgroupid = optional_param('group', 0, PARAM_INT); // Groupid. $user = optional_param('user', '', PARAM_INT); +$outputtarget = optional_param('target', 'html', PARAM_ALPHA); // Default 'html'. Could be 'pdf'. + $userid = $USER->id; switch ($action) { case 'vallasort': @@ -42,36 +53,43 @@ if (!empty($SESSION->instance)) { $instance = $SESSION->instance; } else { - print_error('requiredparameter', 'questionnaire'); + throw new \moodle_exception('requiredparameter', 'mod_questionnaire'); } } $SESSION->instance = $instance; $usergraph = get_config('questionnaire', 'usergraph'); if (! $questionnaire = $DB->get_record("questionnaire", array("id" => $instance))) { - print_error('incorrectquestionnaire', 'questionnaire'); + throw new \moodle_exception('incorrectquestionnaire', 'mod_questionnaire'); } if (! $course = $DB->get_record("course", array("id" => $questionnaire->course))) { - print_error('coursemisconf'); + throw new \moodle_exception('coursemisconf', 'mod_questionnaire'); } if (! $cm = get_coursemodule_from_instance("questionnaire", $questionnaire->id, $course->id)) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } require_course_login($course, true, $cm); -$questionnaire = new questionnaire(0, $questionnaire, $course, $cm); +$questionnaire = new questionnaire($course, $cm, 0, $questionnaire); // Add renderer and page objects to the questionnaire object for display use. $questionnaire->add_renderer($PAGE->get_renderer('mod_questionnaire')); -$questionnaire->add_page(new \mod_questionnaire\output\reportpage()); +if ($outputtarget == 'pdf') { + if ($action == 'vresp') { + $questionnaire->add_page(new \mod_questionnaire\output\responsepagepdf()); + } else { + $questionnaire->add_page(new \mod_questionnaire\output\reportpagepdf()); + } +} else { // Default to HTML. + $questionnaire->add_page(new \mod_questionnaire\output\reportpage()); +} // If you can't view the questionnaire, or can't view a specified response, error out. $context = context_module::instance($cm->id); -if (!has_capability('mod/questionnaire:readallresponseanytime', $context) && - !($questionnaire->capabilities->view && $questionnaire->can_view_response($rid))) { +if (!$questionnaire->can_view_all_responses() && !$individualresponse) { // Should never happen, unless called directly by a snoop... - print_error('nopermissions', 'moodle', $CFG->wwwroot.'/mod/questionnaire/view.php?id='.$cm->id); + throw new \moodle_exception('nopermissions', 'mod_questionnaire'); } $questionnaire->canviewallgroups = has_capability('moodle/site:accessallgroups', $context); @@ -105,6 +123,9 @@ $PAGE->set_url($url); $PAGE->set_context($context); +if ($outputtarget == 'print') { + $PAGE->set_pagelayout('popup'); +} // Tab setup. if (!isset($SESSION->questionnaire)) { @@ -187,19 +208,18 @@ switch ($action) { case 'dresp': // Delete individual response? Ask for confirmation. - require_capability('mod/questionnaire:deleteresponses', $context); if (empty($questionnaire->survey)) { $id = $questionnaire->survey; notify ("questionnaire->survey = /$id/"); - print_error('surveynotexists', 'questionnaire'); + throw new \moodle_exception('surveynotexists', 'mod_questionnaire'); } else if ($questionnaire->survey->courseid != $course->id) { - print_error('surveyowner', 'questionnaire'); + throw new \moodle_exception('surveyowner', 'mod_questionnaire'); } else if (!$rid || !is_numeric($rid)) { - print_error('invalidresponse', 'questionnaire'); + throw new \moodle_exception('invalidresponse', 'mod_questionnaire'); } else if (!($resp = $DB->get_record('questionnaire_response', array('id' => $rid)))) { - print_error('invalidresponserecord', 'questionnaire'); + throw new \moodle_exception('invalidresponserecord', 'mod_questionnaire'); } $ruser = false; @@ -247,7 +267,7 @@ case 'delallresp': // Delete all responses? Ask for confirmation. require_capability('mod/questionnaire:deleteresponses', $context); - if ($DB->count_records('questionnaire_response', array('questionnaireid' => $questionnaire->id, 'complete' => 'y'))) { + if (!empty($respsallparticipants)) { // Print the page header. $PAGE->set_title(get_string('deletingresp', 'questionnaire')); @@ -281,17 +301,16 @@ break; case 'dvresp': // Delete single response. Do it! - require_capability('mod/questionnaire:deleteresponses', $context); if (empty($questionnaire->survey)) { - print_error('surveynotexists', 'questionnaire'); + throw new \moodle_exception('surveynotexists', 'mod_questionnaire'); } else if ($questionnaire->survey->courseid != $course->id) { - print_error('surveyowner', 'questionnaire'); + throw new \moodle_exception('surveyowner', 'mod_questionnaire'); } else if (!$rid || !is_numeric($rid)) { - print_error('invalidresponse', 'questionnaire'); + throw new \moodle_exception('invalidresponse', 'mod_questionnaire'); } else if (!($response = $DB->get_record('questionnaire_response', array('id' => $rid)))) { - print_error('invalidresponserecord', 'questionnaire'); + throw new \moodle_exception('invalidresponserecord', 'mod_questionnaire'); } if (questionnaire_delete_response($response, $questionnaire)) { @@ -330,13 +349,12 @@ break; case 'dvallresp': // Delete all responses in questionnaire (or group). Do it! - require_capability('mod/questionnaire:deleteresponses', $context); if (empty($questionnaire->survey)) { - print_error('surveynotexists', 'questionnaire'); + throw new \moodle_exception('surveynotexists', 'mod_questionnaire'); } else if ($questionnaire->survey->courseid != $course->id) { - print_error('surveyowner', 'questionnaire'); + throw new \moodle_exception('surveyowner', 'mod_questionnaire'); } // Available group modes (0 = no groups; 1 = separate groups; 2 = visible groups). @@ -402,7 +420,6 @@ break; case 'dwnpg': // Download page options. - require_capability('mod/questionnaire:downloadresponses', $context); $PAGE->set_title(get_string('questionnairereport', 'questionnaire')); @@ -432,26 +449,21 @@ } $output = ''; $output .= "

\n"; - $output .= $questionnaire->renderer->help_icon('downloadtextformat', 'questionnaire'); - $output .= ' ' . (get_string('downloadtextformat', 'questionnaire')) . ': ' . - get_string('responses', 'questionnaire').' '.$groupname; - $output .= $questionnaire->renderer->heading(get_string('textdownloadoptions', 'questionnaire')); + $output .= html_writer::tag('h2', (get_string('downloadtextformat', 'questionnaire')) + . ': ' . get_string('responses', 'questionnaire') . ' ' . + $groupname . $questionnaire->renderer->help_icon('downloadtextformat', 'questionnaire')); + $output .= $questionnaire->renderer->heading(get_string('textdownloadoptions', 'questionnaire'), 3); $output .= $questionnaire->renderer->box_start(); - $output .= "
wwwroot}/mod/questionnaire/report.php\" method=\"GET\">\n"; - $output .= "\n"; - $output .= "\n"; - $output .= "\n"; - $output .= "\n"; - $output .= "\n"; - $output .= html_writer::checkbox('choicecodes', 1, true, get_string('includechoicecodes', 'questionnaire')); - $output .= "
\n"; - $output .= html_writer::checkbox('choicetext', 1, true, get_string('includechoicetext', 'questionnaire')); - $output .= "
\n"; - $output .= html_writer::checkbox('complete', 1, false, get_string('includeincomplete', 'questionnaire')); - $output .= "
\n"; - $output .= "
\n"; - $output .= "\n"; - $output .= "
\n"; + $downloadparams = [ + 'instance' => $instance, + 'user' => $user, + 'sid' => $sid, + 'action' => 'dfs', + 'group' => $currentgroupid + ]; + $extrafields = $questionnaire->renderer->render_from_template('mod_questionnaire/extrafields', []); + $output .= $questionnaire->renderer->download_dataformat_selector(get_string('downloadtypes', 'questionnaire'), + 'report.php', 'downloadformat', $downloadparams, $extrafields); $output .= $questionnaire->renderer->box_end(); $questionnaire->page->add_to_page('respondentinfo', $output); @@ -471,35 +483,58 @@ exit(); break; - case 'dcsv': // Download responses data as text (cvs) format. + case 'dfs': require_capability('mod/questionnaire:downloadresponses', $context); - require_once($CFG->libdir.'/dataformatlib.php'); - // Use the questionnaire name as the file name. Clean it and change any non-filename characters to '_'. $name = clean_param($questionnaire->name, PARAM_FILE); $name = preg_replace("/[^A-Z0-9]+/i", "_", trim($name)); $choicecodes = optional_param('choicecodes', '0', PARAM_INT); - $choicetext = optional_param('choicetext', '0', PARAM_INT); - $showincompletes = optional_param('complete', '0', PARAM_INT); - $output = $questionnaire->generate_csv('', $user, $choicecodes, $choicetext, $currentgroupid, $showincompletes); - - // Use Moodle's core download function for outputting csv. - $rowheaders = array_shift($output); - download_as_dataformat($name, 'csv', $rowheaders, $output); + $choicetext = optional_param('choicetext', '0', PARAM_INT); + $showincompletes = optional_param('complete', '0', PARAM_INT); + $rankaverages = optional_param('rankaverages', '0', PARAM_INT); + $dataformat = optional_param('downloadformat', '', PARAM_ALPHA); + $emailroles = optional_param('emailroles', 0, PARAM_INT); + $emailextra = optional_param('emailextra', '', PARAM_RAW); + + $output = $questionnaire->generate_csv($currentgroupid, '', $user, $choicecodes, $choicetext, $showincompletes, + $rankaverages); + + $columns = $output[0]; + unset($output[0]); + + // Check if email report was selected. + $emailreport = optional_param('emailreport', '', PARAM_ALPHA); + if (empty($emailreport)) { + \core\dataformat::download_data($name, $dataformat, $columns, $output); + } else { + // Emailreport button selected. + if (get_config('questionnaire', 'allowemailreporting') && (!empty($emailroles) || !empty($emailextra))) { + require_once('savefileformat.php'); + $users = !empty($emailroles) ? $questionnaire->get_notifiable_users($USER->id) : []; + $otheremails = explode(',', $emailextra); + if (!empty($users) || !empty($otheremails)) { + $thisurl = new moodle_url('report.php', + ['instance' => $instance, 'action' => 'dwnpg', 'group' => $currentgroupid]); + save_as_dataformat($name, $dataformat, $columns, $output, $users, $otheremails, $thisurl); + } + } else { + redirect(new moodle_url('report.php', ['instance' => $instance, 'action' => 'dwnpg', 'group' => $currentgroupid]), + get_string('emailsnotspecified', 'questionnaire')); + } + } exit(); break; case 'vall': // View all responses. case 'vallasort': // View all responses sorted in ascending order. case 'vallarsort': // View all responses sorted in descending order. - $PAGE->set_title(get_string('questionnairereport', 'questionnaire')); $PAGE->set_heading(format_string($course->fullname)); - echo $questionnaire->renderer->header(); if (!$questionnaire->capabilities->readallresponses && !$questionnaire->capabilities->readallresponseanytime) { + echo $questionnaire->renderer->header(); // Should never happen, unless called directly by a snoop. - print_error('nopermissions', '', '', get_string('viewallresponses', 'questionnaire')); + throw new \moodle_exception('nopermissions', 'mod_questionnaire'); // Finish the page. echo $questionnaire->renderer->footer($course); break; @@ -516,7 +551,9 @@ default: $SESSION->questionnaire->current_tab = 'valldefault'; } - include('tabs.php'); + if ($outputtarget != 'print') { + include('tabs.php'); + } $respinfo = ''; $resps = array(); @@ -584,30 +621,72 @@ 'courseid' => $course->id, 'other' => array('action' => $action, 'instance' => $instance, 'groupid' => $currentgroupid) ); - $event = \mod_questionnaire\event\all_responses_viewed::create($params); - $event->trigger(); - $respinfo .= get_string('viewallresponses', 'questionnaire').'. '.$groupname.'. '; - $strsort = get_string('order_'.$sort, 'questionnaire'); - $respinfo .= $strsort; - $respinfo .= $questionnaire->renderer->help_icon('orderresponses', 'questionnaire'); - $questionnaire->page->add_to_page('respondentinfo', $respinfo); + if ($outputtarget == 'pdf') { + $pdf = questionnaire_report_start_pdf(); + if ($currentgroupid > 0) { + $groupname = get_string('group') . ': ' . groups_get_group_name($currentgroupid) . ''; + } else { + $groupname = '' . get_string('allparticipants') . ''; + } + $respinfo = get_string('viewallresponses', 'questionnaire') . '. ' . $groupname . '. '; + $strsort = get_string('order_' . $sort, 'questionnaire'); + $respinfo .= $strsort; + $questionnaire->page->add_to_page('respondentinfo', $respinfo); + $questionnaire->survey_results('', false, true, $currentgroupid, $sort); + $html = $questionnaire->renderer->render($questionnaire->page); + + // Supress any warnings. There is at least one error in the TCPF library at line 16749 where 'text-align' is + // not an array. + $errorreporting = error_reporting(0); + $pdf->writeHTML($html); + @$pdf->Output(clean_param($questionnaire->name, PARAM_FILE) . '.pdf', 'D'); + error_reporting($errorreporting); + + } else { // Default to HTML. + $event = \mod_questionnaire\event\all_responses_viewed::create($params); + $event->trigger(); - $ret = $questionnaire->survey_results(1, 1, '', '', '', false, $currentgroupid, $sort); + if ($outputtarget != 'print') { + $linkname = get_string('downloadpdf', 'mod_questionnaire'); + $link = new moodle_url('/mod/questionnaire/report.php', + ['action' => 'vall', 'instance' => $instance, 'group' => $currentgroupid, 'target' => 'pdf']); + $downpdficon = new pix_icon('f/pdf', $linkname); + $respinfo .= $questionnaire->renderer->action_link($link, null, null, null, $downpdficon); + + $linkname = get_string('print', 'mod_questionnaire'); + $link = new \moodle_url('/mod/questionnaire/report.php', + ['action' => 'vall', 'instance' => $instance, 'group' => $currentgroupid, 'target' => 'print']); + $htmlicon = new pix_icon('t/print', $linkname); + $options = ['menubar' => true, 'location' => false, 'scrollbars' => true, 'resizable' => true, + 'height' => 600, 'width' => 800, 'title' => $linkname]; + $name = 'popup'; + $action = new popup_action('click', $link, $name, $options); + $class = ''; + $respinfo .= $questionnaire->renderer->action_link($link, null, $action, + ['class' => $class, 'title' => $linkname], $htmlicon) . ' '; + + $respinfo .= get_string('viewallresponses', 'questionnaire') . '. ' . $groupname . '. '; + $strsort = get_string('order_' . $sort, 'questionnaire'); + $respinfo .= $strsort; + $respinfo .= $questionnaire->renderer->help_icon('orderresponses', 'questionnaire'); + $questionnaire->page->add_to_page('respondentinfo', $respinfo); + } - echo $questionnaire->renderer->render($questionnaire->page); + $ret = $questionnaire->survey_results('', false, false, $currentgroupid, $sort); - // Finish the page. - echo $questionnaire->renderer->footer($course); + echo $questionnaire->renderer->header(); + echo $questionnaire->renderer->render($questionnaire->page); + echo $questionnaire->renderer->footer($course); + } break; case 'vresp': // View by response. - default: if (empty($questionnaire->survey)) { - print_error('surveynotexists', 'questionnaire'); + throw new \moodle_exception('surveynotexists', 'mod_questionnaire'); } else if ($questionnaire->survey->courseid != $course->id) { - print_error('surveyowner', 'questionnaire'); + throw new \moodle_exception('surveyowner', 'mod_questionnaire'); } $ruser = false; $noresponses = false; @@ -674,28 +753,48 @@ $rid = $rids[0]; } - // Print the page header. - $PAGE->set_title(get_string('questionnairereport', 'questionnaire')); - $PAGE->set_heading(format_string($course->fullname)); - echo $questionnaire->renderer->header(); - - // Print the tabs. - if ($byresponse) { - $SESSION->questionnaire->current_tab = 'vrespsummary'; - } - if ($individualresponse) { - $SESSION->questionnaire->current_tab = 'individualresp'; - } - include('tabs.php'); - - // Print the main part of the page. - // TODO provide option to select how many columns and/or responses per page. - if ($noresponses) { $questionnaire->page->add_to_page('respondentinfo', - get_string('group').' '.groups_get_group_name($currentgroupid).': '. + get_string('group') . ' ' . groups_get_group_name($currentgroupid) . ': ' . get_string('noresponses', 'questionnaire')); - } else { + + } else if ($outputtarget == 'pdf') { + $pdf = questionnaire_report_start_pdf(); + if ($currentgroupid > 0) { + $groupname = get_string('group') . ': ' . groups_get_group_name($currentgroupid) . ''; + } else { + $groupname = '' . get_string('allparticipants') . ''; + } + if (!$byresponse) { // Show respondents individual responses. + $questionnaire->view_response($rid, '', $resps, true, true, false, $currentgroupid, $outputtarget); + } + $html = $questionnaire->renderer->render($questionnaire->page); + // Supress any warnings. There is at least one error in the TCPF library at line 16749 where 'text-align' is + // not an array. + $errorreporting = error_reporting(0); + $pdf->writeHTML($html); + @$pdf->Output(clean_param($questionnaire->name, PARAM_FILE), 'D'); + error_reporting($errorreporting); + + } else { // Default to HTML. + // Print the page header. + $PAGE->set_title(get_string('questionnairereport', 'questionnaire')); + $PAGE->set_heading(format_string($course->fullname)); + + // Print the tabs. + if ($byresponse) { + $SESSION->questionnaire->current_tab = 'vrespsummary'; + } + if ($individualresponse) { + $SESSION->questionnaire->current_tab = 'individualresp'; + } + if ($outputtarget == 'html') { + include('tabs.php'); + } + + // Print the main part of the page. + // TODO provide option to select how many columns and/or responses per page. + $groupname = get_string('group').': '.groups_get_group_name($currentgroupid).''; if ($currentgroupid == 0 ) { $groupname = get_string('allparticipants'); @@ -708,15 +807,47 @@ $respinfo .= $questionnaire->renderer->box_end(); $questionnaire->page->add_to_page('respondentinfo', $respinfo); } - $questionnaire->survey_results_navbar_alpha($rid, $currentgroupid, $cm, $byresponse); + if ($outputtarget == 'html') { + $questionnaire->survey_results_navbar_alpha($rid, $currentgroupid, $cm, $byresponse); + } if (!$byresponse) { // Show respondents individual responses. - $questionnaire->view_response($rid, '', false, $resps, true, true, false, $currentgroupid); + $questionnaire->view_response($rid, '', $resps, true, true, false, $currentgroupid, $outputtarget); } + echo $questionnaire->renderer->header(); + echo $questionnaire->renderer->render($questionnaire->page); + echo $questionnaire->renderer->footer($course); } - - echo $questionnaire->renderer->render($questionnaire->page); - - // Finish the page. - echo $questionnaire->renderer->footer($course); break; } + +/** + * Return a pdf object. + * @return pdf + */ +function questionnaire_report_start_pdf() { + global $CFG; + + require_once($CFG->libdir . '/pdflib.php'); + $pdf = new pdf(); + $pdf->SetCreator(PDF_CREATOR); + $pdf->SetAuthor('Moodle Questionnaire'); + $pdf->SetTitle('All responses'); + $pdf->setPrintHeader(false); + // Set default monospaced font. + $pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED); + + // Set margins. + $pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT); + $pdf->SetHeaderMargin(PDF_MARGIN_HEADER); + $pdf->SetFooterMargin(PDF_MARGIN_FOOTER); + + // Set auto page breaks. + $pdf->SetAutoPageBreak(true, PDF_MARGIN_BOTTOM); + + // Set image scale factor. + $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO); + // Set background color for headings. + $pdf->SetFillColor(238, 238, 238); + $pdf->AddPage('L'); + return $pdf; +} diff --git a/savefileformat.php b/savefileformat.php new file mode 100644 index 00000000..cb01d211 --- /dev/null +++ b/savefileformat.php @@ -0,0 +1,105 @@ +. + +/** + * savefileformat.php - Replaces dataformatlib.php to capture output into files. + * + * @package mod_questionnaire + * @copyright 2019 onward Mike Churchward (mike.churchward@poetopensource.org) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Sends a formated data file to the browser and optionally a file. This is needed until the main data format API provides a way + * to ouput a file as well as stream to the browser. This file relies on capturing output buffers (ugly hack). + * + * @param string $filename The base filename without an extension + * @param string $dataformat A dataformat name + * @param array $columns An ordered map of column keys and labels + * @param Iterator $iterator An iterator over the records, usually a RecordSet + * @param array $users + * @param array $emails + * @param string $redirect + */ +function save_as_dataformat($filename, $dataformat, $columns, $iterator, $users = [], $emails = [], $redirect = '') { + global $CFG, $OUTPUT; + + $classname = 'dataformat_' . $dataformat . '\writer'; + if (!class_exists($classname)) { + throw new coding_exception("Unable to locate dataformat/$dataformat/classes/writer.php"); + } + $format = new $classname; + + // The data format export could take a while to generate... + set_time_limit(0); + + // Close the session so that the users other tabs in the same session are not blocked. + \core\session\manager::write_close(); + + $format->set_filename($filename); + // File creation for any data format is initiated by "send_http_headers()". This is required. But, this also will cause the + // browser to respond with a "save / open" dialogue. To get rid of the dialogue, immediately retract the headers with + // "header_remove()". + $format->send_http_headers(); + header_remove(); + + // Start capturing output to write to a file. + ob_start(); + // This exists to support all dataformats - see MDL-56046. + if (method_exists($format, 'write_header')) { + debugging('The function write_header() does not support multiple sheets. In order to support multiple sheets you ' . + 'must implement start_output() and start_sheet() and remove write_header() in your dataformat.', DEBUG_DEVELOPER); + $format->write_header($columns); + } else { + $format->start_output(); + $format->start_sheet($columns); + } + $c = 0; + foreach ($iterator as $row) { + if ($row === null) { + continue; + } + $format->write_record($row, $c++); + } + // This exists to support all dataformats - see MDL-56046. + if (method_exists($format, 'write_footer')) { + debugging('The function write_footer() does not support multiple sheets. In order to support multiple sheets you ' . + 'must implement close_sheet() and close_output() and remove write_footer() in your dataformat.', DEBUG_DEVELOPER); + $format->write_footer($columns); + } else { + $format->close_sheet($columns); + $format->close_output(); + $output = ob_get_contents(); + $ext = $format->get_extension(); + $filepath = make_temp_directory('mod_questionnaire') . '/' . $filename . $ext; + $fp = fopen($filepath, 'wb'); + fwrite($fp, $output); + fclose($fp); + $subjecttext = get_string('summaryreportattached', 'questionnaire'); + foreach ($users as $user) { + email_to_user($user, $CFG->noreplyaddress, $subjecttext, $subjecttext, '', $filepath, $filename.$ext); + } + foreach ($emails as $email) { + $email = trim($email); + $user = (object)['id' => -10, 'email' => $email, 'firstname' => $email, 'lastname' => $email, 'mailformat' => 1]; + email_to_user($user, $CFG->noreplyaddress, $subjecttext, $subjecttext, '', $filepath, $filename.$ext); + } + unlink($filepath); + } + ob_end_clean(); + echo $OUTPUT->redirect_message($redirect, get_string('emailssent', 'questionnaire'), 3, false); +} diff --git a/settings.php b/settings.php index fede9974..6a21c51d 100644 --- a/settings.php +++ b/settings.php @@ -17,9 +17,10 @@ /** * Setting page for questionaire module * - * @package mod - * @subpackage questionnaire + * @package mod_questionnaire * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright 2016 onward Mike Churchward (mike.churchward@poetgroup.org) + * @author Mike Churchward */ defined('MOODLE_INTERNAL') || die; @@ -42,9 +43,13 @@ 'group' => get_string('group'), 'id' => get_string('id', 'questionnaire'), 'fullname' => get_string('fullname'), - 'username' => get_string('username') + 'username' => get_string('username'), + 'useridentityfields' => get_string('showuseridentity', 'admin') ); $settings->add(new admin_setting_configmultiselect('questionnaire/downloadoptions', get_string('textdownloadoptions', 'questionnaire'), '', array_keys($choices), $choices)); + + $settings->add(new admin_setting_configcheckbox('questionnaire/allowemailreporting', + get_string('configemailreporting', 'questionnaire'), get_string('configemailreportinglong', 'questionnaire'), 0)); } diff --git a/show_nonrespondents.php b/show_nonrespondents.php index ce51da6c..924fd98d 100644 --- a/show_nonrespondents.php +++ b/show_nonrespondents.php @@ -15,11 +15,11 @@ // along with Moodle. If not, see . /** - * + * Show the non-respondents to a questionnaire. * @author Joseph Rézeau (copied from feedback plugin show_nonrespondents by original author Andreas Grabs) + * @copyright 2016 Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package mod - * @subpackage questionnaire + * @package mod_questionnaire * */ @@ -38,8 +38,8 @@ $selectedanonymous = optional_param('selectedanonymous', '', PARAM_ALPHA); $perpage = optional_param('perpage', QUESTIONNAIRE_DEFAULT_PAGE_COUNT, PARAM_INT); // How many per page. $showall = optional_param('showall', false, PARAM_INT); // Should we show all users? -$sid = optional_param('sid', 0, PARAM_INT); -$qid = optional_param('qid', 0, PARAM_INT); +$sid = optional_param('sid', 0, PARAM_INT); +$qid = optional_param('qid', 0, PARAM_INT); $currentgroupid = optional_param('group', 0, PARAM_INT); // Groupid. if (!isset($SESSION->questionnaire)) { @@ -52,25 +52,25 @@ if ($id) { if (! $cm = get_coursemodule_from_id('questionnaire', $id)) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } if (! $course = $DB->get_record("course", array("id" => $cm->course))) { - print_error('coursemisconf'); + throw new \moodle_exception('coursemisconf', 'mod_questionnaire'); } if (! $questionnaire = $DB->get_record("questionnaire", array("id" => $cm->instance))) { - print_error('invalidcoursemodule'); + throw new \moodle_exception('invalidcoursemodule', 'mod_questionnaire'); } } if (!$context = context_module::instance($cm->id)) { - print_error('badcontext'); + throw new \moodle_exception('badcontext', 'mod_questionnaire'); } // We need the coursecontext to allow sending of mass mails. if (!$coursecontext = context_course::instance($course->id)) { - print_error('badcontext'); + throw new \moodle_exception('badcontext', 'mod_questionnaire'); } require_course_login($course, true, $cm); @@ -78,7 +78,7 @@ $url = new moodle_url('/mod/questionnaire/show_nonrespondents.php', array('id' => $cm->id)); $PAGE->set_url($url); -$questionnaire = new questionnaire($sid, $questionnaire, $course, $cm); +$questionnaire = new questionnaire($course, $cm, $sid, $questionnaire); // Add renderer and page objects to the questionnaire object for display use. $questionnaire->add_renderer($PAGE->get_renderer('mod_questionnaire')); @@ -89,7 +89,7 @@ $sid = $questionnaire->sid; if (($formdata = data_submitted()) && !confirm_sesskey()) { - print_error('invalidsesskey'); + throw new \moodle_exception('invalidsesskey', 'mod_questionnaire'); } require_capability('mod/questionnaire:viewsingleresponse', $context); @@ -149,16 +149,16 @@ foreach ($messageuser as $userid) { $senduser = $DB->get_record('user', array('id' => $userid)); $eventdata = new \core\message\message(); - $eventdata->courseid = $course->id; - $eventdata->name = 'message'; - $eventdata->component = 'mod_questionnaire'; - $eventdata->userfrom = $USER; - $eventdata->userto = $senduser; - $eventdata->subject = $subject; - $eventdata->fullmessage = html_to_text($htmlmessage); + $eventdata->courseid = $course->id; + $eventdata->name = 'message'; + $eventdata->component = 'mod_questionnaire'; + $eventdata->userfrom = $USER; + $eventdata->userto = $senduser; + $eventdata->subject = $subject; + $eventdata->fullmessage = html_to_text($htmlmessage); $eventdata->fullmessageformat = FORMAT_PLAIN; - $eventdata->fullmessagehtml = $htmlmessage; - $eventdata->smallmessage = ''; + $eventdata->fullmessagehtml = $htmlmessage; + $eventdata->smallmessage = ''; $good = $good && message_send($eventdata); } if (!empty($good)) { @@ -203,7 +203,11 @@ $tablecolumns = array('userpic', 'fullname'); // Extra columns copied from participants view. - $extrafields = get_extra_user_fields($context); + if (class_exists('\core_user\fields')) { + $extrafields = \core_user\fields::get_identity_fields(null, false); + } else { + $extrafields = get_extra_user_fields($context); + } $tableheaders = array(get_string('userpic'), get_string('fullnameuser')); if (in_array('email', $extrafields) || has_capability('moodle/course:viewhiddenuserfields', $context)) { @@ -243,10 +247,10 @@ $table->set_attribute('id', 'showentrytable'); $table->set_attribute('class', 'flexible generaltable generalbox'); $table->set_control_variables(array( - TABLE_VAR_SORT => 'ssort', - TABLE_VAR_IFIRST => 'sifirst', - TABLE_VAR_ILAST => 'silast', - TABLE_VAR_PAGE => 'spage' + TABLE_VAR_SORT => 'ssort', + TABLE_VAR_IFIRST => 'sifirst', + TABLE_VAR_ILAST => 'silast', + TABLE_VAR_PAGE => 'spage' )); $table->no_sorting('status'); @@ -271,7 +275,11 @@ $usedgroupid = false; } $nonrespondents = questionnaire_get_incomplete_users($cm, $sid, $usedgroupid); - $countnonrespondents = count($nonrespondents); + if (is_array($nonrespondents) || is_object($nonrespondents)) { + $countnonrespondents = count($nonrespondents); + } else { + $countnonrespondents = 0; + } $table->initialbars(false); @@ -299,16 +307,16 @@ $strnever = get_string('never'); $datestring = new stdClass(); -$datestring->year = get_string('year'); +$datestring->year = get_string('year'); $datestring->years = get_string('years'); -$datestring->day = get_string('day'); -$datestring->days = get_string('days'); -$datestring->hour = get_string('hour'); +$datestring->day = get_string('day'); +$datestring->days = get_string('days'); +$datestring->hour = get_string('hour'); $datestring->hours = get_string('hours'); -$datestring->min = get_string('min'); -$datestring->mins = get_string('mins'); -$datestring->sec = get_string('sec'); -$datestring->secs = get_string('secs'); +$datestring->min = get_string('min'); +$datestring->mins = get_string('mins'); +$datestring->sec = get_string('sec'); +$datestring->secs = get_string('secs'); if (!$nonrespondents) { $questionnaire->page->add_to_page('formarea', @@ -391,14 +399,16 @@ $questionnaire->renderer->box_start('mdl-align')); // Selection buttons container. $questionnaire->page->add_to_page('formarea', '
'); $questionnaire->page->add_to_page('formarea', - ' '); + ' '); $questionnaire->page->add_to_page('formarea', - ' '); + ' '); if ($resume) { if ($perpage >= $countnonrespondents) { $questionnaire->page->add_to_page('formarea', - ''."\n"); - $questionnaire->page->add_to_page('formarea', ''."\n"); + $questionnaire->page->add_to_page('formarea', + ''."\n"); } } @@ -459,13 +469,13 @@ $questionnaire->page->add_to_page('formarea', ''.get_string('send_message', 'questionnaire').''); $id = 'message' . '_id'; - $subjecteditor = '   '; $format = ''; $editor = editors_get_preferred_editor(); $editor->use_editor($id, questionnaire_get_editor_options($context)); $texteditor = html_writer::tag('div', html_writer::tag('textarea', $message, - array('id' => $id, 'name' => "message", 'rows' => '10', 'cols' => '60'))); + array('id' => $id, 'name' => "message", 'class' => "form-control", 'rows' => '10', 'cols' => '60'))); $questionnaire->page->add_to_page('formarea', ''); @@ -480,8 +490,8 @@ // Send button. $questionnaire->page->add_to_page('formarea', $questionnaire->renderer->box_start('mdl-left')); $questionnaire->page->add_to_page('formarea', '
'); - $questionnaire->page->add_to_page('formarea', - ''); + $questionnaire->page->add_to_page('formarea', ''); $questionnaire->page->add_to_page('formarea', '
'); $questionnaire->page->add_to_page('formarea', $questionnaire->renderer->box_end()); diff --git a/styles.css b/styles.css index 8070a693..41dcd324 100644 --- a/styles.css +++ b/styles.css @@ -101,15 +101,6 @@ td.selected { border-color: blue; } -#page-mod-questionnaire-complete .surveyTitle, -#page-mod-questionnaire-complete .surveySubtitle, -#page-mod-questionnaire-complete .addInfo { - clear: both; - margin: 0; - margin-bottom: 4px; - padding: 10px; -} - .surveyPage { background-color: #eee; border-bottom-color: #000; @@ -184,11 +175,87 @@ td.selected { display: inline; } +#page-mod-questionnaire-complete .notice .buttons input { + margin-bottom: 10px; +} + .floatprinticon { margin-top: -30px; float: right; } +#page-mod-questionnaire-complete .mod_questionnaire_controlbuttons { + text-align: center; + width: 100%; + position: relative; +} + +#page-mod-questionnaire-complete .mod_questionnaire_controlbuttons .control-button-prev { + float: left; +} + +#page-mod-questionnaire-complete .mod_questionnaire_controlbuttons .control-button-save { + margin-left: 20%; +} + +#page-mod-questionnaire-complete .mod_questionnaire_controlbuttons .control-button-prev + .control-button-save { + margin-left: 0; +} + +#page-mod-questionnaire-complete .mod_questionnaire_controlbuttons .control-button-next, +#page-mod-questionnaire-complete .mod_questionnaire_controlbuttons .control-button-submit { + float: right; +} + +#page-mod-questionnaire-complete .mod_questionnaire_controlbuttons input { + margin-right: 0; +} + +#page-mod-questionnaire-complete .mod_questionnaire_completepage .generalbox .notice { + padding: 0.5em 0 0.5em 0; +} + +/* progress bar styling */ + +#page-mod-questionnaire-complete .questionnaire-progressbar { + height: 14px; + margin-top: 5px; + border: 1px solid #dedede; + border-radius: 5px; + width: auto; + overflow: hidden; +} + +#page-mod-questionnaire-complete .questionnaire-progressbar-progress { + background-color: #1177d1; + height: 100%; + border-bottom-left-radius: 5px; + border-top-left-radius: 5px; +} + +#page-mod-questionnaire-complete .questionnaire-progressbar-info { + float: right; +} + +#page-mod-questionnaire-complete #questionnaire-progressbar-percent { + margin-left: 5px; + margin-right: 5px; + min-width: 25px; + text-align: center; +} + +#page-mod-questionnaire-complete .questionnaire-progressbar-wrapper { + margin-top: 15px; + margin-bottom: 15px; + margin-left: 10px; + height: auto; + overflow: hidden; +} + +#page-mod-questionnaire-complete .mod_questionnaire_completepage.generalbox .homelink ~ .homelink { + padding-top: 10px; +} + .qn-legend { float: left; font-size: inherit; @@ -262,6 +329,25 @@ td.selected { overflow: auto; } +.qn-answer input[type=radio] { + margin-left: 0.5em; +} + +.qn-answer > label + input[type=radio] { + margin-left: 0; +} + +.qn-answer > textarea, +.qn-content .editor_atto_wrap .editor_atto_content { + resize: vertical; +} + +.qn-answer textarea, +.qn-answer input[type="text"] { + box-sizing: border-box; + height: auto; +} + #notice .qn-question { margin: 0; } @@ -305,11 +391,6 @@ td.selected { color: red; display: none; } -.qn-container { - color: black; - display: inherit; - margin-left: 10px; -} #page-mod-questionnaire-fbsections .c0, #page-mod-questionnaire-fbsections .c1 { @@ -353,3 +434,112 @@ td.selected { #page-mod-questionnaire-questions #region-main .mform .fitem .felement { margin-bottom: 0; } + +.qn-indent { + margin-left: 20px; +} + +.mod_questionnaire_flex-container { + display: inline-flex; +} + +#page-mod-questionnaire-view .mod_questionnaire_viewpage div.complete, +#page-mod-questionnaire-view .mod_questionnaire_viewpage div.yourresponse, +#page-mod-questionnaire-view .mod_questionnaire_viewpage div.allresponses { + flex-grow: 1; + margin-right: 10px; +} + +#page-mod-questionnaire-questions #fitem_id_allchoices #id_allchoices, +#page-mod-questionnaire-questions #fitem_id_allnameddegrees #id_allnameddegrees { + resize: both; +} + +.path-mod-questionnaire .slidecontainer { + width: 100%; +} + +.path-mod-questionnaire .slider { + -webkit-appearance: none; + width: 100%; + outline: none; + opacity: 0.7; + -webkit-transition: .2s; + transition: opacity .2s; + float: left; + margin-top: 40px; +} +.path-mod-questionnaire .slider input { + width: 100%; +} + +.path-mod-questionnaire .slider:hover { + opacity: 1; +} + +.path-mod-questionnaire .slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 25px; + background: #04aa6d; + cursor: pointer; + border-radius: 50%; +} + +.path-mod-questionnaire .slider::-moz-range-thumb { + width: 25px; + height: 25px; + background: #04aa6d; + cursor: pointer; +} + +.path-mod-questionnaire .question-slider { + display: flex; + align-items: baseline; +} + +.path-mod-questionnaire .left-side-label { + text-align: right; + padding-right: 20px; + margin-top: 40px; + flex-grow: 1; +} + +.path-mod-questionnaire .right-side-label { + text-align: left; + padding-left: 20px; + margin-top: 40px; + flex-grow: 1; +} + +.path-mod-questionnaire .middle-side-content { + flex-grow: 8; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.path-mod-questionnaire .middle-side-label { + text-align: center; +} + +.path-mod-questionnaire .bubble { + background: #000; + color: white; + padding: 3px; + border-radius: 10px; + left: 50%; + transform: translate(-52%, -50px); + position: relative; + text-align: center; + width: 40px; +} +.path-mod-questionnaire .bubble::after { + content: ""; + position: absolute; + width: 2px; + height: 2px; + left: 50%; +} diff --git a/styles_app.css b/styles_app.css new file mode 100644 index 00000000..e752eca3 --- /dev/null +++ b/styles_app.css @@ -0,0 +1,17 @@ +span.mobileratequestion { + padding-left: 2em; + padding-right: 2em; +} + +.mod_questionnaire_slider .range-has-pin .range-pin { + -webkit-transform: translate3d(0, 0, 0) scale(1); + transform: translate3d(0, 0, 0) scale(1); +} +.mod_questionnaire_slider .range-has-pin::part(pin) { + -webkit-transform: translate3d(0, -24px, 0) scale(1); + transform: translate3d(0, -24px, 0) scale(1); +} + +ion-label.disabled { + opacity: 0.8 !important; +} \ No newline at end of file diff --git a/tabs.php b/tabs.php index 98f21a0c..dc560745 100644 --- a/tabs.php +++ b/tabs.php @@ -27,7 +27,7 @@ global $DB, $SESSION; $tabs = array(); -$row = array(); +$row = array(); $inactive = array(); $activated = array(); if (!isset($SESSION->questionnaire)) { @@ -38,7 +38,7 @@ // In a questionnaire instance created "using" a PUBLIC questionnaire, prevent anyone from editing settings, editing questions, // viewing all responses...except in the course where that PUBLIC questionnaire was originally created. -$owner = !empty($questionnaire->sid) && ($questionnaire->survey->courseid == $questionnaire->course->id); +$owner = $questionnaire->is_survey_owner(); if ($questionnaire->capabilities->manage && $owner) { $row[] = new tabobject('settings', $CFG->wwwroot.htmlspecialchars('/mod/questionnaire/qsettings.php?'. 'id='.$questionnaire->cm->id), get_string('advancedsettings')); @@ -89,7 +89,7 @@ get_string('myresponses', 'questionnaire')); if ($questionnaire->capabilities->downloadresponses) { $argstr2 = $argstr.'&action=dwnpg'; - $link = $CFG->wwwroot.htmlspecialchars('/mod/questionnaire/report.php?'.$argstr2); + $link = $CFG->wwwroot.htmlspecialchars('/mod/questionnaire/report.php?'.$argstr2); $row2[] = new tabobject('mydownloadcsv', $link, get_string('downloadtextformat', 'questionnaire')); } } else if (in_array($currenttab, array('mybyresponse', 'mysummary'))) { @@ -114,9 +114,10 @@ $canviewgroups = groups_has_membership($cm, $USER->id); } $canviewallgroups = has_capability('moodle/site:accessallgroups', $context); +$grouplogic = $canviewallgroups || $canviewgroups; +$resplogic = ($numresp > 0) && ($numselectedresps > 0); -if (($canviewallgroups || ($canviewgroups && $questionnaire->capabilities->readallresponseanytime)) - && $numresp > 0 && $owner && $numselectedresps > 0) { +if ($questionnaire->can_view_all_responses_anytime($grouplogic, $resplogic)) { $argstr = 'instance='.$questionnaire->id; $row[] = new tabobject('allreport', $CFG->wwwroot.htmlspecialchars('/mod/questionnaire/report.php?'. $argstr.'&action=vall'), get_string('viewallresponses', 'questionnaire')); @@ -165,7 +166,7 @@ if ($questionnaire->capabilities->downloadresponses) { $argstr2 = $argstr.'&action=dwnpg&group='.$currentgroupid; - $link = $CFG->wwwroot.htmlspecialchars('/mod/questionnaire/report.php?'.$argstr2); + $link = $CFG->wwwroot.htmlspecialchars('/mod/questionnaire/report.php?'.$argstr2); $row3[] = new tabobject('downloadcsv', $link, get_string('downloadtextformat', 'questionnaire')); } } @@ -182,14 +183,7 @@ } } -} else if ($canviewgroups && $questionnaire->capabilities->readallresponses && ($numresp > 0) && $canviewgroups && - // If resp_view is set to QUESTIONNAIRE_STUDENTVIEWRESPONSES_NEVER, then this will always be false. - ($questionnaire->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_ALWAYS || - ($questionnaire->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENCLOSED - && $questionnaire->is_closed()) || - ($questionnaire->resp_view == QUESTIONNAIRE_STUDENTVIEWRESPONSES_WHENANSWERED - && $usernumresp > 0 )) && - $questionnaire->is_survey_owner()) { +} else if ($questionnaire->can_view_all_responses_with_restrictions($usernumresp, $grouplogic, $resplogic)) { $argstr = 'instance='.$questionnaire->id.'&sid='.$questionnaire->sid; $row[] = new tabobject('allreport', $CFG->wwwroot.htmlspecialchars('/mod/questionnaire/report.php?'. $argstr.'&action=vall&group='.$currentgroupid), get_string('viewallresponses', 'questionnaire')); @@ -220,7 +214,7 @@ if ($questionnaire->capabilities->downloadresponses) { $argstr2 = $argstr.'&action=dwnpg'; - $link = htmlspecialchars('/mod/questionnaire/report.php?'.$argstr2); + $link = htmlspecialchars('/mod/questionnaire/report.php?'.$argstr2); $row2[] = new tabobject('downloadcsv', $link, get_string('downloadtextformat', 'questionnaire')); } if (count($row2) <= 1) { @@ -248,4 +242,4 @@ } $questionnaire->page->add_to_page('tabsarea', print_tabs($tabs, $currenttab, $inactive, $activated, true)); -} \ No newline at end of file +} diff --git a/templates/completepage.mustache b/templates/completepage.mustache index 6a0b3e9b..db04c206 100644 --- a/templates/completepage.mustache +++ b/templates/completepage.mustache @@ -62,14 +62,16 @@ {{{respondentinfo}}} {{#title}}

{{.}}

{{/title}}{{{printblank}}} {{#subtitle}}

{{.}}

{{/subtitle}} + {{#progressbar}}
{{{.}}}
{{/progressbar}} {{#addinfo}}
{{{.}}}
{{/addinfo}} {{#message}}
{{{.}}}
{{/message}} + {{#continue}}{{{continue}}}{{/continue}}
{{{formstart}}} {{#questions}}{{{.}}}{{/questions}} {{#pageinfo}}{{{.}}}{{/pageinfo}} -
-
+
+
{{{controlbuttons}}}
diff --git a/templates/dataformat_selector.mustache b/templates/dataformat_selector.mustache new file mode 100755 index 00000000..109bddd3 --- /dev/null +++ b/templates/dataformat_selector.mustache @@ -0,0 +1,79 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/dataformat_selector + + Template for dataformat selection and download form. + + Context variables required for this template: + * label + * base + * name + * params + * options + * sesskey + * submit + * extrafields + + Example context (json): + { + "base": "http://example.org/", + "name": "test", + "label": "Download table data as", + "params": [ + { + "name": "fieldname", + "value": "defaultvalue" + } + ], + "extrafields": "Input HTML", + "options": [ + { + "label": "CSV", + "name": "csv" + }, + { + "label": "Excel", + "name": "excel" + } + ], + "submit": "Download" + } +}} +
+
+ + {{#extrafields}}{{{extrafields}}}{{/extrafields}} +
+ +

+ {{#allowemailreporting}} +
+ {{{emailroleshelp}}} + + {{{emailextrahelp}}} +
+ {{/allowemailreporting}} + {{#params}} + + {{/params}} +
+
diff --git a/templates/extrafields.mustache b/templates/extrafields.mustache new file mode 100755 index 00000000..fae0995c --- /dev/null +++ b/templates/extrafields.mustache @@ -0,0 +1,29 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/extrafields + + Templates which inserts a couple of extra inputs in the dataformat_selector. + + Example context (json): + { + } + }} +
+
+
+
diff --git a/templates/local/mobile/ionic5/boolean_question.mustache b/templates/local/mobile/ionic5/boolean_question.mustache new file mode 100644 index 00000000..1d5958dd --- /dev/null +++ b/templates/local/mobile/ionic5/boolean_question.mustache @@ -0,0 +1,53 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_boolean_question + + Template which defines a boolean question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * choices - array of objects: choice_id, content, completed and value of each question choice. + + Example context (json): + { + "fieldkey": 985, + "choices": [ + { + "id": 5432, + "content": "Yes", + "completed": 1, + "value": 1 + }, + { + "id": 5432, + "content": "No", + "completed": 1, + "value": 0 + } + ] + } +}} +{{=<% %>=}} + + <%#choices%> + + + disabled="true"<%#value%> checked="true"<%/value%><%/completed%> [value]="<%id%>"> + + <%/choices%> + \ No newline at end of file diff --git a/templates/local/mobile/ionic5/checkbox_question.mustache b/templates/local/mobile/ionic5/checkbox_question.mustache new file mode 100644 index 00000000..0a769fbd --- /dev/null +++ b/templates/local/mobile/ionic5/checkbox_question.mustache @@ -0,0 +1,62 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_checkbox_question + + Template which defines a checkbox question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * choices - array of objects: choice_id, content, completed and value of each question choice. + + Example context (json): + { + "choices": [ + { + "fieldkey": 985, + "id": 5432, + "content": "Red", + "completed": 0, + "value": 1 + }, + { + "fieldkey": 986 , + "id": 5433, + "content": "Blue", + "completed": 0, + "value": 0 + } + ] + } +}} +{{=<% %>=}} + + <%#choices%> + + + checked="true"<%/value%> value="<%id%>" + <%#completed%> disabled="true"<%/completed%> + [(ngModel)]="CONTENT_OTHERDATA.<%choicefieldkey%>"> + + <%#otherchoicekey%> + + disabled="true"<%/completed%>> + + <%/otherchoicekey%> + <%/choices%> + \ No newline at end of file diff --git a/templates/local/mobile/ionic5/date_question.mustache b/templates/local/mobile/ionic5/date_question.mustache new file mode 100644 index 00000000..460c17f5 --- /dev/null +++ b/templates/local/mobile/ionic5/date_question.mustache @@ -0,0 +1,42 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_date_question + + Template which defines a date question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * completed - boolean: True if question already completed. + + Example context (json): + { + "fieldkey": 985, + "completed": 0 + } +}} +{{=<% %>=}} + + + <%#completed%> + + <%/completed%> + + <%^completed%> + + <%/completed%> + diff --git a/templates/local/mobile/ionic5/main_index_page.mustache b/templates/local/mobile/ionic5/main_index_page.mustache new file mode 100644 index 00000000..249b951e --- /dev/null +++ b/templates/local/mobile/ionic5/main_index_page.mustache @@ -0,0 +1,125 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_main_index_page + + Template which defines the main page that displays notices, submissions and the start answering link. + + Context variables required for this template: + * questionnaire - object: "intro" and "autonumquestions" strings for first respondent link. + * previous - object: "url" and "title" strings for previous link. + * respnumber - object: Current positio ("currpos") and "total" number of responses. + * next - object: "url" and "title" strings for next link. + * lastrespondent - object: "url" and "title" strings for last respondent link. + * listlink - string: Url of the link back to the response list. + * printaction - string: HTML to launch the print function. + + Example context (json): + { + "cmid": 985, + "userid": 267, + "intro": "Welcome to the questionnaire", + "autonumquestions": "1", + "id": "342", + "rid": 0, + "surveyid": "23", + "action": "index", + "pagenum": 0, + "nextpage": 1, + "prevpage": 0, + "completed": "1", + "complete_userdate": "Monday, 17 December 2018, 3:34pm", + "emptypage": "0", + "emptypage_content": "This is an empty page.", + "pagequestions": [ + { + "id": 5432, + "type_id": 4, + "qnum": "Q1", + "content": "Answer this question", + "required": "1", + "fieldkey": "response_1_23", + "isselect": "0", + "isbool": "0", + "isradiobutton": "1", + "ischeckbox": "0", + "istextessay": "0", + "israte": "0", + "choices": [ + { + "id": 745, + "content": "Red", + "value": " ", + "choice_id": 745, + "min": 0, + "max": 5, + "minstr": "Low", + "maxstr": "High", + "na": 0 + } + ] + } + ] + } +}} +{{=<% %>=}} +
+ + <%#notifications%> + + + <%notifications%> + + + <%/notifications%> + <%^notifications%> + + + <%#resume%> + + {{ 'plugin.mod_questionnaire.resumesurvey' | translate }} + + <%/resume%> + <%^resume%> + + {{ 'plugin.mod_questionnaire.answerquestions' | translate }} + + <%/resume%> + + + <%/notifications%> + <%#submissions.0%> + + + <%#submissions%> + + + {{ 'plugin.mod_questionnaire.submitted' | translate }} <%submissiondate%> + + + <%/submissions%> + + + <%/submissions.0%> + <%#emptypage%> + <%/emptypage%> +
\ No newline at end of file diff --git a/templates/local/mobile/ionic5/numeric_question.mustache b/templates/local/mobile/ionic5/numeric_question.mustache new file mode 100644 index 00000000..f2988460 --- /dev/null +++ b/templates/local/mobile/ionic5/numeric_question.mustache @@ -0,0 +1,42 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_numeric_question + + Template which defines a numeric question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * completed - boolean: True if question already completed. + + Example context (json): + { + "fieldkey": 985, + "completed": 0 + } +}} +{{=<% %>=}} + + + <%#completed%> + + <%/completed%> + + <%^completed%> + + <%/completed%> + \ No newline at end of file diff --git a/templates/local/mobile/ionic5/radio_question.mustache b/templates/local/mobile/ionic5/radio_question.mustache new file mode 100644 index 00000000..3d57c2f7 --- /dev/null +++ b/templates/local/mobile/ionic5/radio_question.mustache @@ -0,0 +1,61 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_radio_question + + Template which defines a radio question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * choices - array of objects: choice_id, content, completed and value of each question choice. + + Example context (json): + { + "fieldkey": 985, + "choices": [ + { + "id": 5432, + "content": "Red", + "completed": 0, + "value": 1 + }, + { + "id": 5433, + "content": "Blue", + "completed": 0, + "value": 0 + } + ] + } +}} +{{=<% %>=}} + + <%#choices%> + + + checked="true"<%/value%> + value="<%id%>" + <%#completed%> disabled="true"<%/completed%>> + + <%#otherchoicekey%> + + disabled="true"<%/completed%>> + + <%/otherchoicekey%> + <%/choices%> + \ No newline at end of file diff --git a/templates/local/mobile/ionic5/rate_question.mustache b/templates/local/mobile/ionic5/rate_question.mustache new file mode 100644 index 00000000..1d4eca2d --- /dev/null +++ b/templates/local/mobile/ionic5/rate_question.mustache @@ -0,0 +1,120 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_rate_question + + Template which defines a rate question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * choices - array of objects: choice_id, content, completed and value of each question choice. + + Example context (json): + { + "choices": [ + { + "fieldkey": 985, + "content": "Red", + "min": 0, + "max": 5, + "minstr": "zero", + "maxstr": "five", + "na": 1 + }, + { + "fieldkey": 986, + "content": "Blue", + "min": 0, + "max": 5, + "minstr": "zero", + "maxstr": "five", + "na": 1 + } + ], + "rates": [ + { + "value": 0, + "label": "Good" + }, + { + "value": 1, + "label": 1 + } + ] + } +}} +{{=<% %>=}} + + + + <%#choices.0%> + <%#leftlabel%> + + <%/leftlabel%> + <%/choices.0%> + <%#rates%> + + <%label%> + + <%/rates%> + <%#hasnacolumn%> + + <%label%> + + <%/hasnacolumn%> + <%#choices.0%> + <%#rightlabel%> + + <%/rightlabel%> + <%/choices.0%> + + <%#choices%> + + + + + + {{ CONTENT_OTHERDATA.<%fieldkey%> }} + + + + + <%#leftlabel%> + + <%leftlabel%> + + <%/leftlabel%> + <%#rates%> + + disabled="true"<%/completed%>> + + <%/rates%> + <%#hasnacolumn%> + + disabled="true"<%/completed%>> + + <%/hasnacolumn%> + <%#rightlabel%> + + <%rightlabel%> + + <%/rightlabel%> + + + <%/choices%> + + \ No newline at end of file diff --git a/templates/local/mobile/ionic5/select_question.mustache b/templates/local/mobile/ionic5/select_question.mustache new file mode 100644 index 00000000..e609e22c --- /dev/null +++ b/templates/local/mobile/ionic5/select_question.mustache @@ -0,0 +1,51 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_select_question + + Template which defines a select question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * choices - array of objects: choice_id, content, completed and value of each question choice. + + Example context (json): + { + "fieldkey": 985, + "choices": [ + { + "content": "Red", + "completed": 0, + "value": 1 + }, + { + "content": "Blue", + "completed": 0, + "value": 0 + } + ] + } +}} +{{=<% %>=}} + + + + <%#choices%> + disabled="true"<%/completed%><%#value%> selected="true"<%/value%> value="<%id%>"><%content%> + <%/choices%> + + \ No newline at end of file diff --git a/templates/local/mobile/ionic5/slider_question.mustache b/templates/local/mobile/ionic5/slider_question.mustache new file mode 100644 index 00000000..80623356 --- /dev/null +++ b/templates/local/mobile/ionic5/slider_question.mustache @@ -0,0 +1,57 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_slider_question + + Template which defines a slider question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * completed - boolean: True if question already completed. + + Example context (json): + { + "fieldkey": 985, + "extradata": { + "minrange" : 1 + "maxrange" : 10 + "startingvalue" : 5 + "stepvalue" : 1 + "leftlabel" : "left label" + "rightlabel" : "right label" + "centerlabel": "center label" + }, + "completed": 0 + } + +}} +{{=<% %>=}} +<%#extradata%> + + disabled="true"<%/completed%> + min="<%extradata.minrange%>" max="<%extradata.maxrange%>" + pin="true" step="<%extradata.stepvalue%>" + [(ngModel)]="CONTENT_OTHERDATA.<%fieldkey%>"> + <%extradata.leftlabel%> + <%extradata.rightlabel%> + + + class="disabled"<%/completed%>> + + +<%/extradata%> diff --git a/templates/local/mobile/ionic5/text_question.mustache b/templates/local/mobile/ionic5/text_question.mustache new file mode 100644 index 00000000..8113aea9 --- /dev/null +++ b/templates/local/mobile/ionic5/text_question.mustache @@ -0,0 +1,43 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_text_question + + Template which defines a text question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * completed - boolean: True if question already completed. + + Example context (json): + { + "fieldkey": 985, + "completed": 0 + } +}} +{{=<% %>=}} + + + <%#completed%> + + <%/completed%> + + <%^completed%> + + <%/completed%> + \ No newline at end of file diff --git a/templates/local/mobile/ionic5/view_activity_page.mustache b/templates/local/mobile/ionic5/view_activity_page.mustache new file mode 100644 index 00000000..ee5f508b --- /dev/null +++ b/templates/local/mobile/ionic5/view_activity_page.mustache @@ -0,0 +1,215 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_view_activity_page + + Template which defines a questionnaire display in the mobile app. + + Context variables required for this template: + * questionnaire - object: "intro" and "autonumquestions" strings for first respondent link. + * previous - object: "url" and "title" strings for previous link. + * respnumber - object: Current positio ("currpos") and "total" number of responses. + * next - object: "url" and "title" strings for next link. + * lastrespondent - object: "url" and "title" strings for last respondent link. + * listlink - string: Url of the link back to the response list. + * printaction - string: HTML to launch the print function. + + Example context (json): + { + "cmid": 985, + "userid": 267, + "intro": "Welcome to the questionnaire", + "autonumquestions": "1", + "id": "342", + "rid": 0, + "surveyid": "23", + "action": "nextpage", + "pagenum": 0, + "nextpage": 1, + "prevpage": 0, + "hasmorepages": 1, + "completed": "1", + "complete_userdate": "Monday, 17 December 2018, 3:34pm", + "emptypage": "0", + "emptypage_content": "This is an empty page.", + "pagequestions": [ + { + "id": 5432, + "type_id": 4, + "qnum": "Q1", + "content": "Answer this question", + "required": "1", + "fieldkey": "response_1_23", + "isselect": "0", + "isbool": "0", + "isradiobutton": "1", + "ischeckbox": "0", + "istextessay": "0", + "israte": "0", + "choices": [ + { + "id": 745, + "content": "Red", + "value": " ", + "choice_id": 745, + "min": 0, + "max": 5, + "minstr": "Low", + "maxstr": "High", + "na": 0 + } + ] + } + ] + } +}} +{{=<% %>=}} +
+ + <%#notifications%> + + + <%{notifications}%> + + + <%/notifications%> +
+ + + <%#pagequestions%> + + <%#autonumquestions%><%qnum%><%/autonumquestions%> + + + + <%#required%>{{ 'plugin.mod_questionnaire.required' | translate }}<%/required%> + + <%#isselect%> + <%> mod_questionnaire/local/mobile/ionic5/select_question %> + <%/isselect%> + <%#isbool%> + <%> mod_questionnaire/local/mobile/ionic5/boolean_question %> + <%/isbool%> + <%#isradiobutton%> + <%> mod_questionnaire/local/mobile/ionic5/radio_question %> + <%/isradiobutton%> + <%#ischeckbox%> + <%> mod_questionnaire/local/mobile/ionic5/checkbox_question %> + <%/ischeckbox%> + <%#istextessay%> + <%> mod_questionnaire/local/mobile/ionic5/text_question %> + <%/istextessay%> + <%#isnumeric%> + <%> mod_questionnaire/local/mobile/ionic5/numeric_question %> + <%/isnumeric%> + <%#isdate%> + <%> mod_questionnaire/local/mobile/ionic5/date_question %> + <%/isdate%> + <%#israte%> + <%> mod_questionnaire/local/mobile/ionic5/rate_question %> + <%/israte%> + <%#isslider%> + <%> mod_questionnaire/local/mobile/ionic5/slider_question %> + <%/isslider%> + <%/pagequestions%> + + <%^pagequestions%> + + +

No questions found.

+
+
+ <%/pagequestions%> + + <%#hasmorepages%> + + + <%#prevpage%> + + + + {{ 'plugin.mod_questionnaire.previouspage' | translate }} + + + <%/prevpage%> + + <%#nextpage%> + + + + {{ 'plugin.mod_questionnaire.nextpage' | translate }} + + + <%/nextpage%> + + + <%/hasmorepages%> + + <%^nextpage%><%^completed%> + {{ 'plugin.mod_questionnaire.savechanges' | translate }} + <%/completed%><%/nextpage%> +
+
+
+ <%#emptypage%> + <%/emptypage%> +
diff --git a/templates/local/mobile/latest/boolean_question.mustache b/templates/local/mobile/latest/boolean_question.mustache new file mode 100644 index 00000000..bde7cd49 --- /dev/null +++ b/templates/local/mobile/latest/boolean_question.mustache @@ -0,0 +1,54 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_boolean_question + + Template which defines a boolean question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * choices - array of objects: choice_id, content, completed and value of each question choice. + + Example context (json): + { + "fieldkey": 985, + "choices": [ + { + "id": 5432, + "content": "Yes", + "completed": 1, + "value": 1 + }, + { + "id": 5432, + "content": "No", + "completed": 1, + "value": 0 + } + ] + } +}} +{{=<% %>=}} + + <%#choices%> + + disabled="true"<%#value%> checked="true"<%/value%><%/completed%> [value]="<%id%>"> + + + + <%/choices%> + diff --git a/templates/local/mobile/latest/checkbox_question.mustache b/templates/local/mobile/latest/checkbox_question.mustache new file mode 100644 index 00000000..86640f76 --- /dev/null +++ b/templates/local/mobile/latest/checkbox_question.mustache @@ -0,0 +1,63 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_checkbox_question + + Template which defines a checkbox question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * choices - array of objects: choice_id, content, completed and value of each question choice. + + Example context (json): + { + "choices": [ + { + "fieldkey": 985, + "id": 5432, + "content": "Red", + "completed": 0, + "value": 1 + }, + { + "fieldkey": 986 , + "id": 5433, + "content": "Blue", + "completed": 0, + "value": 0 + } + ] + } +}} +{{=<% %>=}} + + <%#choices%> + + checked="true"<%/value%> value="<%id%>" + <%#completed%> disabled="true"<%/completed%> + [(ngModel)]="CONTENT_OTHERDATA.<%choicefieldkey%>"> + + + + <%#otherchoicekey%> + + disabled="true"<%/completed%>> + + <%/otherchoicekey%> + <%/choices%> + diff --git a/templates/local/mobile/latest/date_question.mustache b/templates/local/mobile/latest/date_question.mustache new file mode 100644 index 00000000..1554a6ed --- /dev/null +++ b/templates/local/mobile/latest/date_question.mustache @@ -0,0 +1,50 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_date_question + + Template which defines a date question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * completed - boolean: True if question already completed. + + Example context (json): + { + "fieldkey": 985, + "completed": 0 + } +}} +{{=<% %>=}} + + + <%#completed%> + + <%/completed%> + + <%^completed%> + + + + + {{'core.date' | translate}} + + + + <%/completed%> + diff --git a/templates/local/mobile/latest/main_index_page.mustache b/templates/local/mobile/latest/main_index_page.mustache new file mode 100644 index 00000000..b561bc1a --- /dev/null +++ b/templates/local/mobile/latest/main_index_page.mustache @@ -0,0 +1,124 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_main_index_page + + Template which defines the main page that displays notices, submissions and the start answering link. + + Context variables required for this template: + * questionnaire - object: "intro" and "autonumquestions" strings for first respondent link. + * previous - object: "url" and "title" strings for previous link. + * respnumber - object: Current positio ("currpos") and "total" number of responses. + * next - object: "url" and "title" strings for next link. + * lastrespondent - object: "url" and "title" strings for last respondent link. + * listlink - string: Url of the link back to the response list. + * printaction - string: HTML to launch the print function. + + Example context (json): + { + "cmid": 985, + "userid": 267, + "intro": "Welcome to the questionnaire", + "autonumquestions": "1", + "id": "342", + "rid": 0, + "surveyid": "23", + "action": "index", + "pagenum": 0, + "nextpage": 1, + "prevpage": 0, + "completed": "1", + "complete_userdate": "Monday, 17 December 2018, 3:34pm", + "emptypage": "0", + "emptypage_content": "This is an empty page.", + "pagequestions": [ + { + "id": 5432, + "type_id": 4, + "qnum": "Q1", + "content": "Answer this question", + "required": "1", + "fieldkey": "response_1_23", + "isselect": "0", + "isbool": "0", + "isradiobutton": "1", + "ischeckbox": "0", + "istextessay": "0", + "israte": "0", + "choices": [ + { + "id": 745, + "content": "Red", + "value": " ", + "choice_id": 745, + "min": 0, + "max": 5, + "minstr": "Low", + "maxstr": "High", + "na": 0 + } + ] + } + ] + } +}} +{{=<% %>=}} +
+ + <%#notifications%> + + + <%notifications%> + + + <%/notifications%> + <%^notifications%> + + + <%#resume%> + + {{ 'plugin.mod_questionnaire.resumesurvey' | translate }} + + <%/resume%> + <%^resume%> + + {{ 'plugin.mod_questionnaire.answerquestions' | translate }} + + <%/resume%> + + + <%/notifications%> + <%#submissions.0%> + + + <%#submissions%> + + + {{ 'plugin.mod_questionnaire.submitted' | translate }} <%submissiondate%> + + + <%/submissions%> + + + <%/submissions.0%> + <%#emptypage%> + <%/emptypage%> +
diff --git a/templates/local/mobile/latest/numeric_question.mustache b/templates/local/mobile/latest/numeric_question.mustache new file mode 100644 index 00000000..f2988460 --- /dev/null +++ b/templates/local/mobile/latest/numeric_question.mustache @@ -0,0 +1,42 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_numeric_question + + Template which defines a numeric question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * completed - boolean: True if question already completed. + + Example context (json): + { + "fieldkey": 985, + "completed": 0 + } +}} +{{=<% %>=}} + + + <%#completed%> + + <%/completed%> + + <%^completed%> + + <%/completed%> + \ No newline at end of file diff --git a/templates/local/mobile/latest/radio_question.mustache b/templates/local/mobile/latest/radio_question.mustache new file mode 100644 index 00000000..ccffab0b --- /dev/null +++ b/templates/local/mobile/latest/radio_question.mustache @@ -0,0 +1,62 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_radio_question + + Template which defines a radio question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * choices - array of objects: choice_id, content, completed and value of each question choice. + + Example context (json): + { + "fieldkey": 985, + "choices": [ + { + "id": 5432, + "content": "Red", + "completed": 0, + "value": 1 + }, + { + "id": 5433, + "content": "Blue", + "completed": 0, + "value": 0 + } + ] + } +}} +{{=<% %>=}} + + <%#choices%> + + checked="true"<%/value%> + value="<%id%>" + <%#completed%> disabled="true"<%/completed%>> + + + + <%#otherchoicekey%> + + disabled="true"<%/completed%>> + + <%/otherchoicekey%> + <%/choices%> + diff --git a/templates/local/mobile/latest/rate_question.mustache b/templates/local/mobile/latest/rate_question.mustache new file mode 100644 index 00000000..1d4eca2d --- /dev/null +++ b/templates/local/mobile/latest/rate_question.mustache @@ -0,0 +1,120 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_rate_question + + Template which defines a rate question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * choices - array of objects: choice_id, content, completed and value of each question choice. + + Example context (json): + { + "choices": [ + { + "fieldkey": 985, + "content": "Red", + "min": 0, + "max": 5, + "minstr": "zero", + "maxstr": "five", + "na": 1 + }, + { + "fieldkey": 986, + "content": "Blue", + "min": 0, + "max": 5, + "minstr": "zero", + "maxstr": "five", + "na": 1 + } + ], + "rates": [ + { + "value": 0, + "label": "Good" + }, + { + "value": 1, + "label": 1 + } + ] + } +}} +{{=<% %>=}} + + + + <%#choices.0%> + <%#leftlabel%> + + <%/leftlabel%> + <%/choices.0%> + <%#rates%> + + <%label%> + + <%/rates%> + <%#hasnacolumn%> + + <%label%> + + <%/hasnacolumn%> + <%#choices.0%> + <%#rightlabel%> + + <%/rightlabel%> + <%/choices.0%> + + <%#choices%> + + + + + + {{ CONTENT_OTHERDATA.<%fieldkey%> }} + + + + + <%#leftlabel%> + + <%leftlabel%> + + <%/leftlabel%> + <%#rates%> + + disabled="true"<%/completed%>> + + <%/rates%> + <%#hasnacolumn%> + + disabled="true"<%/completed%>> + + <%/hasnacolumn%> + <%#rightlabel%> + + <%rightlabel%> + + <%/rightlabel%> + + + <%/choices%> + + \ No newline at end of file diff --git a/templates/local/mobile/latest/select_question.mustache b/templates/local/mobile/latest/select_question.mustache new file mode 100644 index 00000000..e609e22c --- /dev/null +++ b/templates/local/mobile/latest/select_question.mustache @@ -0,0 +1,51 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_select_question + + Template which defines a select question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * choices - array of objects: choice_id, content, completed and value of each question choice. + + Example context (json): + { + "fieldkey": 985, + "choices": [ + { + "content": "Red", + "completed": 0, + "value": 1 + }, + { + "content": "Blue", + "completed": 0, + "value": 0 + } + ] + } +}} +{{=<% %>=}} + + + + <%#choices%> + disabled="true"<%/completed%><%#value%> selected="true"<%/value%> value="<%id%>"><%content%> + <%/choices%> + + \ No newline at end of file diff --git a/templates/local/mobile/latest/slider_question.mustache b/templates/local/mobile/latest/slider_question.mustache new file mode 100644 index 00000000..318dce0b --- /dev/null +++ b/templates/local/mobile/latest/slider_question.mustache @@ -0,0 +1,59 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_slider_question + + Template which defines a slider question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * completed - boolean: True if question already completed. + + Example context (json): + { + "fieldkey": 985, + "extradata": { + "minrange" : 1 + "maxrange" : 10 + "startingvalue" : 5 + "stepvalue" : 1 + "leftlabel" : "left label" + "rightlabel" : "right label" + "centerlabel": "center label" + }, + "completed": 0 + } + +}} +{{=<% %>=}} +<%#extradata%> + + disabled="true"<%/completed%> + min="<%extradata.minrange%>" max="<%extradata.maxrange%>" + pin="true" step="<%extradata.stepvalue%>" + [(ngModel)]="CONTENT_OTHERDATA.<%fieldkey%>"> + <%extradata.leftlabel%> + <%extradata.rightlabel%> + + + + class="disabled"<%/completed%>> + + + +<%/extradata%> diff --git a/templates/local/mobile/latest/text_question.mustache b/templates/local/mobile/latest/text_question.mustache new file mode 100644 index 00000000..8113aea9 --- /dev/null +++ b/templates/local/mobile/latest/text_question.mustache @@ -0,0 +1,43 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_text_question + + Template which defines a text question display in the mobile app. + + Context variables required for this template: + * fieldkey - integer: ID of the question. + * completed - boolean: True if question already completed. + + Example context (json): + { + "fieldkey": 985, + "completed": 0 + } +}} +{{=<% %>=}} + + + <%#completed%> + + <%/completed%> + + <%^completed%> + + <%/completed%> + \ No newline at end of file diff --git a/templates/local/mobile/latest/view_activity_page.mustache b/templates/local/mobile/latest/view_activity_page.mustache new file mode 100644 index 00000000..eb7a8a47 --- /dev/null +++ b/templates/local/mobile/latest/view_activity_page.mustache @@ -0,0 +1,221 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/mobile_view_activity_page + + Template which defines a questionnaire display in the mobile app. + + Context variables required for this template: + * questionnaire - object: "intro" and "autonumquestions" strings for first respondent link. + * previous - object: "url" and "title" strings for previous link. + * respnumber - object: Current positio ("currpos") and "total" number of responses. + * next - object: "url" and "title" strings for next link. + * lastrespondent - object: "url" and "title" strings for last respondent link. + * listlink - string: Url of the link back to the response list. + * printaction - string: HTML to launch the print function. + + Example context (json): + { + "cmid": 985, + "userid": 267, + "intro": "Welcome to the questionnaire", + "autonumquestions": "1", + "id": "342", + "rid": 0, + "surveyid": "23", + "action": "nextpage", + "pagenum": 0, + "nextpage": 1, + "prevpage": 0, + "hasmorepages": 1, + "completed": "1", + "complete_userdate": "Monday, 17 December 2018, 3:34pm", + "emptypage": "0", + "emptypage_content": "This is an empty page.", + "pagequestions": [ + { + "id": 5432, + "type_id": 4, + "qnum": "Q1", + "content": "Answer this question", + "required": "1", + "fieldkey": "response_1_23", + "isselect": "0", + "isbool": "0", + "isradiobutton": "1", + "ischeckbox": "0", + "istextessay": "0", + "israte": "0", + "choices": [ + { + "id": 745, + "content": "Red", + "value": " ", + "choice_id": 745, + "min": 0, + "max": 5, + "minstr": "Low", + "maxstr": "High", + "na": 0 + } + ] + } + ] + } +}} +{{=<% %>=}} +
+ <%#intro%> + + + + + + + + <%/intro%> + <%#notifications%> + + + <%{notifications}%> + + + <%/notifications%> +
+ + + <%#pagequestions%> + + <%#autonumquestions%><%qnum%><%/autonumquestions%> + + + + <%#required%>{{ 'plugin.mod_questionnaire.required' | translate }}<%/required%> + + <%#isselect%> + <%> mod_questionnaire/local/mobile/latest/select_question %> + <%/isselect%> + <%#isbool%> + <%> mod_questionnaire/local/mobile/latest/boolean_question %> + <%/isbool%> + <%#isradiobutton%> + <%> mod_questionnaire/local/mobile/latest/radio_question %> + <%/isradiobutton%> + <%#ischeckbox%> + <%> mod_questionnaire/local/mobile/latest/checkbox_question %> + <%/ischeckbox%> + <%#istextessay%> + <%> mod_questionnaire/local/mobile/latest/text_question %> + <%/istextessay%> + <%#isnumeric%> + <%> mod_questionnaire/local/mobile/latest/numeric_question %> + <%/isnumeric%> + <%#isdate%> + <%> mod_questionnaire/local/mobile/latest/date_question %> + <%/isdate%> + <%#israte%> + <%> mod_questionnaire/local/mobile/latest/rate_question %> + <%/israte%> + <%#isslider%> + <%> mod_questionnaire/local/mobile/latest/slider_question %> + <%/isslider%> + <%/pagequestions%> + + <%^pagequestions%> + + +

No questions found.

+
+
+ <%/pagequestions%> + + <%#hasmorepages%> + + + <%#prevpage%> + + + + {{ 'plugin.mod_questionnaire.previouspage' | translate }} + + + <%/prevpage%> + + <%#nextpage%> + + + + {{ 'plugin.mod_questionnaire.nextpage' | translate }} + + + <%/nextpage%> + + + <%/hasmorepages%> + + <%^nextpage%><%^completed%> + {{ 'plugin.mod_questionnaire.savechanges' | translate }} + <%/completed%><%/nextpage%> +
+
+
+ <%#emptypage%> + <%/emptypage%> +
diff --git a/templates/progressbar.mustache b/templates/progressbar.mustache new file mode 100644 index 00000000..b3f83b6d --- /dev/null +++ b/templates/progressbar.mustache @@ -0,0 +1,65 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/completepage + + Template which defines a questionnaire completion page. + + Classes required for JS: + * /questionnaire/module.js + + Data attributes required for JS: + * none + + Context variables required for this template: (Note sections have been used to allow non-inclusion) + * completepercent - string: progress through the questionnaire as a percentage + * targethelp: object - data for the help tooltip template + + Example context (json): + { + "percent": "90", + "progresshelp": { + "title": "Help with something", + "text": "Help with something", + "url": "http://example.org/help", + "linktext": "", + "icon":{ + "extraclasses": "iconhelp", + "attributes": [ + {"name": "src", "value": "../../../pix/help.svg"}, + {"name": "alt", "value": "Help icon"} + ] + } + } + } +}} +
+
+
{{#str}} + progressbar_info, mod_questionnaire, {{percent}}% {{/str}}
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/templates/question_check.mustache b/templates/question_check.mustache index 46073429..a6886544 100644 --- a/templates/question_check.mustache +++ b/templates/question_check.mustache @@ -55,11 +55,13 @@ }} {{#qelements}} -{{#choice}} - - -{{#choice.oname}}{{/choice.oname}} -
-{{/choice}} + {{#choice}} + + + {{#choice.oname}}{{/choice.oname}} +
+ {{/choice}} {{/qelements}} \ No newline at end of file diff --git a/templates/question_container.mustache b/templates/question_container.mustache index 96ee622d..f29c93ac 100644 --- a/templates/question_container.mustache +++ b/templates/question_container.mustache @@ -45,7 +45,7 @@
{{{dependencylist}}} {{#qnum}} - {{# str }}questionnum, mod_questionnaire{{/ str}}{{{qnum}}} + {{# str }}questionnum, mod_questionnaire{{/ str}}{{{qnum}}} {{qlegend}}

{{{qnum}}}

@@ -53,6 +53,11 @@ {{{required}}}
{{/qnum}} + {{^qnum}} +
+ {{{required}}} +
+ {{/qnum}}
{{#label}}
+{{#notifications}}{{{.}}}{{/notifications}} + \ No newline at end of file diff --git a/templates/reportpagepdf.mustache b/templates/reportpagepdf.mustache new file mode 100644 index 00000000..b08caef0 --- /dev/null +++ b/templates/reportpagepdf.mustache @@ -0,0 +1,88 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/reportpagepdf + + Template which defines a questionnaire completion page. This template uses a simple level of HTML, suitable for being + translated into a PDF file by the TCPDF library. + + Classes required for JS: + * /mod/questionnaire/module.js + + Data attributes required for JS: + * none + + Context variables required for this template: + * tabsarea - string: HTML of the tabs management area. + * notifications - string: HTML of any notifications loaded. + * myheaders - string: HTML of the page header. + * navigationbar - string: HTML of the response navigation controls. + * respondentinfo - string: HTML of any specific response/respondent information. + * title - string: Text title of the questionnaire. + * printblank - string: HTML of optional print blank questionnaire control. + * subtitle - string: Optional text subtitle. + * addinfo - string: Optional HTML additional information. + * feedbackcharts - string: HTML of any chart displays. + * feedbackscores - string: HTML of any feedback scores. + * feedbackmessages - string: HTML of any feedback messages. + * feedbacknotes - string: HTML of any feedback notes. + * responses - array: Array of responses HTML. + * bottomnavigationbar - string: HTML of the bottom response navigation controls. + + Example context (json): + { + "notifications": "
Notification one
", + "myheaders": "HTML of the page header.", + "respondentinfo": "
This is respondent information HTML
", + "title": "Test Questionnaire HTML", + "subtitle": "This is a subtitle HTML", + "addinfo": "
This is additional information HTML.
", + "feedbackcharts": "
Feedback charts HTML.
", + "feedbackscores": "
Feedback scores HTML.
", + "feedbackmessages": "
Feedback messages HTML.
", + "feedbacknotes": "
Feedback notes HTML.
", + "responses": [ + "Response 1 HTML", + "Response 2 HTML" + ], + "bottomnavigationbar": "Navigation bar HTML" + } + }} +
+ {{#notifications}}{{{notifications}}}{{/notifications}} +
+ {{#myheaders}}

{{{.}}}

{{/myheaders}} + {{{respondentinfo}}} + {{#title}}

{{{.}}}

{{{printblank}}}{{/title}} + {{#subtitle}}

{{{.}}}

{{/subtitle}} + {{#addinfo}}
{{{.}}}
{{/addinfo}} + {{#feedbackcharts}}{{{.}}}{{/feedbackcharts}} + {{#feedbackscores}}{{{.}}}{{/feedbackscores}} + {{#feedbackmessages}} +

{{# str }} feedbackreport, mod_questionnaire {{/ str }}

+ {{{.}}} + {{/feedbackmessages}} + {{#feedbacknotes}}{{{.}}}{{/feedbacknotes}} + {{#responses}} + + {{#qnum}}{{/qnum}} + +
{{qnum}}{{{qcontent}}}
+
{{{results}}}
+ {{/responses}} +
+
\ No newline at end of file diff --git a/templates/response_check.mustache b/templates/response_check.mustache index f46e3f6c..12d6e43c 100644 --- a/templates/response_check.mustache +++ b/templates/response_check.mustache @@ -45,22 +45,20 @@ } }} -
-{{#choices}} - {{#selected}} - - - {{{content}}} - +
{{! +}}{{#choices}}{{! + }}{{#selected}}{{! + }}{{^pdf}} + {{/pdf}} + {{{content}}}{{#pdf}} ✓{{/pdf}} {{#othercontent}}{{{.}}}{{/othercontent}} - {{/selected}} +{{/selected}} {{^selected}} - - - {{{content}}} - + {{^pdf}} + {{/pdf}} + {{{content}}} {{/selected}} -
+
{{/choices}}
\ No newline at end of file diff --git a/templates/response_container.mustache b/templates/response_container.mustache index 5e10f989..78c50fcd 100644 --- a/templates/response_container.mustache +++ b/templates/response_container.mustache @@ -54,9 +54,9 @@ }}
-
+
{{#qnum}} - {{# str }}questionnum, mod_questionnaire{{/ str}}{{{qnum}}} + {{# str }}questionnum, mod_questionnaire{{/ str}}{{{qnum}}} {{qlegend}}

{{{.}}}

@@ -74,10 +74,10 @@ {{#responses}} {{#respdate}}
{{respdate}}
{{/respdate}}
- {{# str }}questionnum, mod_questionnaire{{/ str}} + {{# str }}questionnum, mod_questionnaire{{/ str}}{{{qnum}}} {{qlegend}}
-
{{# str }}questionnum, mod_questionnaire{{/ str}}
+
{{# str }}questionnum, mod_questionnaire{{/ str}}{{{qnum}}} {{qlegend}}
{{{required}}}
diff --git a/templates/response_drop.mustache b/templates/response_drop.mustache index b14eeb89..a60f0490 100644 --- a/templates/response_drop.mustache +++ b/templates/response_drop.mustache @@ -49,11 +49,18 @@ }}
- {{#options}} {{/options}} {{#selectedlabel}}: {{.}}{{/selectedlabel}} + {{/pdf}} + {{#pdf}} + {{#options}} + {{label}}{{#selected}} ✓{{/selected}}
+ {{/options}} + {{/pdf}}
\ No newline at end of file diff --git a/templates/response_radio.mustache b/templates/response_radio.mustache index 7af89d3c..d99ef865 100644 --- a/templates/response_radio.mustache +++ b/templates/response_radio.mustache @@ -48,22 +48,23 @@ }}
-{{#choices}} - {{#horizontal}}{{/horizontal}} - {{#selected}} - - - {{{content}}}{{#othercontent}} {{.}}{{/othercontent}} - - {{/selected}} - {{^selected}} - - - {{{content}}} - - {{/selected}} - {{#horizontal}}{{/horizontal}} - {{^horizontal}}
{{/horizontal}} -{{/choices}} + {{#choices}} + {{#horizontal}}{{/horizontal}} + {{#selected}} + + {{^pdf}} + {{/pdf}} + {{#pdf}}✓{{/pdf}} {{{content}}}{{#othercontent}} {{.}}{{/othercontent}} + + {{/selected}} + {{^selected}} + {{^pdf}} + {{/pdf}} + {{{content}}}  + {{/selected}} + {{#horizontal}}{{/horizontal}} + {{^horizontal}}
{{/horizontal}} + {{/choices}}
\ No newline at end of file diff --git a/templates/response_rate.mustache b/templates/response_rate.mustache index 9bd09ba8..f4bcc4e7 100644 --- a/templates/response_rate.mustache +++ b/templates/response_rate.mustache @@ -58,7 +58,7 @@ }}
- +
{{#osgood}}{{/osgood}} @@ -74,16 +74,12 @@ {{#cols}} {{#checked}} {{/checked}} {{^checked}} {{/checked}} {{/cols}} diff --git a/templates/response_slider.mustache b/templates/response_slider.mustache new file mode 100644 index 00000000..224eaaef --- /dev/null +++ b/templates/response_slider.mustache @@ -0,0 +1,52 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/response_text + + Template which defines a text type question survey display. + + Classes required for JS: + * /mod/questionnaire/module.js + + Data attributes required for JS: + * none + + Context variables required for this template: + + Example context (json): + { + "content": "HTML for numeric" + } + }} + +
+
+ {{#extradata}} +
{{extradata.leftlabel}}
+
+
+ + +
+
{{extradata.centerlabel}}
+
+
{{extradata.rightlabel}}
+ {{/extradata}} +
+
+ diff --git a/templates/response_yesno.mustache b/templates/response_yesno.mustache index ea02c162..89e5b908 100644 --- a/templates/response_yesno.mustache +++ b/templates/response_yesno.mustache @@ -34,30 +34,32 @@ "stryes": "Yes", "noselected": 0, "noname": "id4102n", - "strno": "No" + "strno": "No", + "alabelyes": "Some question Yes", + "alabelno": "Some question No" } }}
{{#yesselected}} - - {{{stryes}}} - + {{#pdf}}✓{{/pdf}}{{^pdf}} + {{/pdf}} {{{stryes}}} {{/yesselected}} {{^yesselected}} - - {{{stryes}}} - + {{^pdf}} + {{/pdf}} {{{stryes}}} {{/yesselected}} {{#noselected}} - - {{{strno}}} - + {{#pdf}}  ✓{{/pdf}}{{^pdf}} + {{/pdf}} {{{strno}}} {{/noselected}} {{^noselected}} - - {{{strno}}} - + {{^pdf}} + {{/pdf}} {{{strno}}} {{/noselected}}
\ No newline at end of file diff --git a/templates/responsepagepdf.mustache b/templates/responsepagepdf.mustache new file mode 100644 index 00000000..ae0b23d4 --- /dev/null +++ b/templates/responsepagepdf.mustache @@ -0,0 +1,82 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/reportpagepdf + + Template which defines a questionnaire completion page. This template uses a simple level of HTML, suitable for being + translated into a PDF file by the TCPDF library. + + Classes required for JS: + * /mod/questionnaire/module.js + + Data attributes required for JS: + * none + + Context variables required for this template: + * tabsarea - string: HTML of the tabs management area. + * notifications - string: HTML of any notifications loaded. + * myheaders - string: HTML of the page header. + * navigationbar - string: HTML of the response navigation controls. + * respondentinfo - string: HTML of any specific response/respondent information. + * title - string: Text title of the questionnaire. + * printblank - string: HTML of optional print blank questionnaire control. + * subtitle - string: Optional text subtitle. + * addinfo - string: Optional HTML additional information. + * feedbackcharts - string: HTML of any chart displays. + * feedbackscores - string: HTML of any feedback scores. + * feedbackmessages - string: HTML of any feedback messages. + * feedbacknotes - string: HTML of any feedback notes. + * responses - array: Array of responses HTML. + * bottomnavigationbar - string: HTML of the bottom response navigation controls. + + Example context (json): + { + "notifications": "
Notification one
", + "myheaders": "HTML of the page header.", + "respondentinfo": "
This is respondent information HTML
", + "title": "Test Questionnaire HTML", + "subtitle": "This is a subtitle HTML", + "addinfo": "
This is additional information HTML.
", + "feedbackcharts": "
Feedback charts HTML.
", + "feedbackscores": "
Feedback scores HTML.
", + "feedbackmessages": "
Feedback messages HTML.
", + "feedbacknotes": "
Feedback notes HTML.
", + "responses": [ + "Response 1 HTML", + "Response 2 HTML" + ], + "bottomnavigationbar": "Navigation bar HTML" + } + }} +
{{! + }}{{#notifications}}{{{notifications}}}{{/notifications}}{{! + }}
{{! + }}{{#myheaders}}

{{{.}}}

{{/myheaders}}{{! + }}{{{respondentinfo}}} + {{#title}}

{{{.}}}

{{{printblank}}}{{/title}} + {{#subtitle}}

{{{.}}}

{{/subtitle}} + {{#addinfo}}
{{{.}}}
{{/addinfo}} + {{#feedbackcharts}}{{{.}}}{{/feedbackcharts}} + {{#feedbackscores}}{{{.}}}{{/feedbackscores}} + {{#feedbackmessages}} +

{{# str }} feedbackreport, mod_questionnaire {{/ str }}

+ {{{.}}} + {{/feedbackmessages}} + {{#feedbacknotes}}{{{.}}}{{/feedbacknotes}} + {{#responses}}{{{.}}}{{/responses}} +
+
\ No newline at end of file diff --git a/templates/results_choice.mustache b/templates/results_choice.mustache index 89fd1f43..b46f2d1a 100644 --- a/templates/results_choice.mustache +++ b/templates/results_choice.mustache @@ -36,7 +36,7 @@ "alt1": "alt1", "image1": "http://localhost/mod/questionnaire/images/hbar_1.gif", "alt2": "alt2", - "width2": "28px", + "width2": "28", "image2": "http://localhost/mod/questionnaire/images/hbar.gif", "alt3": "alt3", "image3": "http://localhost/mod/questionnaire/images/hbar_r.gif", @@ -50,7 +50,7 @@ "alt1": "alt1", "image1": "http://localhost/mod/questionnaire/images/hbar_1.gif", "alt2": "alt2", - "width2": "28px", + "width2": "28", "image2": "http://localhost/mod/questionnaire/images/hbar.gif", "alt3": "alt3", "image3": "http://localhost/mod/questionnaire/images/hbar_r.gif", @@ -63,7 +63,7 @@ "alt1": "alt1", "image1": "http://localhost/mod/questionnaire/images/hbar_1.gif", "alt2": "alt2", - "width2": "28px", + "width2": "28", "image2": "http://localhost/mod/questionnaire/images/hbar.gif", "alt3": "alt3", "image3": "http://localhost/mod/questionnaire/images/hbar_r.gif", @@ -87,9 +87,9 @@ {{#responses}} {{#response}}
- + @@ -103,7 +103,7 @@ diff --git a/templates/results_date.mustache b/templates/results_date.mustache index a9f6d28b..844341b8 100644 --- a/templates/results_date.mustache +++ b/templates/results_date.mustache @@ -53,8 +53,8 @@
- - - + - - - +
{{response.text}}{{{response.text}}} - {{response.alt1}}{{response.alt2}}{{response.alt3}} + {{response.alt1}}{{response.alt2}}{{response.alt3}} {{{response.percent}}} {{response.total}}
{{#str}} totalresponses, mod_questionnaire{{/str}} - {{alt1}}{{alt2}}{{alt3}} + {{alt1}}{{alt2}}{{alt3}} {{{percent}}} {{total}}
- - + + @@ -62,8 +62,8 @@ {{#responses}} {{#response}} - - + + {{/response}} {{/responses}} @@ -72,8 +72,8 @@ - - + + {{/total}} {{#responses.0}} diff --git a/templates/results_rate.mustache b/templates/results_rate.mustache new file mode 100644 index 00000000..071285d5 --- /dev/null +++ b/templates/results_rate.mustache @@ -0,0 +1,165 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/results_rate + + Template which defines a results display for responses with defined choices. + + Classes required for JS: + * /mod/questionnaire/module.js + + Data attributes required for JS: + * none + + Context variables required for this template: + + Example context (json): + { + "responses": [ + { + "response": { + "text": "choice 1", + "alt1": "alt1", + "image1": "http://localhost/mod/questionnaire/images/hbar_1.gif", + "alt2": "alt2", + "width2": "28px", + "image2": "http://localhost/mod/questionnaire/images/hbar.gif", + "alt3": "alt3", + "image3": "http://localhost/mod/questionnaire/images/hbar_r.gif", + "percent": "20%", + "total": 2 + } + }, + { + "response": { + "text": "choice 2", + "alt1": "alt1", + "image1": "http://localhost/mod/questionnaire/images/hbar_1.gif", + "alt2": "alt2", + "width2": "28px", + "image2": "http://localhost/mod/questionnaire/images/hbar.gif", + "alt3": "alt3", + "image3": "http://localhost/mod/questionnaire/images/hbar_r.gif", + "percent": "20%", + "total": 2 + } + } + ], + "total": { + "alt1": "alt1", + "image1": "http://localhost/mod/questionnaire/images/hbar_1.gif", + "alt2": "alt2", + "width2": "28px", + "image2": "http://localhost/mod/questionnaire/images/hbar.gif", + "alt3": "alt3", + "image3": "http://localhost/mod/questionnaire/images/hbar_r.gif", + "percent": "20%", + "total": "8/10" + } + } + }} + +{{#averages}} +
{{#str}} num, mod_questionnaire {{/str}}{{#str}} response, mod_questionnaire {{/str}}{{#str}} num, mod_questionnaire {{/str}}{{#str}} response, mod_questionnaire {{/str}}
{{response.total}}{{response.text}}{{response.total}}{{response.text}}
{{#str}} totalresponses, mod_questionnaire{{/str}}{{total}}{{#str}} totalresponses, mod_questionnaire{{/str}}{{total}}
+ + + {{#headers}} + + {{/headers}} + + + + + {{#choicelabelrow}} + + {{#column1}} + + {{/column1}} + {{#column2}} + + {{/column2}} + {{#column3}} + + {{/column3}} + {{#column4}} + + {{/column4}} + + {{/choicelabelrow}} + + {{#choiceaverages}} + + {{#column1}} + + {{/column1}} + {{#column2}} + + {{/column2}} + {{#column3}} + + {{/column3}} + {{#column4}} + + {{/column4}} + + {{/choiceaverages}} + {{#nodata.0}} + + {{#nodata}} + + {{/nodata}} + + {{/nodata.0}} + +
{{{text}}}
{{{text}}} + + + + {{#ranks}} + + {{/ranks}} + + +
{{{text}}}
+
{{{text}}}{{{text}}}
{{{text}}} + {{#imageurl}}{{/imageurl}} + {{{text}}}{{{text}}}
{{text}}
+{{/averages}} +{{#totals}} + + + + {{#headers}} + + {{/headers}} + + + + {{#choices}} + + {{#totalcols}} + + {{/totalcols}} + + {{/choices}} + +
{{{text}}}
{{{text}}}
+{{/totals}} +{{#noresponses}} +

{{# str }}noresponsedata, mod_questionnaire{{/ str }}

+{{/noresponses}} + \ No newline at end of file diff --git a/templates/results_text.mustache b/templates/results_text.mustache index 572363d1..662e9052 100644 --- a/templates/results_text.mustache +++ b/templates/results_text.mustache @@ -53,8 +53,8 @@ - - + + @@ -62,8 +62,8 @@ {{#responses}} {{#response}} - - + + {{/response}} {{/responses}} @@ -72,8 +72,8 @@ - - + + {{/total}} {{#responses.0}} diff --git a/templates/resultspdf_choice.mustache b/templates/resultspdf_choice.mustache new file mode 100644 index 00000000..f418d942 --- /dev/null +++ b/templates/resultspdf_choice.mustache @@ -0,0 +1,120 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/results_choice + + Template which defines a results display for responses with defined choices. This template uses a simple level of HTML, + suitable for being translated into a PDF file by the TCPDF library. + + Classes required for JS: + * /mod/questionnaire/module.js + + Data attributes required for JS: + * none + + Context variables required for this template: + + Example context (json): + { + "responses": [ + { + "response": { + "text": "choice 1", + "alt1": "alt1", + "image1": "http://localhost/mod/questionnaire/images/hbar_1.gif", + "alt2": "alt2", + "width2": "28", + "image2": "http://localhost/mod/questionnaire/images/hbar.gif", + "alt3": "alt3", + "image3": "http://localhost/mod/questionnaire/images/hbar_r.gif", + "percent": "20%", + "total": 2 + } + }, + { + "response": { + "text": "choice 2", + "alt1": "alt1", + "image1": "http://localhost/mod/questionnaire/images/hbar_1.gif", + "alt2": "alt2", + "width2": "28", + "image2": "http://localhost/mod/questionnaire/images/hbar.gif", + "alt3": "alt3", + "image3": "http://localhost/mod/questionnaire/images/hbar_r.gif", + "percent": "20%", + "total": 2 + } + } + ], + "total": { + "alt1": "alt1", + "image1": "http://localhost/mod/questionnaire/images/hbar_1.gif", + "alt2": "alt2", + "width2": "28", + "image2": "http://localhost/mod/questionnaire/images/hbar.gif", + "alt3": "alt3", + "image3": "http://localhost/mod/questionnaire/images/hbar_r.gif", + "percent": "20%", + "total": "8/10" + } + } + }} + +{{#responses.0}} +
{{#str}} respondent, mod_questionnaire {{/str}}{{#str}} response, mod_questionnaire {{/str}}{{#str}} respondent, mod_questionnaire {{/str}}{{#str}} response, mod_questionnaire {{/str}}
{{{response.respondent}}}{{{response.text}}}{{{response.respondent}}}{{{response.text}}}
{{#str}} totalresponses, mod_questionnaire{{/str}}{{total}}{{#str}} totalresponses, mod_questionnaire{{/str}}{{total}}
+ + + + + + + + +{{/responses.0}} +{{#responses}} + {{#response}} + + + + + + {{/response}} +{{/responses}} +{{#total}} + + + + + + + + +{{/total}} +{{#responses.0}} + +
{{#str}} response, mod_questionnaire {{/str}}{{#str}} average, mod_questionnaire {{/str}}{{#str}} total, mod_questionnaire {{/str}}
{{response.text}} + {{response.alt1}}{{response.alt2}}{{response.alt3}} + {{{response.percent}}} + {{response.total}}
{{#str}} totalresponses, mod_questionnaire{{/str}} + {{alt1}}{{alt2}}{{alt3}} + {{{percent}}} + {{total}}
+{{/responses.0}} +{{^responses}} +

 {{#str}} noresponsedata, questionnaire{{/str}}

+{{/responses}} + \ No newline at end of file diff --git a/templates/resultspdf_date.mustache b/templates/resultspdf_date.mustache new file mode 100644 index 00000000..147e81e1 --- /dev/null +++ b/templates/resultspdf_date.mustache @@ -0,0 +1,87 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/results_date + + Template which defines a results display for responses with dates. This template uses a simple level of HTML, + suitable for being translated into a PDF file by the TCPDF library. + + Classes required for JS: + * /mod/questionnaire/module.js + + Data attributes required for JS: + * none + + Context variables required for this template: + + Example context (json): + { + "responses": [ + { + "response": { + "text": "04/12/2016", + "total": 2 + } + }, + { + "response": { + "text": "06/01/2012", + "total": 2 + } + } + ], + "total": { + "total": "8/10" + } + } + }} + +{{#responses.0}} + + + + + + + + +{{/responses.0}} +{{#responses}} + {{#response}} + + + + + {{/response}} +{{/responses}} +{{#total}} + + + + + + + +{{/total}} +{{#responses.0}} + +
{{#str}} num, mod_questionnaire {{/str}}{{#str}} response, mod_questionnaire {{/str}}
{{response.total}}{{response.text}}
{{#str}} totalresponses, mod_questionnaire{{/str}}{{total}}
+{{/responses.0}} +{{^responses}} +

 {{#str}} noresponsedata, questionnaire{{/str}}

+{{/responses}} + \ No newline at end of file diff --git a/templates/resultspdf_rate.mustache b/templates/resultspdf_rate.mustache new file mode 100644 index 00000000..22505a27 --- /dev/null +++ b/templates/resultspdf_rate.mustache @@ -0,0 +1,165 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/results_rate + + Template which defines a results display for responses with defined choices. + + Classes required for JS: + * /mod/questionnaire/module.js + + Data attributes required for JS: + * none + + Context variables required for this template: + + Example context (json): + { + "responses": [ + { + "response": { + "text": "choice 1", + "alt1": "alt1", + "image1": "http://localhost/mod/questionnaire/images/hbar_1.gif", + "alt2": "alt2", + "width2": "28px", + "image2": "http://localhost/mod/questionnaire/images/hbar.gif", + "alt3": "alt3", + "image3": "http://localhost/mod/questionnaire/images/hbar_r.gif", + "percent": "20%", + "total": 2 + } + }, + { + "response": { + "text": "choice 2", + "alt1": "alt1", + "image1": "http://localhost/mod/questionnaire/images/hbar_1.gif", + "alt2": "alt2", + "width2": "28px", + "image2": "http://localhost/mod/questionnaire/images/hbar.gif", + "alt3": "alt3", + "image3": "http://localhost/mod/questionnaire/images/hbar_r.gif", + "percent": "20%", + "total": 2 + } + } + ], + "total": { + "alt1": "alt1", + "image1": "http://localhost/mod/questionnaire/images/hbar_1.gif", + "alt2": "alt2", + "width2": "28px", + "image2": "http://localhost/mod/questionnaire/images/hbar.gif", + "alt3": "alt3", + "image3": "http://localhost/mod/questionnaire/images/hbar_r.gif", + "percent": "20%", + "total": "8/10" + } + } + }} + +{{#averages}} + + + + {{#headers}} + + {{/headers}} + + + + + {{#choicelabelrow}} + + {{#column1}} + + {{/column1}} + {{#column2}} + + {{/column2}} + {{#column3}} + + {{/column3}} + {{#column4}} + + {{/column4}} + + {{/choicelabelrow}} + + {{#choiceaverages}} + + {{#column1}} + + {{/column1}} + {{#column2}} + + {{/column2}} + {{#column3}} + + {{/column3}} + {{#column4}} + + {{/column4}} + + {{/choiceaverages}} + {{#nodata.0}} + + {{#nodata}} + + {{/nodata}} + + {{/nodata.0}} + +
{{{text}}}
{{{text}}} + + + + {{#ranks}} + + {{/ranks}} + + +
{{{text}}}
+
{{{text}}}{{{text}}}
{{{text}}} + {{#spacerimage}}{{/spacerimage}}{{#imageurl}}{{/imageurl}} + {{{text}}}{{{text}}}
{{text}}
+{{/averages}} +{{#totals}} + + + + {{#headers}} + + {{/headers}} + + + + {{#choices}} + + {{#totalcols}} + + {{/totalcols}} + + {{/choices}} + +
{{{text}}}
{{{text}}}
+{{/totals}} +{{#noresponses}} +

{{# str }}noresponsedata, mod_questionnaire{{/ str }}

+{{/noresponses}} + \ No newline at end of file diff --git a/templates/resultspdf_text.mustache b/templates/resultspdf_text.mustache new file mode 100644 index 00000000..513c7f39 --- /dev/null +++ b/templates/resultspdf_text.mustache @@ -0,0 +1,87 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_questionnaire/results_text + + Template which defines a results display for text responses. This template uses a simple level of HTML, suitable for being + translated into a PDF file by the TCPDF library. + + Classes required for JS: + * /mod/questionnaire/module.js + + Data attributes required for JS: + * none + + Context variables required for this template: + + Example context (json): + { + "responses": [ + { + "response": { + "respondent" : "User 1", + "text": "Some text" + } + }, + { + "response": { + "respondent" : "User 2", + "text": "Some other text" + } + } + ], + "total": { + "total": "8/10" + } + } + }} + +{{#responses.0}} + + + + + + + + +{{/responses.0}} +{{#responses}} + {{#response}} + + + + + {{/response}} +{{/responses}} +{{#total}} + + + + + + + +{{/total}} +{{#responses.0}} + +
{{#str}} respondent, mod_questionnaire {{/str}}{{#str}} response, mod_questionnaire {{/str}}
{{{response.respondent}}}{{{response.text}}}
{{#str}} totalresponses, mod_questionnaire{{/str}}{{total}}
+{{/responses.0}} +{{^responses}} +

 {{#str}} noresponsedata, questionnaire{{/str}}

+{{/responses}} + \ No newline at end of file diff --git a/templates/viewpage.mustache b/templates/viewpage.mustache index 67a140cc..438e975f 100644 --- a/templates/viewpage.mustache +++ b/templates/viewpage.mustache @@ -47,8 +47,10 @@ }}
{{#message}}
{{{message}}}
{{/message}} - {{#complete}}
{{{complete}}}
{{/complete}} - {{#guestuser}}
{{{guestuser}}}
{{/guestuser}} - {{#yourresponse}}
{{{yourresponse}}}
{{/yourresponse}} - {{#allresponses}}
{{{allresponses}}}
{{/allresponses}} +
+ {{#complete}}
{{{complete}}}
{{/complete}} + {{#guestuser}}
{{{guestuser}}}
{{/guestuser}} + {{#yourresponse}}
{{{yourresponse}}}
{{/yourresponse}} + {{#allresponses}}
{{{allresponses}}}
{{/allresponses}} +
\ No newline at end of file diff --git a/tests/behat/add_feedback.feature b/tests/behat/add_feedback.feature index 06d0a20b..7a499c78 100644 --- a/tests/behat/add_feedback.feature +++ b/tests/behat/add_feedback.feature @@ -46,7 +46,8 @@ Feature: In questionnaire, personality tests can be constructed using feedback o | Nb of scale items | 4 | | Type of rate scale | Normal | | Question Text | Rate these | - | Possible answers | 1=One,2=Two,3=Three,4=Four,Cheese,Bread,Meat,Fruit | + | Possible answers | Cheese,Bread,Meat,Fruit | + | Named degrees | 1=One,2=Two,3=Three,4=Four | Then I should see "[Rate (scale 1..5)] (Q3)" And I add a "Yes/No" question and I fill the form with: | Question Name | Q4 | @@ -78,15 +79,15 @@ Feature: In questionnaire, personality tests can be constructed using feedback o Then I should see "Select one dropdown" And I set the field "Select one dropdown" to "Three" And I click on "Three" "radio" - And I click on "Choice Three for row Cheese" "radio" - And I click on "Choice Three for row Bread" "radio" - And I click on "Choice Three for row Meat" "radio" - And I click on "Choice Three for row Fruit" "radio" + And I click on "Row 2, Cheese: Column 5, Three." "radio" + And I click on "Row 3, Bread: Column 5, Three." "radio" + And I click on "Row 4, Meat: Column 5, Three." "radio" + And I click on "Row 5, Fruit: Column 5, Three." "radio" And I click on "Yes" "radio" And I press "Submit questionnaire" Then I should see "Thank you for completing this Questionnaire." - And I follow "Continue" - Then I should see "Your response" + And I press "Continue" + Then I should see "View your response(s)" And I should see "These are the main Feedback notes" And I should see "Global feedback label" And I should see "76%" @@ -101,17 +102,17 @@ Feature: In questionnaire, personality tests can be constructed using feedback o Then I should see "Select one dropdown" And I set the field "Select one dropdown" to "One" And I click on "One" "radio" - And I click on "Choice Two for row Cheese" "radio" - And I click on "Choice Two for row Bread" "radio" - And I click on "Choice Two for row Meat" "radio" - And I click on "Choice Two for row Fruit" "radio" + And I click on "Row 2, Cheese: Column 4, Two." "radio" + And I click on "Row 3, Bread: Column 4, Two." "radio" + And I click on "Row 4, Meat: Column 4, Two." "radio" + And I click on "Row 5, Fruit: Column 4, Two." "radio" And I click on "Yes" "radio" And I press "Submit questionnaire" Then I should see "Thank you for completing this Questionnaire." - And I follow "Continue" - Then I should see "Your response" + And I press "Continue" + Then I should see "View your response(s)" And I should see "These are the main Feedback notes" And I should see "Global feedback label" And I should see "44%" And I should see "Feedback 50%" - And I log out \ No newline at end of file + And I log out diff --git a/tests/behat/add_multi_feedback_with_sections.feature b/tests/behat/add_multi_feedback_with_sections.feature index 65363db3..e95e97ed 100644 --- a/tests/behat/add_multi_feedback_with_sections.feature +++ b/tests/behat/add_multi_feedback_with_sections.feature @@ -21,16 +21,15 @@ Feature: In questionnaire, personality tests can be constructed using feedback o And the following "activities" exist: | activity | name | description | course | idnumber | resume | navigate | | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | 1 | 1 | - And I log in as "teacher1" - And I am on "Course 1" course homepage - And I follow "Test questionnaire" - And I navigate to "Questions" in current page administration + And the "multilang" filter is "on" + And the "multilang" filter applies to "content and headings" + And I am on the "Test questionnaire" "mod_questionnaire > questions" page logged in as "teacher1" Then I should see "Add questions" And I add a "Dropdown Box" question and I fill the form with: | Question Name | Q1 | | Yes | y | | Question Text | Select one dropdown | - | Possible answers | 1=One,2=Two,3=Three,4=Four | + | Possible answers | 1=One,2=Two,3=Three,4=Four | Then I should see "[Dropdown Box] (Q1)" And I add a "Radio Buttons" question and I fill the form with: | Question Name | Q2 | @@ -45,7 +44,8 @@ Feature: In questionnaire, personality tests can be constructed using feedback o | Nb of scale items | 4 | | Type of rate scale | Normal | | Question Text | Rate these | - | Possible answers | 1=One,2=Two,3=Three,4=Four,Cheese,Bread,Meat,Fruit | + | Possible answers | Cheese,Bread,Meat,Fruit | + | Named degrees | 1=One,2=Two,3=Three,4=Four | Then I should see "[Rate (scale 1..5)] (Q3)" And I add a "Yes/No" question and I fill the form with: | Question Name | Q4 | @@ -58,7 +58,8 @@ Feature: In questionnaire, personality tests can be constructed using feedback o | Nb of scale items | 5 | | Type of rate scale | Normal | | Question Text | Rate these | - | Possible answers | 0=Zero,2=Two,4=Four,8=Eight,16=Sixteen,Clubs,Diamonds,Hearts,Spades | + | Possible answers | Clubs,Diamonds,Hearts,Spades | + | Named degrees | 0=Zero,2=Two,4=Four,8=Eight,16=Sixteen | Then I should see "[Rate (scale 1..5)] (Q5)" And I follow "Feedback" And I should see "Feedback options" @@ -114,20 +115,21 @@ Feature: In questionnaire, personality tests can be constructed using feedback o And I navigate to "Answer the questions..." in current page administration Then I should see "Select one dropdown" And I set the field "dropQ1" to "Three" + Then I should not see "Three" in the "//select[@id='dropQ1']//option[4]" "xpath_element" And I click on "Three" "radio" - And I click on "Choice Three for row Cheese" "radio" - And I click on "Choice Three for row Bread" "radio" - And I click on "Choice Three for row Meat" "radio" - And I click on "Choice Three for row Fruit" "radio" + And I click on "Row 2, Cheese: Column 5, Three." "radio" + And I click on "Row 3, Bread: Column 5, Three." "radio" + And I click on "Row 4, Meat: Column 5, Three." "radio" + And I click on "Row 5, Fruit: Column 5, Three." "radio" And I click on "Yes" "radio" - And I click on "Choice Two for row Clubs" "radio" - And I click on "Choice Four for row Diamonds" "radio" - And I click on "Choice Zero for row Hearts" "radio" - And I click on "Choice Sixteen for row Spades" "radio" + And I click on "Row 2, Clubs: Column 4, Two." "radio" + And I click on "Row 3, Diamonds: Column 5, Four." "radio" + And I click on "Row 4, Hearts: Column 3, Zero." "radio" + And I click on "Row 5, Spades: Column 7, Sixteen." "radio" And I press "Submit questionnaire" Then I should see "Thank you for completing this Questionnaire." - And I follow "Continue" - Then I should see "Your response" + And I press "Continue" + Then I should see "View your response(s)" And I should see "These are the main Feedback notes" And I should see "Section 1 label" And I should see "39%" @@ -145,18 +147,18 @@ Feature: In questionnaire, personality tests can be constructed using feedback o Then I should see "Select one dropdown" And I set the field "dropQ1" to "One" And I click on "One" "radio" - And I click on "Choice Two for row Cheese" "radio" - And I click on "Choice Two for row Bread" "radio" - And I click on "Choice Two for row Meat" "radio" - And I click on "Choice Two for row Fruit" "radio" + And I click on "Row 2, Cheese: Column 4, Two." "radio" + And I click on "Row 3, Bread: Column 4, Two." "radio" + And I click on "Row 4, Meat: Column 4, Two." "radio" + And I click on "Row 5, Fruit: Column 4, Two." "radio" And I click on "Yes" "radio" - And I click on "Choice Zero for row Clubs" "radio" - And I click on "Choice Two for row Diamonds" "radio" - And I click on "Choice Four for row Hearts" "radio" - And I click on "Choice Eight for row Spades" "radio" + And I click on "Row 2, Clubs: Column 3, Zero." "radio" + And I click on "Row 3, Diamonds: Column 4, Two." "radio" + And I click on "Row 4, Hearts: Column 5, Four." "radio" + And I click on "Row 5, Spades: Column 6, Eight." "radio" And I press "Submit questionnaire" Then I should see "Thank you for completing this Questionnaire." - And I follow "Continue" + And I press "Continue" And I should see "These are the main Feedback notes" And I should see "Section 1 label" And I should see "22%" @@ -164,4 +166,4 @@ Feature: In questionnaire, personality tests can be constructed using feedback o And I should see "Section 2 label" And I should see "53%" And I should see "Feedback 2 100%" - And I log out \ No newline at end of file + And I log out diff --git a/tests/behat/add_questionnaire.feature b/tests/behat/add_questionnaire.feature index cdbda806..25dc303e 100644 --- a/tests/behat/add_questionnaire.feature +++ b/tests/behat/add_questionnaire.feature @@ -4,7 +4,7 @@ Feature: Add a questionnaire activity As a teacher I need to add a questionnaire activity to a moodle course -@javascript + @javascript Scenario: Add a questionnaire to a course without questions Given the following "users" exist: | username | firstname | lastname | email | @@ -23,4 +23,4 @@ Feature: Add a questionnaire activity And I log in as "student1" And I am on "Course 1" course homepage And I follow "Test questionnaire" - Then I should see "This questionnaire does not contain any questions." \ No newline at end of file + Then I should see "This questionnaire does not contain any questions." diff --git a/tests/behat/add_questions.feature b/tests/behat/add_questions.feature index 1eb9a160..54b4fdf0 100644 --- a/tests/behat/add_questions.feature +++ b/tests/behat/add_questions.feature @@ -4,7 +4,7 @@ Feature: Add questions to a questionnaire activity As a teacher I need to add a questionnaire activity with questions to a moodle course -@javascript + @javascript Scenario: Add a questionnaire to a course with one of each question type Given the following "users" exist: | username | firstname | lastname | email | @@ -99,4 +99,4 @@ Feature: Add questions to a questionnaire activity And I should see "Choose yes or no" And I set the field "id_type_id" to "----- Page Break -----" And I press "Add selected question type" - Then I should see "[----- Page Break -----]" \ No newline at end of file + Then I should see "[----- Page Break -----]" diff --git a/tests/behat/anonymous_questionnaire.feature b/tests/behat/anonymous_questionnaire.feature index 7f64b7e9..bd80426e 100644 --- a/tests/behat/anonymous_questionnaire.feature +++ b/tests/behat/anonymous_questionnaire.feature @@ -20,10 +20,8 @@ Feature: Questionnaires can be anonymous | questionnaire | Anonymous questionnaire | Anonymous questionnaire description | C1 | questionnaire0 | And I log in as "teacher1" And I am on "Course 1" course homepage - And I follow "Anonymous questionnaire" - And I navigate to "Edit settings" in current page administration + And I am on the "Anonymous questionnaire" "questionnaire activity editing" page And I expand all fieldsets - And I should see "Response options" And I set the field "id_respondenttype" to "anonymous" And I press "Save and display" Then I should see "Anonymous questionnaire" @@ -34,7 +32,7 @@ Feature: Questionnaires can be anonymous | Question Text | Do you like this course | And I log out -@javascript + @javascript Scenario: Student completes an anonymous questionnaire And I log in as "student1" And I am on "Course 1" course homepage @@ -44,7 +42,7 @@ Feature: Questionnaires can be anonymous And I click on "Yes" "radio" And I press "Submit questionnaire" Then I should see "Thank you for completing this Questionnaire." - And I follow "Continue" - Then I should see "Your response" + And I press "Continue" + Then I should see "View your response(s)" And I should see "Anonymous questionnaire" - And I should see "Respondent: - Anonymous -" \ No newline at end of file + And I should see "Respondent: - Anonymous -" diff --git a/tests/behat/behat_mod_questionnaire.php b/tests/behat/behat_mod_questionnaire.php index 29ea984e..57c97c58 100644 --- a/tests/behat/behat_mod_questionnaire.php +++ b/tests/behat/behat_mod_questionnaire.php @@ -19,7 +19,7 @@ * * @package mod_questionnaire * @category test - * @copyright 2016 Mike Churchward - The POET Group + * @copyright 2016 Mike Churchward - Poet Open Source * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -32,17 +32,73 @@ Behat\Gherkin\Node\TableNode as TableNode, Behat\Gherkin\Node\PyStringNode as PyStringNode, Behat\Mink\Exception\ExpectationException as ExpectationException; -; + +#[\AllowDynamicProperties] /** * Questionnaire-related steps definitions. * * @package mod_questionnaire * @category test - * @copyright 2016 Mike Churchward - The POET Group + * @copyright 2016 Mike Churchward - Poet Open Source * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_mod_questionnaire extends behat_base { + /** + * Convert page names to URLs for steps like 'When I am on the "[page name]" page'. + * + * Recognised page names are: + * | None so far! | | + * + * @param string $page name of the page, with the component name removed e.g. 'Admin notification'. + * @return moodle_url the corresponding URL. + * @throws Exception with a meaningful error message if the specified page cannot be found. + */ + protected function resolve_page_url(string $page): moodle_url { + switch (strtolower($page)) { + default: + throw new Exception('Unrecognised quiz page type "' . $page . '."'); + } + } + + /** + * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'. + * + * Recognised page names are: + * | pagetype | name meaning | description | + * | view | Questionnaire name | The questionnaire info page (view.php) | + * | preview | Questionnaire name | The questionnaire preview page (preview.php) | + * | questions | Questionnaire name | The questionnaire questions page (questions.php) | + * | advsettings | Questionnaire name | The advanced settings page (questions.php) | + * + * @param string $type identifies which type of page this is, e.g. 'preview'. + * @param string $identifier identifies the particular page, e.g. 'Test questionnaire > preview > Attempt 1'. + * @return moodle_url the corresponding URL. + * @throws Exception with a meaningful error message if the specified page cannot be found. + */ + protected function resolve_page_instance_url(string $type, string $identifier): moodle_url { + switch (strtolower($type)) { + case 'view': + return new moodle_url('/mod/questionnaire/view.php', + ['id' => $this->get_cm_by_questionnaire_name($identifier)->id]); + + case 'preview': + return new moodle_url('/mod/questionnaire/preview.php', + ['id' => $this->get_cm_by_questionnaire_name($identifier)->id]); + + case 'questions': + return new moodle_url('/mod/questionnaire/questions.php', + ['id' => $this->get_cm_by_questionnaire_name($identifier)->id]); + + case 'advsettings': + return new moodle_url('/mod/questionnaire/qsettings.php', + ['id' => $this->get_cm_by_questionnaire_name($identifier)->id]); + + default: + throw new Exception('Unrecognised questionnaire page type "' . $type . '."'); + } + } + /** * Adds a question to the questionnaire with the provided data. * @@ -63,7 +119,8 @@ public function i_add_a_question_and_i_fill_the_form_with($questiontype, TableNo 'Radio Buttons', 'Rate (scale 1..5)', 'Text Box', - 'Yes/No'); + 'Yes/No', + 'Slider'); if (!in_array($questiontype, $validtypes)) { throw new ExpectationException('Invalid question type specified.', $this->getSession()); @@ -85,6 +142,18 @@ public function i_add_a_question_and_i_fill_the_form_with($questiontype, TableNo } $fielddata = new TableNode($rows); } + if (isset($hashrows['Named degrees'])) { + // Find the row that contained multiline data and add line breaks. Rows are two item arrays where the + // first is an identifier and the second is the value. + foreach ($rows as $key => $row) { + if ($row[0] == 'Named degrees') { + $row[1] = str_replace(',', "\n", $row[1]); + $rows[$key] = $row; + break; + } + } + $fielddata = new TableNode($rows); + } $this->execute('behat_forms::i_set_the_field_to', array('id_type_id', $questiontype)); $this->execute('behat_forms::press_button', 'Add selected question type'); @@ -103,7 +172,7 @@ public function i_add_a_question_and_i_fill_the_form_with($questiontype, TableNo * * @Given /^I click the "([^"]*)" radio button$/ * - * @param string $radiogroupname The "id" attribute of the radio button. + * @param int $radioid */ public function i_click_the_radio_button($radioid) { $session = $this->getSession(); @@ -345,7 +414,7 @@ private function add_response_data($qid, $sid) { * @param array $data Array of data record row arrays. The first row contains the field names. * @param string $datatable The name of the data table to insert records into. * @param string $mapvar The name of the object variable to store oldid / newid mappings (optional). - * @param string $replvars Array of key/value pairs where key is the mapvar and value is the record field + * @param array|null $replvars Array of key/value pairs where key is the mapvar and value is the record field * to replace with mapped values. * @return null */ @@ -374,6 +443,26 @@ private function add_data(array $data, $datatable, $mapvar = '', array $replvars $this->{$mapvar}[$oldid] = $newid; } } + } + /** + * Get a questionnaire by name. + * + * @param string $name questionnaire name. + * @return stdClass the corresponding DB row. + */ + protected function get_questionnaire_by_name(string $name): stdClass { + global $DB; + return $DB->get_record('questionnaire', ['name' => $name], '*', MUST_EXIST); + } + /** + * Get a questionnaire cmid from the quiz name. + * + * @param string $name questionnaire name. + * @return stdClass cm from get_coursemodule_from_instance. + */ + protected function get_cm_by_questionnaire_name(string $name): stdClass { + $questionnaire = $this->get_questionnaire_by_name($name); + return get_coursemodule_from_instance('questionnaire', $questionnaire->id, $questionnaire->course); } } diff --git a/tests/behat/check_responses.feature b/tests/behat/check_responses.feature index 488f2cb8..46065874 100644 --- a/tests/behat/check_responses.feature +++ b/tests/behat/check_responses.feature @@ -4,7 +4,7 @@ Feature: Review responses As a teacher I need to access the view responses features -@javascript + @javascript Scenario: Add a questionnaire to a course without questions Given the following "users" exist: | username | firstname | lastname | email | @@ -23,29 +23,28 @@ Feature: Review responses And "Test questionnaire" has questions and responses And I log in as "admin" And I navigate to "Location > Location settings" in site administration - And I set the field "id_s__timezone" to "Europe/London" - And I set the field "id_s__forcetimezone" to "Europe/London" + And I set the field "Default timezone" to "Europe/London" + And I set the field "Force timezone" to "Europe/London" And I press "Save changes" And I navigate to "Language > Language settings" in site administration - And I set the field "id_s__autolang" to "0" -# And I set the field "id_s__lang" to "en‎" + And I set the field "Language autodetect" to "0" And I log out And I log in as "teacher1" And I am on "Course 1" course homepage And I follow "Test questionnaire" - Then I should see "View All Responses" - And I navigate to "View All Responses" in current page administration - Then I should see "View All Responses." + Then I should see "View all responses" + And I navigate to "View all responses" in current page administration + Then I should see "View all responses." And I should see "All participants." And I should see "View Default order" And I should see "Responses: 6" And I follow "Ascending order" - Then I should see "View All Responses." + Then I should see "View all responses." And I should see "All participants." And I should see "Ascending order" And I should see "Responses: 6" And I follow "Descending order" - Then I should see "View All Responses." + Then I should see "View all responses." And I should see "All participants." And I should see "Descending order" And I should see "Responses: 6" @@ -72,7 +71,7 @@ Feature: Review responses And I follow "Admin User" Then I should see "1 / 5" And I follow "Summary" - Then I should see "View All Responses." + Then I should see "View all responses." And I should see "All participants." And I should see "View Default order" And I should see "Responses: 5" @@ -80,4 +79,45 @@ Feature: Review responses Then I should see "Are you sure you want to delete ALL the responses in this questionnaire?" And I press "Delete" Then I should see "You are not eligible to take this questionnaire." - And I should not see "View All Responses" + And I should not see "View all responses" + + @javascript + Scenario: Choices with HTML should display filtered HTML in the responses on the response page + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | description | course | idnumber | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Questions" in current page administration + And I add a "Check Boxes" question and I fill the form with: + | Question Name | Q1 | + | Yes | y | + | Min. forced responses | 1 | + | Max. forced responses | 2 | + | Question Text | Select one or two choices only | + | Possible answers | One,Two,Three,Four | + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Answer the questions..." in current page administration + Then I should see "Select one or two choices only" + # And I set the field "Do you own a car?" to "y" + And I set the field "One" to "checked" + And I press "Submit questionnaire" + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "View all responses" in current page administration + Then "//b[text()='One']" "xpath_element" should exist diff --git a/tests/behat/check_responses_capabilities.feature b/tests/behat/check_responses_capabilities.feature new file mode 100644 index 00000000..821ed6d7 --- /dev/null +++ b/tests/behat/check_responses_capabilities.feature @@ -0,0 +1,101 @@ +@mod @mod_questionnaire +Feature: Review responses with different capabilities + In order to review and manage questionnaire responses + As a user + I need proper capabilities to access the view responses features + + @javascript + Scenario: A teacher with mod/questionnaire:readallresponseanytime can see all responses. + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And I log in as "admin" + And I set the following system permissions of "Teacher" role: + | capability | permission | + | mod/questionnaire:readallresponseanytime | Allow | + And the following "activities" exist: + | activity | name | description | course | idnumber | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | + And "Test questionnaire" has questions and responses + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + Then I should see "View all responses" + And I navigate to "View all responses" in current page administration + Then I should see "View all responses." + And I should see "All participants." + And I should see "View Default order" + And I should see "Responses: 6" + And I log out + + @javascript + Scenario: A teacher denied mod/questionnaire:readallresponseanytime cannot see all responses. + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And I log in as "admin" + And I set the following system permissions of "Teacher" role: + | capability | permission | + | mod/questionnaire:readallresponseanytime | Prohibit | + | mod/questionnaire:readallresponses | Allow | + And the following "activities" exist: + | activity | name | description | course | idnumber | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | + And "Test questionnaire" has questions and responses + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + Then I should not see "View all responses" + And I log out + + @javascript + Scenario: A teacher with mod/questionnaire:readallresponses can see responses after appropriate time rules. + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And I log in as "admin" + And I set the following system permissions of "Teacher" role: + | capability | permission | + | mod/questionnaire:readallresponseanytime | Prohibit | + | mod/questionnaire:readallresponses | Allow | + And the following "activities" exist: + | activity | name | description | course | idnumber | resp_view | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | 0 | + | questionnaire | Test questionnaire 2 | Test questionnaire 2 description | C1 | questionnaire2 | 3 | + And "Test questionnaire" has questions and responses + And "Test questionnaire 2" has questions and responses + And I log out + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + Then I should not see "View all responses" + And I am on "Course 1" course homepage + And I follow "Test questionnaire 2" + Then I should see "View all responses" + And I log out diff --git a/tests/behat/checkbox_min_max_responses.feature b/tests/behat/checkbox_min_max_responses.feature index f3d229ed..ee5e0287 100644 --- a/tests/behat/checkbox_min_max_responses.feature +++ b/tests/behat/checkbox_min_max_responses.feature @@ -30,12 +30,23 @@ Feature: Checkbox questions can have forced minimum and maximum numbers of boxes | Max. forced responses | 2 | | Question Text | Select one or two choices only | | Possible answers | One,Two,Three,Four | - Then I should see "position 1" - And I should see "[Check Boxes] (Q1)" - And I should see "Select one or two choices only" + And I add a "Check Boxes" question and I fill the form with: + | Question Name | Q2 | + | Yes | y | + | Min. forced responses | 1 | + | Max. forced responses | 0 | + | Question Text | Select one or two choices only | + | Possible answers | Red,Blue,Yellow,Green | + And I add a "Check Boxes" question and I fill the form with: + | Question Name | Q3 | + | Yes | y | + | Min. forced responses | 0 | + | Max. forced responses | 3 | + | Question Text | Select one or two choices only | + | Possible answers | Car,Bike,Plane,Boat | And I log out -@javascript + @javascript Scenario: Student must select exactly one or two boxes to submit the question. And I log in as "student1" And I am on "Course 1" course homepage @@ -43,10 +54,21 @@ Feature: Checkbox questions can have forced minimum and maximum numbers of boxes And I navigate to "Answer the questions..." in current page administration Then I should see "Select one or two choices only" And I press "Submit questionnaire" - Then I should see "Please answer Required question #1." + Then I should see "Please answer required questions: #1. #2. #3." + And I should see "Please answer required question #1." + And I should see "Please answer required question #2." + And I should see "Please answer required question #3." And I set the field "One" to "checked" And I set the field "Two" to "checked" And I set the field "Three" to "checked" + And I set the field "Red" to "checked" + And I set the field "Blue" to "checked" + And I set the field "Car" to "checked" + And I set the field "Bike" to "checked" + And I set the field "Plane" to "checked" + And I set the field "Boat" to "checked" And I press "Submit questionnaire" # Then I should see "There is something wrong with your answer to question: #1." -- Need to figure out why this isn't working. - Then I should see "For this question you must tick a maximum of 2 box(es)." \ No newline at end of file + Then I should see "For this question you must tick a maximum of 2 box(es)." + And I should not see "For this question you must tick exactly 1 box(es)." + And I should see "For this question you must tick a maximum of 3 box(es)." diff --git a/tests/behat/checkbox_other_responses.feature b/tests/behat/checkbox_other_responses.feature new file mode 100644 index 00000000..762ea77b --- /dev/null +++ b/tests/behat/checkbox_other_responses.feature @@ -0,0 +1,48 @@ +@mod @mod_questionnaire +Feature: Checkbox questions can have other options that can be typed in. + + Background: Add a checkbox question to a questionnaire with an 'other' option. + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | description | course | idnumber | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Questions" in current page administration + And I add a "Check Boxes" question and I fill the form with: + | Question Name | Q1 | + | Yes | y | + | Question Text | Select one or two choices only | + | Possible answers | One,Two,Three,Four,!other | + And I add a "Check Boxes" question and I fill the form with: + | Question Name | Q2 | + | No | n | + | Question Text | Select one or two choices only | + | Possible answers | Red,Blue,Yellow,Green,!other=Other colour | + And I log out + + @javascript + Scenario: Student must enter a valid value when "other" is selected. + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Answer the questions..." in current page administration + Then I should see "Select one or two choices only" + And I press "Submit questionnaire" + Then I should see "Please answer required question #1" + And I set the field "Other" to "checked" + And I set the field "Other colour" to "checked" + And I press "Submit questionnaire" + Then I should see "There is something wrong with your answer to questions:" + And I should see "#1. #2." diff --git a/tests/behat/dependency_question.feature b/tests/behat/dependency_question.feature index e43bd6ad..8a9a8ccb 100644 --- a/tests/behat/dependency_question.feature +++ b/tests/behat/dependency_question.feature @@ -5,7 +5,9 @@ Feature: Questions can be defined to be dependent on answers to multiple previou I must specify that branching questions are allowed and then create question dependencies Background: Add a text box question that is dependent on a yes answer to a yes/no question. - Given the following "users" exist: + Given the "multilang" filter is "on" + And the "multilang" filter applies to "content and headings" + And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | @@ -19,6 +21,7 @@ Feature: Questions can be defined to be dependent on answers to multiple previou And the following "activities" exist: | activity | name | description | course | idnumber | resume | navigate | | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | 1 | 1 | + | questionnaire | Test questionnaire 2 | Test questionnaire description | C1 | questionnaire1 | 1 | 1 | And I log in as "teacher1" And I am on "Course 1" course homepage And I follow "Test questionnaire" @@ -47,7 +50,7 @@ Feature: Questions can be defined to be dependent on answers to multiple previou And I should see "Will you buy a car this year?" And I log out -@javascript + @javascript Scenario: Student should only be asked for the car colour if they have answered yes to question 1. And I log in as "student1" And I am on "Course 1" course homepage @@ -64,3 +67,42 @@ Feature: Questions can be defined to be dependent on answers to multiple previou And I click on "No" "radio" And I press "Next Page >>" Then I should see "Will you buy a car this year?" + + @javascript + Scenario: Add a text box question that is dependent on a multilang dropdown box question. + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire 2" + And I navigate to "Questions" in current page administration + And I set the field "id_type_id" to "Dropdown Box" + And I press "Add selected question type" + And I set the field "Question Name" to "Q1" + And I set the field "No" to "n" + And I set the field "Question Text" to "What person are you?" + And I set the field "Possible answers" to multiline: + """ + KatzeCat + HundDog + """ + And I press "Save changes" + Then I should see "[Dropdown Box] (Q1)" + And I should see "What person are you?" + And I add a "Text Box" question and I fill the form with: + | Question Name | Q2a | + | No | n | + | Input box length | 10 | + | Max. text length | 15 | + | id_dependquestions_and_0 | Q1->Cat | + | Question Text | What name has your cat? | + Then I should see "[Text Box] (Q2a)" + And I should see "What name has your cat?" + And I should see "Parent Question : position 1 (Q1->Cat) set" + And I add a "Yes/No" question and I fill the form with: + | Question Name | Q2b | + | No | n | + | id_dependquestions_and_0 | Q1->Dog | + | Question Text | Do you own a dog? | + Then I should see "[Yes/No] (Q2b)" + And I should see "Do you own a dog?" + And I should see "Parent Question : position 1 (Q1->Dog) set" + And I log out diff --git a/tests/behat/download_responses.feature b/tests/behat/download_responses.feature new file mode 100644 index 00000000..ba3f1da5 --- /dev/null +++ b/tests/behat/download_responses.feature @@ -0,0 +1,47 @@ +@mod @mod_questionnaire +Feature: Questionnaire responses can be downloaded as a CSV, etc. + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | introduction | course | idnumber | + | questionnaire | Test questionnaire download | Test questionnaire description | C1 | questionnaire1 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire download" + And I navigate to "Questions" in current page administration + And I add a "Text Box" question and I fill the form with: + | Question Name | Q1 | + | No | n | + | Input box length | 10 | + | Max. text length | 15 | + | Question Text | Enter some text | + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire download" + And I navigate to "Answer the questions..." in current page administration + And I set the field "Enter some text" to "Student response" + And I press "Submit questionnaire" + And I log out + + @javascript + Scenario: Download responses + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire download" + And I navigate to "View all responses" in current page administration + Then I click on "Download" "link" + # Without the ability to check the downloaded file, the absence of an + # exception being thrown here is considered a success. + And I click on "Download" "button" diff --git a/tests/behat/dropdown_feedback_question_type.feature b/tests/behat/dropdown_feedback_question_type.feature index a8ae2d29..e592f98f 100644 --- a/tests/behat/dropdown_feedback_question_type.feature +++ b/tests/behat/dropdown_feedback_question_type.feature @@ -37,4 +37,4 @@ Feature: In questionnaire, dropdown questions can be defined with scores attribu And I follow "Feedback" And I should see "Feedback options" And I should see "Display Scores" - And I log out \ No newline at end of file + And I log out diff --git a/tests/behat/multiple_dependency_question.feature b/tests/behat/multiple_dependency_question.feature index d2e8f8fc..41b375f2 100644 --- a/tests/behat/multiple_dependency_question.feature +++ b/tests/behat/multiple_dependency_question.feature @@ -19,6 +19,8 @@ Feature: Questions can be defined to be dependent on answers to previous questio And the following "activities" exist: | activity | name | description | course | idnumber | resume | navigate | | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | 1 | 1 | + And the "multilang" filter is "on" + And the "multilang" filter applies to "content and headings" And I log in as "teacher1" And I am on "Course 1" course homepage And I follow "Test questionnaire" @@ -34,7 +36,7 @@ Feature: Questions can be defined to be dependent on answers to previous questio | Min. forced responses | 0 | | Max. forced responses | 0 | | Question Text | Are you taking: | - | Possible answers | Math,Physics,Art,Music | + | Possible answers | CalcMath,Physics,Art,Music | | id_dependquestions_and_0 | Q1->Yes | Then I should see "[Check Boxes] (Y.1)" And I add a "Yes/No" question and I fill the form with: @@ -87,11 +89,11 @@ Feature: Questions can be defined to be dependent on answers to previous questio | Yes | y | | Question Text | Are you happy? | Then I should see "[Yes/No] (Q3)" - And I follow "Preview" + And I am on the "Test questionnaire" "mod_questionnaire > Preview" page And I should see "Previewing Questionnaire" And I log out -@javascript + @javascript Scenario: Student should only be asked questions on school if they have answered yes to question 1. And I log in as "student1" And I am on "Course 1" course homepage @@ -151,4 +153,4 @@ Feature: Questions can be defined to be dependent on answers to previous questio And I press "Next Page >>" Then I should see "You did not answer one" And I press "Next Page >>" - Then I should see "Are you happy?" \ No newline at end of file + Then I should see "Are you happy?" diff --git a/tests/behat/no_feedback_question_types.feature b/tests/behat/no_feedback_question_types.feature index 85efa9ba..4bd9ffbe 100644 --- a/tests/behat/no_feedback_question_types.feature +++ b/tests/behat/no_feedback_question_types.feature @@ -64,4 +64,4 @@ Feature: In questionnaire, certain questions will not activate feedback options. Then I should see "[Text Box] (Q8)" And I follow "Advanced settings" And I should not see "Feedback options" - And I log out \ No newline at end of file + And I log out diff --git a/tests/behat/numeric_question_digits.feature b/tests/behat/numeric_question_digits.feature index feb53639..17adc1d3 100644 --- a/tests/behat/numeric_question_digits.feature +++ b/tests/behat/numeric_question_digits.feature @@ -6,35 +6,35 @@ Feature: Numeric questions can specify a maximum number of digits, and minimum n Background: Add a numeric question to a questionnaire with a max digits and nb decimals specified Given the following "users" exist: - | username | firstname | lastname | email | - | teacher1 | Teacher | 1 | teacher1@example.com | - | student1 | Student | 1 | student1@example.com | + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | - | Course 1 | C1 | 0 | + | Course 1 | C1 | 0 | And the following "course enrolments" exist: - | user | course | role | - | teacher1 | C1 | editingteacher | - | student1 | C1 | student | + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | And the following "activities" exist: - | activity | name | description | course | idnumber | - | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | + | activity | name | description | course | idnumber | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | And I log in as "teacher1" And I am on "Course 1" course homepage And I follow "Test questionnaire" And I navigate to "Questions" in current page administration And I add a "Numeric" question and I fill the form with: - | Question Name | Q1 | - | Yes | y | - | Max. digits allowed | 6 | - | Nb of decimal digits | 2 | - | Question Text | Enter no more than six digits including the decimal point | + | Question Name | Q1 | + | Yes | y | + | Max. digits allowed | 6 | + | Nb of decimal digits | 2 | + | Question Text | Enter no more than six digits including the decimal point | Then I should see "position 1" And I should see "[Numeric] (Q1)" And I should see "Enter no more than six digits including the decimal point" And I log out -@javascript + @javascript Scenario: Student must enter no more than six digits and decimal points. And I log in as "student1" And I am on "Course 1" course homepage @@ -44,8 +44,16 @@ Feature: Numeric questions can specify a maximum number of digits, and minimum n And I set the field "Enter no more than six digits including the decimal point" to "1.23456" And I press "Submit questionnaire" Then I should see "Thank you for completing this Questionnaire." - And I follow "Continue" - Then I should see "Your response" + And I press "Continue" + Then I should see "View your response(s)" And I should see "Test questionnaire" And I should see "Enter no more than six digits including the decimal point" - And I should see "1.2345" \ No newline at end of file + And I should see "1.2345" + + @javascript + Scenario: Test question instruction accessibility. + Given I am on the "Course 1" course page logged in as admin + And I follow "Test questionnaire" + When I navigate to "Answer the questions..." in current page administration + Then "span[id^='numerical']" "css_element" should exist + And "input[aria-describedby^='numerical']" "css_element" should exist diff --git a/tests/behat/numeric_question_thousands.feature b/tests/behat/numeric_question_thousands.feature new file mode 100644 index 00000000..5b17ef24 --- /dev/null +++ b/tests/behat/numeric_question_thousands.feature @@ -0,0 +1,60 @@ +@mod @mod_questionnaire +Feature: Numeric questions can specify a maximum number of digits + If three or less, the "don't use thousands separator message" should not be displayed. + As a teacher + I need to specify 3 or less in the max digits in the numeric question fields + + Background: Add a numeric question to a questionnaire with a max digits and nb decimals specified + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | description | course | idnumber | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | + | questionnaire | Test questionnaire2 | Test questionnaire description | C1 | questionnaire1 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Questions" in current page administration + And I add a "Numeric" question and I fill the form with: + | Question Name | Q1 | + | Yes | y | + | Max. digits allowed | 3 | + | Question Text | Enter no more than three digits | + Then I should see "position 1" + And I should see "[Numeric] (Q1)" + And I should see "Enter no more than three digits" + And I am on "Course 1" course homepage + And I follow "Test questionnaire2" + And I navigate to "Questions" in current page administration + And I add a "Numeric" question and I fill the form with: + | Question Name | Q1 | + | Yes | y | + | Max. digits allowed | 4 | + | Question Text | Enter no more than four digits | + Then I should see "position 1" + And I should see "[Numeric] (Q1)" + And I should see "Enter no more than four digits" + And I log out + + @javascript + Scenario: Student must enter no more than six digits and decimal points. + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Answer the questions..." in current page administration + Then I should see "Enter no more than three digits" + And I should not see "Do not use thousands separators." + And I am on "Course 1" course homepage + And I follow "Test questionnaire2" + And I navigate to "Answer the questions..." in current page administration + Then I should see "Enter no more than four digits" + And I should see "Do not use thousands separators." diff --git a/tests/behat/numeric_question_zero.feature b/tests/behat/numeric_question_zero.feature new file mode 100644 index 00000000..d4cb8bc0 --- /dev/null +++ b/tests/behat/numeric_question_zero.feature @@ -0,0 +1,43 @@ +@mod @mod_questionnaire +Feature: Numeric questions can have zero as a valid response + + Background: Add a numeric question to a questionnaire and accept zero + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | description | course | idnumber | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Questions" in current page administration + And I add a "Numeric" question and I fill the form with: + | Question Name | Q1 | + | Yes | y | + | Question Text | Enter a number | + Then I log out + + @javascript + Scenario: Student must enter no more than six digits and decimal points. + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Answer the questions..." in current page administration + Then I should see "Enter a number" + And I set the field "Enter a number" to "0" + And I press "Submit questionnaire" + Then I should see "Thank you for completing this Questionnaire." + And I press "Continue" + Then I should see "View your response(s)" + And I should see "Test questionnaire" + And I should see "Enter a number" + And I should see "0" diff --git a/tests/behat/progress_bar.feature b/tests/behat/progress_bar.feature new file mode 100644 index 00000000..86cddc4f --- /dev/null +++ b/tests/behat/progress_bar.feature @@ -0,0 +1,103 @@ +@mod @mod_questionnaire +Feature: Display a progress bar at the top of a questionnaire +When a student answers a questionnaire with multiple pages the progress bar will fill up as they go + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + + @javascript + Scenario: Progress bar should fill depending on progress through the pages + Given the following "activities" exist: + | activity | name | description | course | idnumber | resume | navigate | progressbar | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | 1 | 1 | 1 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Questions" in current page administration + And I add a "Yes/No" question and I fill the form with: + | Question Name | Q1 | + | Yes | y | + | Question Text | Do you like questions? | + And I add a "Numeric" question and I fill the form with: + | Question Name | Q2 | + | No | y | + | Question Text | Are you sure? | + And I add a "Yes/No" question and I fill the form with: + | Question Name | Q3 | + | Yes | y | + | Question Text | Would you like to answer another question? | + | id_dependquestions_and_0 | Q1->Yes | + And I add a "Yes/No" question and I fill the form with: + | Question Name | Q4 | + | Yes | y | + | Question Text | Is that enough questions? | + | id_dependquestions_and_0 | Q3->Yes | + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + When I navigate to "Answer the questions..." in current page administration + Then I should see "0%" in the "#questionnaire-progressbar-percent" "css_element" + And I click on "Yes" "radio" + When I press "Next Page >>" + Then I should see "50%" in the "#questionnaire-progressbar-percent" "css_element" + And I click on "Yes" "radio" + When I press "Next Page >>" + Then I should see "75%" in the "#questionnaire-progressbar-percent" "css_element" + When I press "<< Previous Page" + Then I should see "50%" in the "#questionnaire-progressbar-percent" "css_element" + + Scenario: Progress bar should not display on a single page questionnaire + Given the following "activities" exist: + | activity | name | description | course | idnumber | resume | navigate | progressbar | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | 1 | 1 | 1 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Questions" in current page administration + And I add a "Yes/No" question and I fill the form with: + | Yes | y | + | Question Text | Do you like questions? | + And I add a "Yes/No" question and I fill the form with: + | Yes | y | + | Question Text | Do you really like questions? | + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + When I navigate to "Answer the questions..." in current page administration + Then ".questionnaire-progressbar" "css_element" should not exist + + Scenario: Progress bar should not display if turned off in settings + Given the following "activities" exist: + | activity | name | description | course | idnumber | resume | navigate | progressbar | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | 1 | 1 | 0 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Questions" in current page administration + And I add a "Yes/No" question and I fill the form with: + | Question Name | Q1 | + | Yes | y | + | Question Text | Do you like questions? | + And I add a "Yes/No" question and I fill the form with: + | Question Name | Q2 | + | Yes | y | + | Question Text | Do you really like questions? | + | id_dependquestions_and_0 | Q1->Yes | + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + When I navigate to "Answer the questions..." in current page administration + Then ".questionnaire-progressbar" "css_element" should not exist diff --git a/tests/behat/public_questionnaire.feature b/tests/behat/public_questionnaire.feature index 0d9f5e6c..8b719d34 100644 --- a/tests/behat/public_questionnaire.feature +++ b/tests/behat/public_questionnaire.feature @@ -7,8 +7,6 @@ Feature: Questionnaires can use an existing public survey to gather responses in Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | - | teacher2 | Teacher | 2 | teacher2@example.com | - | teacher3 | Teacher | 3 | teacher3@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | @@ -18,14 +16,16 @@ Feature: Questionnaires can use an existing public survey to gather responses in And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | manager | - | teacher2 | C2 | editingteacher | + | teacher1 | C2 | manager | | student1 | C2 | student | - | teacher3 | C3 | editingteacher | + | teacher1 | C3 | manager | | student1 | C3 | student | And the following "activities" exist: | activity | name | description | course | idnumber | | questionnaire | Public questionnaire | Anonymous questionnaire description | C1 | questionnaire0 | + @javascript + Scenario: Public questionnaire instances have responses visible in their respective courses And I log in as "teacher1" And I am on "Course 1" course homepage And I follow "Public questionnaire" @@ -37,36 +37,20 @@ Feature: Questionnaires can use an existing public survey to gather responses in | Question Name | Q1 | | Yes | y | | Question Text | Enter a number | - And I log out - And I log in as "teacher2" - And I am on "Course 2" course homepage with editing mode on - And I follow "Add an activity or resource" - And I click on "Questionnaire" "radio" - And I click on "Add" "button" in the "Add an activity or resource" "dialogue" - And I set the field "Name" to "Questionnaire instance 1" - And I expand all fieldsets - Then I should see "Content options" - And I click on "Public questionnaire [Course 1]" "radio" - And I press "Save and display" + And I add a questionnaire activity to course "Course 2" section "1" and I fill the form with: + | Name | Questionnaire instance 1 | + | Description | Description | + | Use public | Public questionnaire [Course 1] | Then I should see "Questionnaire instance 1" - And I log out - And I log in as "teacher3" - And I am on "Course 3" course homepage with editing mode on - And I follow "Add an activity or resource" - And I click on "Questionnaire" "radio" - And I click on "Add" "button" in the "Add an activity or resource" "dialogue" - And I set the field "Name" to "Questionnaire instance 2" - And I expand all fieldsets - Then I should see "Content options" - And I click on "Public questionnaire [Course 1]" "radio" - And I press "Save and display" + And I add a questionnaire activity to course "Course 3" section "1" and I fill the form with: + | Name | Questionnaire instance 2 | + | Description | Description | + | Use public | Public questionnaire [Course 1] | Then I should see "Questionnaire instance 2" And I log out - @javascript - Scenario: Student completes public questionnaire instances in two different courses and sees each response in the proper course And I log in as "student1" And I am on "Course 2" course homepage And I follow "Questionnaire instance 1" @@ -75,36 +59,36 @@ Feature: Questionnaires can use an existing public survey to gather responses in And I set the field "Enter a number" to "1" And I press "Submit questionnaire" Then I should see "Thank you for completing this Questionnaire." - And I follow "Continue" - Then I should see "Your response" + And I press "Continue" + Then I should see "View your response(s)" And I should see "Enter a number" And "//div[contains(@class,'questionnaire_numeric') and contains(@class,'questionnaire_response')]//span[@class='selected' and text()='1']" "xpath_element" should exist And I am on "Course 3" course homepage And I follow "Questionnaire instance 2" And I should see "Answer the questions..." - And I should not see "Your response" + And I should not see "View your response(s)" And I navigate to "Answer the questions..." in current page administration Then I should see "Questionnaire instance 2" And I set the field "Enter a number" to "2" And I press "Submit questionnaire" Then I should see "Thank you for completing this Questionnaire." - And I follow "Continue" - Then I should see "Your response" + And I press "Continue" + Then I should see "View your response(s)" And I should see "Enter a number" And "//div[contains(@class,'questionnaire_numeric') and contains(@class,'questionnaire_response')]//span[@class='selected' and text()='2']" "xpath_element" should exist And I am on "Course 2" course homepage And I follow "Questionnaire instance 1" - Then I should see "Your response" - And I navigate to "Your response" in current page administration + Then I should see "View your response(s)" + And I navigate to "View your response(s)" in current page administration And I should see "Enter a number" And "//div[contains(@class,'questionnaire_numeric') and contains(@class,'questionnaire_response')]//span[@class='selected' and text()='1']" "xpath_element" should exist And I am on "Course 3" course homepage And I follow "Questionnaire instance 2" - Then I should see "Your response" - And I navigate to "Your response" in current page administration + Then I should see "View your response(s)" + And I navigate to "View your response(s)" in current page administration And I should see "Enter a number" And "//div[contains(@class,'questionnaire_numeric') and contains(@class,'questionnaire_response')]//span[@class='selected' and text()='2']" "xpath_element" should exist - And I log out \ No newline at end of file + And I log out diff --git a/tests/behat/public_questionnaire_teacher.feature b/tests/behat/public_questionnaire_teacher.feature index 26be404c..5c51c583 100644 --- a/tests/behat/public_questionnaire_teacher.feature +++ b/tests/behat/public_questionnaire_teacher.feature @@ -41,33 +41,22 @@ Feature: Public questionnaires gather all instance responses in one master cours And I log out And I log in as "teacher2" - And I am on "Course 2" course homepage with editing mode on - And I follow "Add an activity or resource" - And I click on "Questionnaire" "radio" - And I click on "Add" "button" in the "Add an activity or resource" "dialogue" - And I set the field "Name" to "Questionnaire instance 1" - And I expand all fieldsets - Then I should see "Content options" - And I click on "Public questionnaire [Course 1]" "radio" - And I press "Save and display" + And I add a questionnaire activity to course "Course 2" section "1" and I fill the form with: + | Name | Questionnaire instance 1 | + | Description | Description | + | Use public | Public questionnaire [Course 1] | Then I should see "Questionnaire instance 1" And I log out And I log in as "teacher3" - And I am on "Course 3" course homepage with editing mode on - And I follow "Add an activity or resource" - And I click on "Questionnaire" "radio" - And I click on "Add" "button" in the "Add an activity or resource" "dialogue" - And I set the field "Name" to "Questionnaire instance 2" - And I expand all fieldsets - Then I should see "Content options" - And I click on "Public questionnaire [Course 1]" "radio" - And I press "Save and display" + And I add a questionnaire activity to course "Course 3" section "1" and I fill the form with: + | Name | Questionnaire instance 2 | + | Description | Description | + | Use public | Public questionnaire [Course 1] | Then I should see "Questionnaire instance 2" And I log out - And I log in as "student1" - And I am on "Course 2" course homepage + And I am on the "Questionnaire instance 1" "mod_questionnaire > view" page logged in as "student1" And I follow "Questionnaire instance 1" And I navigate to "Answer the questions..." in current page administration Then I should see "Questionnaire instance 1" @@ -76,9 +65,7 @@ Feature: Public questionnaires gather all instance responses in one master cours Then I should see "Thank you for completing this Questionnaire." And I log out - And I log in as "student2" - And I am on "Course 3" course homepage - And I follow "Questionnaire instance 2" + And I am on the "Questionnaire instance 2" "mod_questionnaire > view" page logged in as "student2" And I should see "Answer the questions..." And I navigate to "Answer the questions..." in current page administration Then I should see "Questionnaire instance 2" @@ -89,17 +76,13 @@ Feature: Public questionnaires gather all instance responses in one master cours @javascript Scenario: Teacher should not see responses for a questionnaire using a public instance - And I log in as "teacher2" - And I am on "Course 2" course homepage with editing mode on - And I follow "Questionnaire instance 1" - And I should not see "Your response" - And I should not see "View All Responses" + And I am on the "Questionnaire instance 1" "mod_questionnaire > view" page logged in as "teacher2" + And I should not see "View your response(s)" + And I should not see "View all responses" And I log out # Scenario: Teacher in course with main public questionnaire should see all responses - And I log in as "teacher1" - And I am on "Course 1" course homepage - And I follow "Public questionnaire" - Then I should see "View All Responses" - And I navigate to "View All Responses" in current page administration + And I am on the "Public questionnaire" "mod_questionnaire > view" page logged in as "teacher1" + Then I should see "View all responses" + And I navigate to "View all responses" in current page administration Then I should see "Responses: 2" diff --git a/tests/behat/questionnaire_activity_completion.feature b/tests/behat/questionnaire_activity_completion.feature new file mode 100644 index 00000000..a9082e39 --- /dev/null +++ b/tests/behat/questionnaire_activity_completion.feature @@ -0,0 +1,116 @@ +@mod @mod_questionnaire @core_completion +Feature: View activity completion information in the questionnaire activity + In order to have visibility of questionnaire completion requirements + As a student + I need to be able to view my questionnaire completion progress + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | student1 | Student | 1 | student1@example.com | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | enablecompletion | + | Course 1 | C1 | 1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | teacher1 | C1 | editingteacher | + And the following "activities" exist: + | activity | name | introduction | course | idnumber | completion | completionview | completionpostsenabled | completionposts | + | questionnaire | Test questionnaire completion | Test questionnaire description | C1 | questionnaire2 | 2 | 1 | 1 | 1 | + + @javascript + Scenario: Check questionnaire completion feature in web for Moodle ≤ 4.2. + Given the site is running Moodle version 4.2 or lower + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire completion" + Then I click on "Add questions" "link" + And I add a "Yes/No" question and I fill the form with: + | Question Name | Q1 | + | Yes | y | + | Question Text | Are you still in School? | + Then I should see "[Yes/No] (Q1)" + And I add a "Radio Buttons" question and I fill the form with: + | Question Name | Q2 | + | Yes | y | + | Horizontal | Checked | + | Question Text | Select one choice | + | Possible answers | 1=One,2=Two,3=Three,4=Four | + Then I should see "[Radio Buttons] (Q2)" + And I add a "Text Box" question and I fill the form with: + | Question Name | Q8 | + | No | n | + | Input box length | 10 | + | Max. text length | 15 | + | Question Text | Enter some text | + Then I should see "[Text Box] (Q8)" + And I am on the "Test questionnaire completion" "questionnaire activity editing" page + And I set the following fields to these values: + | Completion tracking | Show activity as complete when conditions are met | + And I click on "Student must submit this questionnaire to complete it" "checkbox" + And I press "Save and display" + + And I am on the "Test questionnaire completion" "questionnaire activity" page + Then I should see "You are not eligible to take this questionnaire." + + And I am on the "Test questionnaire completion" "questionnaire activity" page logged in as "student1" + And I click on "Answer the questions..." "link" + Then I should see "Are you still in School?" + And I should see "Select one choice" + And I should see "Enter some text" + And I click on "Yes" "radio" + And I click on "Three" "radio" + And I press "Submit questionnaire" + Then I should see "Thank you for completing this Questionnaire." + And I press "Continue" + Then I should see "View your response(s)" + + Scenario: Check questionnaire completion feature in web for Moodle ≥ 4.3. + Given the site is running Moodle version 4.3 or higher + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire completion" + Then I click on "Add questions" "link" + And I add a "Yes/No" question and I fill the form with: + | Question Name | Q1 | + | Yes | y | + | Question Text | Are you still in School? | + Then I should see "[Yes/No] (Q1)" + And I add a "Radio Buttons" question and I fill the form with: + | Question Name | Q2 | + | Yes | y | + | Horizontal | Checked | + | Question Text | Select one choice | + | Possible answers | 1=One,2=Two,3=Three,4=Four | + Then I should see "[Radio Buttons] (Q2)" + And I add a "Text Box" question and I fill the form with: + | Question Name | Q8 | + | No | n | + | Input box length | 10 | + | Max. text length | 15 | + | Question Text | Enter some text | + Then I should see "[Text Box] (Q8)" + And I am on the "Test questionnaire completion" "questionnaire activity editing" page + And I click on "Expand all" "link" in the "region-main" "region" + And I set the field "Add requirements" to "1" + And I set the following fields to these values: + | Add requirements | 1 | + | Student must submit this questionnaire to complete it | 1 | + And I press "Save and display" + + And I am on the "Test questionnaire completion" "questionnaire activity" page + Then I should see "You are not eligible to take this questionnaire." + + And I am on the "Test questionnaire completion" "questionnaire activity" page logged in as "student1" + And I click on "Answer the questions..." "link" + Then I should see "Are you still in School?" + And I should see "Select one choice" + And I should see "Enter some text" + And I set the field "Yes" to "1" + And I set the field "Three" to "1" + And I press "Submit questionnaire" + Then I should see "Thank you for completing this Questionnaire." + And I press "Continue" + Then I should see "View your response(s)" diff --git a/tests/behat/radio_feedback_question_type.feature b/tests/behat/radio_feedback_question_type.feature index e0b67214..f66b392b 100644 --- a/tests/behat/radio_feedback_question_type.feature +++ b/tests/behat/radio_feedback_question_type.feature @@ -38,4 +38,4 @@ Feature: In questionnaire, radio questions can be defined with scores attributed And I follow "Feedback" And I should see "Feedback options" And I should see "Display Scores" - And I log out \ No newline at end of file + And I log out diff --git a/tests/behat/radio_question_other.feature b/tests/behat/radio_question_other.feature index bd20b829..84d5659e 100644 --- a/tests/behat/radio_question_other.feature +++ b/tests/behat/radio_question_other.feature @@ -41,7 +41,7 @@ Feature: Radio questions allow optional "other" responses with optional labels And I should see "Select another" And I log out -@javascript + @javascript Scenario: Student selects other options and enters their own text. And I log in as "student1" And I am on "Course 1" course homepage @@ -54,8 +54,8 @@ Feature: Radio questions allow optional "other" responses with optional labels And I set the field "Text for Another colour:" to "Indigo" And I press "Submit questionnaire" Then I should see "Thank you for completing this Questionnaire." - And I follow "Continue" - Then I should see "Your response" + And I press "Continue" + Then I should see "View your response(s)" And I should see "Test questionnaire" And I should see "Other: Yellow" - And I should see "Another colour: Indigo" \ No newline at end of file + And I should see "Another colour: Indigo" diff --git a/tests/behat/rate_feedback_question_type.feature b/tests/behat/rate_feedback_question_type.feature index 7dba034f..a7b135bb 100644 --- a/tests/behat/rate_feedback_question_type.feature +++ b/tests/behat/rate_feedback_question_type.feature @@ -34,9 +34,10 @@ Feature: In questionnaire, rate questions can be defined with scores attributed | Nb of scale items | 4 | | Type of rate scale | Osgood | | Question Text | Rate these | - | Possible answers | 1=One,2=Two,3=Three,4=Four | + | Possible answers | "Cold\|Hot","Wet\|Dry" | + | Named degrees | 1=One,2=Two,3=Three,4=Four | Then I should see "[Rate (scale 1..5)] (Q7)" And I follow "Feedback" And I should see "Feedback options" And I should see "Display Scores" - And I log out \ No newline at end of file + And I log out diff --git a/tests/behat/rate_question_na.feature b/tests/behat/rate_question_na.feature index c0ff592a..230ef2be 100644 --- a/tests/behat/rate_question_na.feature +++ b/tests/behat/rate_question_na.feature @@ -4,7 +4,7 @@ Feature: Rate scale questions have options for displaing "N/A" As a teacher I need to enter a rate question with correct options -@javascript + @javascript Scenario: Add a "N/A" option to an existing rate scale question Given the following "users" exist: | username | firstname | lastname | email | @@ -60,3 +60,41 @@ Feature: Rate scale questions have options for displaing "N/A" Then I should see "Test questionnaire" And I should see "Rate these movies from 1 to 5" And I should see "N/A" + + @javascript + Scenario: Test caption of table + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | description | course | idnumber | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Questions" in current page administration + And I add a "Rate (scale 1..5)" question and I fill the form with: + | Question Name | Q1 | + | Yes | y | + | Nb of scale items | 3 | + | Type of rate scale | No duplicate choices | + | Question Text | What are your top three movies? | + | Possible answers | Star Wars,Casablanca,Airplane,Citizen Kane,Anchorman | + Then I should see "position 1" + And I should see "[Rate (scale 1..5)] (Q1)" + And I should see "What are your top three movies?" + And I log out + + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Answer the questions..." in current page administration + Then "//caption[@class='accesshide' and text()='What are your top three movies?']" "xpath_element" should exist diff --git a/tests/behat/rate_question_named.feature b/tests/behat/rate_question_named.feature index 1a3a895c..13a29b03 100644 --- a/tests/behat/rate_question_named.feature +++ b/tests/behat/rate_question_named.feature @@ -4,7 +4,7 @@ Feature: Rate scale questions can use names for degrees As a teacher I need to enter a rate and specify specific named degrees -@javascript + @javascript Scenario: Specify names for the degrees Given the following "users" exist: | username | firstname | lastname | email | @@ -30,7 +30,8 @@ Feature: Rate scale questions can use names for degrees | Nb of scale items | 3 | | Type of rate scale | Normal | | Question Text | What did you think of these movies? | - | Possible answers | 1=I did not like,2=Ehhh,3=I liked,Star Wars,Casablanca,Airplane | + | Possible answers | Star Wars,Casablanca,Airplane | + | Named degrees | 1=I did not like,2=Ehhh,3=I liked | Then I should see "position 1" And I should see "[Rate (scale 1..5)] (Q1)" And I should see "What did you think of these movies?" @@ -45,7 +46,22 @@ Feature: Rate scale questions can use names for degrees And I should see "I did not like" And I should see "Ehhh" And I should see "I liked" - And I click on "Choice I liked for row Star Wars" "radio" - And I click on "Choice I liked for row Casablanca" "radio" - And I click on "Choice I liked for row Airplane" "radio" + # Check Row 2 with correct labels. + And "Row 2, Star Wars: Column 2, Unanswered." "radio" should exist + And "Row 2, Star Wars: Column 3, I did not like." "radio" should exist + And "Row 2, Star Wars: Column 4, Ehhh." "radio" should exist + And "Row 2, Star Wars: Column 5, I liked." "radio" should exist + # Check Row 3 with correct labels. + And "Row 3, Casablanca: Column 2, Unanswered." "radio" should exist + And "Row 3, Casablanca: Column 3, I did not like." "radio" should exist + And "Row 3, Casablanca: Column 4, Ehhh." "radio" should exist + And "Row 3, Casablanca: Column 5, I liked." "radio" should exist + # Check Row 4 with correct labels. + And "Row 4, Airplane: Column 2, Unanswered." "radio" should exist + And "Row 4, Airplane: Column 3, I did not like." "radio" should exist + And "Row 4, Airplane: Column 4, Ehhh." "radio" should exist + And "Row 4, Airplane: Column 5, I liked." "radio" should exist + And I click on "Row 2, Star Wars: Column 5, I liked." "radio" + And I click on "Row 3, Casablanca: Column 5, I liked." "radio" + And I click on "Row 4, Airplane: Column 5, I liked." "radio" And I press "Submit questionnaire" diff --git a/tests/behat/rate_question_ranking.feature b/tests/behat/rate_question_ranking.feature index 041ab6a6..cf8774bd 100644 --- a/tests/behat/rate_question_ranking.feature +++ b/tests/behat/rate_question_ranking.feature @@ -4,7 +4,7 @@ Feature: Rate scale questions can be used to uniquely rank options As a teacher I need to enter a rate question with with "no duplicate choices" selected -@javascript + @javascript Scenario: Add a rank the top three choices question Given the following "users" exist: | username | firstname | lastname | email | @@ -42,7 +42,7 @@ Feature: Rate scale questions can be used to uniquely rank options And I navigate to "Answer the questions..." in current page administration Then I should see "Test questionnaire" And I should see "What are your top three movies?" - And I click on "Choice 1 for row Star Wars" "radio" - And I click on "Choice 2 for row Airplane" "radio" - And I click on "Choice 3 for row Casablanca" "radio" + And I click on "Row 2, Star Wars: Column 2, 1." "radio" + And I click on "Row 4, Airplane: Column 3, 2." "radio" + And I click on "Row 3, Casablanca: Column 4, 3." "radio" And I press "Submit questionnaire" diff --git a/tests/behat/save_and_resume.feature b/tests/behat/save_and_resume.feature index 8cd0d792..e4d15c08 100644 --- a/tests/behat/save_and_resume.feature +++ b/tests/behat/save_and_resume.feature @@ -50,8 +50,9 @@ Feature: Questionnaire responses can be saved and resumed without submitting. And I set the field "One" to "checked" And I set the field "Two" to "checked" And I set the field "Select one choice" to "Four" - And I press "Save" + And I press "Save and exit" Then I should see "Your progress has been saved." + And I should see "Resume questionnaire" And I am on "Course 1" course homepage And I follow "Questionnaire 1" @@ -66,8 +67,9 @@ Feature: Questionnaire responses can be saved and resumed without submitting. And the field "Select one choice" matches value "Four" And I set the field "Two" to "0" And I set the field "Three" to "checked" - And I press "Save" + And I press "Save and exit" Then I should see "Your progress has been saved." + And I should see "Resume questionnaire" And I am on "Course 1" course homepage And I follow "Questionnaire 1" @@ -81,6 +83,7 @@ Feature: Questionnaire responses can be saved and resumed without submitting. And the field "Select one choice" matches value "Four" And I press "Submit questionnaire" Then I should see "Thank you for completing this Questionnaire." + And I should not see "Resume questionnaire" And I am on "Course 1" course homepage And I follow "Questionnaire 1" diff --git a/tests/behat/slider_feedback_question_type.feature b/tests/behat/slider_feedback_question_type.feature new file mode 100644 index 00000000..716cefe0 --- /dev/null +++ b/tests/behat/slider_feedback_question_type.feature @@ -0,0 +1,82 @@ +@mod @mod_questionnaire +Feature: In questionnaire, slider questions can be defined with scores attributed to specific answers, in order + to provide score dependent feedback. + In order to define a feedback question + As a teacher + I must add a required slider question type. + + @javascript + Scenario: Create a questionnaire with a slider question type and verify that feedback options exist. + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | description | course | idnumber | resume | navigate | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | 1 | 1 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I follow "Feedback" + Then I should not see "Display Scores" + And I navigate to "Questions" in current page administration + Then I should see "Add questions" + And I add a "Slider" question and I fill the form with: + | Question Name | Q1 | + | Question Text | Slider question test 1 | + | Left label | Left | + | Right label | Right | + | Centre label | Center | + | Minimum slider range (left) | -5 | + | Maximum slider range (right) | 5 | + | Slider starting value | 0 | + | Slider increment value | 1 | + Then I should see " [Slider] (Q1)" + And I should see "Slider question test" + And I follow "Feedback" + Then I should not see "Display Scores" + And I navigate to "Questions" in current page administration + Then I should see "Add questions" + And I add a "Slider" question and I fill the form with: + | Question Name | Q2 | + | Question Text | Slider question test 2 | + | Left label | Left | + | Right label | Right | + | Centre label | Center | + | Minimum slider range (left) | 0 | + | Maximum slider range (right) | 5 | + | Slider starting value | 0 | + | Slider increment value | 1 | + Then I should see " [Slider] (Q2)" + And I add a "Slider" question and I fill the form with: + | Question Name | Q3 | + | Question Text | Slider question test 3 | + | Left label | Left | + | Right label | Right | + | Centre label | Center | + | Minimum slider range (left) | 0 | + | Maximum slider range (right) | 2 | + | Slider starting value | 0 | + | Slider increment value | 1 | + Then I should see " [Slider] (Q3)" + And I should see "Slider question test" + And I follow "Feedback" + And I should see "Feedback options" + And I should see "Display Scores" + And I set the field "id_feedbacksections" to "Feedback sections" + And I set the field "id_feedbackscores" to "Yes" + And I set the field "id_feedbacknotes" to "These are the main Feedback notes" + And I press "Save settings and edit Feedback Sections" + Then I should see "[New section] section questions" + And I follow "[New section] section questions" + Then I should see "Add question to section" + And I should not see "Q1" + And I should see "Q2" + And I log out diff --git a/tests/behat/slider_question.feature b/tests/behat/slider_question.feature new file mode 100644 index 00000000..f0acbda3 --- /dev/null +++ b/tests/behat/slider_question.feature @@ -0,0 +1,128 @@ +@mod @mod_questionnaire +Feature: Slider questions can add slider with range for users to choose + In order to setup a slider question + As a teacher + I need to specify the range. + + Background: Add a slider question to a questionnaire. + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | description | course | idnumber | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I am on the "Test questionnaire" "questionnaire activity" page + And I navigate to "Questions" in current page administration + And I add a "Slider" question and I fill the form with: + | Question Name | Q1 | + | Question Text | Slider quesrion test | + | Left label | Left | + | Right label | Right | + | Centre label | Center | + | Minimum slider range (left) | 5 | + | Maximum slider range (right) | 100 | + | Slider starting value | 5 | + | Slider increment value | 5 | + Then I should see "position 1" + And I should see " [Slider] (Q1)" + And I should see "Slider quesrion test" + And I log out + + @javascript + Scenario: Student use slider questionnaire. + And I log in as "student1" + And I am on "Course 1" course homepage + And I am on the "Test questionnaire" "questionnaire activity" page + And I navigate to "Answer the questions..." in current page administration + Then I should see "Slider quesrion test" + And I should see "Left" + And I should see "Right" + And I should see "Center" + And I press "Submit questionnaire" + Then I should see "Thank you for completing this Questionnaire." + And I press "Continue" + Then I should see "View your response(s)" + + @javascript + Scenario: Teacher use slider questionnaire with invalid setting. + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I am on the "Test questionnaire" "questionnaire activity" page + And I navigate to "Questions" in current page administration + And I add a "Slider" question and I fill the form with: + | Question Name | Q1 | + | Question Text | Slider quesrion test | + | Left label | Left | + | Right label | Right | + | Centre label | Center | + | Minimum slider range (left) | 10 | + | Maximum slider range (right) | 5 | + | Slider starting value | 10 | + | Slider increment value | 15 | + And I should see "The maximum slider value must be greater than the minimum slider value." + And I should see "Note that the value increments must be lower than the maximum value. For example, if a scale of 1-10, the increment value would probably be 1." + And I am on "Course 1" course homepage + And I am on the "Test questionnaire" "questionnaire activity" page + And I navigate to "Questions" in current page administration + And I add a "Slider" question and I fill the form with: + | Question Name | Q1 | + | Question Text | Slider quesrion test | + | Left label | Left | + | Right label | Right | + | Centre label | Center | + | Minimum slider range (left) | -999 | + | Maximum slider range (right) | 999 | + | Slider starting value | 10 | + | Slider increment value | 15 | + And I should see "This question type supports an absolute maximum range of -100 to +100. We expect the vast majority of questionnaire designs to use a range of 1-10 or -10 to +10." + + @javascript + Scenario: Test accessibility for slider question type. + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I am on the "Test questionnaire" "questionnaire activity" page + And I navigate to "Questions" in current page administration + And I add a "Slider" question and I fill the form with: + | Question Name | Q2 | + | Question Text | Slider question test normal case | + | Left label | Left | + | Right label | Right | + | Centre label | Center | + | Minimum slider range (left) | 1 | + | Maximum slider range (right) | 9 | + | Slider starting value | 5 | + | Slider increment value | 1 | + And I add a "Slider" question and I fill the form with: + | Question Name | Q3 | + | Question Text | Slider question test Left label only | + | Left label | Left | + | Minimum slider range (left) | -5 | + | Maximum slider range (right) | 5 | + | Slider starting value | 1 | + | Slider increment value | 1 | + And I add a "Slider" question and I fill the form with: + | Question Name | Q4 | + | Question Text | Slider question test no label | + | Minimum slider range (left) | 1 | + | Maximum slider range (right) | 9 | + | Slider starting value | 1 | + | Slider increment value | 1 | + And I navigate to "Preview" in current page administration + Then "//legend[@class='accesshide' and contains(text(), 'Question #1')]" "xpath_element" should exist + Then "//output[@class='bubble' and contains(text(), '5')]/h2[contains(text(), 'where 5 is Left, 50 and 55 are Center and 100 is Right')]" "xpath_element" should exist + Then "//legend[@class='accesshide' and contains(text(), 'Question #2')]" "xpath_element" should exist + Then "//output[@class='bubble' and contains(text(), '5')]/h2[contains(text(), 'where 1 is Left, 5 is Center and 9 is Right')]" "xpath_element" should exist + Then "//legend[@class='accesshide' and contains(text(), 'Question #3')]" "xpath_element" should exist + Then "//output[@class='bubble' and contains(text(), '1')]/h2[contains(text(), 'where -5 is Left, 0 is average and 5 is maximum slider range')]" "xpath_element" should exist + Then "//legend[@class='accesshide' and contains(text(), 'Question #4')]" "xpath_element" should exist + Then "//output[@class='bubble' and contains(text(), '1')]/h2[contains(text(), 'where 1 is minimum slider range, 5 is average and 9 is maximum slider range')]" "xpath_element" should exist diff --git a/tests/behat/text_question_zero.feature b/tests/behat/text_question_zero.feature new file mode 100644 index 00000000..6645709c --- /dev/null +++ b/tests/behat/text_question_zero.feature @@ -0,0 +1,43 @@ +@mod @mod_questionnaire +Feature: Text questions can have zero as a valid response + + Background: Add a text question to a questionnaire and accept zero + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | description | course | idnumber | + | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Questions" in current page administration + And I add a "Text Box" question and I fill the form with: + | Question Name | Q1 | + | Yes | y | + | Question Text | Enter zero | + Then I log out + + @javascript + Scenario: Student must enter no more than six digits and decimal points. + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test questionnaire" + And I navigate to "Answer the questions..." in current page administration + Then I should see "Enter zero" + And I set the field "Enter zero" to "0" + And I press "Submit questionnaire" + Then I should see "Thank you for completing this Questionnaire." + And I press "Continue" + Then I should see "View your response(s)" + And I should see "Test questionnaire" + And I should see "Enter zero" + And "//div[contains(@class,'questionnaire_text') and contains(@class,'questionnaire_response')]//span[@class='selected' and text()='0']" "xpath_element" should exist diff --git a/tests/behat/view_questionnaire.feature b/tests/behat/view_questionnaire.feature index 1a3cd56a..1ec5187d 100644 --- a/tests/behat/view_questionnaire.feature +++ b/tests/behat/view_questionnaire.feature @@ -4,7 +4,7 @@ Feature: Questionnaires can be public, private or template As a user The type of the questionnaire affects how it is displayed. -@javascript + @javascript Scenario: Add a template questionnaire Given the following "users" exist: | username | firstname | lastname | email | @@ -22,17 +22,13 @@ Feature: Questionnaires can be public, private or template And the following "activities" exist: | activity | name | description | course | idnumber | | questionnaire | Test questionnaire | Test questionnaire description | C1 | questionnaire0 | - And I log in as "manager1" - And I am on site homepage - And I am on "Course 1" course homepage - And I follow "Test questionnaire" - And I navigate to "Advanced settings" in current page administration + And I am on the "Test questionnaire" "mod_questionnaire > advsettings" page logged in as "manager1" And I should see "Content options" And I set the field "id_realm" to "template" And I press "Save and display" Then I should see "Template questionnaires are not viewable" -@javascript + @javascript Scenario: Add a questionnaire from a public questionnaire Given the following "users" exist: | username | firstname | lastname | email | @@ -69,20 +65,18 @@ Feature: Questionnaires can be public, private or template | Question Text | Select one or two choices only | | Possible answers | One,Two,Three,Four | # Neither of the following steps work in 3.2, since the admin options are not available on any page but "view". - And I follow "Advanced settings" + And I am on the "Test questionnaire" "mod_questionnaire > advsettings" page And I should see "Content options" And I set the field "id_realm" to "public" And I press "Save and return to course" # Verify that a public questionnaire cannot be used in the same course. - And I turn editing mode on - And I add a "Questionnaire" to section "1" + And I add a questionnaire activity to course "Course 1" section "1" And I expand all fieldsets Then I should see "(No public questionnaires.)" And I press "Cancel" # Verify that a public questionnaire can be used in a different course. And I am on site homepage - And I am on "Course 2" course homepage - And I add a "Questionnaire" to section "1" + And I add a questionnaire activity to course "Course 2" section "1" And I expand all fieldsets And I set the field "name" to "Questionnaire from public" And I click on "Test questionnaire [Course 1]" "radio" @@ -100,12 +94,9 @@ Feature: Questionnaires can be public, private or template And I turn editing mode on And I delete "Test questionnaire" activity And I am on site homepage - And I am on "Course 2" course homepage - And I follow "Questionnaire from public" + And I am on the "Questionnaire from public" "mod_questionnaire > view" page Then I should see "This questionnaire used to depend on a Public questionnaire which has been deleted." And I should see "It can no longer be used and should be deleted." And I log out - And I log in as "student1" - And I am on "Course 2" course homepage - And I follow "Questionnaire from public" - Then I should see "This questionnaire is no longer available. Ask your teacher to delete it." \ No newline at end of file + And I am on the "Questionnaire from public" "mod_questionnaire > view" page logged in as "student1" + Then I should see "This questionnaire is no longer available. Ask your teacher to delete it." diff --git a/tests/behat/yesno_feedback_question_type.feature b/tests/behat/yesno_feedback_question_type.feature index 549acd3b..2c11cfc2 100644 --- a/tests/behat/yesno_feedback_question_type.feature +++ b/tests/behat/yesno_feedback_question_type.feature @@ -36,4 +36,4 @@ Feature: In questionnaire, yes/no questions can be defined with scores attribute And I follow "Feedback" And I should see "Feedback options" And I should see "Display Scores" - And I log out \ No newline at end of file + And I log out diff --git a/tests/csvexport_test.php b/tests/csvexport_test.php index 3a740485..bf90dbef 100644 --- a/tests/csvexport_test.php +++ b/tests/csvexport_test.php @@ -14,24 +14,22 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Test performance of questionnaire. - * @author Guy Thomas - * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -defined('MOODLE_INTERNAL') || die(); - /** * Performance test for questionnaire module. + * @package mod_questionnaire * @group mod_questionnaire * @author Guy Thomas * @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class mod_questionnaire_csvexport_test extends advanced_testcase { +namespace mod_questionnaire; + +/** + * Unit tests for questionnaire_csvexport_test. + * @group mod_questionnaire + */ +class csvexport_test extends \advanced_testcase { public function setUp(): void { global $CFG; @@ -60,6 +58,11 @@ private function get_csv_text(array $rows) { return $lines; } + /** + * Tests the CSV export. + * + * @covers \questionnaire::generate_csv + */ public function test_csvexport() { $this->resetAfterTest(); $dg = $this->getDataGenerator(); @@ -70,17 +73,17 @@ public function test_csvexport() { $questionnaires = $qdg->questionnaires(); foreach ($questionnaires as $questionnaire) { list ($course, $cm) = get_course_and_cm_from_instance($questionnaire->id, 'questionnaire', $questionnaire->course); - $questionnaireinst = new questionnaire(0, $questionnaire, $course, $cm); + $questionnaireinst = new \questionnaire($course, $cm, 0, $questionnaire); // Test for only complete responses. - $newoutput = $this->get_csv_text($questionnaireinst->generate_csv('', '', 0, 0, 0, 0)); + $newoutput = $this->get_csv_text($questionnaireinst->generate_csv(0, '', '', 0, 0, 0)); $this->assertEquals(count($newoutput), count($this->expected_complete_output())); foreach ($newoutput as $key => $output) { $this->assertEquals($this->expected_complete_output()[$key], $output); } // Test for all responses. - $newoutput = $this->get_csv_text($questionnaireinst->generate_csv('', '', 0, 0, 0, 1)); + $newoutput = $this->get_csv_text($questionnaireinst->generate_csv(0, '', '', 0, 0, 1)); $this->assertEquals(count($newoutput), count($this->expected_incomplete_output())); foreach ($newoutput as $key => $output) { $this->assertEquals($this->expected_incomplete_output()[$key], $output); @@ -88,6 +91,113 @@ public function test_csvexport() { } } + /** + * Tests the CSV export with identity fields and anonymous questionnaires. + * + * @covers \questionnaire::generate_csv + */ + public function test_csvexport_identity_fields() { + global $DB; + $this->resetAfterTest(); + + $config = get_config('questionnaire', 'downloadoptions'); + if (strpos($config, 'useridentityfields') === false) { + set_config('downloadoptions', "{$config},useridentityfields", 'questionnaire'); + } + + $dg = $this->getDataGenerator(); + $qdg = $dg->get_plugin_generator('mod_questionnaire'); + $profilefields = ['specialid' => 'Special id', 'staffno' => 'Staff number']; + $qdg->create_and_fully_populate(1, 2, 1, 1, $profilefields); + + $user = $dg->create_user(); + $this->setUser($user); + $roleid = $DB->get_field('role', 'id', ['shortname' => 'student']); + + $questionnaires = $qdg->questionnaires(); + foreach ($questionnaires as $item) { + list($course, $cm) = get_course_and_cm_from_instance($item->id, 'questionnaire', $item->course); + + $this->do_test_csvexport_identity_fields($course, $cm, $user, $roleid, $profilefields, $item, false); + $this->do_test_csvexport_identity_fields($course, $cm, $user, $roleid, $profilefields, $item, true); + } + } + + /** + * Tests the CSV export with identity fields for a questionnaire. + * + * @param object $course + * @param object $cm + * @param object $user + * @param int $roleid + * @param array $profilefields + * @param object $item + * @param bool $anonymous + * @throws coding_exception + * @throws dml_exception + * @throws moodle_exception + */ + private function do_test_csvexport_identity_fields($course, $cm, $user, $roleid, $profilefields, $item, $anonymous): void { + global $DB; + + if ($anonymous) { + // Make questionnaire anonymous. + $row = new \stdClass(); + $row->id = $item->id; + $row->respondenttype = 'anonymous'; + $DB->update_record('questionnaire', $row); + } + + $context = \context_course::instance($course->id); + role_assign($roleid, $user->id, $context); + assign_capability('moodle/site:viewuseridentity', CAP_ALLOW, $roleid, $context); + + // Generate CSV output. + $questionnaire = new \questionnaire($course, $cm, $item->id); + $output = $questionnaire->generate_csv(0, '', '', 0, 0, 1); + + $this->assertNotNull($output); + $this->assertCount(3, $output); + + // Check profile field columns. + $errortext = $anonymous ? 'exists' : 'missing'; + $columns = $output[0]; + $columns1 = []; + foreach ($profilefields as $field => $name) { + $col = array_search($name, $columns); + $this->assertEquals(!$anonymous, $col, "Profile field {$field} {$errortext}"); + if (!$anonymous) { + $columns1[] = $col; + } + } + + // Check profile field values. + for ($i = 1; $i < count($output); $i++) { + $columns2 = []; + foreach ($profilefields as $field => $name) { + $values = $output[$i]; + $id = $field . ($i - 1); + $col = array_search($id, $values); + $this->assertEquals(!$anonymous, $col, "Profile field {$field} {$errortext}"); + if (!$anonymous) { + $columns2[] = $col; + } + } + + if (!$anonymous) { + // Check indexes of columns and values. + $this->assertEquals(count($columns1), count($columns2), "Indexes of columns and values"); + for ($j = 0; $j < count($columns1); $j++) { + $this->assertEquals($columns1[$j], $columns2[$j], "Indexes of columns and values"); + } + } + } + } + + /** + * Return the expected output. + * @return string[] + */ private function expected_complete_output() { return ["Institution Department Course Group Full name Username Q01_Text Box 1000 Q02_Essay Box 1002 " . "Q03_Numeric 1004 Q04_Date 1006 Q05_Radio Buttons 1008 Q06_Drop Down 1010 Q07_Check Boxes 1012->four " . @@ -96,17 +206,21 @@ private function expected_complete_output() { "Q07_Check Boxes 1012->twelve Q07_Check Boxes 1012->thirteen Q08_Rate Scale 1014->fourteen " . "Q08_Rate Scale 1014->fifteen Q08_Rate Scale 1014->sixteen Q08_Rate Scale 1014->seventeen " . "Q08_Rate Scale 1014->eighteen Q08_Rate Scale 1014->nineteen Q08_Rate Scale 1014->twenty " . - "Q08_Rate Scale 1014->happy Q08_Rate Scale 1014->sad Q08_Rate Scale 1014->jealous", + "Q08_Rate Scale 1014->happy Q08_Rate Scale 1014->sad Q08_Rate Scale 1014->jealous Q09_Slider 1016", " Test course 1 Testy Lastname1 username1 Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 ", + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5", " Test course 1 Testy Lastname2 username2 Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 ", + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5", " Test course 1 Testy Lastname3 username3 Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 ", + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5", " Test course 1 Testy Lastname4 username4 Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 "]; + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5"]; } + /** + * Return the exepected incomplete output. + * @return string[] + */ private function expected_incomplete_output() { return ["Institution Department Course Group Full name Username Complete Q01_Text Box 1000 " . "Q02_Essay Box 1002 " . @@ -116,16 +230,16 @@ private function expected_incomplete_output() { "Q07_Check Boxes 1012->twelve Q07_Check Boxes 1012->thirteen Q08_Rate Scale 1014->fourteen " . "Q08_Rate Scale 1014->fifteen Q08_Rate Scale 1014->sixteen Q08_Rate Scale 1014->seventeen " . "Q08_Rate Scale 1014->eighteen Q08_Rate Scale 1014->nineteen Q08_Rate Scale 1014->twenty " . - "Q08_Rate Scale 1014->happy Q08_Rate Scale 1014->sad Q08_Rate Scale 1014->jealous", + "Q08_Rate Scale 1014->happy Q08_Rate Scale 1014->sad Q08_Rate Scale 1014->jealous Q09_Slider 1016", " Test course 1 Testy Lastname1 username1 y Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 ", + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5", " Test course 1 Testy Lastname2 username2 y Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 ", + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5", " Test course 1 Testy Lastname3 username3 y Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 ", + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5", " Test course 1 Testy Lastname4 username4 y Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 ", + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5", " Test course 1 Testy Lastname5 username5 n Test answer Some header textSome paragraph text 83 " . - "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 "]; + "27/12/2017 wind three 0 0 0 0 0 0 0 0 0 1 1 2 3 4 5 1 2 3 4 5"]; } -} \ No newline at end of file +} diff --git a/tests/custom_completion_test.php b/tests/custom_completion_test.php new file mode 100644 index 00000000..e46ce656 --- /dev/null +++ b/tests/custom_completion_test.php @@ -0,0 +1,224 @@ +. + +/** + * Contains unit tests for core_completion/activity_custom_completion. + * + * @package mod_questionnaire + * @copyright 2022 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +declare(strict_types=1); + +namespace mod_questionnaire; + +use cm_info; +use coding_exception; +use mod_questionnaire\completion\custom_completion; +use moodle_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/completionlib.php'); +require_once($CFG->dirroot . '/mod/questionnaire/classes/question/question.php'); + +/** + * Class for unit testing mod_questionnaire/custom_completion. + * + * @package mod_questionnaire + * @copyright 2022 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class custom_completion_test extends \advanced_testcase { + + /** + * Data provider for get_state(). + * + * @return array[] + */ + public function get_state_provider(): array { + return [ + 'Undefined rule' => [ + 'somenonexistentrule', COMPLETION_DISABLED, false, null, coding_exception::class + ], + 'Rule not available' => [ + 'completionsubmit', COMPLETION_DISABLED, false, null, moodle_exception::class + ], + 'Rule available, user has not submitted' => [ + 'completionsubmit', COMPLETION_ENABLED, false, COMPLETION_INCOMPLETE, null + ], + 'Rule available, user has submitted' => [ + 'completionsubmit', COMPLETION_ENABLED, true, COMPLETION_COMPLETE, null + ], + ]; + } + + /** + * Test for get_state(). + * + * @covers \mod_questionnaire\completion\custom_completion::get_state + * @dataProvider get_state_provider + * @param string $rule The custom completion rule. + * @param int $available Whether this rule is available. + * @param bool $submitted + * @param int|null $status Expected status. + * @param string|null $exception Expected exception. + * @throws coding_exception + * + * @covers \mod_questionnaire\completion\custom_completion + */ + public function test_get_state(string $rule, int $available, ?bool $submitted, ?int $status, ?string $exception) { + if (!is_null($exception)) { + $this->expectException($exception); + } + + $this->resetAfterTest(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_questionnaire'); + + $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $questionnaire = $generator->create_instance(['course' => $course->id, 'completion' => COMPLETION_TRACKING_AUTOMATIC, + $rule => $available]); + + $questiondata['type_id'] = 1; + $questiondata['surveyid'] = $questionnaire->sid; + $questiondata['name'] = 'Q1'; + $questiondata['content'] = 'Test content'; + $question = $generator->create_question($questionnaire, $questiondata); + + // For case user done completion. + if ($status !== COMPLETION_INCOMPLETE) { + $response = $generator->create_question_response($questionnaire, $question, 'y', (int)$student->id); + } + + $this->setUser($student); + $cm = get_coursemodule_from_instance('questionnaire', $questionnaire->id); + $cm = cm_info::create($cm); + + $customcompletion = new custom_completion($cm, (int)$student->id); + $this->assertEquals($status, $customcompletion->get_state($rule)); + } + + /** + * Test for get_defined_custom_rules(). + * + * @covers \mod_questionnaire\completion\custom_completion + */ + public function test_get_defined_custom_rules() { + $rules = custom_completion::get_defined_custom_rules(); + $this->assertCount(1, $rules); + $this->assertEquals('completionsubmit', reset($rules)); + } + + /** + * Test for get_defined_custom_rule_descriptions(). + * + * @covers \mod_questionnaire\completion\custom_completion + */ + public function test_get_custom_rule_descriptions() { + // Get defined custom rules. + $rules = custom_completion::get_defined_custom_rules(); + + // Build a mock cm_info instance. + $mockcminfo = $this->getMockBuilder(cm_info::class) + ->disableOriginalConstructor() + ->onlyMethods(['__get']) + ->getMock(); + + // Instantiate a custom_completion object using the mocked cm_info. + $customcompletion = new custom_completion($mockcminfo, 1); + + // Get custom rule descriptions. + $ruledescriptions = $customcompletion->get_custom_rule_descriptions(); + + // Confirm that defined rules and rule descriptions are consistent with each other. + $this->assertEquals(count($rules), count($ruledescriptions)); + foreach ($rules as $rule) { + $this->assertArrayHasKey($rule, $ruledescriptions); + } + } + + /** + * Test for is_defined(). + * + * @covers \mod_questionnaire\completion\custom_completion + */ + public function test_is_defined() { + // Build a mock cm_info instance. + $mockcminfo = $this->getMockBuilder(cm_info::class) + ->disableOriginalConstructor() + ->getMock(); + + $customcompletion = new custom_completion($mockcminfo, 1); + + // Rule is defined. + $this->assertTrue($customcompletion->is_defined('completionsubmit')); + + // Undefined rule. + $this->assertFalse($customcompletion->is_defined('somerandomrule')); + } + + /** + * Data provider for test_get_available_custom_rules(). + * + * @return array[] + */ + public function get_available_custom_rules_provider(): array { + return [ + 'Completion submit available' => [ + COMPLETION_ENABLED, ['completionsubmit'] + ], + 'Completion submit not available' => [ + COMPLETION_DISABLED, [] + ], + ]; + } + + /** + * Test for get_available_custom_rules(). + * + * @covers \mod_questionnaire\completion\custom_completion::get_available_custom_rules + * @dataProvider get_available_custom_rules_provider + * @param int $status + * @param array $expected + * + * @covers \mod_questionnaire\completion\custom_completion + */ + public function test_get_available_custom_rules(int $status, array $expected) { + $customdataval = [ + 'customcompletionrules' => [ + 'completionsubmit' => $status + ] + ]; + + // Build a mock cm_info instance. + $mockcminfo = $this->getMockBuilder(cm_info::class) + ->disableOriginalConstructor() + ->onlyMethods(['__get']) + ->getMock(); + + // Mock the return of magic getter for the customdata attribute. + $mockcminfo->expects($this->any()) + ->method('__get') + ->with('customdata') + ->willReturn($customdataval); + + $customcompletion = new custom_completion($mockcminfo, 1); + $this->assertEquals($expected, $customcompletion->get_available_custom_rules()); + } +} diff --git a/tests/generator/lib.php b/tests/generator/lib.php index 374f5af3..447c5246 100644 --- a/tests/generator/lib.php +++ b/tests/generator/lib.php @@ -14,27 +14,31 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * mod_questionnaire data generator - * - * @package mod_questionnaire - * @copyright 2015 Mike Churchward (mike@churchward.ca) - * @author Mike Churchward - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - - defined('MOODLE_INTERNAL') || die(); use mod_questionnaire\generator\question_response, mod_questionnaire\generator\question_response_rank, - mod_questionnaire\question\base; + mod_questionnaire\question\question; global $CFG; require_once($CFG->dirroot.'/mod/questionnaire/locallib.php'); +require_once($CFG->dirroot . '/mod/questionnaire/classes/question/question.php'); +/** + * The mod_questionnaire data generator. + * + * @package mod_questionnaire + * @copyright 2015 Mike Churchward (mike@churchward.ca) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class mod_questionnaire_generator extends testing_module_generator { + /** + * @var int Current position of assigned options. + */ + protected $curpos = 0; + /** * @var int keep track of how many questions have been created. */ @@ -51,23 +55,18 @@ class mod_questionnaire_generator extends testing_module_generator { protected $questionnaires = []; /** - * To be called from data reset code only, - * do not use in tests. + * To be called from data reset code only, do not use in tests. * @return void */ public function reset() { $this->questioncount = 0; - $this->responsecount = 0; - $this->questionnaires = []; - parent::reset(); } /** * Acessor for questionnaires. - * * @return array */ public function questionnaires() { @@ -84,22 +83,20 @@ public function create_instance($record = null, array $options = null) { $record = (object)(array)$record; $defaultquestionnairesettings = array( - 'qtype' => 0, - 'respondenttype' => 'fullname', - 'resp_eligible' => 'all', - 'resp_view' => 0, - 'useopendate' => true, // Used in form only to indicate opendate can be used. - 'opendate' => 0, - 'useclosedate' => true, // Used in form only to indicate closedate can be used. - 'closedate' => 0, - 'resume' => 0, - 'navigate' => 0, - 'grade' => 0, - 'sid' => 0, - 'timemodified' => time(), - 'completionsubmit' => 0, - 'autonum' => 3, - 'create' => 'new-0', // Used in form only to indicate a new, empty instance. + 'qtype' => 0, + 'respondenttype' => 'fullname', + 'resp_eligible' => 'all', + 'resp_view' => 0, + 'opendate' => 0, + 'closedate' => 0, + 'resume' => 0, + 'navigate' => 0, + 'grade' => 0, + 'sid' => 0, + 'timemodified' => time(), + 'completionsubmit' => 0, + 'autonum' => 3, + 'create' => 'new-0', // Used in form only to indicate a new, empty instance. ); foreach ($defaultquestionnairesettings as $name => $value) { @@ -111,7 +108,7 @@ public function create_instance($record = null, array $options = null) { $instance = parent::create_instance($record, (array)$options); $cm = get_coursemodule_from_instance('questionnaire', $instance->id); $course = get_course($cm->course); - $questionnaire = new questionnaire(0, $instance, $course, $cm, false); + $questionnaire = new \questionnaire($course, $cm, 0, $instance, false); $this->questionnaires[$instance->id] = $questionnaire; @@ -120,9 +117,9 @@ public function create_instance($record = null, array $options = null) { /** * Create a survey instance with data from an existing questionnaire object. - * @param object $questionnaire - * @param array $options - * @return int + * @param questionnaire $questionnaire + * @param array $record + * @return bool|int */ public function create_content($questionnaire, $record = array()) { global $DB; @@ -140,7 +137,7 @@ public function create_content($questionnaire, $record = array()) { * @param questionnaire $questionnaire * @param array|stdClass $record * @param array|stdClass $data - accompanying data for question - e.g. choices - * @return \mod_questionnaire\question\base the question object + * @return \mod_questionnaire\question\question the question object */ public function create_question(questionnaire $questionnaire, $record = null, $data = null) { global $DB; @@ -189,7 +186,7 @@ public function create_question(questionnaire $questionnaire, $record = null, $d // Add the question. $record->id = $DB->insert_record('questionnaire_question', $record); - $question = \mod_questionnaire\question\base::question_builder($record->type_id, $record->id, $record); + $question = \mod_questionnaire\question\question::question_builder($record->type_id, $record->id, $record); // Add the question choices if required. if ($typeid !== QUESPAGEBREAK && $typeid !== QUESSECTIONTEXT) { @@ -207,6 +204,11 @@ public function create_question(questionnaire $questionnaire, $record = null, $d /** * Create a questionnaire with questions and response data for use in other tests. + * @param stdClass $course + * @param null|int $qtype + * @param array $questiondata + * @param null|array|stdClass $choicedata + * @return questionnaire */ public function create_test_questionnaire($course, $qtype = null, $questiondata = array(), $choicedata = null) { $questionnaire = $this->create_instance(array('course' => $course->id)); @@ -218,18 +220,27 @@ public function create_test_questionnaire($course, $qtype = null, $questiondata $questiondata['content'] = isset($questiondata['content']) ? $questiondata['content'] : 'Test content'; $this->create_question($questionnaire, $questiondata, $choicedata); } - $questionnaire = new questionnaire($questionnaire->id, null, $course, $cm, true); + $questionnaire = new \questionnaire($course, $cm, $questionnaire->id, null, true); return $questionnaire; } /** * Create a reponse to the supplied question. + * @param questionnaire $questionnaire + * @param question $question + * @param int|array $respval + * @param int $userid + * @param int $section + * @return false|mixed|stdClass */ public function create_question_response($questionnaire, $question, $respval, $userid = 1, $section = 1) { global $DB; $currentrid = 0; - $_POST['q'.$question->id] = $respval; - $responseid = $questionnaire->response_insert($section, $currentrid, $userid); + if (!is_array($respval)) { + $respval = ['q'.$question->id => $respval]; + } + $respdata = (object)(array_merge(['sec' => $section, 'rid' => $currentrid, 'a' => $questionnaire->id], $respval)); + $responseid = $questionnaire->response_insert($respdata, $userid); $this->response_commit($questionnaire, $responseid); return $DB->get_record('questionnaire_response', array('id' => $responseid)); } @@ -237,6 +248,9 @@ public function create_question_response($questionnaire, $question, $respval, $u /** * Need to create a method to access a private questionnaire method. * TO DO - may not need this with above "TO DO". + * @param questionnaire $questionnaire + * @param int $responseid + * @return mixed */ private function response_commit($questionnaire, $responseid) { $method = new ReflectionMethod('questionnaire', 'response_commit'); @@ -246,7 +260,7 @@ private function response_commit($questionnaire, $responseid) { /** * Validate choice question type - * @param $data + * @param array $data * @throws coding_exception */ protected function validate_question_choice($data) { @@ -257,7 +271,7 @@ protected function validate_question_choice($data) { /** * Validate radio question type - * @param $data + * @param array $data * @throws coding_exception */ protected function validate_question_radio($data) { @@ -268,7 +282,7 @@ protected function validate_question_radio($data) { /** * Validate checkbox question type - * @param $data + * @param array $data * @throws coding_exception */ protected function validate_question_check($data) { @@ -279,7 +293,7 @@ protected function validate_question_check($data) { /** * Validate rating question type - * @param $data + * @param array $data * @throws coding_exception */ protected function validate_question_rate($data) { @@ -291,8 +305,7 @@ protected function validate_question_rate($data) { /** * Thrown an error if the question isn't receiving the data it should receive. * @param string $typeid - * @param $data - * @throws coding_exception + * @param array $data */ protected function validate_question($typeid, $data) { if ($typeid == QUESCHOOSE) { @@ -309,8 +322,8 @@ protected function validate_question($typeid, $data) { /** * Add choices to question. * - * @param \mod_questionnaire\question\base $question - * @param stdClass $data + * @param \mod_questionnaire\question\question $question + * @param array $data */ protected function add_question_choices($question, $data) { foreach ($data as $content) { @@ -330,9 +343,9 @@ protected function add_question_choices($question, $data) { } /** - * TODO - use question object * Does this question have choices. - * @param $typeid + * TODO - use question object + * @param int $typeid * @return bool */ public function question_has_choices($typeid) { @@ -340,6 +353,11 @@ public function question_has_choices($typeid) { return in_array($typeid, $choicequestions); } + /** + * Return a string value for the int id. + * @param int $qtypeid + * @return string + */ public function type_str($qtypeid) { switch ($qtypeid) { case QUESYESNO: @@ -374,10 +392,19 @@ public function type_str($qtypeid) { break; case QUESPAGEBREAK: $qtype = 'sectionbreak'; + break; + case QUESSLIDER: + $qtype = 'Slider'; + break; } return $qtype; } + /** + * Return a display string for the int id. + * @param int $qtypeid + * @return string + */ public function type_name($qtypeid) { switch ($qtypeid) { case QUESYESNO: @@ -412,10 +439,19 @@ public function type_name($qtypeid) { break; case QUESPAGEBREAK: $qtype = 'Section Break'; + break; + case QUESSLIDER: + $qtype = 'Slider'; + break; } return $qtype; } + /** + * Add the response choice. + * @param \mod_questionnaire\responsetype\response\response $questionresponse + * @param int $responseid + */ protected function add_response_choice($questionresponse, $responseid) { global $DB; @@ -500,12 +536,12 @@ protected function add_response_choice($questionresponse, $responseid) { /** * Create response to questionnaire. * - * @param array|stdClass $record * @param array $questionresponses + * @param array|stdClass $record * @param boolean $complete Whether the response is complete or not. * @return stdClass the discussion object */ - public function create_response($record = null, $questionresponses, $complete = true) { + public function create_response($questionresponses, $record = null, $complete = true) { global $DB; // Increment the response count. @@ -544,13 +580,10 @@ public function create_response($record = null, $questionresponses, $complete = /** - * @param int $number - * * Generate an array of assigned options; + * @param int $number */ public function assign_opts($number = 5) { - static $curpos = 0; - $opts = 'blue, red, yellow, orange, green, purple, white, black, earth, wind, fire, space, car, truck, train' . ', van, tram, one, two, three, four, five, six, seven, eight, nine, ten, eleven, twelve, thirteen' . ', fourteen, fifteen, sixteen, seventeen, eighteen, nineteen, twenty, happy, sad, jealous, angry'; @@ -558,15 +591,15 @@ public function assign_opts($number = 5) { $numopts = count($opts); if ($number > (count($opts) / 2)) { - throw new coding_exception('Maxiumum number of options is '.($opts / 2)); + throw new coding_exception('Maxiumum number of options is '.(count($opts) / 2)); } $retopts = []; while (count($retopts) < $number) { - $retopts[] = $opts[$curpos]; + $retopts[] = $opts[$this->curpos]; $retopts = array_unique($retopts); - if (++$curpos == $numopts) { - $curpos = 0; + if (++$this->curpos == $numopts) { + $this->curpos = 0; } } // Return re-indexed version of array (otherwise you can get a weird index of 1,2,5,9, etc). @@ -574,12 +607,12 @@ public function assign_opts($number = 5) { } /** + * Generate a response. * @param questionnaire $questionnaire - * @param \mod_questionnaire\question\base[] $questions - * @param $userid - * @param $complete + * @param \mod_questionnaire\question\question[] $questions + * @param int $userid + * @param bool $complete * @return stdClass - * @throws coding_exception */ public function generate_response($questionnaire, $questions, $userid, $complete = true) { $responses = []; @@ -626,30 +659,58 @@ public function generate_response($questionnaire, $questions, $userid, $complete case QUESRATE : $answers = []; for ($a = 0; $a < count($choices) - 1; $a++) { - $answers[] = new question_response_rank($choices[$a], ($a % 5)); + $answers[] = new question_response_rank($choices[$a], (($a % 5) + 1)); } $responses[] = new question_response($question->id, $answers); break; + case QUESSLIDER : + $responses[] = new question_response($question->id, 5); + break; } } - return $this->create_response(['questionnaireid' => $questionnaire->id, 'userid' => $userid], $responses, $complete); + return $this->create_response($responses, ['questionnaireid' => $questionnaire->id, 'userid' => $userid], $complete); } + /** + * Create fully defined questionnaires into the test database. + * @param int $coursecount + * @param int $studentcount + * @param int $questionnairecount + * @param int $questionspertype + * @param array $profilefields in format ['' => ''] + */ public function create_and_fully_populate($coursecount = 4, $studentcount = 20, $questionnairecount = 2, - $questionspertype = 5) { + $questionspertype = 5, $profilefields = []) { global $DB; $dg = $this->datagenerator; $qdg = $this; - $questiontypes = [QUESTEXT, QUESESSAY, QUESNUMERIC, QUESDATE, QUESRADIO, QUESDROP, QUESCHECK, QUESRATE]; + $this->curpos = 0; + $questiontypes = [QUESTEXT, QUESESSAY, QUESNUMERIC, QUESDATE, QUESRADIO, QUESDROP, QUESCHECK, QUESRATE, QUESSLIDER]; $students = []; $courses = []; $questionnaires = []; + if (!empty($profilefields)) { + // Create profile fields and set them to show for user identity. + $fields = []; + foreach ($profilefields as $field => $name) { + $dg->create_custom_profile_field(['datatype' => 'text', + 'shortname' => $field, 'name' => $name]); + $fields[] = "profile_field_{$field}"; + } + set_config('showuseridentity', implode(',', $fields)); + } + for ($u = 0; $u < $studentcount; $u++) { - $students[] = $dg->create_user(['firstname' => 'Testy']); + $user = ['firstname' => 'Testy']; + // Set values for the profile fields. + foreach ($profilefields as $field => $name) { + $user["profile_field_{$field}"] = "{$field}{$u}"; + } + $students[] = $dg->create_user($user); } $manplugin = enrol_get_plugin('manual'); @@ -680,8 +741,8 @@ public function create_and_fully_populate($coursecount = 4, $studentcount = 20, $questionnaire, [ 'surveyid' => $questionnaire->sid, - 'name' => $qdg->type_name($questiontype), - 'type_id' => QUESSECTIONTEXT + 'name' => $qdg->type_name($questiontype), + 'type_id' => QUESSECTIONTEXT ] ); // Create questions. @@ -694,8 +755,8 @@ public function create_and_fully_populate($coursecount = 4, $studentcount = 20, $questionnaire, [ 'surveyid' => $questionnaire->sid, - 'name' => $qdg->type_name($questiontype).' '.$qname++, - 'type_id' => $questiontype + 'name' => $qdg->type_name($questiontype).' '.$qname++, + 'type_id' => $questiontype ], $opts ); @@ -724,6 +785,5 @@ public function create_and_fully_populate($coursecount = 4, $studentcount = 20, } } } - } -} \ No newline at end of file +} diff --git a/tests/generator_test.php b/tests/generator_test.php index ec221f93..707ecf6a 100644 --- a/tests/generator_test.php +++ b/tests/generator_test.php @@ -23,13 +23,22 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); +namespace mod_questionnaire; /** - * Unit tests for {@link questionnaire_generator_testcase}. + * Unit tests for questionnaire_generator_testcase. * @group mod_questionnaire */ -class mod_questionnaire_generator_testcase extends advanced_testcase { +class generator_test extends \advanced_testcase { + /** + * Test generator create_instance function. + * + * @return void + * @throws coding_exception + * @throws dml_exception + * + * @covers \mod_questionnaire\generator\ + */ public function test_create_instance() { global $DB; @@ -51,7 +60,7 @@ public function test_create_instance() { $this->assertEquals('questionnaire', $cm->modname); $this->assertEquals($course->id, $cm->course); - $context = context_module::instance($cm->id); + $context = \context_module::instance($cm->id); $this->assertEquals($questionnaire->cmid, $context->instanceid); $survey = $DB->get_record('questionnaire_survey', array('id' => $questionnaire->sid)); @@ -64,6 +73,15 @@ public function test_create_instance() { // Should test event creation if open dates and close dates are specified? } + /** + * Test generator create_content function. + * + * @return void + * @throws coding_exception + * @throws dml_exception + * + * @covers \mod_questionnaire\generator\ + */ public function test_create_content() { global $DB; @@ -73,16 +91,16 @@ public function test_create_content() { $generator = $this->getDataGenerator()->get_plugin_generator('mod_questionnaire'); $questionnaire = $generator->create_instance(array('course' => $course->id)); $cm = get_coursemodule_from_instance('questionnaire', $questionnaire->id); - $questionnaire = new questionnaire($questionnaire->id, null, $course, $cm, false); + $questionnaire = new \questionnaire($course, $cm, $questionnaire->id, null, false); $newcontent = array( - 'title' => 'New title', - 'email' => 'test@email.com', - 'subtitle' => 'New subtitle', - 'info' => 'New info', - 'thanks_page' => 'http://thankurl.com', - 'thank_head' => 'New thank header', - 'thank_body' => 'New thank body', + 'title' => 'New title', + 'email' => 'test@email.com', + 'subtitle' => 'New subtitle', + 'info' => 'New info', + 'thanks_page' => 'http://thankurl.com', + 'thank_head' => 'New thank header', + 'thank_body' => 'New thank body', ); $sid = $generator->create_content($questionnaire, $newcontent); $this->assertEquals($sid, $questionnaire->sid); @@ -91,4 +109,4 @@ public function test_create_content() { $this->assertEquals($survey->{$name}, $value); } } -} \ No newline at end of file +} diff --git a/tests/lib_test.php b/tests/lib_test.php index ff141081..4f828306 100644 --- a/tests/lib_test.php +++ b/tests/lib_test.php @@ -23,19 +23,29 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); +namespace mod_questionnaire; + +use mod_questionnaire\question\question; -use mod_questionnaire\question\base; +defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/mod/questionnaire/lib.php'); -require_once($CFG->dirroot.'/mod/questionnaire/classes/question/base.php'); +require_once($CFG->dirroot.'/mod/questionnaire/classes/question/question.php'); /** - * Unit tests for {@link questionnaire_lib_testcase}. + * Unit tests for questionnaire_lib_testcase. * @group mod_questionnaire */ -class mod_questionnaire_lib_testcase extends advanced_testcase { +class lib_test extends \advanced_testcase { + + /** + * Test for questionnaire_supports. + * + * @return void + * + * @covers \questionnaire_supports + */ public function test_questionnaire_supports() { $this->assertTrue(questionnaire_supports(FEATURE_BACKUP_MOODLE2)); $this->assertFalse(questionnaire_supports(FEATURE_COMPLETION_TRACKS_VIEWS)); @@ -49,20 +59,35 @@ public function test_questionnaire_supports() { $this->assertNull(questionnaire_supports('unknown option')); } + /** + * Test for questionnaire_get_extra_capabilities. + * + * @return void + * + * @covers \questionnaire_get_extra_capabilities + */ public function test_questionnaire_get_extra_capabilities() { $caps = questionnaire_get_extra_capabilities(); - $this->assertInternalType('array', $caps); + $this->assertIsArray($caps); $this->assertEquals(1, count($caps)); $this->assertEquals('moodle/site:accessallgroups', reset($caps)); } + /** + * Test for questionnaire_add_instance. + * + * @return void + * @throws moodle_exception + * + * @covers \questionnaire_add_instance + */ public function test_add_instance() { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); // Create test data as a record. - $questdata = new stdClass(); + $questdata = new \stdClass(); $questdata->course = $course->id; $questdata->coursemodule = ''; $questdata->name = 'Test questionnaire'; @@ -86,6 +111,14 @@ public function test_add_instance() { $this->assertTrue(questionnaire_add_instance($questdata) > 0); } + /** + * Test for questionnaire_update_instance(). + * + * @return void + * @throws dml_exception + * + * @covers \questionnaire_update_instance + */ public function test_update_instance() { global $DB; @@ -107,9 +140,7 @@ public function test_update_instance() { $qrow->respondenttype = 'anonymous'; $qrow->resp_eligible = 'none'; $qrow->resp_view = 2; - $qrow->useopendate = true; $qrow->opendate = 99; - $qrow->useclosedate = true; $qrow->closedate = 50; $qrow->resume = 1; $qrow->navigate = 1; @@ -143,9 +174,15 @@ public function test_update_instance() { $this->assertEquals($qrow->autonum, $questrecord->autonum); } - /* + /** + * Test for questionnaire_delete_instance(). + * * Need to verify that delete_instance deletes all data associated with a questionnaire. * + * @return void + * @throws dml_exception + * + * @covers \questionnaire_delete_instance */ public function test_delete_instance() { global $DB; @@ -178,6 +215,14 @@ public function test_delete_instance() { $this->assertEmpty($DB->get_records('event', array("modulename" => 'questionnaire', "instance" => $questionnaire->id))); } + /** + * Test for questionnaire_user_outline(). + * + * @return void + * @throws coding_exception + * + * @covers \questionnaire_user_outline + */ public function test_questionnaire_user_outline() { $this->resetAfterTest(); $this->setAdminUser(); @@ -198,6 +243,14 @@ public function test_questionnaire_user_outline() { $this->assertEquals('1 '.get_string("response", "questionnaire"), $outline->info); } + /** + * Test for questionnaire_user_complete(). + * + * @return void + * @throws coding_exception + * + * @covers \questionnaire_user_complete + */ public function test_questionnaire_user_complete() { $this->resetAfterTest(); $this->setAdminUser(); @@ -210,18 +263,39 @@ public function test_questionnaire_user_complete() { $this->expectOutputString(get_string('noresponses', 'questionnaire')); } + /** + * Test for questionnaire_print_recent_activity(). + * + * @return void + * + * @covers \questionnaire_print_recent_activity + */ public function test_questionnaire_print_recent_activity() { $this->resetAfterTest(); $this->setAdminUser(); $this->assertFalse(questionnaire_print_recent_activity(null, null, null)); } + /** + * Test for questionnaire_grades(). + * + * @return void + * + * @covers \questionnaire_grades + */ public function test_questionnaire_grades() { $this->resetAfterTest(); $this->setAdminUser(); $this->assertNull(questionnaire_grades(null)); } + /** + * Test for questionnaire_get_user_grades(). + * + * @return void + * + * @covers \questionnaire_get_user_grades + */ public function test_questionnaire_get_user_grades() { $this->resetAfterTest(); $this->setAdminUser(); @@ -233,18 +307,32 @@ public function test_questionnaire_get_user_grades() { // Test for an array when user specified. $grades = questionnaire_get_user_grades($questionnaire, $user->id); - $this->assertInternalType('array', $grades); + $this->assertIsArray($grades); // Test for an array when no user specified. $grades = questionnaire_get_user_grades($questionnaire); - $this->assertInternalType('array', $grades); + $this->assertIsArray($grades); } + /** + * Test for questionnaire_update_grades(). + * + * @return void + * + * @covers \questionnaire_update_grades + */ public function test_questionnaire_update_grades() { // Don't know how to test this yet! It doesn't return anything. $this->assertNull(questionnaire_update_grades()); } + /** + * Test for questionnaire_grade_item_update(). + * + * @return void + * + * @covers \questionnaire_grade_item_update + */ public function test_questionnaire_grade_item_update() { $this->resetAfterTest(); $this->setAdminUser(); diff --git a/tests/privacy_provider_test.php b/tests/privacy_provider_test.php new file mode 100644 index 00000000..0cdd6122 --- /dev/null +++ b/tests/privacy_provider_test.php @@ -0,0 +1,230 @@ +. + +/** + * Privacy test for the mod questionnaire. + * + * @package mod_questionnaire + * @copyright 2019, onwards Poet + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_questionnaire; + +use \mod_questionnaire\privacy\provider; + +/** + * Privacy test for the mod questionnaire. + * + * @package mod_questionnaire + * @copyright 2019, onwards Poet + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @group mod_questionnaire + */ +class privacy_provider_test extends \core_privacy\tests\provider_testcase { + /** + * Tests set up. + */ + public function setUp(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + } + + /** + * Check that the expected context is returned if there is any user data for this module. + * + * @covers \mod_questionnaire\privacy\provider::get_contexts_for_userid + */ + public function test_get_contexts_for_userid() { + global $DB; + + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + $qdg = $dg->get_plugin_generator('mod_questionnaire'); + $qdg->create_and_fully_populate(1, 1, 1, 1); + $user = $DB->get_record('user', ['firstname' => 'Testy']); + $questionnaires = $qdg->questionnaires(); + $questionnaire = current($questionnaires); + list ($course, $cm) = get_course_and_cm_from_instance($questionnaire->id, 'questionnaire', $questionnaire->course); + + $contextlist = provider::get_contexts_for_userid($user->id); + // Check that we only get back one context. + $this->assertCount(1, $contextlist); + + // Check that a context is returned and is the expected context. + $cmcontext = \context_module::instance($cm->id); + $this->assertEquals($cmcontext->id, $contextlist->get_contextids()[0]); + } + + /** + * Test that only users with a questionnaire context are fetched. + * + * @covers \mod_questionnaire\privacy\provider::get_users_in_context + */ + public function test_get_users_in_context() { + global $DB; + + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + $qdg = $dg->get_plugin_generator('mod_questionnaire'); + $qdg->create_and_fully_populate(1, 1, 1, 1); + $user = $DB->get_record('user', ['firstname' => 'Testy']); + $questionnaires = $qdg->questionnaires(); + $questionnaire = current($questionnaires); + list ($course, $cm) = get_course_and_cm_from_instance($questionnaire->id, 'questionnaire', $questionnaire->course); + $cmcontext = \context_module::instance($cm->id); + + $userlist = new \core_privacy\local\request\userlist($cmcontext, 'mod_questionnaire'); + + // The list of users for this context should return the user. + provider::get_users_in_context($userlist); + $this->assertCount(1, $userlist); + $expected = [$user->id]; + $actual = $userlist->get_userids(); + $this->assertEquals($expected, $actual); + + // The list of users for other contexts should not return any users. + $userlist = new \core_privacy\local\request\userlist(\context_system::instance(), 'mod_questionnaire'); + provider::get_users_in_context($userlist); + $this->assertCount(0, $userlist); + } + + /** + * Test that user data is exported correctly. + * + * @covers \mod_questionnaire\privacy\provider::export_user_data + */ + public function test_export_user_data() { + global $DB; + + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + $qdg = $dg->get_plugin_generator('mod_questionnaire'); + $qdg->create_and_fully_populate(1, 1, 1, 1); + $user = $DB->get_record('user', ['firstname' => 'Testy']); + $questionnaires = $qdg->questionnaires(); + $questionnaire = current($questionnaires); + list ($course, $cm) = get_course_and_cm_from_instance($questionnaire->id, 'questionnaire', $questionnaire->course); + $cmcontext = \context_module::instance($cm->id); + + $writer = \core_privacy\local\request\writer::with_context($cmcontext); + $this->assertFalse($writer->has_any_data()); + + $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'mod_questionnaire', [$cmcontext->id]); + provider::export_user_data($approvedlist); + $data = $writer->get_data([]); + + $this->assertStringContainsString($questionnaire->name, strip_tags($data->name)); + $this->assertEquals($questionnaire->intro, strip_tags($data->intro)); + $this->assertNotEmpty($data->responses[0]['questions']); + $this->assertEquals('1. Text Box 1000', $data->responses[0]['questions'][1]->questionname); + $this->assertEquals('Test answer', $data->responses[0]['questions'][1]->answers[0]); + $this->assertEquals('7. Numeric 1004', $data->responses[0]['questions'][7]->questionname); + $this->assertEquals(83, $data->responses[0]['questions'][7]->answers[0]); + $this->assertEquals('22. Rate Scale 1014', $data->responses[0]['questions'][22]->questionname); + $this->assertEquals('fourteen = 1', $data->responses[0]['questions'][22]->answers[0]); + $this->assertEquals('happy = 3', $data->responses[0]['questions'][22]->answers[7]); + } + + /** + * Test deleting all user data for a specific context. + * + * @covers \mod_questionnaire\privacy\provider::delete_data_for_all_users_in_context + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + $qdg = $dg->get_plugin_generator('mod_questionnaire'); + $qdg->create_and_fully_populate(1, 2, 1, 1); + $user = $DB->get_record('user', ['username' => 'username1']); + $questionnaires = $qdg->questionnaires(); + $questionnaire = current($questionnaires); + list ($course, $cm) = get_course_and_cm_from_instance($questionnaire->id, 'questionnaire', $questionnaire->course); + $cmcontext = \context_module::instance($cm->id); + + // Get all accounts. There should be two. + $this->assertCount(2, $DB->get_records('questionnaire_response', [])); + + // Delete everything for the context. + provider::delete_data_for_all_users_in_context($cmcontext); + $this->assertCount(0, $DB->get_records('questionnaire_response', [])); + } + + /** + * This should work identical to the above test. + * + * @covers \mod_questionnaire\privacy\provider::delete_data_for_user + */ + public function test_delete_data_for_user() { + global $DB; + + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + $qdg = $dg->get_plugin_generator('mod_questionnaire'); + $qdg->create_and_fully_populate(1, 2, 1, 1); + $user = $DB->get_record('user', ['username' => 'username1']); + $questionnaires = $qdg->questionnaires(); + $questionnaire = current($questionnaires); + list ($course, $cm) = get_course_and_cm_from_instance($questionnaire->id, 'questionnaire', $questionnaire->course); + $cmcontext = \context_module::instance($cm->id); + + // Get all accounts. There should be two. + $this->assertCount(2, $DB->get_records('questionnaire_response', [])); + + // Delete everything for the first user. + $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'questionnaire_response', [$cmcontext->id]); + provider::delete_data_for_user($approvedlist); + + $this->assertCount(0, $DB->get_records('questionnaire_response', ['userid' => $user->id])); + + // Get all accounts. There should be one. + $this->assertCount(1, $DB->get_records('questionnaire_response', [])); + } + + /** + * Test that data for users in approved userlist is deleted. + * + * @covers \mod_questionnaire\privacy\provider::delete_data_for_users + */ + public function test_delete_data_for_users() { + global $DB; + + $this->resetAfterTest(); + $dg = $this->getDataGenerator(); + $qdg = $dg->get_plugin_generator('mod_questionnaire'); + $qdg->create_and_fully_populate(1, 3, 1, 1); + $user = $DB->get_record('user', ['username' => 'username1']); + $user3 = $DB->get_record('user', ['username' => 'username3']); + $questionnaires = $qdg->questionnaires(); + $questionnaire = current($questionnaires); + list ($course, $cm) = get_course_and_cm_from_instance($questionnaire->id, 'questionnaire', $questionnaire->course); + $cmcontext = \context_module::instance($cm->id); + + $approveduserlist = new \core_privacy\local\request\approved_userlist($cmcontext, 'questionnaire', [$user->id, $user3->id]); + + // Get all accounts. There should be three. + $this->assertCount(3, $DB->get_records('questionnaire_response', [])); + + provider::delete_data_for_users($approveduserlist); + + // Get all accounts. There should be one now. + $this->assertCount(1, $DB->get_records('questionnaire_response', [])); + } +} diff --git a/tests/questiontypes_test.php b/tests/questiontypes_test.php index 44fc5f76..53944a09 100644 --- a/tests/questiontypes_test.php +++ b/tests/questiontypes_test.php @@ -23,31 +23,63 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); +namespace mod_questionnaire; + +use mod_questionnaire\question\question; -use mod_questionnaire\question\base; +defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/mod/questionnaire/locallib.php'); +require_once($CFG->dirroot . '/mod/questionnaire/classes/question/question.php'); /** - * Unit tests for {@link questionnaire_questiontypes_testcase}. + * Unit tests for questionnaire_questiontypes_testcase. * @group mod_questionnaire */ -class mod_questionnaire_questiontypes_testcase extends advanced_testcase { +class questiontypes_test extends \advanced_testcase { + + /** + * Create a check boxes test question. + * + * @return void + * + * @covers \mod_questionnaire\questiontypes_test::create_test_question + */ public function test_create_question_checkbox() { $this->create_test_question_with_choices(QUESCHECK, '\\mod_questionnaire\\question\\check', array('content' => 'Check one')); } + /** + * Create a date test question. + * + * @return void + * + * @covers \mod_questionnaire\questiontypes_test::create_test_question + */ public function test_create_question_date() { $this->create_test_question(QUESDATE, '\\mod_questionnaire\\question\\date', array('content' => 'Enter a date')); } + /** + * Create a dropdown box test question. + * + * @return void + * + * @covers \mod_questionnaire\questiontypes_test::create_test_question + */ public function test_create_question_dropdown() { $this->create_test_question_with_choices(QUESDROP, '\\mod_questionnaire\\question\\drop', array('content' => 'Select one')); } + /** + * Create an essay test question. + * + * @return void + * + * @covers \mod_questionnaire\questiontypes_test::create_test_question + */ public function test_create_question_essay() { $questiondata = array( 'content' => 'Enter an essay', @@ -56,11 +88,25 @@ public function test_create_question_essay() { $this->create_test_question(QUESESSAY, '\\mod_questionnaire\\question\\essay', $questiondata); } + /** + * Create a sectiontext test question. + * + * @return void + * + * @covers \mod_questionnaire\questiontypes_test::create_test_question + */ public function test_create_question_sectiontext() { $this->create_test_question(QUESSECTIONTEXT, '\\mod_questionnaire\\question\\sectiontext', array('name' => null, 'content' => 'This a section label.')); } + /** + * Create a numerical test question. + * + * @return void + * + * @covers \mod_questionnaire\questiontypes_test::create_test_question + */ public function test_create_question_numeric() { $questiondata = array( 'content' => 'Enter a number', @@ -69,15 +115,36 @@ public function test_create_question_numeric() { $this->create_test_question(QUESNUMERIC, '\\mod_questionnaire\\question\\numerical', $questiondata); } + /** + * Create a radio test question. + * + * @return void + * + * @covers \mod_questionnaire\questiontypes_test::create_test_question + */ public function test_create_question_radiobuttons() { $this->create_test_question_with_choices(QUESRADIO, '\\mod_questionnaire\\question\\radio', array('content' => 'Choose one')); } + /** + * Create a rate test question. + * + * @return void + * + * @covers \mod_questionnaire\questiontypes_test::create_test_question + */ public function test_create_question_ratescale() { $this->create_test_question_with_choices(QUESRATE, '\\mod_questionnaire\\question\\rate', array('content' => 'Rate these')); } + /** + * Create a text test question. + * + * @return void + * + * @covers \mod_questionnaire\questiontypes_test::create_test_question + */ public function test_create_question_textbox() { $questiondata = array( 'content' => 'Enter some text', @@ -86,6 +153,26 @@ public function test_create_question_textbox() { $this->create_test_question(QUESTEXT, '\\mod_questionnaire\\question\\text', $questiondata); } + /** + * Create a slider test question. + * + * @return void + * + * @covers \mod_questionnaire\questiontypes_test::create_test_question + */ + public function test_create_question_slider() { + $questiondata = array( + 'content' => 'Enter a number'); + $this->create_test_question(QUESSLIDER, '\\mod_questionnaire\\question\\slider', $questiondata); + } + + /** + * Create a yes/no test question. + * + * @return void + * + * @covers \mod_questionnaire\questiontypes_test::create_test_question + */ public function test_create_question_yesno() { $this->create_test_question(QUESYESNO, '\\mod_questionnaire\\question\\yesno', array('content' => 'Enter yes or no')); } @@ -93,6 +180,13 @@ public function test_create_question_yesno() { // General tests to call from specific tests above. + /** + * Create a test question. + * @param int $qtype + * @param question $questionclass + * @param array $questiondata + * @param null|array $choicedata + */ private function create_test_question($qtype, $questionclass, $questiondata = array(), $choicedata = null) { global $DB; @@ -129,7 +223,7 @@ private function create_test_question($qtype, $questionclass, $questiondata = ar } // Questionnaire object should now have question record(s). - $questionnaire = new questionnaire($questionnaire->id, null, $course, $cm, true); + $questionnaire = new \questionnaire($course, $cm, $questionnaire->id, null, true); $this->assertTrue($DB->record_exists('questionnaire_question', array('id' => $question->id))); $this->assertEquals('array', gettype($questionnaire->questions)); $this->assertTrue(array_key_exists($question->id, $questionnaire->questions)); @@ -139,6 +233,13 @@ private function create_test_question($qtype, $questionclass, $questiondata = ar } } + /** + * Create a test question with choices. + * @param int $qtype + * @param question $questionclass + * @param array $questiondata + * @param null|array $choicedata + */ private function create_test_question_with_choices($qtype, $questionclass, $questiondata = array(), $choicedata = null) { if ($choicedata === null) { $choicedata = array( diff --git a/tests/responsetypes_test.php b/tests/responsetypes_test.php index 07c74d8e..e10db5ef 100644 --- a/tests/responsetypes_test.php +++ b/tests/responsetypes_test.php @@ -23,20 +23,31 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die(); +namespace mod_questionnaire; + +use mod_questionnaire\question\question; -use mod_questionnaire\question\base; +defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/mod/questionnaire/locallib.php'); require_once($CFG->dirroot . '/mod/questionnaire/tests/generator_test.php'); require_once($CFG->dirroot . '/mod/questionnaire/tests/questiontypes_test.php'); +require_once($CFG->dirroot . '/mod/questionnaire/classes/question/question.php'); /** - * Unit tests for {@link questionnaire_responsetypes_testcase}. + * Unit tests for questionnaire_responsetypes_testcase. * @group mod_questionnaire */ -class mod_questionnaire_responsetypes_testcase extends advanced_testcase { +class responsetypes_test extends \advanced_testcase { + /** + * Test responses in a yes/no question. + * + * @return void + * @throws dml_exception + * + * @covers \mod_questionnaire\question\yesno + */ public function test_create_response_boolean() { global $DB; @@ -48,7 +59,7 @@ public function test_create_response_boolean() { // Set up a questinnaire with one boolean response question. $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_questionnaire'); - $questionnaire = $generator->create_test_questionnaire($course, QUESYESNO, array('content' => 'Enter yes or no')); + $questionnaire = $generator->create_test_questionnaire($course, QUESYESNO, ['content' => 'Enter yes or no']); $question = reset($questionnaire->questions); $response = $generator->create_question_response($questionnaire, $question, 'y', $userid); @@ -56,13 +67,21 @@ public function test_create_response_boolean() { $this->response_tests($questionnaire->id, $response->id, $userid); // Retrieve the specific boolean response. - $booleanresponses = $DB->get_records('questionnaire_response_bool', array('response_id' => $response->id)); + $booleanresponses = $DB->get_records('questionnaire_response_bool', ['response_id' => $response->id]); $this->assertEquals(1, count($booleanresponses)); $booleanresponse = reset($booleanresponses); $this->assertEquals($question->id, $booleanresponse->question_id); $this->assertEquals('y', $booleanresponse->choice_id); } + /** + * Test responses in a essay question. + * + * @return void + * @throws dml_exception + * + * @covers \mod_questionnaire\question\essay + */ public function test_create_response_text() { global $DB; @@ -74,10 +93,7 @@ public function test_create_response_text() { // Set up a questionnaire with one text response question. $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_questionnaire'); - $questiondata = array( - 'content' => 'Enter some text', - 'length' => 0, - 'precise' => 5); + $questiondata = ['content' => 'Enter some text', 'length' => 0, 'precise' => 5]; $questionnaire = $generator->create_test_questionnaire($course, QUESESSAY, $questiondata); $question = reset($questionnaire->questions); $response = $generator->create_question_response($questionnaire, $question, 'This is my essay.', $userid); @@ -86,13 +102,56 @@ public function test_create_response_text() { $this->response_tests($questionnaire->id, $response->id, $userid); // Retrieve the specific text response. - $textresponses = $DB->get_records('questionnaire_response_text', array('response_id' => $response->id)); + $textresponses = $DB->get_records('questionnaire_response_text', ['response_id' => $response->id]); $this->assertEquals(1, count($textresponses)); $textresponse = reset($textresponses); $this->assertEquals($question->id, $textresponse->question_id); $this->assertEquals('This is my essay.', $textresponse->response); } + /** + * Test responses in a slider question. + * + * @return void + * @throws dml_exception + * + * @covers \mod_questionnaire\question\slider + */ + public function test_create_response_slider() { + global $DB; + + $this->resetAfterTest(); + + // Some common variables used below. + $userid = 1; + + // Set up a questionnaire with one text response question. + $course = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_questionnaire'); + $questiondata = ['content' => 'Enter some text']; + $questionnaire = $generator->create_test_questionnaire($course, QUESSLIDER, $questiondata); + $question = reset($questionnaire->questions); + $response = $generator->create_question_response($questionnaire, $question, 5, $userid); + + // Test the responses for this questionnaire. + $this->response_tests($questionnaire->id, $response->id, $userid); + + // Retrieve the specific text response. + $textresponses = $DB->get_records('questionnaire_response_text', ['response_id' => $response->id]); + $this->assertEquals(1, count($textresponses)); + $textresponse = reset($textresponses); + $this->assertEquals($question->id, $textresponse->question_id); + $this->assertEquals(5, $textresponse->response); + } + + /** + * Test responses in a date question. + * + * @return void + * @throws dml_exception + * + * @covers \mod_questionnaire\question\date + */ public function test_create_response_date() { global $DB; @@ -104,16 +163,16 @@ public function test_create_response_date() { // Set up a questionnaire with one text response question. $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_questionnaire'); - $questionnaire = $generator->create_test_questionnaire($course, QUESDATE, array('content' => 'Enter a date')); + $questionnaire = $generator->create_test_questionnaire($course, QUESDATE, ['content' => 'Enter a date']); $question = reset($questionnaire->questions); // Date format is configured per site. This won't work unless it matches the configured format. - $response = $generator->create_question_response($questionnaire, $question, '27/1/2015', $userid); + $response = $generator->create_question_response($questionnaire, $question, '2015-01-27', $userid); // Test the responses for this questionnaire. $this->response_tests($questionnaire->id, $response->id, $userid); // Retrieve the specific date response. - $dateresponses = $DB->get_records('questionnaire_response_date', array('response_id' => $response->id)); + $dateresponses = $DB->get_records('questionnaire_response_date', ['response_id' => $response->id]); $this->assertEquals(1, count($dateresponses)); $dateresponse = reset($dateresponses); $this->assertEquals($question->id, $dateresponse->question_id); @@ -121,6 +180,14 @@ public function test_create_response_date() { $this->assertEquals('2015-01-27', $dateresponse->response); } + /** + * Test responses in a single choice radio question. + * + * @return void + * @throws dml_exception + * + * @covers \mod_questionnaire\question\radio + */ public function test_create_response_single() { global $DB; @@ -132,12 +199,13 @@ public function test_create_response_single() { // Set up a questinnaire with one question with choices including an "other" option. $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_questionnaire'); - $choicedata = array( - (object)array('content' => 'One', 'value' => 1), - (object)array('content' => 'Two', 'value' => 2), - (object)array('content' => 'Three', 'value' => 3), - (object)array('content' => '!other=Something else', 'value' => 4)); - $questionnaire = $generator->create_test_questionnaire($course, QUESRADIO, array('content' => 'Select one'), $choicedata); + $choicedata = [ + (object)['content' => 'One', 'value' => 1], + (object)['content' => 'Two', 'value' => 2], + (object)['content' => 'Three', 'value' => 3], + (object)['content' => '!other=Something else', 'value' => 4] + ]; + $questionnaire = $generator->create_test_questionnaire($course, QUESRADIO, ['content' => 'Select one'], $choicedata); // Create a response using one of the choices. $question = reset($questionnaire->questions); @@ -153,7 +221,7 @@ public function test_create_response_single() { $this->response_tests($questionnaire->id, $response->id, $userid); // Retrieve the specific single response. - $singresponses = $DB->get_records('questionnaire_resp_single', array('response_id' => $response->id)); + $singresponses = $DB->get_records('questionnaire_resp_single', ['response_id' => $response->id]); $this->assertEquals(1, count($singresponses)); $singresponse = reset($singresponses); $this->assertEquals($question->id, $singresponse->question_id); @@ -165,16 +233,16 @@ public function test_create_response_single() { $val = $cid; } } - // Need an extra $_POST variable for an "other" response. - $_POST['q'.$question->id.'_'.$val] = 'Forty-four'; + $vals = ['q'.$question->id => $val, + 'q'.$question->id. \mod_questionnaire\question\choice::id_other_choice_name($val) => 'Forty-four']; $userid = 2; - $response = $generator->create_question_response($questionnaire, $question, $val, $userid); + $response = $generator->create_question_response($questionnaire, $question, $vals, $userid); // Test the responses for this questionnaire. $this->response_tests($questionnaire->id, $response->id, $userid, 1, 2); // Retrieve the specific single response. - $singresponses = $DB->get_records('questionnaire_resp_single', array('response_id' => $response->id)); + $singresponses = $DB->get_records('questionnaire_resp_single', ['response_id' => $response->id]); $this->assertEquals(1, count($singresponses)); $singresponse = reset($singresponses); $this->assertEquals($question->id, $singresponse->question_id); @@ -182,13 +250,21 @@ public function test_create_response_single() { // Retrieve the 'other' response data. $otherresponses = $DB->get_records('questionnaire_response_other', - array('response_id' => $response->id, 'question_id' => $question->id)); + ['response_id' => $response->id, 'question_id' => $question->id]); $this->assertEquals(1, count($otherresponses)); $otherresponse = reset($otherresponses); $this->assertEquals($val, $otherresponse->choice_id); $this->assertEquals('Forty-four', $otherresponse->response); } + /** + * Test responses in a multiple choices question. + * + * @return void + * @throws dml_exception + * + * @covers \mod_questionnaire\question\rate + */ public function test_create_response_multiple() { global $DB; @@ -200,30 +276,33 @@ public function test_create_response_multiple() { // Set up a questionnaire with one question with choices including an "other" option. $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_questionnaire'); - $choicedata = array( - (object)array('content' => 'One', 'value' => 1), - (object)array('content' => 'Two', 'value' => 2), - (object)array('content' => 'Three', 'value' => 3), - (object)array('content' => '!other=Another number', 'value' => 4)); - $questionnaire = $generator->create_test_questionnaire($course, QUESCHECK, array('content' => 'Select any'), $choicedata); + $choicedata = [ + (object)['content' => 'One', 'value' => 1], + (object)['content' => 'Two', 'value' => 2], + (object)['content' => 'Three', 'value' => 3], + (object)['content' => '!other=Another number', 'value' => 4] + ]; + $questionnaire = $generator->create_test_questionnaire($course, QUESCHECK, ['content' => 'Select any'], $choicedata); $question = reset($questionnaire->questions); - $val = array(); + $val = []; foreach ($question->choices as $cid => $choice) { if (($choice->content == 'Two') || ($choice->content == 'Three')) { - $val[] = $cid; + $val[$cid] = $cid; } else if ($choice->content == '!other=Another number') { - $val2 = $cid; + $val[$cid] = $cid; + $val[\mod_questionnaire\question\choice::id_other_choice_name($cid)] = 'Forty-four'; + $ocid = $cid; } } - $_POST['q'.$question->id.'_'.$val2] = 'Forty-four'; - $response = $generator->create_question_response($questionnaire, $question, $val, $userid); + $vals = ['q'.$question->id => $val]; + $response = $generator->create_question_response($questionnaire, $question, $vals, $userid); // Test the responses for this questionnaire. $this->response_tests($questionnaire->id, $response->id, $userid); // Retrieve the specific multiples responses. - $multresponses = $DB->get_records('questionnaire_resp_multiple', array('response_id' => $response->id)); + $multresponses = $DB->get_records('questionnaire_resp_multiple', ['response_id' => $response->id]); $this->assertEquals(3, count($multresponses)); $multresponse = reset($multresponses); $this->assertEquals($question->id, $multresponse->question_id); @@ -234,13 +313,21 @@ public function test_create_response_multiple() { // Retrieve the specific other response. $otherresponses = $DB->get_records('questionnaire_response_other', - array('response_id' => $response->id, 'question_id' => $question->id)); + ['response_id' => $response->id, 'question_id' => $question->id]); $this->assertEquals(1, count($otherresponses)); $otherresponse = reset($otherresponses); - $this->assertEquals($val2, $otherresponse->choice_id); + $this->assertEquals($ocid, $otherresponse->choice_id); $this->assertEquals('Forty-four', $otherresponse->response); } + /** + * Test response's ranks in a rate question. + * + * @return void + * @throws dml_exception + * + * @covers \mod_questionnaire\question\rate + */ public function test_create_response_rank() { global $DB; @@ -252,31 +339,29 @@ public function test_create_response_rank() { // Set up a questionnaire with one ranking question. $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_questionnaire'); - $choicedata = array( - (object)array('content' => 'One', 'value' => 1), - (object)array('content' => 'Two', 'value' => 2), - (object)array('content' => 'Three', 'value' => 3)); - $questiondata = array( - 'content' => 'Rank these', - 'length' => 5, - 'precise' => 0); + $choicedata = [ + (object)['content' => 'One', 'value' => 1], + (object)['content' => 'Two', 'value' => 2], + (object)['content' => 'Three', 'value' => 3] + ]; + $questiondata = ['content' => 'Rank these', 'length' => 5, 'precise' => 0]; $questionnaire = $generator->create_test_questionnaire($course, QUESRATE, $questiondata, $choicedata); // Create a response for each choice. $question = reset($questionnaire->questions); - $vals = array(); + $vals = []; $i = 1; foreach ($question->choices as $cid => $choice) { $vals[$cid] = $i; - $_POST['q'.$question->id.'_'.$cid] = $i++; + $vals['q'.$question->id.'_'.$cid] = $i++; } - $response = $generator->create_question_response($questionnaire, $question, null, $userid); + $response = $generator->create_question_response($questionnaire, $question, $vals, $userid); // Test the responses for this questionnaire. $this->response_tests($questionnaire->id, $response->id, $userid); // Retrieve the specific rank response. - $multresponses = $DB->get_records('questionnaire_response_rank', array('response_id' => $response->id)); + $multresponses = $DB->get_records('questionnaire_response_rank', ['response_id' => $response->id]); $this->assertEquals(3, count($multresponses)); foreach ($multresponses as $multresponse) { $this->assertEquals($question->id, $multresponse->question_id); @@ -286,12 +371,20 @@ public function test_create_response_rank() { // General tests to call from specific tests above. - public function create_test_questionnaire($qtype, $questiondata = array(), $choicedata = null) { + /** + * Create a test questionnaire. + * + * @param int $qtype + * @param array $questiondata + * @param null $choicedata + * @return questionnaire + */ + public function create_test_questionnaire($qtype, $questiondata = [], $choicedata = null) { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $generator = $this->getDataGenerator()->get_plugin_generator('mod_questionnaire'); - $questionnaire = $generator->create_instance(array('course' => $course->id)); + $questionnaire = $generator->create_instance(['course' => $course->id]); $cm = get_coursemodule_from_instance('questionnaire', $questionnaire->id); $questiondata['type_id'] = $qtype; @@ -300,11 +393,20 @@ public function create_test_questionnaire($qtype, $questiondata = array(), $choi $questiondata['content'] = isset($questiondata['content']) ? $questiondata['content'] : 'Test content'; $generator->create_question($questionnaire, $questiondata, $choicedata); - $questionnaire = new questionnaire($questionnaire->id, null, $course, $cm, true); + $questionnaire = new \questionnaire( $course, $cm, $questionnaire->id, null, true); return $questionnaire; } + /** + * General assertions for responses. + * + * @param int $questionnaireid + * @param int $responseid + * @param int $userid + * @param int $attemptcount + * @param int $responsecount + */ private function response_tests($questionnaireid, $responseid, $userid, $attemptcount = 1, $responsecount = 1) { global $DB; diff --git a/version.php b/version.php index 439a3c8b..e18630bb 100644 --- a/version.php +++ b/version.php @@ -19,15 +19,16 @@ * * @package mod_questionnaire * @author Mike Churchward + * @copyright 2016 Mike Churchward (mike.churchward@poetopensource.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2019072400; // The current module version (Date: YYYYMMDDXX) -$plugin->requires = 2017042800; // Moodle version. +$plugin->version = 2022121601; // The current module version (Date: YYYYMMDDXX). +$plugin->requires = 2022112800.00; // Moodle version (4.1.0). $plugin->component = 'mod_questionnaire'; -$plugin->release = '3.5.3 (Build - 2018121000)'; -$plugin->maturity = MATURITY_STABLE; +$plugin->release = '4.1.1 (Build - 2024082900)'; +$plugin->maturity = MATURITY_STABLE; diff --git a/view.php b/view.php index 9d3510da..5500e99d 100644 --- a/view.php +++ b/view.php @@ -14,6 +14,15 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * This main view page for a questionnaire. + * + * @package mod_questionnaire + * @copyright 2016 Mike Churchward (mike.churchward@poetgroup.org) + * @author Mike Churchward + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + */ require_once("../../config.php"); require_once($CFG->dirroot.'/mod/questionnaire/locallib.php'); require_once($CFG->libdir . '/completionlib.php'); @@ -47,7 +56,7 @@ $PAGE->set_url($url); $PAGE->set_context($context); -$questionnaire = new questionnaire(0, $questionnaire, $course, $cm); +$questionnaire = new questionnaire($course, $cm, 0, $questionnaire); // Add renderer and page objects to the questionnaire object for display use. $questionnaire->add_renderer($PAGE->get_renderer('mod_questionnaire')); $questionnaire->add_page(new \mod_questionnaire\output\viewpage()); @@ -56,12 +65,7 @@ $PAGE->set_heading(format_string($course->fullname)); echo $questionnaire->renderer->header(); -$questionnaire->page->add_to_page('questionnairename', format_string($questionnaire->name)); - -// Print the main part of the page. -if ($questionnaire->intro) { - $questionnaire->page->add_to_page('intro', format_module_intro('questionnaire', $questionnaire, $cm->id)); -} +// No need to print out intro or name in Moodle 4 and above. $cm = $questionnaire->cm; $currentgroupid = groups_get_activity_group($cm); @@ -69,60 +73,22 @@ $currentgroupid = 0; } -if (!$questionnaire->is_active()) { - if ($questionnaire->capabilities->manage) { - $msg = 'removenotinuse'; - } else { - $msg = 'notavail'; - } - $questionnaire->page->add_to_page('message', get_string($msg, 'questionnaire')); - -} else if ($questionnaire->survey->realm == 'template') { - // If this is a template survey, notify and exit. - $questionnaire->page->add_to_page('message', get_string('templatenotviewable', 'questionnaire')); - echo $questionnaire->renderer->render($questionnaire->page); - echo $questionnaire->renderer->footer($questionnaire->course); - exit(); - -} else if (!$questionnaire->is_open()) { - $questionnaire->page->add_to_page('message', get_string('notopen', 'questionnaire', userdate($questionnaire->opendate))); - -} else if ($questionnaire->is_closed()) { - $questionnaire->page->add_to_page('message', get_string('closed', 'questionnaire', userdate($questionnaire->closedate))); - -} else if (!$questionnaire->user_is_eligible($USER->id)) { - if ($questionnaire->questions) { - $questionnaire->page->add_to_page('message', get_string('noteligible', 'questionnaire')); - } - -} else if (!$questionnaire->user_can_take($USER->id)) { - switch ($questionnaire->qtype) { - case QUESTIONNAIREDAILY: - $msgstring = ' '.get_string('today', 'questionnaire'); - break; - case QUESTIONNAIREWEEKLY: - $msgstring = ' '.get_string('thisweek', 'questionnaire'); - break; - case QUESTIONNAIREMONTHLY: - $msgstring = ' '.get_string('thismonth', 'questionnaire'); - break; - default: - $msgstring = ''; - break; - } - $questionnaire->page->add_to_page('message', get_string("alreadyfilled", "questionnaire", $msgstring)); - +$message = $questionnaire->user_access_messages($USER->id); +if ($message !== false) { + $questionnaire->page->add_to_page('message', $message); } else if ($questionnaire->user_can_take($USER->id)) { if ($questionnaire->questions) { // Sanity check. if (!$questionnaire->user_has_saved_response($USER->id)) { $questionnaire->page->add_to_page('complete', ''.get_string('answerquestions', 'questionnaire').''); + 'id=' . $questionnaire->cm->id) . '" class="btn btn-primary">' . + get_string('answerquestions', 'questionnaire') . ''); } else { $resumesurvey = get_string('resumesurvey', 'questionnaire'); $questionnaire->page->add_to_page('complete', ''.$resumesurvey.''); + 'id='.$questionnaire->cm->id.'&resume=1').'" title="'.$resumesurvey. + '" class="btn btn-primary">'.$resumesurvey.''); } } else { $questionnaire->page->add_to_page('message', get_string('noneinuse', 'questionnaire')); @@ -132,7 +98,8 @@ if ($questionnaire->capabilities->editquestions && !$questionnaire->questions && $questionnaire->is_active()) { $questionnaire->page->add_to_page('complete', ''.''.get_string('addquestions', 'questionnaire').''); + 'id=' . $questionnaire->cm->id) . '" class="btn btn-primary">' . + get_string('addquestions', 'questionnaire') . ''); } if (isguestuser()) { @@ -165,13 +132,14 @@ $argstr .= '&byresponse=1&action=vresp'; } $questionnaire->page->add_to_page('yourresponse', - ''.$titletext.''); + '' . $titletext . ''); } if ($questionnaire->can_view_all_responses($usernumresp)) { $argstr = 'instance='.$questionnaire->id.'&group='.$currentgroupid; $questionnaire->page->add_to_page('allresponses', - ''. + ''. get_string('viewallresponses', 'questionnaire').''); }