diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..467d27b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +modules/scorm/example_scorms/ \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE 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. + + 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 +convey 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 2 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision 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, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This 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. diff --git a/js/opigno.admin.js b/js/opigno.admin.js new file mode 100755 index 0000000..ac69410 --- /dev/null +++ b/js/opigno.admin.js @@ -0,0 +1,48 @@ +(function($) { + + /** + * Automatically enables required permissions on demand. + * + * Many users do not understand that two permissions are required for the + * administration menu to appear. Since Drupal core provides no facility for + * this, we implement a simple manual confirmation for automatically enabling + * the "other" permission. + */ + Drupal.behaviors.opignoPermissions = { + attach: function (context, settings) { + $('#permissions', context).once('opigno-permissions-setup', function () { + // Retrieve matrix/mapping - these need to use the same indexes for the + // same permissions and roles. + var $roles = $(this).find('th:not(:first)'); + var $admin = $(this).find('input[name$="[access administration pages]"]'); + var $opigno = $(this).find('input[name$="[access opigno administration pages]"]'); + + // Retrieve the permission label - without description. + var adminPermission = $.trim($admin.eq(0).parents('td').prev().children().get(0).firstChild.textContent); + var opignoPermission = $.trim($opigno.eq(0).parents('td').prev().children().get(0).firstChild.textContent); + + $admin.each(function (index) { + // Only proceed if both are not enabled already. + if (!(this.checked && $opigno[index].checked)) { + // Stack both checkboxes and attach a click event handler to both. + $(this).add($opigno[index]).click(function () { + // Do nothing when disabling a permission. + if (this.checked) { + // Figure out which is the other, check whether it still disabled, + // and if so, ask whether to auto-enable it. + var other = (this == $admin[index] ? $opigno[index] : $admin[index]); + if (!other.checked && confirm(Drupal.t('Also allow !name role to !permission?', { + '!name': $roles[index].textContent, + '!permission': (this == $admin[index] ? opignoPermission : adminPermission) + }))) { + other.checked = 'checked'; + } + } + }); + } + }); + }); + } + }; + +})(jQuery); \ No newline at end of file diff --git a/modules/breadcrumbs/opigno_breadcrumbs.info b/modules/breadcrumbs/opigno_breadcrumbs.info new file mode 100644 index 0000000..fcf0fce --- /dev/null +++ b/modules/breadcrumbs/opigno_breadcrumbs.info @@ -0,0 +1,12 @@ +name = Opigno Breadcrumbs +description = API for providing useful and intuitive breadcrumbs when in a course context. +core = 7.x +package = Opigno +dependencies[] = opigno +dependencies[] = og_context +; Information added by Drupal.org packaging script on 2014-10-19 +version = "7.x-1.9" +core = "7.x" +project = "opigno" +datestamp = "1413751430" + diff --git a/modules/breadcrumbs/opigno_breadcrumbs.module b/modules/breadcrumbs/opigno_breadcrumbs.module new file mode 100755 index 0000000..f1b6cd4 --- /dev/null +++ b/modules/breadcrumbs/opigno_breadcrumbs.module @@ -0,0 +1,65 @@ + $breadcrumb)); +} + +/** + * Helper function to get the group title. + * + * We use this instead of loading the entire node object from the DB. + * + * @param int $nid + * + * @return string + */ +function opigno_breadcrumbs_get_group_title($nid) { + $query = db_select('node', 'n'); + $query->leftJoin('node_revision', 'v', 'n.vid = v.vid'); + return $query->fields('v', array('title')) + ->condition('n.nid', $nid) + ->execute() + ->fetchField(); +} diff --git a/modules/og_prereq/opigno_og_prereq.info b/modules/og_prereq/opigno_og_prereq.info new file mode 100644 index 0000000..b42391c --- /dev/null +++ b/modules/og_prereq/opigno_og_prereq.info @@ -0,0 +1,16 @@ +name = Opigno Course Prerequisite +description = Allows courses to have other courses as prerequisites. +core = 7.x +package = Opigno + +dependencies[] = og +dependencies[] = rules +dependencies[] = rules_conditional +dependencies[] = entityreference +dependencies[] = opigno +; Information added by Drupal.org packaging script on 2014-10-19 +version = "7.x-1.9" +core = "7.x" +project = "opigno" +datestamp = "1413751430" + diff --git a/modules/og_prereq/opigno_og_prereq.install b/modules/og_prereq/opigno_og_prereq.install new file mode 100755 index 0000000..973067b --- /dev/null +++ b/modules/og_prereq/opigno_og_prereq.install @@ -0,0 +1,68 @@ + 1, + 'cardinality' => FIELD_CARDINALITY_UNLIMITED, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'course_required_course_ref', + 'foreign keys' => array( + 'node' => array( + 'columns' => array( + 'target_id' => 'nid', + ), + 'table' => 'node', + ), + ), + 'indexes' => array( + 'target_id' => array( + 0 => 'target_id', + ), + ), + 'locked' => 0, + 'module' => 'entityreference', + 'settings' => array( + 'handler' => 'base', + 'handler_settings' => array( + 'behaviors' => array( + 'views-select-list' => array( + 'status' => 0, + ), + ), + 'sort' => array( + 'type' => 'none', + ), + 'target_bundles' => array( + 'course' => 'course', + ), + ), + 'target_type' => 'node', + ), + 'translatable' => 1, + 'type' => 'entityreference', + )); + } + + $instance = field_info_instance('node', 'course_required_course_ref', OPIGNO_COURSE_BUNDLE); + if (empty($instance)) { + field_create_instance(array( + 'field_name' => 'course_required_course_ref', + 'entity_type' => 'node', + 'bundle' => OPIGNO_COURSE_BUNDLE, + 'label' => "Required course", + 'description' => "Makes this course dependent on another one.", + 'required' => FALSE, + )); + } +} \ No newline at end of file diff --git a/modules/og_prereq/opigno_og_prereq.module b/modules/og_prereq/opigno_og_prereq.module new file mode 100755 index 0000000..fce2537 --- /dev/null +++ b/modules/og_prereq/opigno_og_prereq.module @@ -0,0 +1,210 @@ + array( + 'title' => t("Skip the pretest when added as a member"), + 'description' => t("Whenever a user with this permission is added, even if he didn't answer the pretest, his membership get's approved."), + ), + ); +} + +/** + * Implements hook_og_role_grant(). + */ +function opigno_og_prereq_og_role_grant($entity_type, $gid, $uid, $rid) { + $account = user_load($uid, TRUE); + + if ($entity_type == 'node') { + if (!og_user_access($entity_type, $gid, 'skip pre-required group when added as a member', $account)) { + $required_courses = opigno_og_prereq_fetch_required_courses(node_load($gid)); + + if (!empty($required_courses)) { + foreach ($required_courses as $required_course) { + // If the user already passed the pre-test, don't deactivate his membership. + if (!opigno_og_prereq_user_passed_course($uid, $required_course)) { + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'og_membership', '=') + ->propertyCondition('gid', $gid, '=') + ->propertyCondition('entity_type', 'user', '=') + ->propertyCondition('etid', $uid, '='); + $result = $query->execute(); + + if (!empty($result['og_membership'])) { + $og_membership = og_membership_load(current($result['og_membership'])->id); + if ($og_membership->state == OG_STATE_ACTIVE) { + $og_membership->state = OG_STATE_PENDING; + og_membership_save($og_membership); + drupal_set_message(t("The membership for %user was temporarily set to pending. She must first fulfill the required courses to get full access to this group.", array('%user' => $account->name)), 'warning', FALSE); + } + } + } + } + } + } + else { + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'og_membership', '=') + ->propertyCondition('gid', $gid, '=') + ->propertyCondition('entity_type', 'user', '=') + ->propertyCondition('etid', $uid, '='); + $result = $query->execute(); + + if (!empty($result['og_membership'])) { + $og_membership = og_membership_load(current($result['og_membership'])->id); + if ($og_membership->state == OG_STATE_PENDING) { + $og_membership->state = OG_STATE_ACTIVE; + og_membership_save($og_membership); + } + } + } + } +} + +function opigno_og_prereq_fetch_required_courses($node) { + $nodes = array(); + if (!empty($node->course_required_course_ref)) { + foreach ($node->course_required_course_ref as $lang => $items) { + foreach ($items as $item) { + $nodes[$item['target_id']] = node_load($item['target_id']); + } + } + } + return $nodes; +} + +function opigno_og_prereq_user_passed_course($uid, $course) { + if (!empty($course->course_required_quiz_ref)) { + foreach ($course->course_required_quiz_ref as $lang => $items) { + foreach ($items as $item) { + $quiz = node_load($item['target_id']); + + if (isset($quiz)) { + $score = @current(quiz_get_score_data(array($quiz->nid), $uid)); + if (empty($score->percent_score) || $score->percent_score < $score->percent_pass) { + return FALSE; + } + } + } + } + } + return TRUE; +} + +/** + * Implements hook_og_role_revoke(). + */ +function opigno_og_prereq_og_role_revoke($entity_type, $gid, $uid, $rid) { + opigno_og_prereq_og_role_grant($entity_type, $gid, $uid, $rid); +} + +/** + * Enable a rule depending on Opigno Quiz App. + */ +function _opgino_og_prereq_install_opigno_quiz_app_rules() { + return array( + 'rules_course_required_courses_are_completed' => '{ "rules_course_required_courses_are_completed" : { + "LABEL" : "Course required courses are completed", + "PLUGIN" : "rule condition set", + "TAGS" : [ "og", "opigno", "quiz" ], + "REQUIRES" : [ "rules_conditional", "rules" ], + "USES VARIABLES" : { + "course" : { "label" : "Course", "type" : "node" }, + "user" : { "label" : "User", "type" : "user" }, + "has_finished_courses" : { "label" : "Has finished required courses", "type" : "boolean" } + }, + "DO" : [ + { "component_rules_get_course_required_courses" : { + "USING" : { "course" : [ "course" ] }, + "PROVIDE" : { "required_courses" : { "required_courses" : "Required courses" } } + } + }, + { "LOOP" : { + "USING" : { "list" : [ "required-courses" ] }, + "ITEM" : { "list_item" : "Current list item" }, + "DO" : [ + { "CONDITIONAL" : [ + { + "IF" : { "NOT component_rules_user_has_answered_all_required_quizzes" : { "course" : [ "course" ], "user" : [ "user" ] } }, + "DO" : [ { "data_set" : { "data" : [ "has-finished-courses" ], "value" : 0 } } ] + } + ] + } + ] + } + } + ], + "RESULT" : [ ] + } + }', + 'rules_activate_membership_when_required_course_is_passed' => '{ "rules_activate_membership_when_required_course_is_passed" : { + "LABEL" : "Activate membership when required course is passed", + "PLUGIN" : "reaction rule", + "OWNER" : "rules", + "TAGS" : [ "og", "opigno", "quiz" ], + "REQUIRES" : [ "rules", "rules_conditional", "og", "opigno_quiz_app" ], + "ON" : { "opigno_quiz_app_rules_quiz_passed" : [] }, + "DO" : [ + { "component_rules_get_course_from_quiz" : { + "USING" : { "quiz" : [ "node" ] }, + "PROVIDE" : { "course" : { "course" : "Course" } } + } + }, + { "CONDITIONAL" : [ + { + "IF" : { "component_rules_user_has_passed_all_required_quizzes" : { "course" : [ "course" ], "user" : [ "user" ] } }, + "DO" : [ + { "component_rules_get_courses_that_require_this_course" : { + "USING" : { "course" : [ "course" ] }, + "PROVIDE" : { "requiring_courses" : { "requiring_courses" : "Requiring courses" } } + } + }, + { "LOOP" : { + "USING" : { "list" : [ "requiring-courses" ] }, + "ITEM" : { "list_item" : "Current list item" }, + "DO" : [ + { "CONDITIONAL" : [ + { + "IF" : { "og_user_in_group" : { + "account" : [ "user" ], + "group" : [ "list-item" ], + "states" : { "value" : { "2" : "2" } } + } + }, + "DO" : [ + { "component_rules_activate_group_membership" : { "course" : [ "list-item" ], "user" : [ "user" ] } } + ] + } + ] + } + ] + } + } + ] + } + ] + } + ] + } +}', + ); +} diff --git a/modules/og_prereq/opigno_og_prereq.rules.inc b/modules/og_prereq/opigno_og_prereq.rules.inc new file mode 100755 index 0000000..210a730 --- /dev/null +++ b/modules/og_prereq/opigno_og_prereq.rules.inc @@ -0,0 +1,99 @@ + 'Opigno', + 'label' => t('Get requiring courses'), + 'arguments' => array( + 'node' => array( + 'type' => 'node', + 'label' => t('Course node'), + ), + ), + 'provides' => array( + 'entities_fetched' => array( + 'type' => 'list', + 'label' => t('Requiring courses'), + ), + ), + 'base' => 'opigno_og_prereq_rules_get_required_og', + ); + $items['opigno_og_prereq_gpc_with_required_course'] = array( + 'label' => t("Get passed courses with required course"), + 'group' => t('Opigno'), + 'parameter' => array( + 'course' => array( + 'type' => 'node', + 'label' => t('node'), + 'description' => t('The course'), + ), + ), + 'provides' => array( + 'entities_fetched' => array( + 'type' => 'list', + 'label' => t('Requiring courses'), + ), + ), + 'base' => 'opigno_og_prereq_gpc_with_required_course', + ); + return $items; +} + +/** + * Action: Get a property from a quiz. + */ +function opigno_og_prereq_rules_get_required_og($node, $settings) { + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'node') + ->propertyCondition('status', 1) + ->entityCondition('bundle', 'course') + ->fieldCondition('course_required_course_ref', 'target_id', $node->nid, '=') + ->addMetaData('account', user_load(1)); // Run as admin. + + $result = $query->execute(); + $courses = array(); + if (isset($result['node'])) { + $nids = array_keys($result['node']); + foreach ($nids as $nid) { + $courses[$nid] = node_load($nid); + } + } + + return array('entities_fetched' => $courses); +} + +function opigno_og_prereq_gpc_with_required_course($course) { + Global $user; + $nodes = node_load_multiple(array(), array('type' => 'course')); + $required_courses = array(); + foreach ($nodes as $node) { + if (isset($node->course_required_course_ref[LANGUAGE_NONE][0]['target_id'])) { + $hastheone = FALSE; + $completed_all = TRUE; + foreach ($node->course_required_course_ref[LANGUAGE_NONE] as $index => $target) { + if ($target['target_id'] != $course->nid) { + if (!opigno_quiz_app_user_passed($target['target_id'], $user->uid)) //passed it + { + $completed_all = FALSE; + } + } + else { + $hastheone = TRUE; + } + } + if (($completed_all == TRUE) && ($hastheone == TRUE)) { + $required_courses[] = $node; + } + } + } + return array('entities_fetched' => $required_courses); +} diff --git a/modules/og_prereq/opigno_og_prereq.rules_defaults.inc b/modules/og_prereq/opigno_og_prereq.rules_defaults.inc new file mode 100755 index 0000000..014e442 --- /dev/null +++ b/modules/og_prereq/opigno_og_prereq.rules_defaults.inc @@ -0,0 +1,148 @@ + $rule) { + $items[$key] = entity_import('rules_config', $rule); + } + } + return $items; +} diff --git a/modules/opigno_og_access/js/opigno_og_access.js b/modules/opigno_og_access/js/opigno_og_access.js new file mode 100755 index 0000000..6a4a205 --- /dev/null +++ b/modules/opigno_og_access/js/opigno_og_access.js @@ -0,0 +1,24 @@ +(function($) { + + Drupal.behaviors.opignoogaccess = { + attach: function (context, settings) { + var $checkboxes=$('[name="requires_validation[und]"],[name="anomymous_visibility[und]"]',context); + if ($checkboxes.length) + { + var group_access=$('[name="group_access[und]"]',context); + group_access.change(function(){ + var value=$('[name="group_access[und]"]:checked',context).val(); + if (value==1) + { + $checkboxes.parent().show(); + } + else + { + $checkboxes.parent().hide(); + } + }).change() + } + } + }; + +})(jQuery); \ No newline at end of file diff --git a/modules/opigno_og_access/opigno_og_access.info b/modules/opigno_og_access/opigno_og_access.info new file mode 100644 index 0000000..cec0b7e --- /dev/null +++ b/modules/opigno_og_access/opigno_og_access.info @@ -0,0 +1,23 @@ +name = Opigno Og access control +description = "Enables opigno og access control, do not enable with og_access at the same time" +core = 7.x +package = Opigno Apps +dependencies[] = og +core = 7.x +version = VERSION +files[] = opigno_og_access.module +files[] = opigno_og_access.install + +; Information added by drush on 2014-04-09 +version = "7.x-1.3+5-dev" +core = "7.x" +project = "opigno" +datestamp = "1397034870" + + +; Information added by Drupal.org packaging script on 2014-10-19 +version = "7.x-1.9" +core = "7.x" +project = "opigno" +datestamp = "1413751430" + diff --git a/modules/opigno_og_access/opigno_og_access.install b/modules/opigno_og_access/opigno_og_access.install new file mode 100755 index 0000000..4596ae9 --- /dev/null +++ b/modules/opigno_og_access/opigno_og_access.install @@ -0,0 +1,835 @@ +fields(array('weight' => 20)) + ->condition('name', 'opigno_og_access') + ->execute(); + + $field = field_info_field('group_access'); + if (empty($field)) { + field_create_field(array( + 'translatable' => '0', + 'entity_types' => array(), + 'settings' => array( + 'allowed_values' => array( + 'Public (World-wide open, for example for demo courses)', + 'Semi-public (registered users can subscribe to the course)', + 'Private (users can only be subscribed by a course administrator or a teacher, the course is hidden from course catalogue)', + ), + 'allowed_values_function' => '', + ), + 'storage' => array( + 'type' => 'field_sql_storage', + 'settings' => array(), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => array( + 'sql' => array( + 'FIELD_LOAD_CURRENT' => array( + 'field_data_group_access' => array( + 'value' => 'group_access_value', + ), + ), + 'FIELD_LOAD_REVISION' => array( + 'field_revision_group_access' => array( + 'value' => 'group_access_value', + ), + ), + ), + ), + ), + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 'value', + ), + ), + 'field_name' => 'group_access', + 'type' => 'list_integer', + 'module' => 'list', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'bundles' => array( + 'node' => array( + 'course', + ), + ), + )); + } + + $instance = field_info_instance('node', 'group_access', 'course'); + if (empty($instance)) { + field_create_instance(array( + 'label' => 'Group visibility', + 'widget' => array( + 'weight' => '1', + 'type' => 'options_buttons', + 'module' => 'options', + 'active' => 1, + 'settings' => array(), + ), + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'display' => array( + 'default' => array( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => array(), + 'module' => 'list', + 'weight' => 12, + ), + 'teaser' => array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ), + ), + 'required' => 1, + 'description' => '', + 'default_value' => NULL, + 'field_name' => 'group_access', + 'entity_type' => 'node', + 'bundle' => 'course', + 'deleted' => '0', + )); + } + + $field = field_info_field('requires_validation'); + if (empty($field)) { + field_create_field(array( + 'translatable' => '0', + 'entity_types' => array(), + 'settings' => array( + 'allowed_values' => array( + '0', + '1', + ), + 'allowed_values_function' => '', + ), + 'storage' => array( + 'type' => 'field_sql_storage', + 'settings' => array(), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => array( + 'sql' => array( + 'FIELD_LOAD_CURRENT' => array( + 'field_data_requires_validation' => array( + 'value' => 'requires_validation_value', + ), + ), + 'FIELD_LOAD_REVISION' => array( + 'field_revision_requires_validation' => array( + 'value' => 'requires_validation_value', + ), + ), + ), + ), + ), + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 'value', + ), + ), + 'field_name' => 'requires_validation', + 'type' => 'list_boolean', + 'module' => 'list', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'bundles' => array( + 'node' => array( + 'course', + ), + ), + ));} + + $instance = field_info_instance('node', 'requires_validation', 'course'); + if (empty($instance)) { + field_create_instance(array( + 'label' => 'Requires validation', + 'widget' => array( + 'weight' => '2', + 'type' => 'options_onoff', + 'module' => 'options', + 'active' => 1, + 'settings' => array( + 'display_label' => 1, + ), + ), + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'display' => array( + 'default' => array( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => array(), + 'module' => 'list', + 'weight' => 11, + ), + 'teaser' => array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ), + ), + 'required' => 0, + 'description' => '', + 'default_value' => array( + array( + 'value' => 0, + ), + ), + 'field_name' => 'requires_validation', + 'entity_type' => 'node', + 'bundle' => 'course', + 'deleted' => '0', + )); + } + + $field = field_info_field('anomymous_visibility'); + if (empty($field)) { + field_create_field(array( + 'translatable' => '0', + 'entity_types' => array(), + 'settings' => array( + 'allowed_values' => array( + '0', + '1', + ), + 'allowed_values_function' => '', + ), + 'storage' => array( + 'type' => 'field_sql_storage', + 'settings' => array(), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => array( + 'sql' => array( + 'FIELD_LOAD_CURRENT' => array( + 'field_data_anomymous_visibility' => array( + 'value' => 'anomymous_visibility_value', + ), + ), + 'FIELD_LOAD_REVISION' => array( + 'field_revision_anomymous_visibility' => array( + 'value' => 'anomymous_visibility_value', + ), + ), + ), + ), + ), + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 'value', + ), + ), + 'field_name' => 'anomymous_visibility', + 'type' => 'list_boolean', + 'module' => 'list', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'bundles' => array( + 'node' => array( + 'course', + ), + ), + )); + } + + $instance = field_info_instance('node', 'anomymous_visibility', 'course'); + if (empty($instance)) { + field_create_instance(array( + 'label' => 'Hide for anonymous users', + 'widget' => array( + 'weight' => '2', + 'type' => 'options_onoff', + 'module' => 'options', + 'active' => 1, + 'settings' => array( + 'display_label' => 1, + ), + ), + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'display' => array( + 'default' => array( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => array(), + 'module' => 'list', + 'weight' => 10, + ), + 'teaser' => array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ), + ), + 'required' => 0, + 'description' => '', + 'default_value' => array( + array( + 'value' => 0, + ), + ), + 'field_name' => 'anomymous_visibility', + 'entity_type' => 'node', + 'bundle' => 'course', + 'deleted' => '0', + ));} + + $field = field_info_field('catalogue_visibility'); + if (empty($field)) { + field_create_field(array( + 'translatable' => '0', + 'entity_types' => array(), + 'settings' => array( + 'allowed_values' => array( + '0', + '1', + ), + 'allowed_values_function' => '', + ), + 'storage' => array( + 'type' => 'field_sql_storage', + 'settings' => array(), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => array( + 'sql' => array( + 'FIELD_LOAD_CURRENT' => array( + 'field_data_catalogue_visibility' => array( + 'value' => 'catalogue_visibility_value', + ), + ), + 'FIELD_LOAD_REVISION' => array( + 'field_revision_catalogue_visibility' => array( + 'value' => 'catalogue_visibility_value', + ), + ), + ), + ), + ), + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 'value', + ), + ), + 'field_name' => 'catalogue_visibility', + 'type' => 'list_boolean', + 'module' => 'list', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'bundles' => array( + 'node' => array( + 'course', + ), + ), + ));} + + $instance = field_info_instance('node', 'catalogue_visibility', 'course'); + if (empty($instance)) { + field_create_instance(array( + 'label' => 'Hide from Catalogue', + 'widget' => array( + 'weight' => '36', + 'type' => 'options_onoff', + 'module' => 'options', + 'active' => 1, + 'settings' => array( + 'display_label' => 1, + ), + ), + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'display' => array( + 'default' => array( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => array(), + 'module' => 'list', + 'weight' => 13, + ), + 'teaser' => array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ), + ), + 'required' => 0, + 'description' => '', + 'default_value' => array( + array( + 'value' => 0, + ), + ), + 'field_name' => 'catalogue_visibility', + 'entity_type' => 'node', + 'bundle' => 'course', + 'deleted' => '0', + )); + } + og_create_field(OG_DEFAULT_ACCESS_FIELD, 'node', "course"); + + $field = field_info_field('group_access'); + if (empty($field)) { + field_create_field(array( + 'translatable' => '0', + 'entity_types' => array(), + 'settings' => array( + 'allowed_values' => array( + 'Public (World-wide open, for example for demo courses)', + 'Semi-public (registered users can subscribe to the course)', + 'Private (users can only be subscribed by a course administrator or a teacher, the course is hidden from course catalogue)', + ), + 'allowed_values_function' => '', + ), + 'storage' => array( + 'type' => 'field_sql_storage', + 'settings' => array(), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => array( + 'sql' => array( + 'FIELD_LOAD_CURRENT' => array( + 'field_data_group_access' => array( + 'value' => 'group_access_value', + ), + ), + 'FIELD_LOAD_REVISION' => array( + 'field_revision_group_access' => array( + 'value' => 'group_access_value', + ), + ), + ), + ), + ), + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 'value', + ), + ), + 'field_name' => 'group_access', + 'type' => 'list_integer', + 'module' => 'list', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'bundles' => array( + 'node' => array( + 'class', + ), + ), + )); + } + + $instance = field_info_instance('node', 'group_access', 'class'); + if (empty($instance)) { + field_create_instance(array( + 'label' => 'Group visibility', + 'widget' => array( + 'weight' => '35', + 'type' => 'options_buttons', + 'module' => 'options', + 'active' => 1, + 'settings' => array(), + ), + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'display' => array( + 'default' => array( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => array(), + 'module' => 'list', + 'weight' => 12, + ), + 'teaser' => array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ), + ), + 'required' => 1, + 'description' => '', + 'default_value' => NULL, + 'field_name' => 'group_access', + 'entity_type' => 'node', + 'bundle' => 'class', + 'deleted' => '0', + )); + } + + $field = field_info_field('requires_validation'); + if (empty($field)) { + field_create_field(array( + 'translatable' => '0', + 'entity_types' => array(), + 'settings' => array( + 'allowed_values' => array( + '0', + '1', + ), + 'allowed_values_function' => '', + ), + 'storage' => array( + 'type' => 'field_sql_storage', + 'settings' => array(), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => array( + 'sql' => array( + 'FIELD_LOAD_CURRENT' => array( + 'field_data_requires_validation' => array( + 'value' => 'requires_validation_value', + ), + ), + 'FIELD_LOAD_REVISION' => array( + 'field_revision_requires_validation' => array( + 'value' => 'requires_validation_value', + ), + ), + ), + ), + ), + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 'value', + ), + ), + 'field_name' => 'requires_validation', + 'type' => 'list_boolean', + 'module' => 'list', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'bundles' => array( + 'node' => array( + 'class', + ), + ), + ));} + + $instance = field_info_instance('node', 'requires_validation', 'class'); + if (empty($instance)) { + field_create_instance(array( + 'label' => 'Requires validation', + 'widget' => array( + 'weight' => '37', + 'type' => 'options_onoff', + 'module' => 'options', + 'active' => 1, + 'settings' => array( + 'display_label' => 1, + ), + ), + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'display' => array( + 'default' => array( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => array(), + 'module' => 'list', + 'weight' => 14, + ), + 'teaser' => array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ), + ), + 'required' => 0, + 'description' => '', + 'default_value' => array( + array( + 'value' => 0, + ), + ), + 'field_name' => 'requires_validation', + 'entity_type' => 'node', + 'bundle' => 'class', + 'deleted' => '0', + )); + } + + $field = field_info_field('anomymous_visibility'); + if (empty($field)) { + field_create_field(array( + 'translatable' => '0', + 'entity_types' => array(), + 'settings' => array( + 'allowed_values' => array( + '0', + '1', + ), + 'allowed_values_function' => '', + ), + 'storage' => array( + 'type' => 'field_sql_storage', + 'settings' => array(), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => array( + 'sql' => array( + 'FIELD_LOAD_CURRENT' => array( + 'field_data_anomymous_visibility' => array( + 'value' => 'anomymous_visibility_value', + ), + ), + 'FIELD_LOAD_REVISION' => array( + 'field_revision_anomymous_visibility' => array( + 'value' => 'anomymous_visibility_value', + ), + ), + ), + ), + ), + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 'value', + ), + ), + 'field_name' => 'anomymous_visibility', + 'type' => 'list_boolean', + 'module' => 'list', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'bundles' => array( + 'node' => array( + 'class', + ), + ), + )); + } + + $instance = field_info_instance('node', 'anomymous_visibility', 'class'); + if (empty($instance)) { + field_create_instance(array( + 'label' => 'Hide for anonymous users', + 'widget' => array( + 'weight' => '36', + 'type' => 'options_onoff', + 'module' => 'options', + 'active' => 1, + 'settings' => array( + 'display_label' => 1, + ), + ), + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'display' => array( + 'default' => array( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => array(), + 'module' => 'list', + 'weight' => 13, + ), + 'teaser' => array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ), + ), + 'required' => 0, + 'description' => '', + 'default_value' => array( + array( + 'value' => 0, + ), + ), + 'field_name' => 'anomymous_visibility', + 'entity_type' => 'node', + 'bundle' => 'class', + 'deleted' => '0', + ));} + + $field = field_info_field('catalogue_visibility'); + if (empty($field)) { + field_create_field(array( + 'translatable' => '0', + 'entity_types' => array(), + 'settings' => array( + 'allowed_values' => array( + '0', + '1', + ), + 'allowed_values_function' => '', + ), + 'storage' => array( + 'type' => 'field_sql_storage', + 'settings' => array(), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => array( + 'sql' => array( + 'FIELD_LOAD_CURRENT' => array( + 'field_data_catalogue_visibility' => array( + 'value' => 'catalogue_visibility_value', + ), + ), + 'FIELD_LOAD_REVISION' => array( + 'field_revision_catalogue_visibility' => array( + 'value' => 'catalogue_visibility_value', + ), + ), + ), + ), + ), + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 'value', + ), + ), + 'field_name' => 'catalogue_visibility', + 'type' => 'list_boolean', + 'module' => 'list', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'bundles' => array( + 'node' => array( + 'class', + ), + ), + ));} + + $instance = field_info_instance('node', 'catalogue_visibility', 'class'); + if (empty($instance)) { + field_create_instance(array( + 'label' => 'Hide from Catalogue', + 'widget' => array( + 'weight' => '36', + 'type' => 'options_onoff', + 'module' => 'options', + 'active' => 1, + 'settings' => array( + 'display_label' => 1, + ), + ), + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'display' => array( + 'default' => array( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => array(), + 'module' => 'list', + 'weight' => 13, + ), + 'teaser' => array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ), + ), + 'required' => 0, + 'description' => '', + 'default_value' => array( + array( + 'value' => 0, + ), + ), + 'field_name' => 'catalogue_visibility', + 'entity_type' => 'node', + 'bundle' => 'class', + 'deleted' => '0', + )); + } + + og_create_field(OG_DEFAULT_ACCESS_FIELD, 'node', "class"); +} diff --git a/modules/opigno_og_access/opigno_og_access.module b/modules/opigno_og_access/opigno_og_access.module new file mode 100755 index 0000000..23fe7a2 --- /dev/null +++ b/modules/opigno_og_access/opigno_og_access.module @@ -0,0 +1,1240 @@ + $gids) { + foreach ($gids as $gid) { + $realm = OG_ACCESS_REALM . ':' . $group_type; + $grants[$realm][] = $gid; + } + } + } + + if ($account->uid) { + $realm = OG_ACCESS_REALM . ':only_auth'; + $grants[$realm][] = 1; + } + + return !empty($grants) ? $grants : array(); +} + + +/** + * Implements hook_node_access_records(). + */ +function opigno_og_access_node_access_records($node) { + if (empty($node->status)) { + // Node is unpublished, so we don't allow every group member to see + // it. + return array(); + } + + $gids = array(); + $wrapper = entity_metadata_wrapper('node', $node); + $hideforanonymous=FALSE; + + + if ((!empty($wrapper->{OG_ACCESS_FIELD})) && (og_is_group('node', $node))) { + if (!_opigno_og_access_node_access($wrapper)) { + $gids['node'][] = $node->nid; + } + $hideforanonymous = $wrapper->anomymous_visibility->value()==1; + } + + if (og_is_group_content_type('node', $node->type)) { + $has_private = FALSE; + $entity_groups = og_get_entity_groups('node', $node); + foreach ($entity_groups as $group_type => $values) { + entity_load($group_type, $values); + foreach ($values as $gid) { + $list_gids[$group_type][] = $gid; + if ($has_private) { + // We already know we have a private group, so we can avoid + // re-checking it. + continue; + } + $group_wrapper = entity_metadata_wrapper($group_type, $gid); + if (!opigno_og_access_node_content_access($group_wrapper)) { + $has_private = TRUE; + } + if (!$hideforanonymous) { + if (!$has_private) + { + $hideforanonymous = $group_wrapper->anomymous_visibility->value()==1; + } + } + } + } + if ($has_private) { + $gids = array_merge_recursive($gids, $list_gids); + } + } + + foreach ($gids as $group_type => $values) { + foreach ($values as $gid) { + $grants[] = array( + 'realm' => OG_ACCESS_REALM . ':' . $group_type, + 'gid' => $gid, + 'grant_view' => 1, + 'grant_update' => 0, + 'grant_delete' => 0, + 'priority' => 1, + ); + } + } + + if (($hideforanonymous)&&($wrapper->{OG_ACCESS_FIELD}->value()!=2)) { + $grants[] = array( + 'realm' => OG_ACCESS_REALM . ':only_auth', + 'gid' => 1, + 'grant_view' => 1, + 'grant_update' => 0, + 'grant_delete' => 0, + 'priority' => 1, + ); + } + + return !empty($grants) ? $grants : array(); +} + + +function _opigno_og_access_node_access($wrapper) { + if (empty($wrapper->{OG_ACCESS_FIELD})) { + return TRUE; + } + $wrapper = entity_metadata_wrapper('node', $wrapper); + if (($wrapper->{OG_ACCESS_FIELD}->value() == 0) || ($wrapper->{OG_ACCESS_FIELD}->value() == 1)) { + return TRUE; + } + return FALSE; +} + +function opigno_og_access_node_content_access($wrapper) { + if (empty($wrapper->{OG_ACCESS_FIELD})) { + return TRUE; + } + $wrapper = entity_metadata_wrapper('node', $wrapper); + if (($wrapper->{OG_ACCESS_FIELD}->value() == 0)) { + return TRUE; + } + return FALSE; +} + + +function opigno_og_access_entity_insert($entity, $entity_type) { + if (($entity_type == "node") && (($entity->type == "course")||($entity->type == "class"))) { + $entity->{OG_DEFAULT_ACCESS_FIELD}[LANGUAGE_NONE][0]['value'] = 1; + opigno_og_access_node_set_og_permissions($entity); + } +} + +function opigno_og_access_entity_update($entity, $entity_type) { + if (($entity_type == "node") && (($entity->type == "course")||($entity->type == "class"))) { + $entity->{OG_DEFAULT_ACCESS_FIELD}[LANGUAGE_NONE][0]['value'] = 1; + opigno_og_access_node_set_og_permissions($entity); + } +} + +function opigno_og_access_node_set_og_permissions($entity) { + $roles = og_roles("node",$entity->type, $entity->nid, FALSE, TRUE); + + $ak = array_keys($roles); + $rid = $ak[0]; + $role_names = array($rid => $roles[$rid]); + // Fetch permissions for all roles or the one selected role. + $role_permissions = og_role_permissions($role_names); + $tosave = array(); + $og_permissions = og_get_permissions(); + foreach ($og_permissions as $index => $value) { + if (isset($role_permissions[$rid][$index])&&($role_permissions[$rid][$index] == TRUE)) { + $tosave[$index] = $index; + } + else { + $tosave[$index] = 0; + } + } + if ($entity->{OG_ACCESS_FIELD}[LANGUAGE_NONE][0]['value']==0) + { + $tosave['vote on polls'] = 'vote on polls'; + $tosave['tft access file tree']='tft access file tree'; + $tosave['access quiz']='access quiz'; + $tosave['view in_house_app content']='view in_house_app content'; + } + else + { + $tosave['vote on polls'] = 0; + $tosave['tft access file tree']= 0; + $tosave['access quiz']=0; + $tosave['view in_house_app content']=0; + } + + og_role_change_permissions($rid, $tosave); + opigno_og_access_node_set_subscribe_permissions($entity); +} + + +function opigno_og_access_node_set_subscribe_permissions($entity) { + $roles = og_roles("node", $entity->type, $entity->nid, FALSE, TRUE); + //$role_names=array($roles); + $role_permissions = og_role_permissions($roles); + $og_permissions = og_get_permissions(); + foreach ($role_permissions as $rid => $permission) { + $tosave = array(); + foreach ($og_permissions as $index => $value) { + if (isset($role_permissions[$rid][$index])&&($role_permissions[$rid][$index] == TRUE)) { + $tosave[$index] = $index; + } + else { + $tosave[$index] = 0; + } + } + + if ($entity->requires_validation[LANGUAGE_NONE][0]['value'] != 1) { + $tosave['subscribe without approval'] = 'subscribe without approval'; + } + else { + $tosave['subscribe without approval'] = 0; + } + + if ($entity->{OG_ACCESS_FIELD}[LANGUAGE_NONE][0]['value'] == 2) { + $tosave['subscribe'] = 0; + $tosave['subscribe without approval'] = 0; + } + else { + $tosave['subscribe'] = 'subscribe'; + } + og_role_change_permissions($rid, $tosave); + } +} + +function opigno_og_access_node_update($node) { + $loaded_node = node_load($node->nid, NULL, TRUE); + if (($loaded_node->type == "course")||($loaded_node->type == "class")) + { + $content = opigno_og_group_get_contents_in_group($loaded_node->nid); + while ($record = $content->fetchAssoc()) { + $nd = node_load($record['nid']); + node_access_acquire_grants($nd, TRUE); + } + } +} + +function opigno_og_access_remove_anonymous_node_access(&$permissions) { + if ($permissions['view'][0] == 1) { + unset($permissions['view'][0]); + sort($permissions['view']); + } + if ($permissions['view_own'][0] == 1) { + unset($permissions['view_own'][0]); + sort($permissions['view_own']); + } +} + +function opigno_og_access_give_anonymous_node_access(&$permissions) { + $permissions['view'][] = 1; + sort($permissions['view']); + $permissions['view_own'][] = 1; + sort($permissions['view_own']); +} + +function opigno_og_group_get_contents_in_group($gid) { + $query = db_select('node', 'n'); + $query->condition('n.status', 1, '=') + ->fields('n', array('nid', 'title')) + ->join('og_membership', 'ogm', "ogm.gid = :gid AND n.nid = ogm.etid AND ogm.entity_type = 'node'", array(':gid' => $gid)); + return $query->execute(); +} + +/** + * Implements hook_views_api(). + */ +function opigno_og_access_views_api() { + return array( + 'api' => '3.0', + ); +} + +function opigno_og_access_views_query_alter(&$view, &$query) { + if ($view->name == "opigno_course_catalgue") { + if (!user_is_logged_in()) { + $condition = array( + 'field' => 'field_data_anomymous_visibility.anomymous_visibility_value', + 'value' => 0, + 'operator' => '=', + ); + $query->where[1]['conditions'][]=$condition; + } + elseif (user_access('administer nodes')) { + foreach($query->where[1]['conditions'] as &$condition) { + if ($condition['field'] == 'field_data_group_access.group_access_value') { + $condition['value'][2] = 2; + } + } + } + } +} + +/** + * Implementation of hook_views_default_views_alter(). + */ +function opigno_og_access_views_default_views_alter(&$views) { + global $user; + // Alter the catalogue view and add a taxonomy filter. + if (array_key_exists('opigno_course_catalgue', $views)) { + $display = &$views['opigno_course_catalgue']->display['default']; + /* Field: Content: Hide for anonymous users */ + $display->display_options['fields']['anomymous_visibility']['id'] = 'anomymous_visibility'; + $display->display_options['fields']['anomymous_visibility']['table'] = 'field_data_anomymous_visibility'; + $display->display_options['fields']['anomymous_visibility']['field'] = 'anomymous_visibility'; + $display->display_options['fields']['anomymous_visibility']['label'] = ''; + $display->display_options['fields']['anomymous_visibility']['exclude'] = TRUE; + $display->display_options['fields']['anomymous_visibility']['element_label_colon'] = FALSE; + /* Field: Content: Hide from Catalogue */ + /*$display->display_options['fields']['catalogue_visibility']['id'] = 'catalogue_visibility'; + $display->display_options['fields']['catalogue_visibility']['table'] = 'field_data_catalogue_visibility'; + $display->display_options['fields']['catalogue_visibility']['field'] = 'catalogue_visibility'; + $display->display_options['fields']['catalogue_visibility']['label'] = ''; + $display->display_options['fields']['catalogue_visibility']['exclude'] = TRUE; + $display->display_options['fields']['catalogue_visibility']['element_label_colon'] = FALSE;*/ + + /* Filter criterion: Content: Hide from Catalogue (catalogue_visibility) */ + /*$display->display_options['filters']['catalogue_visibility_value']['id'] = 'catalogue_visibility_value'; + $display->display_options['filters']['catalogue_visibility_value']['table'] = 'field_data_catalogue_visibility'; + $display->display_options['filters']['catalogue_visibility_value']['field'] = 'catalogue_visibility_value'; + $display->display_options['filters']['catalogue_visibility_value']['value'] = array( + 0 => '0', + );*/ + /* Filter criterion: Content: Hide for anonymous users (anomymous_visibility) */ + $display->display_options['filters']['anomymous_visibility_value']['id'] = 'anomymous_visibility_value'; + $display->display_options['filters']['anomymous_visibility_value']['table'] = 'field_data_anomymous_visibility'; + $display->display_options['filters']['anomymous_visibility_value']['field'] = 'anomymous_visibility_value'; + $display->display_options['filters']['anomymous_visibility_value']['value'] = array( + 0 => '0', + 1 => '1', + ); + /* Filter criterion: Content: Group visibility (group_access) */ + $display->display_options['filters']['group_access_value']['id'] = 'group_access_value'; + $display->display_options['filters']['group_access_value']['table'] = 'field_data_group_access'; + $display->display_options['filters']['group_access_value']['field'] = 'group_access_value'; + $display->display_options['filters']['group_access_value']['value'] = array( + 0 => '0', + 1 => '1', + ); + + unset($display->display_options['access']); + $display->display_options['access']['type'] = 'none'; + } +} + +function opigno_og_access_modules_enabled($modules) +{ + if (in_array('opigno', $modules)) { + opigno_og_access_install_course_fields(); + } + + if (in_array('opigno_class_app', $modules)) { + opigno_og_access_install_class_fields(); + } +} + +function opigno_og_access_install_course_fields() +{ + $field = field_info_field('group_access'); + if (empty($field)) { + field_create_field(array( + 'translatable' => '0', + 'entity_types' => array(), + 'settings' => array( + 'allowed_values' => array( + 'Public (World-wide open, for example for demo courses)', + 'Semi-public (registered users can subscribe to the course)', + 'Private (users can only be subscribed by a course administrator or a teacher ; the course is hidden from course catalogue)', + ), + 'allowed_values_function' => '', + ), + 'storage' => array( + 'type' => 'field_sql_storage', + 'settings' => array(), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => array( + 'sql' => array( + 'FIELD_LOAD_CURRENT' => array( + 'field_data_group_access' => array( + 'value' => 'group_access_value', + ), + ), + 'FIELD_LOAD_REVISION' => array( + 'field_revision_group_access' => array( + 'value' => 'group_access_value', + ), + ), + ), + ), + ), + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 'value', + ), + ), + 'field_name' => 'group_access', + 'type' => 'list_integer', + 'module' => 'list', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'bundles' => array( + 'node' => array( + 'course', + ), + ), + )); + } + + $instance = field_info_instance('node', 'group_access', 'course'); + if (empty($instance)) { + field_create_instance(array( + 'label' => 'Group visibility', + 'widget' => array( + 'weight' => '35', + 'type' => 'options_buttons', + 'module' => 'options', + 'active' => 1, + 'settings' => array(), + ), + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'display' => array( + 'default' => array( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => array(), + 'module' => 'list', + 'weight' => 12, + ), + 'teaser' => array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ), + ), + 'required' => 1, + 'description' => '', + 'default_value' => NULL, + 'field_name' => 'group_access', + 'entity_type' => 'node', + 'bundle' => 'course', + 'deleted' => '0', + )); + } + + $field = field_info_field('requires_validation'); + if (empty($field)) { + field_create_field(array( + 'translatable' => '0', + 'entity_types' => array(), + 'settings' => array( + 'allowed_values' => array( + '0', + '1', + ), + 'allowed_values_function' => '', + ), + 'storage' => array( + 'type' => 'field_sql_storage', + 'settings' => array(), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => array( + 'sql' => array( + 'FIELD_LOAD_CURRENT' => array( + 'field_data_requires_validation' => array( + 'value' => 'requires_validation_value', + ), + ), + 'FIELD_LOAD_REVISION' => array( + 'field_revision_requires_validation' => array( + 'value' => 'requires_validation_value', + ), + ), + ), + ), + ), + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 'value', + ), + ), + 'field_name' => 'requires_validation', + 'type' => 'list_boolean', + 'module' => 'list', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'bundles' => array( + 'node' => array( + 'course', + ), + ), + ));} + + $instance = field_info_instance('node', 'requires_validation', 'course'); + if (empty($instance)) { + field_create_instance(array( + 'label' => 'Requires validation', + 'widget' => array( + 'weight' => '34', + 'type' => 'options_onoff', + 'module' => 'options', + 'active' => 1, + 'settings' => array( + 'display_label' => 1, + ), + ), + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'display' => array( + 'default' => array( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => array(), + 'module' => 'list', + 'weight' => 11, + ), + 'teaser' => array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ), + ), + 'required' => 0, + 'description' => '', + 'default_value' => array( + array( + 'value' => 0, + ), + ), + 'field_name' => 'requires_validation', + 'entity_type' => 'node', + 'bundle' => 'course', + 'deleted' => '0', + )); + } + + $field = field_info_field('anomymous_visibility'); + if (empty($field)) { + field_create_field(array( + 'translatable' => '0', + 'entity_types' => array(), + 'settings' => array( + 'allowed_values' => array( + '0', + '1', + ), + 'allowed_values_function' => '', + ), + 'storage' => array( + 'type' => 'field_sql_storage', + 'settings' => array(), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => array( + 'sql' => array( + 'FIELD_LOAD_CURRENT' => array( + 'field_data_anomymous_visibility' => array( + 'value' => 'anomymous_visibility_value', + ), + ), + 'FIELD_LOAD_REVISION' => array( + 'field_revision_anomymous_visibility' => array( + 'value' => 'anomymous_visibility_value', + ), + ), + ), + ), + ), + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 'value', + ), + ), + 'field_name' => 'anomymous_visibility', + 'type' => 'list_boolean', + 'module' => 'list', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'bundles' => array( + 'node' => array( + 'course', + ), + ), + )); + } + + $instance = field_info_instance('node', 'anomymous_visibility', 'course'); + if (empty($instance)) { + field_create_instance(array( + 'label' => 'Hide for anonymous users', + 'widget' => array( + 'weight' => '33', + 'type' => 'options_onoff', + 'module' => 'options', + 'active' => 1, + 'settings' => array( + 'display_label' => 1, + ), + ), + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'display' => array( + 'default' => array( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => array(), + 'module' => 'list', + 'weight' => 10, + ), + 'teaser' => array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ), + ), + 'required' => 0, + 'description' => '', + 'default_value' => array( + array( + 'value' => 0, + ), + ), + 'field_name' => 'anomymous_visibility', + 'entity_type' => 'node', + 'bundle' => 'course', + 'deleted' => '0', + ));} + + $field = field_info_field('catalogue_visibility'); + if (empty($field)) { + field_create_field(array( + 'translatable' => '0', + 'entity_types' => array(), + 'settings' => array( + 'allowed_values' => array( + '0', + '1', + ), + 'allowed_values_function' => '', + ), + 'storage' => array( + 'type' => 'field_sql_storage', + 'settings' => array(), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => array( + 'sql' => array( + 'FIELD_LOAD_CURRENT' => array( + 'field_data_catalogue_visibility' => array( + 'value' => 'catalogue_visibility_value', + ), + ), + 'FIELD_LOAD_REVISION' => array( + 'field_revision_catalogue_visibility' => array( + 'value' => 'catalogue_visibility_value', + ), + ), + ), + ), + ), + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 'value', + ), + ), + 'field_name' => 'catalogue_visibility', + 'type' => 'list_boolean', + 'module' => 'list', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'bundles' => array( + 'node' => array( + 'course', + ), + ), + ));} + + $instance = field_info_instance('node', 'catalogue_visibility', 'course'); + if (empty($instance)) { + field_create_instance(array( + 'label' => 'Hide from Catalogue', + 'widget' => array( + 'weight' => '36', + 'type' => 'options_onoff', + 'module' => 'options', + 'active' => 1, + 'settings' => array( + 'display_label' => 1, + ), + ), + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'display' => array( + 'default' => array( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => array(), + 'module' => 'list', + 'weight' => 13, + ), + 'teaser' => array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ), + ), + 'required' => 0, + 'description' => '', + 'default_value' => array( + array( + 'value' => 0, + ), + ), + 'field_name' => 'catalogue_visibility', + 'entity_type' => 'node', + 'bundle' => 'course', + 'deleted' => '0', + )); + } + og_create_field(OG_DEFAULT_ACCESS_FIELD, 'node', "course"); +} + +function opigno_og_access_install_class_fields() +{ + $field = field_info_field('group_access'); + if (empty($field)) { + field_create_field(array( + 'translatable' => '0', + 'entity_types' => array(), + 'settings' => array( + 'allowed_values' => array( + 'Public (World-wide open, for example for demo courses)', + 'Semi-public (registered users can subscribe to the course)', + 'Private (users can only be subscribed by a course administrator or a teacher, the course is hidden from course catalogue)', + ), + 'allowed_values_function' => '', + ), + 'storage' => array( + 'type' => 'field_sql_storage', + 'settings' => array(), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => array( + 'sql' => array( + 'FIELD_LOAD_CURRENT' => array( + 'field_data_group_access' => array( + 'value' => 'group_access_value', + ), + ), + 'FIELD_LOAD_REVISION' => array( + 'field_revision_group_access' => array( + 'value' => 'group_access_value', + ), + ), + ), + ), + ), + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 'value', + ), + ), + 'field_name' => 'group_access', + 'type' => 'list_integer', + 'module' => 'list', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'bundles' => array( + 'node' => array( + 'class', + ), + ), + )); + } + + $instance = field_info_instance('node', 'group_access', 'class'); + if (empty($instance)) { + field_create_instance(array( + 'label' => 'Group visibility', + 'widget' => array( + 'weight' => '35', + 'type' => 'options_buttons', + 'module' => 'options', + 'active' => 1, + 'settings' => array(), + ), + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'display' => array( + 'default' => array( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => array(), + 'module' => 'list', + 'weight' => 12, + ), + 'teaser' => array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ), + ), + 'required' => 1, + 'description' => '', + 'default_value' => NULL, + 'field_name' => 'group_access', + 'entity_type' => 'node', + 'bundle' => 'class', + 'deleted' => '0', + )); + } + + $field = field_info_field('requires_validation'); + if (empty($field)) { + field_create_field(array( + 'translatable' => '0', + 'entity_types' => array(), + 'settings' => array( + 'allowed_values' => array( + '0', + '1', + ), + 'allowed_values_function' => '', + ), + 'storage' => array( + 'type' => 'field_sql_storage', + 'settings' => array(), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => array( + 'sql' => array( + 'FIELD_LOAD_CURRENT' => array( + 'field_data_requires_validation' => array( + 'value' => 'requires_validation_value', + ), + ), + 'FIELD_LOAD_REVISION' => array( + 'field_revision_requires_validation' => array( + 'value' => 'requires_validation_value', + ), + ), + ), + ), + ), + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 'value', + ), + ), + 'field_name' => 'requires_validation', + 'type' => 'list_boolean', + 'module' => 'list', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'bundles' => array( + 'node' => array( + 'class', + ), + ), + ));} + + $instance = field_info_instance('node', 'requires_validation', 'class'); + if (empty($instance)) { + field_create_instance(array( + 'label' => 'Requires validation', + 'widget' => array( + 'weight' => '34', + 'type' => 'options_onoff', + 'module' => 'options', + 'active' => 1, + 'settings' => array( + 'display_label' => 1, + ), + ), + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'display' => array( + 'default' => array( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => array(), + 'module' => 'list', + 'weight' => 11, + ), + 'teaser' => array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ), + ), + 'required' => 0, + 'description' => '', + 'default_value' => array( + array( + 'value' => 0, + ), + ), + 'field_name' => 'requires_validation', + 'entity_type' => 'node', + 'bundle' => 'class', + 'deleted' => '0', + )); + } + + $field = field_info_field('anomymous_visibility'); + if (empty($field)) { + field_create_field(array( + 'translatable' => '0', + 'entity_types' => array(), + 'settings' => array( + 'allowed_values' => array( + '0', + '1', + ), + 'allowed_values_function' => '', + ), + 'storage' => array( + 'type' => 'field_sql_storage', + 'settings' => array(), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => array( + 'sql' => array( + 'FIELD_LOAD_CURRENT' => array( + 'field_data_anomymous_visibility' => array( + 'value' => 'anomymous_visibility_value', + ), + ), + 'FIELD_LOAD_REVISION' => array( + 'field_revision_anomymous_visibility' => array( + 'value' => 'anomymous_visibility_value', + ), + ), + ), + ), + ), + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 'value', + ), + ), + 'field_name' => 'anomymous_visibility', + 'type' => 'list_boolean', + 'module' => 'list', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'bundles' => array( + 'node' => array( + 'class', + ), + ), + )); + } + + $instance = field_info_instance('node', 'anomymous_visibility', 'class'); + if (empty($instance)) { + field_create_instance(array( + 'label' => 'Hide for anonymous users', + 'widget' => array( + 'weight' => '33', + 'type' => 'options_onoff', + 'module' => 'options', + 'active' => 1, + 'settings' => array( + 'display_label' => 1, + ), + ), + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'display' => array( + 'default' => array( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => array(), + 'module' => 'list', + 'weight' => 10, + ), + 'teaser' => array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ), + ), + 'required' => 0, + 'description' => '', + 'default_value' => array( + array( + 'value' => 0, + ), + ), + 'field_name' => 'anomymous_visibility', + 'entity_type' => 'node', + 'bundle' => 'class', + 'deleted' => '0', + ));} + + $field = field_info_field('catalogue_visibility'); + if (empty($field)) { + field_create_field(array( + 'translatable' => '0', + 'entity_types' => array(), + 'settings' => array( + 'allowed_values' => array( + '0', + '1', + ), + 'allowed_values_function' => '', + ), + 'storage' => array( + 'type' => 'field_sql_storage', + 'settings' => array(), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => array( + 'sql' => array( + 'FIELD_LOAD_CURRENT' => array( + 'field_data_catalogue_visibility' => array( + 'value' => 'catalogue_visibility_value', + ), + ), + 'FIELD_LOAD_REVISION' => array( + 'field_revision_catalogue_visibility' => array( + 'value' => 'catalogue_visibility_value', + ), + ), + ), + ), + ), + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 'value', + ), + ), + 'field_name' => 'catalogue_visibility', + 'type' => 'list_boolean', + 'module' => 'list', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'bundles' => array( + 'node' => array( + 'class', + ), + ), + ));} + + $instance = field_info_instance('node', 'catalogue_visibility', 'class'); + if (empty($instance)) { + field_create_instance(array( + 'label' => 'Hide from Catalogue', + 'widget' => array( + 'weight' => '36', + 'type' => 'options_onoff', + 'module' => 'options', + 'active' => 1, + 'settings' => array( + 'display_label' => 1, + ), + ), + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'display' => array( + 'default' => array( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => array(), + 'module' => 'list', + 'weight' => 13, + ), + 'teaser' => array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ), + ), + 'required' => 0, + 'description' => '', + 'default_value' => array( + array( + 'value' => 0, + ), + ), + 'field_name' => 'catalogue_visibility', + 'entity_type' => 'node', + 'bundle' => 'class', + 'deleted' => '0', + )); + } + + og_create_field(OG_DEFAULT_ACCESS_FIELD, 'node', "class"); +} + + +function opigno_og_access_form_alter(&$form, &$form_state, $form_id) +{ + if (($form_id=="course_node_form")||(($form_id=="class_node_form"))) + { + if (isset($form['#node_edit_form'])&&($form['#node_edit_form']==TRUE)) + { + /// Hide the og roles permissions and set them to overwrite always ///////////////////// + $form['og_roles_permissions'][LANGUAGE_NONE]['#default_value'][0]=1; + $form['og_roles_permissions'][LANGUAGE_NONE]['#type']='hidden'; + //////////////////////////////////////////////////////////////////////////////////////// + + // Hide fields /////////////////////////////////////////////////////////// + $form['catalogue_visibility'][LANGUAGE_NONE]['#type']='hidden'; + /////////////////////////////////////////////////////////////////////////////////////// + + $form['#submit'][] = 'opigno_og_access_form_submit_alter'; + + $form['#attached']['js'] = array( + drupal_get_path('module', 'opigno_og_access') . '/js/opigno_og_access.js', + ); + + } + } +} + + +function opigno_og_access_form_submit_alter($form, &$form_state) +{ + if ($form_state['values']['group_access'][LANGUAGE_NONE][0]['value']==0) + { + $form_state['values']['catalogue_visibility'][LANGUAGE_NONE][0]['value']=0; + $form_state['values']['anomymous_visibility'][LANGUAGE_NONE][0]['value']=0; + $form_state['values']['requires_validation'][LANGUAGE_NONE][0]['value']=0; + } + elseif($form_state['values']['group_access'][LANGUAGE_NONE][0]['value']==1) + { + $form_state['values']['catalogue_visibility'][LANGUAGE_NONE][0]['value']=1; + } + elseif(($form_state['values']['group_access'][LANGUAGE_NONE][0]['value']==2)) + { + $form_state['values']['catalogue_visibility'][LANGUAGE_NONE][0]['value']=1; + $form_state['values']['anomymous_visibility'][LANGUAGE_NONE][0]['value']=1; + $form_state['values']['requires_validation'][LANGUAGE_NONE][0]['value']=1; + } +} + diff --git a/modules/opigno_quiz_helper/opigno_quiz_helper.info b/modules/opigno_quiz_helper/opigno_quiz_helper.info new file mode 100644 index 0000000..5409c87 --- /dev/null +++ b/modules/opigno_quiz_helper/opigno_quiz_helper.info @@ -0,0 +1,32 @@ +name = Opigno Quiz helper +description = Makes questions og audience +core = 7.x +package = Opigno + +dependencies[] = opigno +dependencies[] = opigno_quiz_app +dependencies[] = og +dependencies[] = og_quiz +dependencies[] = number +dependencies[] = quiz +dependencies[] = long_answer +dependencies[] = matching +dependencies[] = quiz_directions +dependencies[] = multichoice +dependencies[] = scale +dependencies[] = short_answer +dependencies[] = truefalse +dependencies[] = quiz_ddlines +dependencies[] = views +dependencies[] = views_content +dependencies[] = entityreference +dependencies[] = rules +dependencies[] = rules_conditional +dependencies[] = quizfileupload + +; Information added by Drupal.org packaging script on 2014-10-19 +version = "7.x-1.9" +core = "7.x" +project = "opigno" +datestamp = "1413751430" + diff --git a/modules/opigno_quiz_helper/opigno_quiz_helper.install b/modules/opigno_quiz_helper/opigno_quiz_helper.install new file mode 100755 index 0000000..a9ec14b --- /dev/null +++ b/modules/opigno_quiz_helper/opigno_quiz_helper.install @@ -0,0 +1,33 @@ +xml_parser = xml_parser_create(); + xml_set_object($this->xml_parser, $this); + xml_set_element_handler($this->xml_parser, "tagOpen", "tagClosed"); + + xml_set_character_data_handler($this->xml_parser, "tagData"); + + $this->xml_string = xml_parse($this->xml_parser, $xlm_string); + if (!$this->xml_string) { + watchdog('opigno_scorm', sprintf( + "XML error: %s at line %d", + xml_error_string(xml_get_error_code($this->xml_parser)), + xml_get_current_line_number($this->xml_parser) + ), array(), WATCHDOG_ERROR); + } + + xml_parser_free($this->xml_parser); + + return $this->output; + } + + public function tagOpen($parser, $name, $attrs) { + $tag = array("name" => $name, "attrs" => $attrs); + array_push($this->output, $tag); + } + + public function tagData($parser, $tagData) { + if (trim($tagData)) { + if (isset($this->output[count($this->output) - 1]['tagData'])) { + $this->output[count($this->output) - 1]['tagData'] .= $tagData; + } + else { + $this->output[count($this->output) - 1]['tagData'] = $tagData; + } + } + } + + public function tagClosed($parser, $name) { + $this->output[count($this->output) - 2]['children'][] = $this->output[count($this->output) - 1]; + array_pop($this->output); + } +} \ No newline at end of file diff --git a/modules/scorm/includes/opigno_scorm.ajax.inc b/modules/scorm/includes/opigno_scorm.ajax.inc new file mode 100755 index 0000000..ce79440 --- /dev/null +++ b/modules/scorm/includes/opigno_scorm.ajax.inc @@ -0,0 +1,26 @@ +uid, $sco_id, $cmi_key, $_POST['value']); + drupal_json_output(array('success' => 1)); + } + else { + $data = opigno_scorm_sco_cmi_get($user->uid, $sco_id, $cmi_key); + drupal_json_output(array('data' => $data)); + } +} \ No newline at end of file diff --git a/modules/scorm/includes/opigno_scorm.manifest.inc b/modules/scorm/includes/opigno_scorm.manifest.inc new file mode 100755 index 0000000..f61ecf7 --- /dev/null +++ b/modules/scorm/includes/opigno_scorm.manifest.inc @@ -0,0 +1,581 @@ +uri); + $zip = new ZipArchive(); + $result = $zip->open($path); + if ($result === TRUE) { + $extract_dir = OPIGNO_SCORM_DIRECTORY . '/scorm_' . $fid; + $zip->extractTo($extract_dir); + $zip->close(); + + // This is a standard: the manifest file will always be here. + $manifest_file = $extract_dir . '/imsmanifest.xml'; + + if (file_exists($manifest_file)) { + // Prepare the Scorm DB entry. + $scorm = (object) array( + 'fid' => $fid, + 'extracted_dir' => $extract_dir, + 'manifest_file' => $manifest_file, + 'manifest_id' => '', + 'metadata' => '', + ); + + // Parse the manifest file and extract the data. + $manifest_data = opigno_scorm_extract_manifest_data($manifest_file); + + // Get the manifest ID, if it's given. + if (!empty($manifest_data['manifest_id'])) { + $scorm->manifest_id = $manifest_data['manifest_id']; + } + + // If the file contains (global) metadata, serialize it. + if (!empty($manifest_data['metadata'])) { + $scorm->metadata = serialize($manifest_data['metadata']); + } + + // Try saving the SCORM to the DB. + if (opigno_scorm_scorm_save($scorm)) { + // Store each SCO. + if (!empty($manifest_data['scos']['items'])) { + foreach ($manifest_data['scos']['items'] as $i => $sco_item) { + $sco = (object) array( + 'scorm_id' => $scorm->id, + 'organization' => $sco_item['organization'], + 'identifier' => $sco_item['identifier'], + 'parent_identifier' => $sco_item['parent_identifier'], + 'launch' => $sco_item['launch'], + 'type' => $sco_item['type'], + 'scorm_type' => $sco_item['scorm_type'], + 'title' => $sco_item['title'], + 'weight' => empty($sco_item['weight']) ? $sco_item['weight'] : 0, + 'attributes' => $sco_item['attributes'], + ); + + if (opigno_scorm_sco_save($sco)) { + // @todo Store SCO attributes. + } + else { + watchdog('opigno_scorm', "An error occured when saving an SCO.", array(), WATCHDOG_ERROR); + } + } + } + return TRUE; + } + else { + watchdog('opigno_scorm', "An error occured when saving the SCORM package data.", array(), WATCHDOG_ERROR); + } + } + } + else { + $error = 'none'; + switch ($result) { + case ZipArchive::ER_EXISTS: + $error = 'ER_EXISTS'; + break; + + case ZipArchive::ER_INCONS: + $error = 'ER_INCONS'; + break; + + case ZipArchive::ER_INVAL: + $error = 'ER_INVAL'; + break; + + case ZipArchive::ER_NOENT: + $error = 'ER_NOENT'; + break; + + case ZipArchive::ER_NOZIP: + $error = 'ER_NOZIP'; + break; + + case ZipArchive::ER_OPEN: + $error = 'ER_OPEN'; + break; + + case ZipArchive::ER_READ: + $error = 'ER_READ'; + break; + + case ZipArchive::ER_SEEK: + $error = 'ER_SEEK'; + break; + } + watchdog('opigno_scorm', "An error occured when unziping the SCORM package data. Error: !error", array('!error' => $error), WATCHDOG_ERROR); + } + + return FALSE; +} + +/** + * Extract the manifest data. + * + * Parse the manifest XML with XML2Array (located in includes/XML2Array.php) + * and extract data that is relevant to us. + * + * @param string $manifest_file + * + * @return array + */ +function opigno_scorm_extract_manifest_data($manifest_file) { + $data = array(); + + // Get the XML as a string. + $manifest_string = file_get_contents($manifest_file); + + // Parse it as an array. + $parser = new XML2Array(); + $manifest = $parser->parse($manifest_string); + + // The parser returns an array of arrays - skip the first element. + $manifest = array_shift($manifest); + + // Get the manifest ID, if any. + if (!empty($manifest['attrs'][OPIGNO_SCORM_MANIFEST_MANIFEST_ATTR])) { + $data['manifest_id'] = $manifest['attrs'][OPIGNO_SCORM_MANIFEST_MANIFEST_ATTR]; + } + else { + $data['manifest_id'] = ''; + } + + // Extract the global metadata information. + $data['metadata'] = opigno_scorm_extract_manifest_metadata($manifest); + + // Extract the SCOs (course items). Gets the default SCO and a list of all SCOs. + $data['scos'] = opigno_scorm_extract_manifest_scos($manifest); + + // Extract the resources, so we can combine the SCOs and resources. + $data['resources'] = opigno_scorm_extract_manifest_resources($manifest); + + // Combine the resources and SCOs. + $data['scos']['items'] = opigno_scorm_combine_manifest_sco_and_resources($data['scos']['items'], $data['resources']); + + return $data; +} + +/** + * Extract the manifest metadata. + * + * Find the metadata of this manifest file and return it. + * We only treat global metadata - we don't parse metadata on SCOs or + * resources. + * + * @param array $manifest + * + * @return array + */ +function opigno_scorm_extract_manifest_metadata($manifest) { + foreach ($manifest['children'] as $child) { + if ($child['name'] == OPIGNO_SCORM_MANIFEST_METADATA) { + $meta = array(); + foreach ($child['children'] as $metadata) { + $meta[strtolower($metadata['name'])] = $metadata['tagData']; + } + return $meta; + } + } + return array(); +} + +/** + * Extract the manifest SCO items. + * + * @see _opigno_scorm_extract_manifest_scos_items(). + * + * @param array $manifest + * + * @return array + * 'items' => array of SCOs + * 'default' => default SCO identifier + */ +function opigno_scorm_extract_manifest_scos($manifest) { + $items = array('items' => array()); + foreach ($manifest['children'] as $child) { + if ($child['name'] == OPIGNO_SCORM_MANIFEST_ORGANIZATIONS) { + if (!empty($child['attrs'][OPIGNO_SCORM_MANIFEST_DEFAULT_ATTR])) { + $items['default'] = $child['attrs'][OPIGNO_SCORM_MANIFEST_DEFAULT_ATTR]; + } + else { + $items['default'] = ''; + } + + $items['items'] = array_merge(_opigno_scorm_extract_manifest_scos_items($child['children']), $items['items']); + } + } + return $items; +} + +/** + * Helper function to recursively extract the manifest SCO items. + * + * The data is extracted as a flat array - it contains to hierarchy. Because of this, + * the items are not extracted in logical order. However, each "level" is given a weight + * which allows us to know how to organize them. + * + * @param array $manifest + * @param string|int $parent_identifier = 0 + * @param string $organization = '' + * + * @return array + */ +function _opigno_scorm_extract_manifest_scos_items($manifest, $parent_identifier = 0, $organization = '') { + $items = array(); + $weight = 0; + + foreach ($manifest as $item) { + if (in_array($item['name'], array(OPIGNO_SCORM_MANIFEST_ORGANIZATION, OPIGNO_SCORM_MANIFEST_ITEM)) && !empty($item['children'])) { + $attributes = array(); + if (!empty($item['attrs'][OPIGNO_SCORM_MANIFEST_ID_ATTR])) { + $identifier = $item['attrs'][OPIGNO_SCORM_MANIFEST_ID_ATTR]; + } + else { + $identifier = uniqid(); + } + + if (!empty($item['attrs'][OPIGNO_SCORM_MANIFEST_LAUNCH_ATTR])) { + $launch = $item['attrs'][OPIGNO_SCORM_MANIFEST_LAUNCH_ATTR]; + } + else { + $launch = ''; + } + + if (!empty($item['attrs'][OPIGNO_SCORM_MANIFEST_REFID_ATTR])) { + $resource_identifier = $item['attrs'][OPIGNO_SCORM_MANIFEST_REFID_ATTR]; + } + else { + $resource_identifier = ''; + } + + if (!empty($item['attrs'][OPIGNO_SCORM_MANIFEST_PARAM_ATTR])) { + $attributes['parameters'] = $item['attrs'][OPIGNO_SCORM_MANIFEST_PARAM_ATTR]; + } + + if (!empty($item['attrs'][OPIGNO_SCORM_MANIFEST_TYPE_ATTR])) { + $type = $item['attrs'][OPIGNO_SCORM_MANIFEST_TYPE_ATTR]; + } + else { + $type = ''; + } + + if (!empty($item['attrs'][OPIGNO_SCORM_MANIFEST_SCORM_TYPE_ATTR])) { + $scorm_type = $item['attrs'][OPIGNO_SCORM_MANIFEST_SCORM_TYPE_ATTR]; + } + else { + $scorm_type = ''; + } + + // Find the title, which is also a child node. + foreach ($item['children'] as $child) { + if ($child['name'] == OPIGNO_SCORM_MANIFEST_TITLE) { + $title = $child['tagData']; + break; + } + } + + // Find any sequencing control modes, which are also child nodes. + $control_modes = array(); + foreach ($item['children'] as $child) { + if ($child['name'] == OPIGNO_SCORM_MANIFEST_SEQUENCING) { + $control_modes = opigno_scorm_extract_item_sequencing_control_modes($child); + $attributes['objectives'] = opigno_scorm_extract_item_sequencing_objectives($child); + } + } + + // Failsafe - we cannot have elements without a title. + if (empty($title)) { + $title = 'NO TITLE'; + } + + $items[] = array( + 'manifest' => '', // @deprecated + 'organization' => $organization, + 'title' => $title, + 'identifier' => $identifier, + 'parent_identifier' => $parent_identifier, + 'launch' => $launch, + 'resource_identifier' => $resource_identifier, + 'type' => $type, + 'scorm_type' => $scorm_type, + 'weight' => $weight, + 'attributes' => $control_modes + $attributes, + ); + + // The first item is not an "item", but an "organization" node. This is the organization + // for the remainder of the tree. Get it, and pass it along, so we know to which organization + // the SCOs belong. + if (empty($organization) && $item['name'] == OPIGNO_SCORM_MANIFEST_ORGANIZATION) { + $organization = $identifier; + } + + // Recursively get child items, and merge them to get a flat list. + $items = array_merge(_opigno_scorm_extract_manifest_scos_items($item['children'], $identifier, $organization), $items); + } + $weight++; + } + + return $items; +} + +/** + * Extract the manifest SCO item sequencing objective. + * + * This extracts sequencing objectives from an item. Objectives allow the system + * to know how to "grade" the SCORM object. + * + * @param array $item_manifest + * + * @return array + */ +function opigno_scorm_extract_item_sequencing_objectives($item_manifest) { + $objectives = array(); + foreach ($item_manifest['children'] as $child) { + if ($child['name'] == OPIGNO_SCORM_MANIFEST_OBJECTIVES) { + foreach ($child['children'] as $child_objective) { + if (!empty($child_objective['attrs'][OPIGNO_SCORM_MANIFEST_OBJECTIVE_ID_ATTR])) { + $id = $child_objective['attrs'][OPIGNO_SCORM_MANIFEST_OBJECTIVE_ID_ATTR]; + } + else { + $id = uniqid(); + } + + if ($child_objective['name'] == OPIGNO_SCORM_MANIFEST_PRIMARY_OBJECTIVE) { + // Note: boolean attributes are stored as a strings. PHP does not know + // how to cast 'false' to FALSE. Use string comparisons to bypass + // this limitation by PHP. See below. + $satisfied_by_measure = FALSE; + if (!empty($child_objective['attrs'][OPIGNO_SCORM_MANIFEST_OBJECTIVE_SATISFIED_BY_MEASURE_ATTR])) { + $satisfied_by_measure = strtolower($child_objective['attrs'][OPIGNO_SCORM_MANIFEST_OBJECTIVE_SATISFIED_BY_MEASURE_ATTR]) === 'true'; + } + + $objective = array( + 'primary' => TRUE, + 'secondary' => FALSE, + 'id' => $id, + 'satisfied_by_measure' => $satisfied_by_measure, + ); + + foreach ($child_objective['children'] as $primary_obj_child) { + if ($primary_obj_child['name'] == OPIGNO_SCORM_MANIFEST_MIN_NORMALIZED_MEASURE) { + $objective['min_normalized_measure'] = $primary_obj_child['tagData']; + } + elseif ($primary_obj_child['name'] == OPIGNO_SCORM_MANIFEST_MAX_NORMALIZED_MEASURE) { + $objective['max_normalized_measure'] = $primary_obj_child['tagData']; + } + } + + $objectives[] = $objective; + } + elseif ($child_objective['name'] == OPIGNO_SCORM_MANIFEST_OBJECTIVE) { + $objectives[] = array( + 'primary' => FALSE, + 'secondary' => TRUE, + 'id' => $id, + ); + } + } + } + } + + return $objectives; +} + +/** + * Extract the manifest SCO item sequencing control modes. + * + * This extracts sequencing control modes from an item. Control modes + * describe how the user can navigate around the course + * (e.g.: display the tree or not, skip SCOs, etc). + * + * @param array $item_manifest + * + * @return array + */ +function opigno_scorm_extract_item_sequencing_control_modes($item_manifest) { + $defaults = array( + 'control_mode_choice' => TRUE, + 'control_mode_flow' => FALSE, + 'control_mode_choice_exit' => TRUE, + 'control_mode_forward_only' => FALSE, + ); + + $control_modes = array(); + + foreach ($item_manifest['children'] as $child) { + if ($child['name'] == OPIGNO_SCORM_MANIFEST_CTRL_MODE) { + // Note: boolean attributes are stored as a strings. PHP does not know + // how to cast 'false' to FALSE. Use string comparisons to bypass + // this limitation by PHP. See below. + + if (!empty($child['attrs'][OPIGNO_SCORM_MANIFEST_CHOICE_ATTR])) { + $control_modes['control_mode_choice'] = strtolower($child['attrs'][OPIGNO_SCORM_MANIFEST_CHOICE_ATTR]) === 'true'; + } + + if (!empty($child['attrs'][OPIGNO_SCORM_MANIFEST_FLOW_ATTR])) { + $control_modes['control_mode_flow'] = strtolower($child['attrs'][OPIGNO_SCORM_MANIFEST_FLOW_ATTR]) === 'true'; + } + + if (!empty($child['attrs'][OPIGNO_SCORM_MANIFEST_CHOICE_EXIT_ATTR])) { + $control_modes['control_mode_choice_exit'] = strtolower($child['attrs'][OPIGNO_SCORM_MANIFEST_CHOICE_EXIT_ATTR]) === 'true'; + } + } + } + + return $control_modes + $defaults; +} + +/** + * Extract the manifest SCO resources. + * + * We only extract resource information that is relevant to us. We don't care about + * references files, dependencies, etc. Only about the href attribute, type and + * identifier. + * + * @param array $manifest + * + * @return array + */ +function opigno_scorm_extract_manifest_resources($manifest) { + $items = array(); + foreach ($manifest['children'] as $child) { + if ($child['name'] == OPIGNO_SCORM_MANIFEST_RESOURCES) { + foreach ($child['children'] as $resource) { + if ($resource['name'] == OPIGNO_SCORM_MANIFEST_RESOURCE) { + if (!empty($resource['attrs'][OPIGNO_SCORM_MANIFEST_ID_ATTR])) { + $identifier = $resource['attrs'][OPIGNO_SCORM_MANIFEST_ID_ATTR]; + } + else { + $identifier = uniqid(); + } + + if (!empty($resource['attrs'][OPIGNO_SCORM_MANIFEST_HREF_ATTR])) { + $href = $resource['attrs'][OPIGNO_SCORM_MANIFEST_HREF_ATTR]; + } + else { + $href = ''; + } + + if (!empty($resource['attrs'][OPIGNO_SCORM_MANIFEST_TYPE_ATTR])) { + $type = $resource['attrs'][OPIGNO_SCORM_MANIFEST_TYPE_ATTR]; + } + else { + $type = ''; + } + + if (!empty($resource['attrs'][OPIGNO_SCORM_MANIFEST_SCORM_TYPE_ATTR])) { + $scorm_type = $resource['attrs'][OPIGNO_SCORM_MANIFEST_SCORM_TYPE_ATTR]; + } + else { + $scorm_type = ''; + } + + $items[] = array( + 'identifier' => $identifier, + 'href' => $href, + 'type' => $type, + 'scorm_type' => $scorm_type, + ); + } + } + } + } + return $items; +} + +/** + * Combine resources and SCO data. + * + * Update SCO data to include resource information (if necessary). Return the updated + * SCO list. + * + * @param array $scos + * @param array $resources + * + * @return array + */ +function opigno_scorm_combine_manifest_sco_and_resources($scos, $resources) { + foreach ($scos as &$sco) { + // If the SCO has a resource identifier ("identifierref"), + // we need to combine them. + if (!empty($sco['resource_identifier'])) { + // Check all resources, and break when the correct one is found. + foreach ($resources as $resource) { + if (!empty($resource['identifier']) && $resource['identifier'] == $sco['resource_identifier']) { + // If the SCO has no launch attribute, get the resource href. + if (!empty($resource['href']) && empty($sco['launch'])) { + $sco['launch'] = $resource['href']; + } + + // Set the SCO type, if available. + if (!empty($resource['type']) && empty($sco['type'])) { + $sco['type'] = $resource['type']; + } + + // Set the SCO scorm type, if available. + if (!empty($resource['scorm_type']) && empty($sco['scorm_type'])) { + $sco['scorm_type'] = $resource['scorm_type']; + } + break; + } + } + } + } + return $scos; +} diff --git a/modules/scorm/js/lib/api-1.2.js b/modules/scorm/js/lib/api-1.2.js new file mode 100755 index 0000000..c79ee1d --- /dev/null +++ b/modules/scorm/js/lib/api-1.2.js @@ -0,0 +1,78 @@ +/** + * @file + * Defines the SCORM 1.2 API object. + * + * This is the Opigno SCORM UI implementation of the SCORM API + * object, used for communicating with the Opigno LMS. + */ + +;(function($, Drupal, window, undefined) { + + /** + * Implementation of the SCORM API. + * + * @constructor + */ + var OpignoScorm12API = function() { }; + + /** + * Implements LMSInitialize(). + */ + OpignoScorm12API.prototype.LMSInitialize = function() { + console.log('LMSInitialize'); + } + + /** + * Implements LMSFinish(). + */ + OpignoScorm12API.prototype.LMSFinish = function() { + console.log('LMSFinish'); + } + + /** + * Implements LMSGetValue(). + */ + OpignoScorm12API.prototype.LMSGetValue = function() { + console.log('LMSGetValue'); + } + + /** + * Implements LMSSetValue(). + */ + OpignoScorm12API.prototype.LMSSetValue = function() { + console.log('LMSSetValue'); + } + + /** + * Implements LMSCommit(). + */ + OpignoScorm12API.prototype.LMSCommit = function() { + console.log('LMSCommit'); + } + + /** + * Implements LMSGetLastError(). + */ + OpignoScorm12API.prototype.LMSGetLastError = function() { + console.log('LMSGetLastError'); + return '0'; + } + + /** + * Implements LMSGetErrorString(). + */ + OpignoScorm12API.prototype.LMSGetErrorString = function() { + console.log('LMSGetErrorString'); + } + + /** + * Implements LMSGetDiagnostic(). + */ + OpignoScorm12API.prototype.LMSGetDiagnostic = function() { + console.log('LMSGetDiagnostic'); + } + + // Export. + window.API = new OpignoScorm12API(); + +})(jQuery, Drupal, window); \ No newline at end of file diff --git a/modules/scorm/js/lib/api-2004.js b/modules/scorm/js/lib/api-2004.js new file mode 100755 index 0000000..4d18e5c --- /dev/null +++ b/modules/scorm/js/lib/api-2004.js @@ -0,0 +1,877 @@ +/** + * @file + * Defines the SCORM 2004 API object. + * + * This is the Opigno SCORM UI implementation of the SCORM API + * object, used for communicating with the Opigno LMS. + */ + +;(function($, Drupal, window, undefined) { + + /** + * Implementation of the SCORM API. + * + * @constructor + */ + var OpignoScorm2004API = function() { + this.version = '1.0.0'; + this.error = '0'; + this.isInitialized = false; + this.isTerminated = false; + this.skipCheck = false; + this.registeredCMIPaths = ['cmi._version']; + this.readOnlyCMIPaths = ['cmi._version']; + this.writeOnlyCMIPaths = []; + + // Event callbacks. + this.eventCallbacks = { + initialize: [], + terminate: [], + commit: [], + 'pre-commit': [], + 'post-commit': [], + 'pre-getvalue': [], + 'post-getvalue': [], + 'pre-setvalue': [], + 'post-setvalue': [] + }; + + // Set default data values. + this.data = { + cmi: { + _version: '1.0' + } + }; + }; + + /** + * @const Requested CMI value is currently not available. + */ + OpignoScorm2004API.VALUE_NOT_AVAILABLE = 'VALUE_NOT_AVAILABLE'; + + /** + * @const Requested CMI value is invalid. + */ + OpignoScorm2004API.CMI_NOT_VALID = 'CMI_NOT_VALID'; + + /** + * @const Requested CMI value is not yet implemented by Opigno. + */ + OpignoScorm2004API.CMI_NOT_IMPLEMENTED = 'CMI_NOT_IMPLEMENTED'; + + /** + * @const Requested CMI value is write-only. + */ + OpignoScorm2004API.VALUE_WRITE_ONLY = 'VALUE_WRITE_ONLY'; + + /** + * @const Requested CMI value is read-only. + */ + OpignoScorm2004API.VALUE_READ_ONLY = 'VALUE_READ_ONLY'; + + /** + * @const Requested CMI child value does not exist. + */ + OpignoScorm2004API.CHILD_DOES_NOT_EXIST = 'CHILD_DOES_NOT_EXIST'; + + + + /** + * @defgroup scorm_2004_rte_api SCORM 2004 RTE API definition + * @{ + * Method definitions of the SCORM 2004 Runtime Environment API. + */ + + /** + * Implements Initialize(). + * + * @param {String} value + * Expected to be an empty string. If not, will + * return a 201 error. + * + * @returns {String} 'true' or 'false' + * The SCO expects boolean values to be returned as strings. + */ + OpignoScorm2004API.prototype.Initialize = function(value) { + // The value MUST be an empty string (per SCORM.2004.3ED.ConfReq.v1.0). + // If it's not empty, don't bother initializing the package. + if (value !== '') { + this.error = '201'; + return 'false'; + } + + if (!this.isInitialized) { + // If the communication fails, set the error to 102 + // and return 'false'. + if (!this._initCommunication()) { + this.error = '102'; + return 'false'; + } + else { + this.isInitialized = true; + } + } + // If terminated, set the error to 104 and return 'false'. + else if (this.isTerminated) { + this.error = '104'; + return 'false'; + } + // If already initialized, set the error to 103 and return 'false'. + else if (this.isInitialized) { + this.error = '103'; + return 'false'; + } + + this.trigger('initialize', value); + + // Successfully initialized the package. + this.error = '0'; + return 'true'; + } + + /** + * Implements Terminate(). + * + * @param {String} value + * Expected to be an empty string. If not, will + * return a 201 error. + * + * @returns {String} 'true' or 'false' + * The SCO expects boolean values to be returned as strings. + */ + OpignoScorm2004API.prototype.Terminate = function(value) { + // The value MUST be an empty string (per SCORM.2004.3ED.ConfReq.v1.0). + // If it's not empty, don't bother terminating the package. + if (value !== '') { + this.error = '201'; + return 'true'; // 'false'; As per SCORM.2004.3ED.ConfReq.v1.0, should return false. However, to prevent annoying alerts from popping up in certain, malfunctioning packages, we return true. + } + + // Can only terminate if the session was initialized. Else, set error to + // 112 and return 'false'. + if (!this.isInitialized) { + this.error = '112'; + return 'true'; // 'false'; As per SCORM.2004.3ED.ConfReq.v1.0, should return false. However, to prevent annoying alerts from popping up in certain, malfunctioning packages, we return true. + } + // If already terminated, set the error to 113 and return 'false'. + else if (this.isTerminated) { + this.error = '113'; + return 'true'; // 'false'; As per SCORM.2004.3ED.ConfReq.v1.0, should return false. However, to prevent annoying alerts from popping up in certain, malfunctioning packages, we return true. + } + // Terminate must call Commit to persist all data. + else if (this.Commit('') === 'false') { + this.error = '391'; + return 'true'; // 'false'; As per SCORM.2004.3ED.ConfReq.v1.0, should return false. However, to prevent annoying alerts from popping up in certain, malfunctioning packages, we return true. + } + else { + // Terminate the communication. + // If the termination fails, set the error to 111 end return 'false'. + if (false) { + this.error = '111'; + return 'true'; // 'false'; As per SCORM.2004.3ED.ConfReq.v1.0, should return false. However, to prevent annoying alerts from popping up in certain, malfunctioning packages, we return true. + } + else { + this.isTerminated = true; + } + } + + this.trigger('terminate', value); + + this.error = '0'; + return 'true'; + } + + /** + * Implements GetValue(). + * + * @param {String} cmiElement + * + * @returns {String} + */ + OpignoScorm2004API.prototype.GetValue = function(cmiElement) { + console.log('GetValue', cmiElement); + // Cannot get a value if not initialized. + // Set the error to 122 end return ''. + if (!this.isInitialized) { + this.error = '122'; + return ''; + } + // Cannot get a value if terminated. + // Set the error to 123 end return ''. + else if (this.isTerminated) { + this.error = '123'; + return ''; + } + + // Must provide a cmiElement. If no valid identifier is provided, + // set the error to 301 and return ''. + if (cmiElement === undefined || cmiElement === null || cmiElement === '') { + this.error = '301'; + return ''; + } + + this.trigger('pre-getvalue', cmiElement); + + // Find the CMI value. + try { + if (/^cmi\./.test(cmiElement)) { + var result = this._getCMIData(cmiElement); + + // If the value is not available, set the error to 403 + // and return ''. + if (result === OpignoScorm2004API.VALUE_NOT_AVAILABLE) { + this.error = '403'; + return ''; + } + // If the value does not exist, set the error to 401 + // and return ''. + else if (result === OpignoScorm2004API.CMI_NOT_VALID) { + this.error = '401'; + return ''; + } + // If the value is supposed to be a child value, but the parent + // doesn't have it, set the error to 301 and return ''. + else if (result === OpignoScorm2004API.CHILD_DOES_NOT_EXIST) { + this.error = '301'; + return ''; + } + // If the value is write-only, set the error to 405 and + // return ''. + else if (result === OpignoScorm2004API.VALUE_WRITE_ONLY) { + this.error = '405'; + return ''; + } + // For currently unimplemented values, set the error to 402 + // and return ''. + else if (result === OpignoScorm2004API.CMI_NOT_IMPLEMENTED) { + this.error = '402'; + return ''; + } + // If the value was found, return it and set the error to '0'. + else { + this.error = '0'; + console.log('GetValueFound', cmiElement, result); + this.trigger('post-getvalue', cmiElement, result); + return result; + } + } + // For unknown values, set the error to 401 and return ''. + else { + this.error = '401'; + return ''; + } + } + catch (e) { + // If anything fails, for whatever reason, set the error to 301 and + // return ''. + this.error = '301'; + return ''; + } + } + + /** + * Implements SetValue(). + * + * @param {String} cmiElement + * @param {String} value + * + * @return {String} + */ + OpignoScorm2004API.prototype.SetValue = function(cmiElement, value) { + console.log('SetValue', cmiElement, value); + // Cannot get a value if not initialized. + // Set the error to 122 end return ''. + if (!this.isInitialized) { + this.error = '132'; + return 'true'; // 'false'; As per SCORM.2004.3ED.ConfReq.v1.0, should return false. However, to prevent annoying alerts from popping up in certain, malfunctioning packages, we return true. + } + // Cannot get a value if terminated. + // Set the error to 123 end return ''. + else if (this.isTerminated) { + this.error = '133'; + return 'true'; // 'false'; As per SCORM.2004.3ED.ConfReq.v1.0, should return false. However, to prevent annoying alerts from popping up in certain, malfunctioning packages, we return true. + } + + // Must provide a cmiElement. If no valid identifier is provided, + // set the error to 301 and return ''. + if (cmiElement === undefined || cmiElement === null || cmiElement === '' || typeof cmiElement !== 'string') { + this.error = '351'; + return 'true'; // 'false'; As per SCORM.2004.3ED.ConfReq.v1.0, should return false. However, to prevent annoying alerts from popping up in certain, malfunctioning packages, we return true. + } + + // The value must either be a String or a number. All other values have to be rejected. + // Return 'false' and set the error to 406. + if (typeof value !== 'string' && typeof value !== 'number') { + this.error = '406'; + return 'true'; // 'false'; As per SCORM.2004.3ED.ConfReq.v1.0, should return false. However, to prevent annoying alerts from popping up in certain, malfunctioning packages, we return true. + } + + this.trigger('pre-setvalue', cmiElement, value); + + // Find the CMI value. + try { + if (/^cmi\./.test(cmiElement)) { + var result = this._setCMIData(cmiElement, value); + + // If the value does not exist, set the error to 401 + // and return 'false'. + if (result === OpignoScorm2004API.CMI_NOT_VALID) { + console.log('SetValue', 'NOT VALID'); + this.error = '401'; + return 'true'; // 'false'; As per SCORM.2004.3ED.ConfReq.v1.0, should return false. However, to prevent annoying alerts from popping up in certain, malfunctioning packages, we return true. + } + // For currently unimplemented values, set the error to 402 + // and return 'false'. + else if (result === OpignoScorm2004API.CMI_NOT_IMPLEMENTED) { + console.log('SetValue', 'NOT IMPLEMENTED'); + this.error = '402'; + return 'true'; // 'false'; As per SCORM.2004.3ED.ConfReq.v1.0, should return false. However, to prevent annoying alerts from popping up in certain, malfunctioning packages, we return true. + } + // For read-only values, set the error to 404 and return 'false'. + else if (result === OpignoScorm2004API.VALUE_READ_ONLY) { + console.log('SetValue', 'NOT WRITABLE'); + this.error = '404'; + return 'true'; // 'false'; As per SCORM.2004.3ED.ConfReq.v1.0, should return false. However, to prevent annoying alerts from popping up in certain, malfunctioning packages, we return true. + } + } + // For unknown values, set the error to 401 and return ''. + else { + console.log('SetValue', 'UNKNOWN ERROR'); + this.error = '401'; + return 'true'; // 'false'; As per SCORM.2004.3ED.ConfReq.v1.0, should return false. However, to prevent annoying alerts from popping up in certain, malfunctioning packages, we return true. + } + } + catch (e) { + // If anything fails, for whatever reason, set the error to 351 and + // return ''. + console.log('SetValue', 'THREW ERROR'); + this.error = '351'; + return 'true'; // 'false'; As per SCORM.2004.3ED.ConfReq.v1.0, should return false. However, to prevent annoying alerts from popping up in certain, malfunctioning packages, we return true. + } + + this.trigger('post-setvalue', cmiElement, value); + + this.error = '0'; + return 'true'; + } + + /** + * Implements Commit(). + * + * @param {String} value + * Expected to be an empty string. If not, will + * return a 201 error. + * + * @returns {String} 'true' or 'false' + * The SCO expects boolean values to be returned as strings. + */ + OpignoScorm2004API.prototype.Commit = function(value) { + // The value MUST be an empty string (per SCORM.2004.3ED.ConfReq.v1.0). + // If it's not empty, don't bother terminating the package. + if (value !== '') { + this.error = '201'; + return 'false'; + } + + // Can only commit if the session was initialized. Else, set error to + // 142 and return 'false'. + if (!this.isInitialized) { + this.error = '142'; + return 'false'; + } + // If already terminated, set the error to 143 and return 'false'. + else if (this.isTerminated) { + this.error = '143'; + return 'true'; // 'false'; As per SCORM.2004.3ED.ConfReq.v1.0, should return false. However, to prevent annoying alerts from popping up in certain, malfunctioning packages, we return true. + } + + this.trigger('pre-commit', value, this.data); + + try { + this.trigger('commit', value, this.data); + } + catch (e) { + // If anything fails, for whatever reason, set the error to 391 and + // return ''. + this.error = '391'; + return 'false'; + } + + this.trigger('post-commit', value, this.data); + + this.error = '0'; + return 'true'; + } + + /** + * Implements GetLastError(). + * + * @returns {String} + */ + OpignoScorm2004API.prototype.GetLastError = function() { + return this.error; + } + + /** + * Implements GetErrorString(). + * + * @param {String} cmiErrorCode + * + * @return {String} + */ + OpignoScorm2004API.prototype.GetErrorString = function(cmiErrorCode) { + // @todo + return ''; + } + + /** + * Implements GetDiagnostic(). + * + * @param {String} cmiErrorCode + * + * @return {String} + */ + OpignoScorm2004API.prototype.GetDiagnostic = function(cmiErrorCode) { + // @todo + return ''; + } + + /** + * @} End of "defgroup scorm_2004_rte_api". + */ + + + + /** + * @defgroup scorm_2004_public Public methods + * @{ + * Public method definitions of the SCORM 2004 API class. + * + * These function can be used for generic data manipulation or interacting with + * the OpignoScorm2004API object directly. + */ + + /** + * + */ + OpignoScorm2004API.prototype.normalizeCMIPath = function(cmiPath) { + return cmiPath.replace(/\.[0-9]+\./g, '.n.'); + } + + /** + * Bind an event listener to the API. + * + * @param {String} event + * @param {Function} callback + */ + OpignoScorm2004API.prototype.bind = function(event, callback) { + if (this.eventCallbacks[event] === undefined) { + throw { name: "ScormAPIUnknownEvent", message: "Can't bind/trigger event '" + event + "'" }; + } + else { + this.eventCallbacks[event].push(callback); + } + } + + /** + * Trigger the passed event. All parameters (except the event name) are passed + * to the registered callback. + * + * @param {String} event + */ + OpignoScorm2004API.prototype.trigger = function() { + var args = Array.prototype.slice.call(arguments), + event = args.shift(); + + if (this.eventCallbacks[event] === undefined) { + throw { name: "ScormAPIUnknownEvent", message: "Can't bind/trigger event '" + event + "'" }; + } + else { + for (var i = 0, len = this.eventCallbacks[event].length; i < len; i++) { + this.eventCallbacks[event][i].apply(this, args); + } + } + } + + /** + * Register CMI paths. + * + * This will make the API tell the SCO the passed paths + * are available and implemented. When reading/writing these values, + * the API will behave as the SCO expects. + * + * @param {Object} cmiPaths + * A hash map of paths, where each item has a writeOnly or readOnly property. + */ + OpignoScorm2004API.prototype.registerCMIPaths = function(cmiPaths) { + for (var cmiPath in cmiPaths) { + if (cmiPath) { + this.registeredCMIPaths.push(cmiPath); + if (cmiPaths[cmiPath].readOnly !== undefined && cmiPaths[cmiPath].readOnly) { + this.readOnlyCMIPaths.push(cmiPath); + } + else if (cmiPaths[cmiPath].writeOnly !== undefined && cmiPaths[cmiPath].writeOnly) { + this.writeOnlyCMIPaths.push(cmiPath); + } + } + } + } + + /** + * Register CMI data. + * + * This is different from SetValue, as it allows developers to set entire + * data structures very quickly. This should be used on initialization for + * providing data the SCO will need. + * + * Warning ! This can override data previously set by other callers. Use with caution. + * + * @see _setCMIData(). + * + * @param {String} cmiPath + * @param {Object} data + */ + OpignoScorm2004API.prototype.registerCMIData = function(cmiPath, data) { + this._setCMIData(cmiPath, data, true); + } + + /** + * @} End of "defgroup scorm_2004_public". + */ + + + + /** + * @defgroup scorm_2004_private Private methods + * @{ + * Private method definitions of the SCORM 2004 API class. + * + * These function should not be used directly. Prefer using the public methods + * or extending the OpignoScorm2004API object. Method signatures can change. + */ + + /** + * Initialize the communication between the SCORM package and Opigno. + * + * @return {Boolean} + */ + OpignoScorm2004API.prototype._initCommunication = function() { + // The SCORM 2004 edition does not provide any asynchronous logic, or the concept + // of promises. This means establishing the communication between the SCORM + // and Opigno must always be considered "active", and can never "fail". + // We return true in any case. + return true; + } + + /** + * Fetch the CMI data by recursively checking the CMI data tree. + * + * @param {String} cmiPath + * @param {Boolean} skipValidation + * + * @returns {String} + */ + OpignoScorm2004API.prototype._getCMIData = function(cmiPath, skipValidation) { + if (!skipValidation) { + // Special test values. + if (cmiPath === 'cmi.__value__') { + return 'value'; + } + else if (cmiPath === 'cmi.__write_only__') { + return OpignoScorm2004API.VALUE_WRITE_ONLY; + } + else if (cmiPath === 'cmi.__unimplemented__') { + return OpignoScorm2004API.CMI_NOT_IMPLEMENTED; + } + else if (cmiPath === 'cmi.__unknown__') { + return OpignoScorm2004API.CMI_NOT_VALID; + } + + // Check if the CMI path is valid. If not, return CMI_NOT_VALID. + if (!this._validCMIDataPath(cmiPath)) { + return OpignoScorm2004API.CMI_NOT_VALID; + } + // Check if the CMI path is write-only. If so, return VALUE_WRITE_ONLY. + else if (this._writeOnlyCMIDataPath(cmiPath)) { + return OpignoScorm2004API.VALUE_WRITE_ONLY; + } + // Check if the CMI path is implemented. If not, return CMI_NOT_IMPLEMENTED. + else if (!this._implementedCMIDataPath(cmiPath)) { + return OpignoScorm2004API.CMI_NOT_IMPLEMENTED; + } + } + + // Recursively walk the data tree and get the requested leaf. + var pathTree = cmiPath.split('.'), + // Get the first path element, usually 'cmi'. + path = pathTree.shift(), + // Get the root element data. + data = this.data[path] !== undefined ? this.data[path] : null, + // Are there more parts ? If so, flag this as looking for children. + checkChildren = pathTree.length > 1; + + // Recursively walk the tree. + while (data && pathTree.length) { + path = pathTree.shift(); + + // Special case: if we request the length of an array, check if the current + // data is an array. If so, get its length and break out of the loop. + // If not, throw an error. + if (path === '_count') { + if (data.length !== undefined) { + data = data.length; + break; + } + else { + throw new EvalError("Can only get the '_count' property for array data. CMI path: " + cmiPath); + } + } + else { + data = data[path] !== undefined ? data[path] : null; + } + } + + if (data !== null) { + return data; + } + else { + // If we were looking for an element children, return CHILD_DOES_NOT_EXIST. + if (checkChildren) { + return OpignoScorm2004API.CHILD_DOES_NOT_EXIST; + } + // Else, return VALUE_NOT_AVAILABLE. + else { + return OpignoScorm2004API.VALUE_NOT_AVAILABLE; + } + } + } + + /** + * Set the CMI data by recursively checking the CMI data tree. + * Create elements in the tree that do not exist yet. + * + * @param {String} cmiPath + * @param {String} value + * @param {Boolean} skipValidation + * + * @returns {String} + */ + OpignoScorm2004API.prototype._setCMIData = function(cmiPath, value, skipValidation) { + if (!skipValidation) { + // Check if the CMI path is valid. If not, return CMI_NOT_VALID. + if (!this._validCMIDataPath(cmiPath)) { + return OpignoScorm2004API.CMI_NOT_VALID; + } + // Check if the CMI path is implemented. If not, return CMI_NOT_IMPLEMENTED. + else if (!this._implementedCMIDataPath(cmiPath)) { + return OpignoScorm2004API.CMI_NOT_IMPLEMENTED; + } + // Check if the CMI path is read-only. If so, return VALUE_READ_ONLY. + else if (this._readOnlyCMIDataPath(cmiPath)) { + return OpignoScorm2004API.VALUE_READ_ONLY; + } + } + + // Recursively walk the data tree and get the requested leaf. + var pathTree = cmiPath.split('.'), + // Get the first path element, usually 'cmi'. + path = pathTree.shift(), + // Get the last path element. + leaf = pathTree.length ? pathTree.pop() : false; + + // If the root does not exist, initialize an empty object. + if (this.data[path] === undefined) { + this.data[path] = {}; + } + + // Get the root element data. + var data = this.data[path]; + + // If the leaf is not set, we don't need to walk any tree. Set the value immediately. + if (!leaf) { + data = value; + } + // Else, we walk the tree recursively creating all elements if needed. + else { + var prevPaths = [path]; + // Recursively walk the tree. + while (pathTree.length) { + path = pathTree.shift(); + + // If the property does not exist yet, create it. + if (data[path] === undefined) { + // If the property is numerical, we're dealing with an array. + if (/^[0-9]+$/.test(path)) { + // If the key is 0, and the parent is not an array, reset the parent to an array object. + // Push an empty element onto the array. + if (path === '0' && data.length === undefined) { + // Just resetting data to [] loses it's relationship with this.data. We have no choice + // but to use eval() here. + eval('this.data.' + prevPaths.join('.') + ' = [];'); + eval('data = this.data.' + prevPaths.join('.') + ';'); + data.push({}); + } + // If the parent is an array object, but the given key is out of bounds, throw an error. + else if (data.length < path) { + throw { name: "CMIDataOutOfBounds", message: "Out of bounds. Cannot set [" + path + "] on " + cmiPath + ", as it contains only " + data.length + " elements." }; + } + // Finally, if this is an array, and the key is valid, but there's no element yet, + // push an empty element onto the array. + else if (data[path] === undefined) { + data.push({}); + } + } + // Else, we're dealing with a hash. + else { + data[path] = {}; + } + } + + data = data[path]; + prevPaths.push(path); + } + + data[leaf] = value; + } + } + + /** + * Check if the given CMI path is valid and usable. + * + * @param {String} cmiPath + * + * @returns {Boolean} + */ + OpignoScorm2004API.prototype._validCMIDataPath = function(cmiPath) { + // Normalize the path. + var normalizedPath = this.normalizeCMIPath(cmiPath); + + var keys = [ + // Special test paths. + 'cmi.__value__', + 'cmi.__read_only__', + 'cmi.__unimplemented__', + 'cmi.__test__', + 'cmi.__test__._count', + 'cmi.__test__.n.child', + + // Real CMI paths, from SORM 2004 3rd edition requirement document. + 'cmi._version', + 'cmi.exit', + 'cmi.success_status', + 'cmi.completion_status', + 'cmi.score.raw', + 'cmi.score.min', + 'cmi.score.max', + 'cmi.score.scaled', + 'cmi.location', + 'cmi.objectives', + 'cmi.objectives._children', + 'cmi.objectives._count', + 'cmi.objectives.n.score', + 'cmi.objectives.n.score.scaled', + 'cmi.objectives.n.score.raw', + 'cmi.objectives.n.score.min', + 'cmi.objectives.n.score.max', + 'cmi.objectives.n.id', + 'cmi.objectives.n.success_status', + 'cmi.objectives.n.completion_status', + 'cmi.objectives.n.progress_measure', + 'cmi.objectives.n.description', + 'cmi.comments_from_learner', + 'cmi.comments_from_learner._children', + 'cmi.comments_from_learner._count', + 'cmi.comments_from_learner.n.comment', + 'cmi.comments_from_learner.n.location', + 'cmi.comments_from_learner.n.timestamp' + ]; + + return keys.indexOf(normalizedPath) !== -1; + } + + /** + * Check if the given CMI path is write-only. + * + * @param {String} cmiPath + * + * @returns {Boolean} + */ + OpignoScorm2004API.prototype._writeOnlyCMIDataPath = function(cmiPath) { + // Normalize the path. + var normalizedPath = this.normalizeCMIPath(cmiPath); + + // Check implemented paths. + return this.writeOnlyCMIPaths.indexOf(normalizedPath) !== -1; + } + + /** + * Check if the given CMI path is read-only. + * + * @param {String} cmiPath + * + * @returns {Boolean} + */ + OpignoScorm2004API.prototype._readOnlyCMIDataPath = function(cmiPath) { + // Array properties are always read-only. + if (/\._(count|children)/.test(cmiPath)) { + return true; + } + + // Normalize the path. + var normalizedPath = this.normalizeCMIPath(cmiPath); + + var keys = [ + // Special test paths. + 'cmi.__read_only__' + ]; + + if (keys.indexOf(normalizedPath) !== -1) { + return true; + } + + // Check implemented paths. + return this.readOnlyCMIPaths.indexOf(normalizedPath) !== -1; + } + + /** + * Check if the given CMI path is implemented by Opigno. + * + * @param {String} cmiPath + * + * @returns {Boolean} + */ + OpignoScorm2004API.prototype._implementedCMIDataPath = function(cmiPath) { + // In some cases, we may want to use every CMI path anyway. + if (this.skipCheck) { + return true; + } + + // Normalize the path. + var normalizedPath = this.normalizeCMIPath(cmiPath); + + // Special test paths. + var keys = [ + 'cmi.__value__', + 'cmi.__read_only__', + 'cmi.__test__', + 'cmi.__test__._count', + 'cmi.__test__.n.child', + ]; + + if (keys.indexOf(normalizedPath) !== -1) { + return true; + } + + // Check implemented paths. + console.log(this.registeredCMIPaths, normalizedPath, 'found: ', this.registeredCMIPaths.indexOf(normalizedPath) !== -1) + return this.registeredCMIPaths.indexOf(normalizedPath) !== -1; + } + + /** + * @} End of "defgroup scorm_2004_private". + */ + + // Export. + window.OpignoScorm2004API = OpignoScorm2004API; + +})(jQuery, Drupal, window); \ No newline at end of file diff --git a/modules/scorm/js/tests/api-2004.js b/modules/scorm/js/tests/api-2004.js new file mode 100755 index 0000000..1990db3 --- /dev/null +++ b/modules/scorm/js/tests/api-2004.js @@ -0,0 +1,427 @@ +/** + * @file + * Test suite for the SCORM 2004 RTE API. + */ + +;(function($, Drupal, window, OpignoScorm2004API, undefined) { + + /** + * Fixture object. + * + * Stub AJAX responses, so the API doesn't tie to the actual server. + */ + var fixture = { + paths: { + 'api/init': { + id: '1234' + } + }, + ajaxResponse: function(path) { + if (fixture.paths[path] !== undefined) { + return fixture.paths[path]; + } + else { + throw new URIError('Path ' + path + ' not stubbed.'); + } + } + }; + + Drupal.tests.OpignoScorm2004API = { + getInfo: function() { + return { + name: 'Opigno SCORM 2004 API', + description: 'Test the SCORM 2004 3rd Edition RTE API', + group: 'Opigno' + }; + }, + test: function() { + /* + module('API detection and method implementations.'); + ok(window.API_1484_11, 'REQ_2.5: A global API_1484_11 object exists.'); + ok(window.API_1484_11.version, 'REQ_2.6: The API implements a version property.'); + ok(/^1\.0\./.test(window.API_1484_11.version), 'REQ_2.6.1, REQ_2.6.2: The API version is correctly formatted.'); + ok(window.API_1484_11.Initialize, 'REQ_4.1: The API implements the Initialize() method.'); + ok(window.API_1484_11.Terminate, 'REQ_5.1: The API implements the Terminate() method.'); + ok(window.API_1484_11.GetValue, 'REQ_6.1: The API implements the GetValue() method.'); + ok(window.API_1484_11.SetValue, 'REQ_7.1: The API implements the SetValue() method.'); + ok(window.API_1484_11.Commit, 'REQ_8.1: The API implements the Commit() method.'); + ok(window.API_1484_11.GetLastError, 'REQ_9.1: The API implements the GetLastError() method.'); + ok(window.API_1484_11.GetErrorString, 'REQ_10.1: The API implements the GetErrorString() method.'); + ok(window.API_1484_11.GetDiagnostic, 'REQ_11.1: The API implements the GetDiagnostic() method.'); + */ + + // Get a new API implementation, so we can easily reset its internals. + var api = new OpignoScorm2004API(), value = ''; + + /** + * @defgroup scorm_2004_object_specs Object specs + * @{ + * Specs for the OpignoScorm2004API custom methods. + * + * OpignoScorm2004API provides several methods for easy integration + * with other libraries. This is different from the SCORM API methods, + * which are defined by the ADL Conformance Requirements. + */ + + /** + * We use a event powered system for abstracting communication + * between the SCO and the LMS. Test the events. + */ + module('Events'); + api = new OpignoScorm2004API(); + + var spy = {}; + api.bind('initialize', function() { + spy.called = true; + spy.that = this; + }); + api.Initialize(''); + ok(spy.called, 'Spy got called for the "initialize" event.'); + equal(spy.that, api, 'Spy called with correct context (API object).'); + + spy = {}; + api.bind('pre-getvalue', function(cmiPath) { + spy.preGetValueCalled = true; + spy.preGetValueThat = this; + spy.preGetValueCMIPath = cmiPath; + }); + api.bind('post-getvalue', function(cmiPath, value) { + spy.postGetValueCalled = true; + spy.postGetValueThat = this; + spy.postGetValueCMIPath = cmiPath; + spy.postGetValueValue = value; + }); + api.GetValue('cmi._version'); + ok(spy.preGetValueCalled, 'Spy got called for the "pre-getvalue" event.'); + equal(spy.preGetValueThat, api, 'Spy called with correct context (API object).'); + equal(spy.preGetValueCMIPath, 'cmi._version', 'Spy got called for the "pre-getvalue" event with the correct cmi path.'); + ok(spy.postGetValueCalled, 'Spy got called for the "post-getvalue" event.'); + equal(spy.postGetValueThat, api, 'Spy called with correct context (API object).'); + equal(spy.postGetValueCMIPath, 'cmi._version', 'Spy got called for the "post-getvalue" event with the correct cmi path.'); + equal(spy.postGetValueValue, api.data.cmi._version, 'Spy got called for the "post-getvalue" event with the correct value.'); + + spy = {}; + api.bind('pre-setvalue', function(cmiPath, value) { + spy.preSetValueCalled = true; + spy.preSetValueThat = this; + spy.preSetValueCMIPath = cmiPath; + spy.preSetValueValue = value; + }); + api.bind('post-setvalue', function(cmiPath, value) { + spy.postSetValueCalled = true; + spy.postSetValueThat = this; + spy.postSetValueCMIPath = cmiPath; + spy.postSetValueValue = value; + }); + api.SetValue('cmi.__value__', 'value'); + ok(spy.preSetValueCalled, 'Spy got called for the "pre-setvalue" event.'); + equal(spy.preSetValueThat, api, 'Spy called with correct context (API object).'); + equal(spy.preSetValueCMIPath, 'cmi.__value__', 'Spy got called for the "pre-setvalue" event with the correct cmi path.'); + equal(spy.preSetValueValue, 'value', 'Spy got called for the "pre-setvalue" event with the correct value.'); + ok(spy.postSetValueCalled, 'Spy got called for the "post-setvalue" event.'); + equal(spy.postSetValueThat, api, 'Spy called with correct context (API object).'); + equal(spy.postSetValueCMIPath, 'cmi.__value__', 'Spy got called for the "post-setvalue" event with the correct cmi path.'); + equal(spy.postSetValueValue, 'value', 'Spy got called for the "post-setvalue" event with the correct value.'); + + spy = {}; + api.bind('pre-commit', function() { + spy.preCommitCalled = true; + spy.preCommitThat = this; + }); + api.bind('commit', function() { + spy.commitCalled = true; + spy.commitThat = this; + }); + api.bind('post-commit', function() { + spy.postCommitCalled = true; + spy.postCommitThat = this; + }); + api.Commit(''); + ok(spy.preCommitCalled, 'Spy got called for the "pre-commit" event.'); + equal(spy.preCommitThat, api, 'Spy called with correct context (API object).'); + ok(spy.commitCalled, 'Spy got called for the "commit" event.'); + equal(spy.commitThat, api, 'Spy called with correct context (API object).'); + ok(spy.postCommitCalled, 'Spy got called for the "pre-commit" event.'); + equal(spy.postCommitThat, api, 'Spy called with correct context (API object).'); + + spy = {}; + api.bind('terminate', function() { + spy.called = true; + spy.that = this; + }); + api.Terminate(''); + ok(spy.called, 'Spy got called for the "terminate" event.'); + equal(spy.that, api, 'Spy called with correct context (API object).'); + + + /** + * We allow third party JS to register CMI paths, flag them as write only, read only, etc. + */ + module('Register paths'); + api = new OpignoScorm2004API(); + + ok(!api._implementedCMIDataPath('cmi.write_only_path'), 'Path cmi.write_only_path is not registered.'); + ok(!api._implementedCMIDataPath('cmi.read_only_path'), 'Path cmi.read_only_path is not registered.'); + ok(!api._implementedCMIDataPath('cmi.read_write_path'), 'Path cmi.read_write_path is not registered.'); + ok(!api._implementedCMIDataPath('cmi.read_write_path_2'), 'Path cmi.read_write_path_2 is not registered.'); + + api.registerCMIPaths({ + 'cmi.write_only_path': { + writeOnly: true + }, + 'cmi.read_only_path': { + readOnly: true + }, + 'cmi.read_write_path': { }, + 'cmi.read_write_path_2': { + writeOnly: false, + readOnly: false + } + }); + + ok(!api._implementedCMIDataPath('cmi.__unkown__'), "Registering paths don't make all paths magically available."); + ok(api._implementedCMIDataPath('cmi.write_only_path'), 'Path cmi.write_only_path got registered correctly.'); + ok(api._implementedCMIDataPath('cmi.read_only_path'), 'Path cmi.read_only_path got registered correctly.'); + ok(api._implementedCMIDataPath('cmi.read_write_path'), 'Path cmi.read_write_path got registered correctly.'); + ok(api._implementedCMIDataPath('cmi.read_write_path_2'), 'Path cmi.read_write_path_2 got registered correctly.'); + + + /** + * We allow third party JS to register data needed for the SCO at startup. + */ + module('Register data'); + api = new OpignoScorm2004API(); + + notEqual(api._getCMIData('cmi.__data_value__', true), 'value', 'Data cmi.__data_value__ got correctly registered.'); + notDeepEqual(api._getCMIData('cmi.__tree_value__', true), { tree: { leaf: 1 } }, 'Data cmi.__tree_value__ got correctly registered.'); + notDeepEqual(api._getCMIData('cmi.__array_value__', true), [{ key: 'value' }, { key: 'value2' }], 'Data cmi.__array_value__ got correctly registered.'); + + api.registerCMIData('cmi.__data_value__', 'value'); + api.registerCMIData('cmi.__tree_value__', { tree: { leaf: 1 } }); + api.registerCMIData('cmi.__array_value__', [{ key: 'value' }, { key: 'value2' }]); + + equal(api._getCMIData('cmi.__data_value__', true), 'value', 'Data cmi.__data_value__ got correctly registered.'); + deepEqual(api._getCMIData('cmi.__tree_value__', true), { tree: { leaf: 1 } }, 'Data cmi.__tree_value__ got correctly registered.'); + deepEqual(api._getCMIData('cmi.__array_value__', true), [{ key: 'value' }, { key: 'value2' }], 'Data cmi.__array_value__ got correctly registered.'); + + /** + * @} End of "defgroup scorm_2004_object_specs". + */ + + + + /** + * IMPORTANT NOTE: + * REQ_9.x are all covered by the following specs, as GetLastError() is called many times + * throughout the test suite. + */ + + + module('API, initial state.'); + equal(api.GetLastError(), '0', 'REQ_3.1: The original error code is "0".'); + + + module('API::Initialize()'); + api = new OpignoScorm2004API(); + equal(api.Initialize(), 'false', 'REQ_3.2: Initializing the API without a parameter fails.'); + equal(api.GetLastError(), '201', 'REQ_3.2: Initializing the API without a parameter gives a 201 error.'); + equal(api.Initialize('any string'), 'false', 'REQ_3.2: Initializing the API with any string fails.'); + equal(api.GetLastError(), '201', 'REQ_3.2: Initializing the API with any string gives a 201 error.'); + equal(api.Initialize(654), 'false', 'REQ_3.2: Initializing the API with a parameter that is not a string fails.'); + equal(api.GetLastError(), '201', 'REQ_3.2: Initializing the API with a parameter that is not a string gives a 201 error.'); + equal(api.Initialize(null), 'false', 'REQ_3.2: Initializing the API with a parameter that is not a string fails.'); + equal(api.GetLastError(), '201', 'REQ_3.2: Initializing the API with a parameter that is not a string gives a 201 error.'); + equal(api.Initialize(''), 'true', 'REQ_4.1.1, REQ_4.5: Initializing the API with an empty string succeeds.'); + equal(api.GetLastError(), '0', 'REQ_4.5: Initializing the API with an empty string gives no error.'); + equal(api.Initialize(''), 'false', 'REQ_4.3: Initializing the API twice fails.'); + equal(api.GetLastError(), '103', 'REQ_4.3: Initializing the API twice gives a 103 error.'); + // Terminate the communication. + api.Terminate(''); + equal(api.Initialize(''), 'false', 'REQ_4.4: Initializing the API after termination fails.'); + equal(api.GetLastError(), '104', 'REQ_4.4: Initializing the API after termination gives a 104 error.'); + + + module('API::Terminate()'); + // @todo Missing specs for REQ_5.2.1m REQ_5.2.1.1 and REQ_5.3. + api = new OpignoScorm2004API(); + equal(api.Terminate(''), 'false', 'REQ_5.4: Terminating the API before initializing fails.'); + equal(api.GetLastError(), '112', 'REQ_5.4: Terminating the API before initializing gives a 112 error.'); + // Initialize the communication. + api.Initialize(''); + equal(api.Terminate(), 'false', 'REQ_3.2: Terminating the API without a parameter fails.'); + equal(api.GetLastError(), '201', 'REQ_3.2: Terminating the API without a parameter gives a 201 error.'); + equal(api.Terminate('any string'), 'false', 'REQ_3.2: Terminating the API with any string fails.'); + equal(api.GetLastError(), '201', 'REQ_3.2: Terminating the API with any string gives a 201 error.'); + equal(api.Terminate(654), 'false', 'REQ_3.2: Terminating the API with a parameter that is not a string fails.'); + equal(api.GetLastError(), '201', 'REQ_3.2: Terminating the API with a parameter that is not a string gives a 201 error.'); + equal(api.Terminate(null), 'false', 'REQ_3.2: Terminating the API with a parameter that is not a string fails.'); + equal(api.GetLastError(), '201', 'REQ_3.2: Terminating the API with a parameter that is not a string gives a 201 error.'); + equal(api.Terminate(''), 'true', 'REQ_5.1.1, REQ_5.2: Terminating the API with an empty string succeeds.'); + equal(api.GetLastError(), '0', 'REQ_5.2: Terminating the API with an empty string gives no error.'); + equal(api.Terminate(''), 'false', 'REQ_5.5: Terminating the API twice.'); + equal(api.GetLastError(), '113', 'REQ_5.5: Terminating the API twice gives a 113 error.'); + + + module('API::GetValue()'); + // @todo Missing specs for REQ_6.5 and REQ_6.7. + api = new OpignoScorm2004API(); + equal(api.GetValue(''), '', 'REQ_6.8: Requesting a value before initializing fails.'); + equal(api.GetLastError(), '122', 'REQ_6.8: Requesting a value before initializing gives a 122 error.'); + // Initialize the communication. + api.Initialize(''); + equal(api.GetValue('cmi.__value__'), 'value', 'REQ_6.2: Requesting a recognized value succeeds.'); + equal(api.GetLastError(), '0', 'REQ_6.2: Requesting a recognized value gives no error.'); + equal(api.GetValue('cmi.__unknown__'), '', 'REQ_6.3: Requesting an unknown value fails.'); + equal(api.GetLastError(), '401', 'REQ_6.3: Requesting an unknown value gives a 401 error.'); + equal(api.GetValue('__unknown__'), '', 'REQ_6.3: Requesting an unknown value fails.'); + equal(api.GetLastError(), '401', 'REQ_6.3: Requesting an unknown value gives a 401 error.'); + equal(api.GetValue('cmi.__unimplemented__'), '', 'REQ_6.4: Requesting an unimplemented value fails.'); + equal(api.GetLastError(), '402', 'REQ_6.4: Requesting an unimplemented value gives a 402 error.'); + equal(api.GetValue('cmi.__write_only__'), '', 'REQ_6.6: Requesting a write-only value fails.'); + equal(api.GetLastError(), '405', 'REQ_6.6: Requesting an write-only value gives a 405 error.'); + equal(api.GetValue(''), '', 'REQ_6.10: Requesting an empty string key fails.'); + equal(api.GetLastError(), '301', 'REQ_6.10: Requesting an empty string key gives a 301 error.'); + equal(api.GetValue(), '', 'REQ_6.10: Requesting a "null" key fails.'); + equal(api.GetLastError(), '301', 'REQ_6.10: Requesting a "null" key gives a 301 error.'); + equal(api.GetValue('cmi.__test__.1.child'), '', 'REQ_6.11: Requesting an nonexistent child fails.'); + equal(api.GetLastError(), '301', 'REQ_6.11: Requesting an nonexistent child gives a 301 error.'); + equal(api.GetValue('cmi.__test__._count'), '', 'REQ_6.12: Requesting the count property of an element that is not an array fails.'); + equal(api.GetLastError(), '301', 'REQ_6.12: Requesting the count property of an element that is not an array gives a 301 error.'); + // Set some array value to test array properties. + api.SetValue('cmi.__test__.0.child', 'value'); + equal(api.GetValue('cmi.__test__.0.child'), 'value', 'REQ_6.2: Requesting a recognized child value succeeds.'); + equal(api.GetLastError(), '0', 'REQ_6.2: Requesting a recognized child value gives no error.'); + equal(api.GetValue('cmi.__test__._count'), 1, 'REQ_6.12: Requesting the count property of an element that is an array succeeds.'); + equal(api.GetLastError(), '0', 'REQ_6.12: Requesting the count property of an element that is an array gives no error.'); + // Terminate the communication. + api.Terminate(''); + equal(api.GetValue(''), '', 'REQ_6.9: Requesting a value after termination fails.'); + equal(api.GetLastError(), '123', 'REQ_6.9: Requesting a value after termination gives a 123 error.'); + + + module('API::SetValue()'); + // @todo Missing specs for REQ_7.7, REQ_7.8, REQ_7.11, REQ_7.12, REQ_7.15. + api = new OpignoScorm2004API(); + equal(api.SetValue('', ''), 'false', 'REQ_7.9: Setting a value before initializing fails.'); + equal(api.GetLastError(), '132', 'REQ_7.8: Setting a value before initializing gives a 132 error.'); + // Initialize the communication. + api.Initialize(''); + equal(api.SetValue('cmi.__value__', 'value'), 'true', 'REQ_7.2: Setting a recognized value succeeds.'); + equal(api.GetLastError(), '0', 'REQ_7.2: Setting a recognized value gives no error.'); + equal(api.SetValue('cmi.__unknown__', 'value'), 'false', 'REQ_7.3: Setting an unknown value fails.'); + equal(api.GetLastError(), '401', 'REQ_7.3: Setting an unknown value gives a 401 error.'); + equal(api.SetValue('__unknown__', 'value'), 'false', 'REQ_7.3: Setting an unknown value fails.'); + equal(api.GetLastError(), '401', 'REQ_7.3: Setting an unknown value gives a 401 error.'); + equal(api.SetValue('cmi.__unimplemented__', 'value'), 'false', 'REQ_7.4: Setting an unimplemented value fails.'); + equal(api.GetLastError(), '402', 'REQ_7.4: Setting an unimplemented value gives a 402 error.'); + equal(api.SetValue('cmi.__read_only__', 'value'), 'false', 'REQ_7.5: Setting a read-only value fails.'); + equal(api.GetLastError(), '404', 'REQ_7.5: Setting a read-only value gives a 404 error.'); + equal(api.SetValue('cmi.__test__._count', 2), 'false', 'REQ_7.5: Setting a read-only value fails.'); + equal(api.GetLastError(), '404', 'REQ_7.5: Setting a read-only value gives a 404 error.'); + equal(api.SetValue('cmi.__value__', { key: 'value' }), 'false', 'REQ_7.6: Setting a non-string value fails.'); + equal(api.GetLastError(), '406', 'REQ_7.6: Setting a non-string value gives a 406 error.'); + equal(api.SetValue('', 'value'), 'false', 'REQ_7.13: Providing an empty string path fails.'); + equal(api.GetLastError(), '351', 'REQ_7.13: Providing an empty string path gives a 351 error.'); + equal(api.SetValue(null, 'value'), 'false', 'REQ_7.13: Providing an non-string path fails.'); + equal(api.GetLastError(), '351', 'REQ_7.13: Providing an non-string path gives a 351 error.'); + equal(api.SetValue(89, 'value'), 'false', 'REQ_7.13: Providing an non-string path fails.'); + equal(api.GetLastError(), '351', 'REQ_7.13: Providing an non-string path gives a 351 error.'); + equal(api.SetValue('cmi.__test__.0.child', 'value'), 'true', 'REQ_7.2: Setting a valid array value succeeds.'); + equal(api.GetLastError(), '0', 'REQ_7.2: Setting a valid array value gives no error.'); + equal(api.SetValue('cmi.__test__.2.child', 'value'), 'false', 'REQ_7.14: Setting an array key that is out of bounds fails.'); + equal(api.GetLastError(), '351', 'REQ_7.14: Setting an array key that is out of bounds gives a 351 error.'); + // Terminate the communication. + api.Terminate(''); + equal(api.SetValue('cmi.__value__', 'value'), 'false', 'REQ_7.10: Setting a value after termination fails.'); + equal(api.GetLastError(), '133', 'REQ_7.10: Setting a value after termination gives a 123 error.'); + + + module('API::Commit()'); + // @todo Missing specs for REQ_8.2.1, REQ_8.3. + api = new OpignoScorm2004API(); + equal(api.Commit(''), 'false', 'REQ_8.4: Committing the API before initializing fails.'); + equal(api.GetLastError(), '142', 'REQ_8.4: Committing the API before initializing gives a 142 error.'); + // Initialize the communication. + api.Initialize(''); + equal(api.Commit(), 'false', 'REQ_3.2: Committing the API without a parameter fails.'); + equal(api.GetLastError(), '201', 'REQ_3.2: Committing the API without a parameter gives a 201 error.'); + equal(api.Commit('any string'), 'false', 'REQ_3.2: Committing the API with any string fails.'); + equal(api.GetLastError(), '201', 'REQ_3.2: Committing the API with any string gives a 201 error.'); + equal(api.Commit(123), 'false', 'REQ_3.2: Committing the API with a parameter that is not a string fails.'); + equal(api.GetLastError(), '201', 'REQ_3.2: Committing the API with a parameter that is not a string gives a 201 error.'); + equal(api.Commit(null), 'false', 'REQ_3.2: Committing the API with a parameter that is not a string fails.'); + equal(api.GetLastError(), '201', 'REQ_3.2: Committing the API with a parameter that is not a string gives a 201 error.'); + equal(api.Commit(''), 'true', 'REQ_8.2: Committing the API after initializing succeeds.'); + equal(api.GetLastError(), '0', 'REQ_8.2: Committing the API after initializing gives no error.'); + // Terminate the communication. + api.Terminate(''); + equal(api.Commit(''), 'false', 'REQ_8.5: Committing the API after termination fails.'); + equal(api.GetLastError(), '143', 'REQ_8.5: Committing the API after termination gives a 143 error.'); + + + module('RTE Data Model Conformance'); + api = new OpignoScorm2004API(); + api.Initialize(''); + + // REQ_55 + equal(api.GetValue('cmi._version'), '1.0', 'REQ_55.1, REQ_55.2, REQ_55.3: Requesting cmi._version succeeds and returns "1.0".'); +/* + // REQ_57 + equal(api.GetValue('cmi.comments_from_learner._children'), 'comment,location,timestamp', + 'REQ_57.1, REQ_57.1.2, REQ_75.1.3: Requesting cmi.comments_from_learner._children succeeds and returns a list of properties.'); + equal(api.SetValue('cmi.comments_from_learner._children', 'value'), 'false', + 'REQ_57.1.1: cmi.comments_from_learner._children is read-only.'); + equal(api.GetLastError(), '404', + 'REQ_57.1.1: cmi.comments_from_learner._children is read-only.'); + equal(api.GetValue('cmi.comments_from_learner._count'), 0, + 'REQ_57.2, REQ_57.1.2, REQ_75.2.3: Requesting cmi.comments_from_learner._children succeeds and returns a list of properties.'); + equal(api.SetValue('cmi.comments_from_learner._count', 'value'), 'false', + 'REQ_57.2.1: cmi.comments_from_learner._count is read-only.'); + equal(api.GetLastError(), '404', + 'REQ_57.2.1: cmi.comments_from_learner._count is read-only.'); + + // REQ_72 + equal(api.GetValue('cmi.objectives._children'), 'id,score,success_status,completion_status,progress_measure,description', + 'REQ_72.1: Requesting cmi.objectives._children succeeds and returns "id,score,success_status,completion_status,progress_measure,description".'); + equal(api.GetValue('cmi.objectives._children'), 'id,score,success_status,completion_status,progress_measure,description', + 'REQ_72.1.1, REQ_72.1.3: Requesting cmi.objectives._children succeeds and returns "id,score,success_status,completion_status,progress_measure,description".'); + equal(api.SetValue('cmi.objectives._children', 'value'), 'false', + 'REQ_72.1.2: cmi.objectives._children is read only.'); + equal(api.GetValue('cmi.objectives._count'), 0, + 'REQ_72.2, REQ_72.2.3: Requesting cmi.objectives._count succeeds and returns the number of objectives.'); + equal(api.SetValue('cmi.objectives._count', 1), 'false', + 'REQ_72.2.1: cmi.objectives._count is read-only.'); + + // Set some objectives. + api.data.cmi.objectives.push({ + id: 'primary_obj', + score: {}, + success_status: 'failed', + completion_status: 'completed', + progress_measure: '', + description: '' + }); + api.data.cmi.objectives.push({ + id: 'secondary_obj', + score: {}, + success_status: 'passed', + completion_status: 'completed', + progress_measure: '', + description: '' + }); + + equal(api.GetValue('cmi.objectives.0.id'), 'primary_obj', + 'REQ_72.3, REQ_72.3.1: Requesting cmi.objectives.n.id succeeds and returns the identifier.'); + equal(api.GetValue('cmi.objectives.1.id'), 'secondary_obj', + 'REQ_72.3, REQ_72.3.1: Requesting cmi.objectives.n.id succeeds and returns the identifier.'); + equal(api.SetValue('cmi.objectives.0.id', 'new id'), 'true', + 'REQ_72.3.1: cmi.objectives.n.id is also writable.'); + equal(api.GetValue('cmi.objectives.0.id'), 'new id', + 'REQ_72.3.1: cmi.objectives.n.id is also writable.');*/ + + + + } + }; + +})(jQuery, Drupal, window, OpignoScorm2004API); diff --git a/modules/scorm/opigno_scorm.api.php b/modules/scorm/opigno_scorm.api.php new file mode 100755 index 0000000..7f49d77 --- /dev/null +++ b/modules/scorm/opigno_scorm.api.php @@ -0,0 +1,116 @@ +first_name} {$profile->last_name}"; + } +} + +/** + * Implements hook_opigno_scorm_cmi_set_alter(). + * + * This hook allows modules to alter CMI data that is about to + * be stored. If the module wants to take over the persistence + * entirely, it can set the value to NULL. Opigno Scorm will then + * skip the persisting of the data and assume some other module + * took over. + * + * @param mixed $value + * The data to be persisted. + * @param string $cmi_key + * The CMI data indentifier for the value. + * @param array $context + * Context array, with at least the following keys: + * - uid: The user ID + * - scorm_id: The SCORM ID. + * - original_value: The original value, in case some + * module alters it. + */ +function hook_opigno_scorm_scorm_cmi_set_alter(&$value, $cmi_key, $context) { + if ($cmi_key === 'cmi.last_location') { + // Store this somewhere else. + // db_insert(...) + + // Let Opigno Scorm know we stored this. + $value = NULL; + } +} + +/** + * Implements hook_opigno_scorm_cmi_get_alter(). + * + * This hook allows modules to alter CMI data that is about to + * be sent back to the SCORM. + * + * @param mixed $value + * The data that will be returned. Can be NULL if not + * previously persisted in the database. + * @param string $cmi_key + * The CMI data indentifier for the value. + * @param array $context + * Context array, with at least the following keys: + * - uid: The user ID + * - scorm_id: The SCORM ID. + * - original_value: The original value, in case some + * module alters it. + */ +function hook_opigno_scorm_scorm_cmi_get_alter(&$value, $cmi_key, $context) { + if ($cmi_key === 'cmi.learner_name') { + $profile = profile_load($context['uid']); + $value = "{$profile->first_name} {$profile->last_name}"; + } +} diff --git a/modules/scorm/opigno_scorm.info b/modules/scorm/opigno_scorm.info new file mode 100644 index 0000000..8fa7178 --- /dev/null +++ b/modules/scorm/opigno_scorm.info @@ -0,0 +1,12 @@ +name = Opigno SCORM API +description = SCORM API for Opigno +core = 7.x +package = Opigno + +files[] = includes/XML2Array.php +; Information added by Drupal.org packaging script on 2014-10-19 +version = "7.x-1.9" +core = "7.x" +project = "opigno" +datestamp = "1413751430" + diff --git a/modules/scorm/opigno_scorm.install b/modules/scorm/opigno_scorm.install new file mode 100755 index 0000000..f34b194 --- /dev/null +++ b/modules/scorm/opigno_scorm.install @@ -0,0 +1,208 @@ + array( + 'description' => 'Uploaded SCORM packages.', + 'fields' => array( + 'id' => array( + 'type' => 'serial', + ), + 'fid' => array( + 'description' => 'The managed file ID that references the SCORM package (ZIP file).', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'extracted_dir' => array( + 'descripition' => 'The location where the SCORM was extracted.', + 'type' => 'text', + ), + 'manifest_file' => array( + 'description' => 'The location of the manifest file.', + 'type' => 'text', + ), + 'manifest_id' => array( + 'type' => 'text', + ), + 'metadata' => array( + 'description' => 'The serialized meta data of the manifest file.', + 'type' => 'text', + 'size' => 'medium', + ), + ), + 'primary key' => array('id'), + 'indexes' => array('fid' => array('fid')), + 'foreign keys' => array( + 'file_managed' => array('fid' => 'fid'), + ), + ), + 'opigno_scorm_package_scos' => array( + 'description' => 'Uploaded SCORM package SCO items.', + 'fields' => array( + 'id' => array( + 'type' => 'serial', + ), + 'scorm_id' => array( + 'description' => 'The SCORM package ID.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'organization' => array( + 'descripition' => 'The SCO organization.', + 'type' => 'text', + ), + 'identifier' => array( + 'descripition' => 'The SCO item identifier.', + 'type' => 'text', + ), + 'parent_identifier' => array( + 'descripition' => 'The SCO item parent identifier. Equals 0 if at the root of the tree.', + 'type' => 'text', + ), + 'launch' => array( + 'descripition' => 'The SCO item launch URL, if any.', + 'type' => 'text', + ), + 'type' => array( + 'descripition' => 'The SCO item internal type.', + 'type' => 'text', + ), + 'scorm_type' => array( + 'descripition' => 'The SCO item SCORM compliant type.', + 'type' => 'text', + ), + 'title' => array( + 'descripition' => 'The SCO item title.', + 'type' => 'text', + ), + 'weight' => array( + 'descripition' => 'The SCO item weight. The heavier the weight, the later it will show up in a navigation tree.', + 'type' => 'int', + 'default' => 0, + ), + ), + 'primary key' => array('id'), + 'indexes' => array('scorm_id' => array('scorm_id')), + 'foreign keys' => array( + 'opigno_scorm_packages' => array('scorm_id' => 'id'), + ), + ), + 'opigno_scorm_package_sco_attributes' => array( + 'description' => 'Uploaded SCORM package SCO item attributes.', + 'fields' => array( + 'id' => array( + 'type' => 'serial', + ), + 'sco_id' => array( + 'description' => 'The SCORM item ID.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'attribute' => array( + 'type' => 'text', + ), + 'value' => array( + 'type' => 'text', + ), + 'serialized' => array( + 'type' => 'int', + 'default' => 0, + ), + ), + 'primary key' => array('id'), + 'indexes' => array('sco_id' => array('sco_id')), + 'foreign keys' => array( + 'opigno_scorm_package_scos' => array('sco_id' => 'id'), + ), + ), + 'opigno_scorm_scorm_cmi_data' => array( + 'description' => 'SCORM package SCORM CMI data attributes.', + 'fields' => array( + 'uid' => array( + 'description' => 'The user ID this data belongs to.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'scorm_id' => array( + 'description' => 'The SCORM ID.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'cmi_key' => array( + 'description' => 'The CMI data key string.', + 'type' => 'varchar', + 'length' => 255, + ), + 'value' => array( + 'type' => 'text', + ), + 'serialized' => array( + 'type' => 'int', + 'default' => 0, + ), + ), + 'primary key' => array('uid', 'scorm_id', 'cmi_key'), + 'indexes' => array( + 'scorm_id' => array('scorm_id'), + 'uid' => array('uid'), + 'cmi_key' => array('cmi_key') + ), + 'foreign keys' => array( + 'opigno_scorm_packages' => array('scorm_id' => 'id'), + 'users' => array('uid' => 'uid'), + ), + ), + 'opigno_scorm_sco_cmi_data' => array( + 'description' => 'SCORM package SCO CMI data attributes.', + 'fields' => array( + 'uid' => array( + 'description' => 'The user ID this data belongs to.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'sco_id' => array( + 'description' => 'The SCORM item ID.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'cmi_key' => array( + 'description' => 'The CMI data key string.', + 'type' => 'varchar', + 'length' => 255, + ), + 'value' => array( + 'type' => 'text', + ), + 'serialized' => array( + 'type' => 'int', + 'default' => 0, + ), + ), + 'primary key' => array('uid', 'sco_id', 'cmi_key'), + 'indexes' => array( + 'sco_id' => array('sco_id'), + 'uid' => array('uid'), + 'cmi_key' => array('cmi_key') + ), + 'foreign keys' => array( + 'opigno_scorm_package_scos' => array('sco_id' => 'id'), + 'users' => array('uid' => 'uid'), + ), + ), + ); +} \ No newline at end of file diff --git a/modules/scorm/opigno_scorm.module b/modules/scorm/opigno_scorm.module new file mode 100755 index 0000000..43e6c55 --- /dev/null +++ b/modules/scorm/opigno_scorm.module @@ -0,0 +1,461 @@ + array( + 'page callback' => 'opigno_scorm_ajax_sco_cmi', + 'page arguments' => array(4, 6, 7), + 'access callback' => 'opigno_scorm_access', + 'access arguments' => array(2), + 'file' => 'includes/opigno_scorm.ajax.inc', + 'type' => MENU_CALLBACK, + ), + ); +} + +/** + * Implements hook_library_alter(). + */ +function opigno_scorm_library_alter(&$libraries, $module) { + // Add available JavaScript tests and dependencies. + if ($module == 'qunit') { + $path = drupal_get_path('module', 'opigno_scorm'); + $libraries['qunit']['js'][$path . '/js/lib/api-2004.js'] = array(); + $libraries['qunit']['js'][$path . '/js/tests/api-2004.js'] = array(); + } +} + +/** + * Access callback: verify access to the SCORM object. + * + * Invokes hook_opigno_scorm_access() to verify access. If any module + * GRANTS access, returns true, even if some other module DENIES access. + * + * @param object $scorm + * + * @return bool + */ +function opigno_scorm_access($scorm) { + $access = NULL; + foreach (module_implements('opigno_scorm_access') as $module) { + $result = module_invoke($module, 'opigno_scorm_access', $scorm); + // Does the module have something to say about this SCORM package ? + // If it is NULL, skip. If it is set, check it. + if (isset($result)) { + // Access granted, end here. + if ($result) { + return TRUE; + } + else { + // Access denied, but someone might still grant access. + // Continue. + $access = $result; + } + } + } + + // If no-one had anything to say about granting access, return 'access content'. + // Else, return the access result. + if (isset($access)) { + return $access; + } + else { + return user_access('access content'); + } + +} + +/** + * Save a SCORM package information. + * + * @param object $scorm + * + * @return bool + */ +function opigno_scorm_scorm_save($scorm) { + if (!empty($scorm->id)) { + return db_update('opigno_scorm_packages') + ->fields((array) $scorm) + ->condition('id', $scorm->id) + ->execute(); + } + else { + $id = db_insert('opigno_scorm_packages') + ->fields((array) $scorm) + ->execute(); + + $scorm->id = $id; + + return !!$id; + } +} + +/** + * Load a SCORM package information. + * + * @param int $id + * + * @return object|false + */ +function opigno_scorm_scorm_load($id) { + return db_select('opigno_scorm_packages', 'o') + ->fields('o', array()) + ->condition('id', $id) + ->execute() + ->fetchObject(); +} + +/** + * Load SCORM package information by file ID. + * + * @param int $fid + * + * @return object|false + */ +function opigno_scorm_scorm_load_by_fid($fid) { + return db_select('opigno_scorm_packages', 'o') + ->fields('o', array()) + ->condition('fid', $fid) + ->execute() + ->fetchObject(); +} + +/** + * Delete a SCORM package information. + * + * @todo Delete all SCOs as well. + * + * @param object $scorm + */ +function opigno_scorm_scorm_delete($scorm) { + db_delete('opigno_scorm_packages') + ->condition('id', $scorm->id) + ->execute(); +} + +/** + * Save a SCO information. + * + * @param object $sco + * + * @return bool + */ +function opigno_scorm_sco_save($sco) { + // The attributes is not a field in the database, but + // a representation of a table relationship. + // Cache them here and unset the property for the + // DB query. + if (isset($sco->attributes)) { + $attributes = $sco->attributes; + unset($sco->attributes); + } + + if (!empty($sco->id)) { + $res = db_update('opigno_scorm_package_scos') + ->fields((array) $sco) + ->condition('id', $sco->id) + ->execute(); + } + else { + $id = db_insert('opigno_scorm_package_scos') + ->fields((array) $sco) + ->execute(); + + $sco->id = $id; + + $res = !!$id; + } + + if ($res && !empty($attributes)) { + // Remove all old attributes, to prevent duplicates. + db_delete('opigno_scorm_package_sco_attributes') + ->condition('sco_id', $sco->id) + ->execute(); + + foreach ($attributes as $key => $value) { + $serialized = 0; + if (is_array($value) || is_object($value)) { + $value = serialize($value); + $serialized = 1; + } + elseif (is_bool($value)) { + $value = (int) $value; + } + + db_insert('opigno_scorm_package_sco_attributes') + ->fields(array( + 'sco_id' => $sco->id, + 'attribute' => $key, + 'value' => $value, + 'serialized' => $serialized, + )) + ->execute(); + } + } + + return $res; +} + +/** + * Load a SCO information. + * + * @param int $id + * + * @return object|false + */ +function opigno_scorm_sco_load($id) { + $sco = db_select('opigno_scorm_package_scos', 'o') + ->fields('o', array()) + ->condition('id', $id) + ->execute() + ->fetchObject(); + + if ($sco) { + $sco->attributes = _opigno_scorm_sco_load_attributes($sco->id); + } + + return $sco; +} + +/** + * Load a SCO information by SCORM id. + * + * @param int $scorm_id + * + * @return object|false + */ +function opigno_scorm_sco_load_by_scorm_id($scorm_id) { + $sco = db_select('opigno_scorm_package_scos', 'o') + ->fields('o', array()) + ->condition('scorm_id', $scorm_id) + ->execute() + ->fetchObject(); + + if ($sco) { + $sco->attributes = _opigno_scorm_sco_load_attributes($sco->id); + } + + return $sco; +} + +/** + * Helper function to load a SCO attributes. + * + * @param int $sco_id + * + * @return array + */ +function _opigno_scorm_sco_load_attributes($sco_id) { + $attributes = array(); + + $result = db_select('opigno_scorm_package_sco_attributes', 'o') + ->fields('o', array('attribute', 'value', 'serialized')) + ->condition('sco_id', $sco_id) + ->execute(); + + while ($row = $result->fetchObject()) { + $attributes[$row->attribute] = !empty($row->serialized) ? unserialize($row->value) : $row->value; + } + + return $attributes; +} + +/** + * Delete a SCO package information. + * + * @param object $sco + */ +function opigno_scorm_sco_delete($sco) { + db_delete('opigno_scorm_package_scos') + ->condition('id', $sco->id) + ->execute(); + + // Remove all old attributes, to prevent duplicates. + db_delete('opigno_scorm_package_sco_attributes') + ->condition('sco_id', $sco->id) + ->execute(); +} + +/** + * Set a CMI data value for the given SCORM. + * + * @param int $uid + * @param int $scorm_id + * @param string $cmi_key + * @param string $value + * + * @return bool + */ +function opigno_scorm_scorm_cmi_set($uid, $scorm_id, $cmi_key, $value) { + // Allow modules to alter the value. If the $value is set to NULL, it is assumed + // a module takes over the persisting of the data and the insertion query + // will be skipped. + $context = array( + 'uid' => $uid, + 'scorm_id' => $scorm_id, + 'original_value' => $value, + ); + drupal_alter('opigno_scorm_scorm_cmi_set', $value, $cmi_key, $context); + + if (isset($value)) { + $serialized = 0; + if (is_array($value) || is_object($value)) { + $value = serialize($value); + $serialized = 1; + } + elseif (is_bool($value)) { + $value = (int) $value; + } + + $result = db_merge('opigno_scorm_scorm_cmi_data') + ->key(array( + 'uid' => $uid, + 'scorm_id' => $scorm_id, + 'cmi_key' => $cmi_key, + )) + ->fields(array( + 'uid' => $uid, + 'scorm_id' => $scorm_id, + 'cmi_key' => $cmi_key, + 'value' => $value, + 'serialized' => $serialized, + )) + ->execute(); + + return !!$result; + } + else { + return TRUE; + } +} + +/** + * Get a CMI data value for the given SCORM. + * + * @param int $uid + * @param int $scorm_id + * @param string $cmi_key + * + * @return mixed|null + */ +function opigno_scorm_scorm_cmi_get($uid, $scorm_id, $cmi_key) { + $data = NULL; + $result = db_select('opigno_scorm_scorm_cmi_data', 'o') + ->fields('o', array('value', 'serialized')) + ->condition('o.uid', $uid) + ->condition('o.scorm_id', $scorm_id) + ->condition('o.cmi_key', $cmi_key) + ->execute() + ->fetchObject(); + + if (isset($result->value)) { + $data = !empty($result->serialized) ? unserialize($result->value) : $result->value; + } + + // Allow modules to alter the data (or even set it if it doesn't exist). + $context = array( + 'uid' => $uid, + 'scorm_id' => $scorm_id, + 'original_value' => $data, + ); + drupal_alter('opigno_scorm_scorm_cmi_get', $data, $cmi_key, $context); + + return $data; +} + +/** + * Set a CMI data value for the given SCO. + * + * @param int $uid + * @param int $sco_id + * @param string $cmi_key + * @param string $value + * + * @return bool + */ +function opigno_scorm_sco_cmi_set($uid, $sco_id, $cmi_key, $value) { + // Allow modules to alter the value. If the $value is set to NULL, it is assumed + // a module takes over the persisting of the data and the insertion query + // will be skipped. + $context = array( + 'uid' => $uid, + 'sco_id' => $sco_id, + 'original_value' => $value, + ); + drupal_alter('opigno_scorm_sco_cmi_set', $value, $cmi_key, $context); + + if (isset($value)) { + $serialized = 0; + if (is_array($value) || is_object($value)) { + $value = serialize($value); + $serialized = 1; + } + elseif (is_bool($value)) { + $value = (int) $value; + } + + $result = db_merge('opigno_scorm_sco_cmi_data') + ->key(array( + 'uid' => $uid, + 'sco_id' => $sco_id, + 'cmi_key' => $cmi_key, + )) + ->fields(array( + 'uid' => $uid, + 'sco_id' => $sco_id, + 'cmi_key' => $cmi_key, + 'value' => $value, + 'serialized' => $serialized, + )) + ->execute(); + + return !!$result; + } + else { + return TRUE; + } +} + +/** + * Get a CMI data value for the given SCO. + * + * @param int $uid + * @param int $sco_id + * @param string $cmi_key + * + * @return mixed|null + */ +function opigno_scorm_sco_cmi_get($uid, $sco_id, $cmi_key) { + $data = NULL; + $result = db_select('opigno_scorm_sco_cmi_data', 'o') + ->fields('o', array('value', 'serialized')) + ->condition('o.uid', $uid) + ->condition('o.sco_id', $sco_id) + ->condition('o.cmi_key', $cmi_key) + ->execute() + ->fetchObject(); + + if (isset($result->value)) { + $data = !empty($result->serialized) ? unserialize($result->value) : $result->value; + } + + // Allow modules to alter the data (or even set it if it doesn't exist). + $context = array( + 'uid' => $uid, + 'sco_id' => $sco_id, + 'original_value' => $data, + ); + drupal_alter('opigno_scorm_sco_cmi_get', $data, $cmi_key, $context); + + return $data; +} diff --git a/modules/scorm/quiz/includes/opigno_scorm_quiz.question.inc b/modules/scorm/quiz/includes/opigno_scorm_quiz.question.inc new file mode 100644 index 0000000..40ab72a --- /dev/null +++ b/modules/scorm/quiz/includes/opigno_scorm_quiz.question.inc @@ -0,0 +1,51 @@ + array( + '#type' => 'hidden', + '#default_value' => 1, + ), + ); + } + + /** + * @copydoc QuizQuestion::getMaximumScore() + */ + public function getMaximumScore() { + return variable_get('opigno_scorm_quiz_max_score', 50); + } + + /** + * @copydoc QuizQuestion::saveNodeProperties() + */ + public function saveNodeProperties($is_new = FALSE) { + // No properties to save. + } + +} diff --git a/modules/scorm/quiz/includes/opigno_scorm_quiz.response.inc b/modules/scorm/quiz/includes/opigno_scorm_quiz.response.inc new file mode 100755 index 0000000..5169dc3 --- /dev/null +++ b/modules/scorm/quiz/includes/opigno_scorm_quiz.response.inc @@ -0,0 +1,148 @@ +question->opigno_scorm_package[LANGUAGE_NONE][0]['fid'])) { + $scorm = opigno_scorm_scorm_load_by_fid($this->question->opigno_scorm_package[LANGUAGE_NONE][0]['fid']); + + // We get the latest result. The way the SCORM API works always overwrites attempts + // for the global CMI storage. The result stored is always the latest. Get it, + // and presist it again in the user results table so we can track results through + // time. + module_load_include('module', 'opigno_scorm_ui'); + $scaled = opigno_scorm_ui_scorm_cmi_get($user->uid, $scorm->id, 'cmi.score.scaled', ''); + } + + $completion=opigno_scorm_ui_scorm_cmi_get($user->uid, $scorm->id, 'cmi.completion_status', ''); + $raw=opigno_scorm_ui_scorm_cmi_get($user->uid, $scorm->id, 'cmi.score.raw', ''); + $max=opigno_scorm_ui_scorm_cmi_get($user->uid, $scorm->id, 'cmi.score.max', ''); + $success_status=opigno_scorm_ui_scorm_cmi_get($user->uid, $scorm->id, 'cmi.success_status', ''); + + + if (empty($completion)) { + $scaled = 0; + } + if (($completion == "completed") && (empty($raw)) && (!is_numeric($raw))) { + $scaled = 1; + } + if (($completion == "incomplete") && (empty($raw)) && (!is_numeric($raw))) { + $scaled = 0; + } + + // Something went wrong. Set a score of -1. + if (!isset($scaled) || !is_numeric($scaled)) { + $scaled = -1; + } + + db_insert('opigno_scorm_quiz_user_results') + ->fields(array( + 'question_nid' => $this->question->nid, + 'question_vid' => $this->question->vid, + 'result_id' => $this->rid, + 'score_scaled' => $scaled, + )) + ->execute(); + } + + /** + * @copydoc QuizQuestionResponse::delete() + */ + public function delete() { + db_delete('opigno_scorm_quiz_user_results') + ->condition('question_nid', $this->question->nid) + ->condition('question_vid', $this->question->vid) + ->condition('result_id', $this->rid) + ->execute(); + } + + /** + * @copydoc QuizQuestionResponse::delete() + */ + public function score() { + $scaled = db_select('opigno_scorm_quiz_user_results', 'o') + ->fields('o', array('score_scaled')) + ->condition('question_nid', $this->question->nid) + ->condition('question_vid', $this->question->vid) + ->condition('result_id', $this->rid) + ->execute() + ->fetchField(); + + $round_method = !empty($this->question->opigno_scorm_round_method[LANGUAGE_NONE][0]['value']) ? $this->question->opigno_scorm_round_method[LANGUAGE_NONE][0]['value'] : 'round'; + + $scaled *= $this->getMaxScore(); + + switch ($round_method) { + case 'ceil': + $scaled = ceil($scaled); + break; + + case 'floor': + $scaled = floor($scaled); + break; + + default: + $scaled = round($scaled); + break; + } + + return $scaled; + } + + /** + * @copydoc QuizQuestionResponse::getResponse() + */ + public function getResponse() { + return 'In SCORM package'; + } + + /** + * @copydoc QuizQuestionResponse::getReportFormResponse() + */ + /*public function getReportFormResponse($showpoints = TRUE, $showfeedback = TRUE, $allow_scoring = FALSE) { + if (empty($this->question->answers)) { + return array( + '#markup' => t('Missing question.'), + ); + } + + + // @todo ...... + return array( + '#markup' => 'SCORE', + ); + }*/ + + /*public function getReportFormScore($showfeedback = TRUE, $showpoints = TRUE, $allow_scoring = FALSE) { + return array( + '#markup' => $this->score(), + ); + }*/ + + /** + * Implementation of getReportForm + * + * @see QuizQuestionResponse#getReportForm($showpoints, $showfeedback, $allow_scoring) + */ + public function getReportForm($showpoints = TRUE, $showfeedback = TRUE, $allow_scoring = FALSE) { + return array( + '#no_report' => TRUE, + ); + } + + + public function getReportFormAnswerFeedback($showpoints = TRUE, $showfeedback = TRUE, $allow_scoring = FALSE) { + return FALSE; + } + +} diff --git a/modules/scorm/quiz/js/opigno_scorm_quiz.js b/modules/scorm/quiz/js/opigno_scorm_quiz.js new file mode 100755 index 0000000..4b5c38b --- /dev/null +++ b/modules/scorm/quiz/js/opigno_scorm_quiz.js @@ -0,0 +1,22 @@ +/** + * @file + * JS Quiz logic for SCORM player. + */ + +;(function($, Drupal, window, undefined) { + + Drupal.behaviors.opignoScormQuiz = { + + attach: function(context, settings) { + if (window.API_1484_11 !== undefined) { + try { + // Add '_children' properties, as we cannot set them server-side through PHP. + window.API_1484_11.data.cmi.objectives._children = 'id,score,success_status,completion_status,progress_measure,description'; + } + catch (e) { } + } + } + + }; + +})(jQuery, Drupal, window); \ No newline at end of file diff --git a/modules/scorm/quiz/opigno_scorm_quiz.info b/modules/scorm/quiz/opigno_scorm_quiz.info new file mode 100644 index 0000000..a54cf81 --- /dev/null +++ b/modules/scorm/quiz/opigno_scorm_quiz.info @@ -0,0 +1,16 @@ +name = Opigno SCORM Quiz +description = SCORM API integration for the Quiz module +core = 7.x +package = Opigno +dependencies[] = opigno_scorm +dependencies[] = opigno_scorm_ui + +files[] = tests/OpignoScormQuizUnitTest.test +files[] = includes/opigno_scorm_quiz.question.inc +files[] = includes/opigno_scorm_quiz.response.inc +; Information added by Drupal.org packaging script on 2014-10-19 +version = "7.x-1.9" +core = "7.x" +project = "opigno" +datestamp = "1413751430" + diff --git a/modules/scorm/quiz/opigno_scorm_quiz.install b/modules/scorm/quiz/opigno_scorm_quiz.install new file mode 100755 index 0000000..f17f9bf --- /dev/null +++ b/modules/scorm/quiz/opigno_scorm_quiz.install @@ -0,0 +1,38 @@ + array( + 'fields' => array( + 'question_nid' => array( + 'type' => 'int', + 'unsiged' => TRUE, + 'not null' => TRUE, + ), + 'question_vid' => array( + 'type' => 'int', + 'unsiged' => TRUE, + 'not null' => TRUE, + ), + 'result_id' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'score_scaled' => array( + 'type' => 'float', + 'size' => 'big', + ), + ), + 'primary key' => array('question_nid', 'question_vid', 'result_id'), + ), + ); +} diff --git a/modules/scorm/quiz/opigno_scorm_quiz.module b/modules/scorm/quiz/opigno_scorm_quiz.module new file mode 100755 index 0000000..44bf6f8 --- /dev/null +++ b/modules/scorm/quiz/opigno_scorm_quiz.module @@ -0,0 +1,356 @@ + array(), + 'cmi.score.min' => array(), + 'cmi.score.max' => array(), + 'cmi.score.scaled' => array(), + 'cmi.success_status' => array(), + 'cmi.objectives' => array(), + 'cmi.objectives._count' => array('readOnly' => 1), + 'cmi.objectives._children' => array('readOnly' => 1), + 'cmi.objectives.n.id' => array(), + 'cmi.objectives.n.score' => array(), + 'cmi.objectives.n.score._children' => array('readOnly' => 1), + 'cmi.objectives.n.score.scaled' => array(), + 'cmi.objectives.n.score.raw' => array(), + 'cmi.objectives.n.score.min' => array(), + 'cmi.objectives.n.score.max' => array(), + 'cmi.objectives.n.success_status' => array(), + 'cmi.objectives.n.completion_status' => array(), + 'cmi.objectives.n.progress_measure' => array(), + 'cmi.objectives.n.description' => array(), + ); +} + +/** + * Implements hook_opigno_scorm_ui_register_cmi_data(). + */ +function opigno_scorm_quiz_opigno_scorm_ui_register_cmi_data($scorm, $scos) { + global $user; + + $data = array( + 'cmi.score.raw' => opigno_scorm_ui_scorm_cmi_get($user->uid, $scorm->id, 'cmi.score.raw', ''), + 'cmi.score.min' => opigno_scorm_ui_scorm_cmi_get($user->uid, $scorm->id, 'cmi.score.min', ''), + 'cmi.score.max' => opigno_scorm_ui_scorm_cmi_get($user->uid, $scorm->id, 'cmi.score.max', ''), + 'cmi.score.scaled' => opigno_scorm_ui_scorm_cmi_get($user->uid, $scorm->id, 'cmi.score.scaled', ''), + 'cmi.success_status' => opigno_scorm_ui_scorm_cmi_get($user->uid, $scorm->id, 'cmi.success_status', ''), + 'cmi.objectives' => array(), + ); + + // Fetch the objectives. + foreach ($scos as $sco) { + if (!empty($sco->attributes['objectives'])) { + foreach ($sco->attributes['objectives'] as $objective) { + $stored_objective = opigno_scorm_quiz_load_objective($user->uid, $scorm->id, $objective['id']); + $defaults = array( + 'id' => $objective['id'], + 'score' => array( + 'scaled' => 0, + 'raw' => 0, + 'min' => 0, + 'max' => 0, + ), + 'success_status' => '', + 'completion_status' => '', + 'progress_measure' => '', + 'description' => '', + ); + + if (!empty($stored_objective)) { + $stored_objective = (array) $stored_objective; + $stored_objective += $defaults; + } + else { + $stored_objective = $defaults; + } + + $data['cmi.objectives'][] = $stored_objective; + } + } + } + + return $data; +} + +/** + * Implements hook_opigno_scorm_ui_add_assets(). + */ +function opigno_scorm_quiz_opigno_scorm_ui_add_assets() { + $path = drupal_get_path('module', 'opigno_scorm_quiz'); + drupal_add_js("$path/js/opigno_scorm_quiz.js"); +} + +/** + * Implements hook_opigno_scorm_ui_commit(). + */ +function opigno_scorm_quiz_opigno_scorm_ui_commit($scorm, $data) { + global $user; + + // Store objectives and results. + if (!empty($data->cmi->objectives)) { + for ($i = 0, $len = count($data->cmi->objectives); $i < $len; $i++) { + opigno_scorm_scorm_cmi_set($user->uid, $scorm->id, "cmi.objectives.$i", $data->cmi->objectives[$i]); + } + } + + // Store the score. + if (!empty($data->cmi->score)) { + foreach (array('raw', 'min', 'max', 'scaled') as $key) { + if (isset($data->cmi->score->{$key})) { + opigno_scorm_scorm_cmi_set($user->uid, $scorm->id, "cmi.score.{$key}", $data->cmi->score->{$key}); + } + } + } + + // Store the success status. + if (!empty($data->cmi->success_status)) { + opigno_scorm_scorm_cmi_set($user->uid, $scorm->id, 'cmi.success_status', $data->cmi->success_status); + } +} + +/** + * Implements of hook_quiz_question_info(). + */ +function opigno_scorm_quiz_quiz_question_info() { + return array( + 'opigno_scorm_quiz_question' => array( + 'name' => t('SCORM Package'), + 'description' => t('Question using SCORM packages.'), + 'question provider' => 'OpignoScormQuizQuestion', + 'response provider' => 'OpignoScormQuizResponse', + 'module' => 'quiz_question', + ), + ); +} + +/** + * Implements hook_config(). + * + * Quiz Question triggers a Fatal Error if this function is not present. + * This is a no op. + */ +function opigno_scorm_quiz_question_config() { + // No op +} + +/** + * Implements hook_node_type_insert(). + */ +function opigno_scorm_quiz_node_type_insert($info) { + if ($info->type == 'opigno_scorm_quiz_question') { + quiz_question_add_body_field('opigno_scorm_quiz_question'); + variable_set('node_options_opigno_scorm_quiz_question', array('status')); + + // Disable comments by default. + if (module_exists('comment')) { + variable_set('comment_opigno_scorm_quiz_question', COMMENT_NODE_CLOSED); + } + + variable_set('node_submitted_opigno_scorm_quiz_question', 0); + + $field = field_info_field('opigno_scorm_package'); + if (empty($field)) { + field_create_field(array( + 'active' => 1, + 'cardinality' => 1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'opigno_scorm_package', + 'foreign keys' => array( + 'fid' => array( + 'columns' => array( + 'fid' => 'fid', + ), + 'table' => 'file_managed', + ), + ), + 'indexes' => array( + 'fid' => array( + 0 => 'fid', + ), + ), + 'locked' => 0, + 'module' => 'opigno_scorm_ui', + 'settings' => array( + 'display_default' => 0, + 'display_field' => 0, + 'uri_scheme' => 'public', + ), + 'translatable' => 0, + 'type' => 'opigno_scorm_package', + )); + } + + $field = field_info_field('opigno_scorm_round_method'); + if (empty($field)) { + field_create_field(array( + 'active' => 1, + 'cardinality' => 1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'opigno_scorm_round_method', + 'foreign keys' => array(), + 'indexes' => array( + 'value' => array( + 0 => 'value', + ), + ), + 'locked' => 0, + 'module' => 'list', + 'settings' => array( + 'allowed_values' => array( + 'ceil' => 'Round up', + 'round' => 'Round to closest', + 'floor' => 'Round down', + ), + 'allowed_values_function' => '', + ), + 'translatable' => 0, + 'type' => 'list_text', + )); + } + + $instance = field_info_instance('node', 'opigno_scorm_quiz_question', 'opigno_scorm_package'); + if (empty($instance)) { + field_create_instance(array( + 'bundle' => 'opigno_scorm_quiz_question', + 'deleted' => 0, + 'description' => '', + 'display' => array( + 'default' => array( + 'label' => 'hidden', + 'module' => 'opigno_scorm_ui', + 'settings' => array(), + 'type' => 'opigno_scorm_player', + 'weight' => 0, + ), + 'teaser' => array( + 'label' => 'hidden', + 'module' => 'opigno_scorm_ui', + 'settings' => array(), + 'type' => 'opigno_scorm_player', + 'weight' => 0, + ), + ), + 'entity_type' => 'node', + 'field_name' => 'opigno_scorm_package', + 'label' => 'SCORM', + 'required' => 1, + 'settings' => array( + 'description_field' => 0, + 'file_directory' => 'opigno_scorm', + 'file_extensions' => 'zip', + 'max_filesize' => '', + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 1, + 'module' => 'file', + 'settings' => array( + 'progress_indicator' => 'throbber', + ), + 'type' => 'file_generic', + 'weight' => 32, + ), + )); + } + + $instance = field_info_instance('node', 'opigno_scorm_quiz_question', 'opigno_scorm_round_method'); + if (empty($instance)) { + field_create_instance(array( + 'bundle' => 'opigno_scorm_quiz_question', + 'default_value' => array( + 0 => array( + 'value' => 'round', + ), + ), + 'deleted' => 0, + 'description' => 'Some SCORM packages use a float point number for scoring, whereas we require an integer. Choose the appropriate method for rounding the final score.', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'settings' => array(), + 'type' => 'hidden', + 'weight' => 2, + ), + 'teaser' => array( + 'label' => 'above', + 'settings' => array(), + 'type' => 'hidden', + 'weight' => 0, + ), + ), + 'entity_type' => 'node', + 'field_name' => 'opigno_scorm_round_method', + 'label' => 'Round method', + 'required' => 1, + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 1, + 'module' => 'options', + 'settings' => array(), + 'type' => 'options_buttons', + 'weight' => 33, + ), + )); + } + } +} + +/** + * Load all objective data for the given SCORM. + * + * Helper function to load objective CMI data that was stored. Pass the ID + * of the objective to fetch the data for it. + * + * @param int $uid + * @param int $scorm_id + * @param string $objective_id + * + * @return object|null + */ +function opigno_scorm_quiz_load_objective($uid, $scorm_id, $objective_id) { + $objectives = &drupal_static(__FUNCTION__); + + if (!isset($objectives)) { + // We query the database ourselves here instead of relying on opigno_scorm_scorm_cmi_get(), + // as we need a LIKE query. + $result = db_select('opigno_scorm_scorm_cmi_data', 'o') + ->fields('o') + ->condition('o.uid', $uid) + ->condition('o.scorm_id', $scorm_id) + ->condition('o.cmi_key', 'cmi.objectives.%', 'LIKE') + ->execute(); + + while ($row = $result->fetchObject()) { + // Make sure this is one of ours. + if (preg_match('/^cmi\.objectives\.[0-9]+$/', $row->cmi_key)) { + $data = unserialize($row->value); + + // Allow modules to alter the data. + $context = array( + 'uid' => $uid, + 'scorm_id' => $scorm_id, + 'original_value' => $data, + ); + drupal_alter('opigno_scorm_scorm_cmi_get', $data, $row->cmi_key, $context); + + $objectives[$data->id] = $data; + } + } + } + + return isset($objectives[$objective_id]) ? $objectives[$objective_id] : NULL; +} diff --git a/modules/scorm/quiz/tests/OpignoScormQuizUnitTest.test b/modules/scorm/quiz/tests/OpignoScormQuizUnitTest.test new file mode 100755 index 0000000..6335e5a --- /dev/null +++ b/modules/scorm/quiz/tests/OpignoScormQuizUnitTest.test @@ -0,0 +1,26 @@ + 'Opigno Scorm Quiz tests', + 'description' => 'Functional unit tests for the Opigno SCORM Quiz integration.', + 'group' => 'Opigno', + 'dependencies' => array('opigno_scorm', 'opigno_scorm_ui'), + ); + } + + /** + * Test the CMI data integration. + */ + public function testCMIDataIntegration() { + $this->fail('write some tests'); + } + +} \ No newline at end of file diff --git a/modules/scorm/ui/css/opigno_scorm_ui.player.css b/modules/scorm/ui/css/opigno_scorm_ui.player.css new file mode 100755 index 0000000..11916c0 --- /dev/null +++ b/modules/scorm/ui/css/opigno_scorm_ui.player.css @@ -0,0 +1,59 @@ +/** + * @file + * SCORM player styles. + */ + +/* Navigation tree */ +.scorm-ui-player-tree-wrapper { + float: left; + padding-right: 1em; + box-sizing: border-box; + width: 24%; + font-size: .8em; +} + +.scorm-ui-player-tree-wrapper ul { + margin: 0; + padding: 0 0 0 1em; +} + +.scorm-ui-player-tree-wrapper li { + margin: 0; + padding: 0; + list-style: none; +} + +.scorm-ui-player-tree-wrapper .scorm-ui-sco-title { + padding: .25em 0; +} + +.js-processed .scorm-ui-player-tree-wrapper .scorm-ui-sco-title { + cursor: pointer; +} + +.scorm-ui-player-tree-wrapper > ul { + padding-left: 0; +} + + +.scorm-ui-player-tree-wrapper .scorm-ui-sco-aggregation > .scorm-ui-sco-title { + font-weight: bold; +} + +/* The first element of the Tree is the Course title (in 99% of all cases) */ +.scorm-ui-player-tree-wrapper > ul > li > .scorm-ui-player-tree-item-title { + font-size: 1.2em; + font-weight: bold; +} + +/* IFrame */ +.scorm-ui-player-tree-wrapper + .scorm-ui-player-iframe-wrapper { + float: left; + width: 75%; +} + +.scorm-ui-player-iframe-wrapper iframe { + width: 100%; + min-height: 800px; + border: 0; +} \ No newline at end of file diff --git a/modules/scorm/ui/includes/opigno_scorm_ui.ajax.inc b/modules/scorm/ui/includes/opigno_scorm_ui.ajax.inc new file mode 100755 index 0000000..3410bf3 --- /dev/null +++ b/modules/scorm/ui/includes/opigno_scorm_ui.ajax.inc @@ -0,0 +1,21 @@ + 1)); + } + else { + drupal_json_output(array('error' => 1, 'message' => 'no data received')); + } +} diff --git a/modules/scorm/ui/includes/opigno_scorm_ui.player.inc b/modules/scorm/ui/includes/opigno_scorm_ui.player.inc new file mode 100755 index 0000000..296643d --- /dev/null +++ b/modules/scorm/ui/includes/opigno_scorm_ui.player.inc @@ -0,0 +1,215 @@ +id); +} + +/** + * Helper function to recursively create the SCO tree. + * + * @param int $scorm_id + * @param int $parent_identifier = 0 + * + * @return array + */ +function _opigno_scorm_ui_player_scorm_tree($scorm_id, $parent_identifier = 0) { + $tree = array(); + + $result = db_select('opigno_scorm_package_scos', 'sco') + ->fields('sco', array('id')) + ->condition('sco.scorm_id', $scorm_id) + ->condition('sco.parent_identifier', $parent_identifier) + ->execute(); + + while ($sco_id = $result->fetchField()) { + $sco = opigno_scorm_sco_load($sco_id); + + $children = _opigno_scorm_ui_player_scorm_tree($scorm_id, $sco->identifier); + + $sco->children = $children; + + $tree[] = $sco; + } + + return $tree; +} + +/** + * Helper function to flatten the SCORM tree. + * + * @param array $tree + * + * @return array + */ +function opigno_scorm_ui_player_scorm_flatten_tree($tree) { + $items = array(); + + if (!empty($tree)) { + foreach ($tree as $sco) { + $items[] = $sco; + if (!empty($sco->children)) { + $items = array_merge($items, opigno_scorm_ui_player_scorm_flatten_tree($sco->children)); + } + } + } + + return $items; +} + +/** + * Determine the start SCO for the SCORM package. + * + * @todo Get last viewed SCO. + * + * @param array $flat_tree + * + * @return object + */ +function opigno_scorm_ui_player_start_sco($flat_tree) { + foreach ($flat_tree as $sco) { + if (!empty($sco->launch)) { + return $sco; + } + } + + // Failsafe. Just get the first element. + return array_shift($flat_tree); +} + +/** + * Integrate a SCO object and return it (wrapped if necessary). + * + * This page callback bypasses the Drupal page rendering and includes the SCO directly. + * + * @param object $sco + */ +function opigno_scorm_ui_player_integrate_sco($sco) { + // @todo Use hooks for different SCO types. + + // Does the SCO have a launch property ? + if (!empty($sco->launch)) { + $query = array(); + + // Load the SCO data. + $scorm = opigno_scorm_scorm_load($sco->scorm_id); + + // Remove the URL parameters from the launch URL. + if (!empty($sco->attributes['parameters'])) { + $sco->launch .= $sco->attributes['parameters']; + } + $parts = explode('?', $sco->launch); + $launch = array_shift($parts); + + if (!empty($parts)) { + // Failsafe - in case a launch URL has 2 or more '?'. + $parameters = implode('&', $parts); + } + + // Get the SCO location on the filesystem + $sco_location = "{$scorm->extracted_dir}/$launch"; + $sco_path = file_create_url($sco_location); + + // Where there any parameters ? If so, prepare them for Drupal. + if (!empty($parameters)) { + foreach (explode('&', $parameters) as $param) { + list($key, $value) = explode('=', $param); + $query[$key] = !empty($value) ? $value : ''; + } + } + + drupal_goto($sco_path, array('query' => $query)); + } + else { + drupal_not_found(); + } +} + +/** + * Parse the SCO attributes and return them in an HTML ready format. + * + * @param object $sco + * + * @return string + */ +function opigno_scorm_ui_player_sco_attributes($sco) { + $attributes = array(); + + // Some default "attributes". + $attributes["data-sco-id"] = $sco->id; + $attributes["data-sco-can-launch"] = (int) !empty($sco->launch); + + if (!empty($sco->attributes)) { + foreach ($sco->attributes as $key => $value) { + if (is_bool($value)) { + $value = (int) $value; + } + elseif (is_array($value) || is_object($value)) { + $value = drupal_json_encode($value); + } + + $key = str_replace('_', '-', $key); + + $attributes["data-sco-$key"] = $value; + } + } + + return drupal_attributes($attributes); +} + +/** + * Helper function to add JS and CSS assets for the SCORM player. + */ +function opigno_scorm_ui_add_assets() { + $path = drupal_get_path('module', 'opigno_scorm'); + $ui_path = drupal_get_path('module', 'opigno_scorm_ui'); + drupal_add_js("$path/js/lib/api-2004.js"); + drupal_add_js("$path/js/lib/api-1.2.js"); + drupal_add_css("$ui_path/css/opigno_scorm_ui.player.css"); + drupal_add_js("$ui_path/js/lib/player.js"); + drupal_add_js("$ui_path/js/opigno_scorm_ui.player.js"); + + module_invoke_all('opigno_scorm_ui_add_assets'); +} + +/** + * Get the CMI data for the SCORM player. + * + * Invokes the hook_opigno_scorm_ui_register_cmi_data() on all implementing modules + * to retrieve data to pass to the SCORM player. + * + * @param object $scorm + * @param array $scos + * + * @return array + */ +function opigno_scorm_ui_add_cmi_data($scorm, $scos) { + $data = module_invoke_all('opigno_scorm_ui_register_cmi_data', $scorm, $scos); + drupal_alter('opigno_scorm_ui_register_cmi_data', $data, $scorm, $scos); + return $data; +} + +/** + * Get the available CMI paths for the SCORM player. + * + * Invokes the hook_opigno_scorm_ui_register_cmi_paths() on all implementing modules + * to retrieve data to pass to the SCORM player. + * + * @return array + */ +function opigno_scorm_ui_add_cmi_paths() { + $paths = module_invoke_all('opigno_scorm_ui_register_cmi_paths'); + drupal_alter('opigno_scorm_ui_register_cmi_paths', $paths); + return $paths; +} \ No newline at end of file diff --git a/modules/scorm/ui/js/lib/player.js b/modules/scorm/ui/js/lib/player.js new file mode 100755 index 0000000..5d67487 --- /dev/null +++ b/modules/scorm/ui/js/lib/player.js @@ -0,0 +1,162 @@ +/** + * @file + * Defines the SCORM player object. + * + * The SCORM player has simple methods for navigating between SCOs. + * It does not communicate with the SCOs. + */ + +;(function($, Drupal, window, undefined) { + + /** + * Representation of the SCORM player object. + * + * @param element + * @constructor + */ + var OpignoScormUIPlayer = function(element) { + this.el = element; + this.$el = $(element); + this.$iframe = this.$el.find('.scorm-ui-player-iframe-wrapper iframe'); + this.iframe = this.$iframe[0]; + this.$lis = this.$el.find('.scorm-ui-player-tree-wrapper li'); + } + + // @const SCO integration path pattern. + OpignoScormUIPlayer.PATH_PATTERN = 'opigno-scorm/ui/player/sco/%sco_id'; + + /** + * Initialize the player. + */ + OpignoScormUIPlayer.prototype.init = function() { + this.initNavigation(); + this.initEvents(); + } + + /** + * Initialize the navigation logic. + */ + OpignoScormUIPlayer.prototype.initNavigation = function() { + // Tree navigation. + // Register the tree click handlers. + this.registerTreeClick(); + + // Handle control modes. + this.handleTreeControlModes(); + } + + /** + * Register navigation tree click events. + */ + OpignoScormUIPlayer.prototype.registerTreeClick = function() { + var player = this; + + // Register click events for each
  • in the navigation tree. + this.$lis.click(function(e) { + e.stopPropagation(); + + var $this = $(this); + + // Exit if this has been disabled. + if ($this.hasClass('disable-click')) { + return; + } + + if ($this.data('sco-can-launch')) { + player.launch($this.data('sco-id')); + } + else { + // Trigger click on child item, if any. + // This will happen recursively until one item can be launched. + $this.find('li:eq(0)').click(); + } + }); + } + + /** + * Handle control modes. + * + * SCORM defines several ways the user can navigate between SCOs. + * This data is stored directly in the tree as data attributes. + * Make sure the user can only navigate the way the SCORM intended + * her to. + */ + OpignoScormUIPlayer.prototype.handleTreeControlModes = function() { + var player = this; + this.$lis.each(function() { + var $li = $(this); + + // If this is the root element, don't check any control mode. + if ($li.hasClass('root') || $li.parent().parent().hasClass('scorm-ui-player-tree-wrapper')) { + $li.addClass('root'); + return; + } + + // Control modes are only for aggregations. + if ($li.hasClass('scorm-ui-sco-aggregation')) { + // If this is an aggregation, and the control mode "choice" is false, + // disable child lis. + if (!$li.data('sco-control-mode-choice')) { + // @todo + // $li.find('> ul').hide().find('li').addClass('disable-click'); + } + + // If this is an aggregation, and the control mode "flow" is false, + // disable the flow navigation (forward-backward navigation). + if (!$li.data('sco-control-mode-flow')) { + // @todo + // $li.find('> ul').hide().find('li').addClass('disable-click'); + } + } + }); + } + + /** + * Register events. + * + * Use events to update the navigation and UI. + */ + OpignoScormUIPlayer.prototype.initEvents = function() { + var player = this; + + // Each time a SCO has finished loading, trigger the sco:loaded event on the corresponding SCO + // item in the tree. + this.$iframe.load(function() { + player.$lis.filter('[data-sco-id="' + player.$iframe.data('sco-id') + '"]').trigger('sco:loaded'); + }); + + // When a SCO is loading, add a "loading" class to the SCO tree item for styling. + this.$lis.bind('sco:loading', function() { + $(this).addClass('loading'); + }); + + + // When a SCO has finished loading, remove the "loading" class from the SCO tree item + // and check what control modes are available. + this.$lis.bind('sco:loaded', function() { + $(this).removeClass('loading'); + + // Check if flow is allowed. + }); + } + + /** + * Launch a SCO. + * + * @param scoID + */ + OpignoScormUIPlayer.prototype.launch = function(scoID) { + // Set the SCO ID as an attribute on the iframe. + this.$iframe.data('sco-id', scoID); + + // Load the SCO. + this.iframe.src = Drupal.settings.basePath + OpignoScormUIPlayer.PATH_PATTERN.replace('%sco_id', scoID); + + // Trigger a loading event on the tree item. + this.$lis.filter('[data-sco-id="' + scoID + '"]').trigger('sco:loading'); + } + + // Export. + window.OpignoScormUIPlayer = OpignoScormUIPlayer; + +})(jQuery, Drupal, window); \ No newline at end of file diff --git a/modules/scorm/ui/js/opigno_scorm_ui.player.js b/modules/scorm/ui/js/opigno_scorm_ui.player.js new file mode 100755 index 0000000..c18e11e --- /dev/null +++ b/modules/scorm/ui/js/opigno_scorm_ui.player.js @@ -0,0 +1,88 @@ +/** + * @file + * JS UI logic for SCORM player. + * + * @see js/lib/player.js + * @see js/lib/api.js + */ + +;(function($, Drupal, window, undefined) { + + Drupal.behaviors.opignoScormUIPlayer = { + + attach: function(context, settings) { + + // Initiate the API. + if (window.API_1484_11 === undefined) { + window.API_1484_11 = new OpignoScorm2004API(settings.scorm_data || {}); + } + + // Register CMI paths. + if (settings.opignoScormUIPlayer && settings.opignoScormUIPlayer.cmiPaths) { + window.API_1484_11.registerCMIPaths(settings.opignoScormUIPlayer.cmiPaths); + } + + // Register default CMI data. + if (settings.opignoScormUIPlayer && settings.opignoScormUIPlayer.cmiData) { + for (var item in settings.opignoScormUIPlayer.cmiData) { + window.API_1484_11.registerCMIData(item, settings.opignoScormUIPlayer.cmiData[item]); + } + } + + // Get all SCORM players in our context. + var $players = $('.scorm-ui-player', context); + + // If any players were found... + if ($players.length) { + // Register each player. + // NOTE: SCORM only allows on SCORM package on the page at any given time. + // Skip after the first one. + var first = true; + $players.each(function() { + if (!first) { + return false; + } + + var element = this, + $element = $(element), + // Create a new OpignoScormUIPlayer(). + player = new OpignoScormUIPlayer(element), + alertDataStored = false; + + player.init(); + + // Listen on commit event, and send the data to the server. + window.API_1484_11.bind('commit', function(value, data) { + $.ajax({ + url: Drupal.settings.basePath + '?q=opigno-scorm/ui/scorm/' + $element.data('scorm-id') + '/ajax/commit', + data: { data: JSON.stringify(data) }, + dataType: 'json', + type: 'post', + success: function(json) { + if (alertDataStored) { + alert(Drupal.t('We successfully stored your results. You can now proceed further.')); + } + } + }); + }); + + // Listen to the unload event. Some users click "Next" or go to a different page, expecting + // their data to be saved. We try to commit the data for them, hoping ot will get stored. + $(window).bind('beforeunload', function() { + if (!window.API_1484_11.isTerminated) { + window.API_1484_11.Terminate(''); + alertDataStored = true; + //return Drupal.t('It seems you did not finish the SCORM course, or maybe the SCORM course did not save your results. Should we try to store it for you ?'); + } + }); + + // Add a class to the player, so the CSS can style it differently if needed. + $element.addClass('js-processed'); + first = false; + }); + } + } + + }; + +})(jQuery, Drupal, window); diff --git a/modules/scorm/ui/opigno_scorm_ui.api.php b/modules/scorm/ui/opigno_scorm_ui.api.php new file mode 100755 index 0000000..705a129 --- /dev/null +++ b/modules/scorm/ui/opigno_scorm_ui.api.php @@ -0,0 +1,80 @@ + array(), + 'cmi.objectives._count' => array('readOnly' => 1), + 'cmi.objectives._children' => array('writeOnly' => 1), + ); +} + +/** + * Implements hook_opigno_scorm_ui_register_cmi_data(). + * + * If the SCORM package or some of it SCOs require data, module can provide it + * here. This should be used in conjunction with hook_opigno_scorm_ui_register_cmi_paths() + * to let the SCO know what CMI data is available to it. + * + * @param object $scorm + * @param array $scos + * + * @return array + */ +function hook_opigno_scorm_ui_register_cmi_data($scorm, $scos) { + $data = array( + 'cmi.location' => 0, + ); + + return $data; +} + +/** + * Implements hook_opigno_scorm_ui_add_assets(). + * + * Allows module to easily provide assets (CSS or JS) when a player + * is about to be rendered on the page. + */ +function hook_opigno_scorm_ui_add_assets() { + $path = drupal_get_path('module', 'opigno_scorm_ui'); + drupal_add_js("$path/js/script.js"); +} + +/** + * Implements hook_opigno_scorm_ui_commit(). + * + * Allows module to persist data provided by the SCORM package SCOs. + * The $data parameter contains a complete representation of the data + * set on the API object. + * + * Important note! Unlike hook_opigno_scorm_ui_register_cmi_data(), which accepts + * either associate arrays or objects, the $data variable is an object. + * + * @param object $scorm + * @param object $data + */ +function hook_opigno_scorm_ui_commit($scorm, $data) { + global $user; + + // Store the last position. + if (!empty($data->cmi->location)) { + opigno_scorm_scorm_cmi_set($user->uid, $scorm->id, 'cmi.location', $data->cmi->location); + } +} diff --git a/modules/scorm/ui/opigno_scorm_ui.info b/modules/scorm/ui/opigno_scorm_ui.info new file mode 100644 index 0000000..2e8257e --- /dev/null +++ b/modules/scorm/ui/opigno_scorm_ui.info @@ -0,0 +1,14 @@ +name = Opigno SCORM UI +description = User interface for the Opigno SCORM API. Provides a SCORM player. +core = 7.x +package = Opigno +dependencies[] = file +dependencies[] = opigno_scorm + +files[] = tests/OpignoScormUIUnitTest.test +; Information added by Drupal.org packaging script on 2014-10-19 +version = "7.x-1.9" +core = "7.x" +project = "opigno" +datestamp = "1413751430" + diff --git a/modules/scorm/ui/opigno_scorm_ui.install b/modules/scorm/ui/opigno_scorm_ui.install new file mode 100755 index 0000000..8b94e1d --- /dev/null +++ b/modules/scorm/ui/opigno_scorm_ui.install @@ -0,0 +1,14 @@ + array( + 'page callback' => 'opigno_scorm_ui_player_integrate_sco', + 'page arguments' => array(4), + 'access callback' => TRUE, // @todo + 'file' => 'includes/opigno_scorm_ui.player.inc', + 'type' => MENU_CALLBACK, + ), + 'opigno-scorm/ui/scorm/%opigno_scorm_scorm/ajax/commit' => array( + 'page callback' => 'opigno_scorm_ui_ajax_commit', + 'page arguments' => array(3), + 'access callback' => 'opigno_scorm_access', + 'access arguments' => array(3), + 'file' => 'includes/opigno_scorm_ui.ajax.inc', + 'type' => MENU_CALLBACK, + ), + ); +} + +/** + * Implements hook_theme(). + */ +function opigno_scorm_ui_theme() { + return array( + 'opigno_scorm_ui__player' => array( + 'variables' => array('scorm_id' => NULL, 'tree' => array(), 'start_sco' => NULL), + 'template' => 'theme/opigno-scorm-ui--player', + ), + 'opigno_scorm_ui__player_tree' => array( + 'variables' => array('tree' => array()), + 'template' => 'theme/opigno-scorm-ui--player-tree', + ), + 'opigno_scorm_ui__player_tree_item' => array( + 'variables' => array('sco' => NULL), + 'template' => 'theme/opigno-scorm-ui--player-tree-item', + ), + ); +} + +/** + * Implements hook_field_info(). + */ +function opigno_scorm_ui_field_info() { + return array( + 'opigno_scorm_package' => array( + 'label' => t('SCORM package'), + 'description' => t("This field allows users to upload SCORM packages"), + 'settings' => array( + 'display_field' => 0, + 'display_default' => 0, + 'uri_scheme' => variable_get('file_default_scheme', 'public'), + ), + 'instance_settings' => array( + 'file_extensions' => 'zip', + 'file_directory' => 'opigno_scorm', + 'max_filesize' => '', + 'description_field' => 0, + ), + 'default_widget' => 'file_generic', + 'default_formatter' => 'opigno_scorm_player', + ), + ); +} + +/** + * Implements hook_field_widget_info_alter(). + */ +function opigno_scorm_ui_field_widget_info_alter(&$info) { + $info['file_generic']['field types'] = array_merge($info['file_generic']['field types'], array('opigno_scorm_package')); +} + +/** + * Implements hook_field_formatter_info(). + */ +function opigno_scorm_ui_field_formatter_info() { + return array( + 'opigno_scorm_player' => array( + 'label' => t('SCORM player'), + 'field types' => array('opigno_scorm_package'), + ), + ); +} + +/** + * Implements hook_field_formatter_info_alter(). + */ +function opigno_scorm_ui_field_formatter_info_alter(&$info) { + foreach(array('file_default', 'file_url_plain') as $formatter) { + $info[$formatter]['field types'][] = 'opigno_scorm_package'; + } +} + +/** + * Implements hook_field_is_empty(). + */ +function opigno_scorm_ui_field_is_empty($item, $field) { + return file_field_is_empty($item, $field); +} + +/** + * Implements hook_field_load(). + */ +function opigno_scorm_ui_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) { + file_field_load($entity_type, $entities, $field, $instances, $langcode, $items, $age); +} + +/** + * Implements hook_field_presave(). + */ +function opigno_scorm_ui_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) { + file_field_presave($entity_type, $entity, $field, $instance, $langcode, $items); +} + +/** + * Implements hook_field_insert(). + */ +function opigno_scorm_ui_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) { + module_load_include('inc', 'opigno_scorm', 'includes/opigno_scorm.manifest'); + file_field_insert($entity_type, $entity, $field, $instance, $langcode, $items); + foreach ($items as $item) { + opigno_scorm_extract($item['fid']); + } +} + +/** + * Implements hook_field_update(). + */ +function opigno_scorm_ui_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) { + module_load_include('inc', 'opigno_scorm', 'includes/opigno_scorm.manifest'); + file_field_update($entity_type, $entity, $field, $instance, $langcode, $items); + foreach ($items as $item) { + $scorm = opigno_scorm_scorm_load_by_fid($item['fid']); + if (empty($scorm->id)) { + opigno_scorm_extract($item['fid']); + } + } +} + +/** + * Implements hook_field_delete(). + */ +function opigno_scorm_ui_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) { + file_field_delete($entity_type, $entity, $field, $instance, $langcode, $items); +} + +/** + * Implements hook_field_delete_revision(). + */ +function opigno_scorm_ui_field_delete_revision($entity_type, $entity, $field, $instance, $langcode, &$items) { + file_field_delete_revision($entity_type, $entity, $field, $instance, $langcode, $items); +} + +/** + * Implements hook_field_formatter_view(). + */ +function opigno_scorm_ui_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { + $element = array(); + + switch ($display['type']) { + case 'opigno_scorm_player': + $first = TRUE; + foreach ($items as $delta => $item) { + if ($first) { + $scorm = opigno_scorm_scorm_load_by_fid($item['fid']); + $element[$delta] = array( + '#markup' => opigno_scorm_ui_render_player($scorm), + ); + $first = FALSE; + } + else { + $element[$delta] = array( + '#markup' => t("As per SCORM.2004.3ED.ConfReq.v1.0, only only one SCO can be launched at a time. To enforce this, only one SCORM package is loaded inside the player on this page at a time.", array('!link' => 'http://www.adlnet.gov/wp-content/uploads/2011/07/SCORM.2004.3ED.ConfReq.v1.0.pdf')), + ); + } + } + break; + } + + return $element; +} + +/** + * Implements hook_opigno_scorm_ui_register_cmi_paths(). + */ +function opigno_scorm_ui_opigno_scorm_ui_register_cmi_paths() { + return array( + 'cmi.location' => array(), + 'cmi.completion_status' => array(), + 'cmi.exit' => array(), + ); +} + +/** + * Implements hook_opigno_scorm_ui_register_cmi_data(). + */ +function opigno_scorm_ui_opigno_scorm_ui_register_cmi_data($scorm, $scos) { + global $user; + + $data = array( + 'cmi.location' => opigno_scorm_ui_scorm_cmi_get($user->uid, $scorm->id, 'cmi.location', 0), + 'cmi.completion_status' => opigno_scorm_ui_scorm_cmi_get($user->uid, $scorm->id, 'cmi.completion_status', ''), + 'cmi.exit' => opigno_scorm_ui_scorm_cmi_get($user->uid, $scorm->id, 'cmi.exit', ''), + ); + + return $data; +} + +/** + * Implements hook_opigno_scorm_ui_commit(). + */ +function opigno_scorm_ui_opigno_scorm_ui_commit($scorm, $data) { + global $user; + + // Store the last position. + if (!empty($data->cmi->location)) { + opigno_scorm_scorm_cmi_set($user->uid, $scorm->id, 'cmi.location', $data->cmi->location); + } + + // Store the completion status. + if (!empty($data->cmi->completion_status)) { + opigno_scorm_scorm_cmi_set($user->uid, $scorm->id, 'cmi.completion_status', $data->cmi->completion_status); + } +} + +/** + * Render a SCORM player for the given SCORM object. + * + * @param object $scorm + * + * @return string + */ +function opigno_scorm_ui_render_player($scorm) { + module_load_include('inc', 'opigno_scorm_ui', 'includes/opigno_scorm_ui.player'); + + // Get the SCO tree. + $tree = opigno_scorm_ui_player_scorm_tree($scorm); + $flat_tree = opigno_scorm_ui_player_scorm_flatten_tree($tree); + + // Get the start SCO. + $start_sco = opigno_scorm_ui_player_start_sco($flat_tree); + + // Add assets. + opigno_scorm_ui_add_assets(); + + // Get implemented CMI paths. + $paths = opigno_scorm_ui_add_cmi_paths(); + + // Get CMI data for each SCO. + $data = opigno_scorm_ui_add_cmi_data($scorm, $flat_tree); + + // Pass it to JS. + drupal_add_js(array( + 'opignoScormUIPlayer' => array( + 'cmiPaths' => $paths, + 'cmiData' => $data, + ), + ), 'setting'); + + return theme('opigno_scorm_ui__player', array('scorm_id' => $scorm->id, 'tree' => count($flat_tree) == 2 ? NULL : $tree, 'start_sco' => $start_sco)); +} + +/** + * Helper function to get SCORM CMI data while also providing a default value. + * + * @param int $uid + * @param int $scorm_id + * @param string $cmi_key + * @param mixed $default_value + * + * @return mixed|null + */ +function opigno_scorm_ui_scorm_cmi_get($uid, $scorm_id, $cmi_key, $default_value = NULL) { + $value = opigno_scorm_scorm_cmi_get($uid, $scorm_id, $cmi_key); + return isset($value) ? $value : $default_value; +} + +/** + * Helper function to get SCO CMI data while also providing a default value. + * + * @param int $uid + * @param int $scorm_id + * @param string $cmi_key + * @param mixed $default_value + * + * @return mixed|null + */ +function opigno_scorm_ui_sco_cmi_get($uid, $scorm_id, $cmi_key, $default_value = NULL) { + $value = opigno_scorm_sco_cmi_get($uid, $scorm_id, $cmi_key); + return isset($value) ? $value : $default_value; +} \ No newline at end of file diff --git a/modules/scorm/ui/tests/OpignoScormUIUnitTest.test b/modules/scorm/ui/tests/OpignoScormUIUnitTest.test new file mode 100755 index 0000000..23c0efa --- /dev/null +++ b/modules/scorm/ui/tests/OpignoScormUIUnitTest.test @@ -0,0 +1,24 @@ + 'Opigno Scorm UI tests', + 'description' => 'Functional unit tests for the Opigno SCORM UI.', + 'group' => 'Opigno', + 'dependencies' => array('opigno_scorm'), + ); + } + + public function testCMIDataIntegration() { + $this->fail('write some tests'); + } + +} diff --git a/modules/scorm/ui/theme/opigno-scorm-ui--player-tree-item.tpl.php b/modules/scorm/ui/theme/opigno-scorm-ui--player-tree-item.tpl.php new file mode 100755 index 0000000..72c0c96 --- /dev/null +++ b/modules/scorm/ui/theme/opigno-scorm-ui--player-tree-item.tpl.php @@ -0,0 +1,6 @@ +
  • > + title); ?> + children)): ?> + $sco->children)); ?> + +
  • \ No newline at end of file diff --git a/modules/scorm/ui/theme/opigno-scorm-ui--player-tree.tpl.php b/modules/scorm/ui/theme/opigno-scorm-ui--player-tree.tpl.php new file mode 100755 index 0000000..605ae52 --- /dev/null +++ b/modules/scorm/ui/theme/opigno-scorm-ui--player-tree.tpl.php @@ -0,0 +1,5 @@ +
      + + $sco)); ?> + +
    \ No newline at end of file diff --git a/modules/scorm/ui/theme/opigno-scorm-ui--player.tpl.php b/modules/scorm/ui/theme/opigno-scorm-ui--player.tpl.php new file mode 100755 index 0000000..a519450 --- /dev/null +++ b/modules/scorm/ui/theme/opigno-scorm-ui--player.tpl.php @@ -0,0 +1,11 @@ +
    + +
    + $tree)); ?> +
    + + +
    + +
    +
    \ No newline at end of file diff --git a/modules/simple_ui/includes/opigno_simple_ui.install.inc b/modules/simple_ui/includes/opigno_simple_ui.install.inc new file mode 100755 index 0000000..3e2a367 --- /dev/null +++ b/modules/simple_ui/includes/opigno_simple_ui.install.inc @@ -0,0 +1,26 @@ +name = "Slide"; + $type->description = "Provide info for a scenario."; + node_type_save($type); + + $instance = field_info_instance('node', 'body', 'quiz_directions'); + if (!empty($instance)) { + $instance['label'] = "Content"; + field_update_instance($instance); + } + } +} \ No newline at end of file diff --git a/modules/simple_ui/includes/opigno_simple_ui.og.inc b/modules/simple_ui/includes/opigno_simple_ui.og.inc new file mode 100755 index 0000000..2c55f52 --- /dev/null +++ b/modules/simple_ui/includes/opigno_simple_ui.og.inc @@ -0,0 +1,217 @@ + array( + 'title' => "Remove member", + 'page callback' => 'drupal_get_form', + 'page arguments' => array('opigno_simple_ui_remove_member_confirm', 1, 4), + 'access arguments' => array(0, 1, 'manage members'), + 'access callback' => 'og_user_access', + 'file' => 'includes/opigno_simple_ui.og.inc', + 'type' => MENU_CALLBACK, + ), + ); +} + +/** + * Implements hook_form_og_ui_edit_membership_alter(). + */ +function opigno_simple_ui_form_og_ui_edit_membership_alter(&$form, $form_state) { + $og_membership = og_membership_load($form['id']['#value']); + $form['og_user']['state'] = array( + '#type' => 'radios', + '#title' => t("State"), + '#options' => og_group_content_states(), + '#default_value' => $og_membership->state, + ); + + $form['actions']['remove'] = array( + '#type' => 'submit', + '#value' => t("Remove user from group"), + '#submit' => array('opigno_simple_ui_form_og_ui_edit_membership_remove_from_group_submit'), + ); + + // We need to set the state *before* changing the roles, because some Rules or some Opigno apps + // can change the status based on permissions automatically. And many react on role change. + $form['#submit'] = array_merge(array('opigno_simple_ui_form_og_ui_edit_membership_submit'), $form['#submit']); +} + +/** + * Submit callback for hook_form_og_ui_edit_membership_alter(). + * Update membership state. + */ +function opigno_simple_ui_form_og_ui_edit_membership_submit($form, $form_state) { + $form_state['og_membership']->state = $form_state['values']['state']; + og_membership_save($form_state['og_membership']); +} + +/** + * Submit callback for hook_form_og_ui_edit_membership_alter(). + * Remove a user from the group. Redirect to confirmation form. + */ +function opigno_simple_ui_form_og_ui_edit_membership_remove_from_group_submit($form, &$form_state) { + unset($_GET['destination']); + drupal_goto("node/{$form_state['values']['gid']}/group/remove/{$form_state['values']['id']}"); +} + +/** + * Implements hook_form_og_ui_add_users_alter(). + */ +function opigno_simple_ui_form_og_ui_add_users_alter(&$form, $form_state) { + $form['og_user']['state'] = array( + '#type' => 'radios', + '#title' => t("State"), + '#options' => og_group_content_states(), + '#default_value' => OG_STATE_ACTIVE, + ); +} + +/** + * Implements hook_form_og_massadd_massadd_form_alter(). + */ +function opigno_simple_ui_form_og_massadd_massadd_form_alter(&$form, $form_state) { + $gid = current($form['group_ids']['#value']); + $node = node_load($gid); + + $form['massadd'] = array( + '#type' => 'multiselect', + '#title' => t('Select students to add to the group'), + '#default_value' => NULL, + '#size' => 15, + '#required' => TRUE, + ); + + // Exclude current members. + $exclude = array(); + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'og_membership', '=') + ->propertyCondition('gid', $gid, '=') + ->propertyCondition('entity_type', 'user', '='); + $result = $query->execute(); + if (!empty($result['og_membership'])) { + foreach ($result['og_membership'] as $id => $info) { + $og_membership = og_membership_load($id); + $exclude[] = $og_membership->etid; + } + } + + $users = entity_load('user'); + foreach ($users as $uid => $account) { + if ($uid && !in_array($uid, $exclude)) { + $form['massadd']['#options'][$account->uid] = $account->name; + } + } + + $form['massadd']['#weight'] = -50; + + $form['roles'] = array( + '#type' => 'checkboxes', + '#title' => t("Roles"), + '#options' => og_roles($form['group_type']['#value'], $node->type, $gid, FALSE, FALSE), + '#default_value' => OG_STATE_ACTIVE, + '#weight' => -40, + ); + + $form['state'] = array( + '#type' => 'radios', + '#title' => t("State"), + '#options' => og_group_content_states(), + '#default_value' => OG_STATE_ACTIVE, + '#weight' => -30, + ); + + $form['submit']['#weight'] = 0; + // We validate our own way. + unset($form['#validate']); + $form['#submit'][] = 'opigno_simple_ui_form_og_massadd_massadd_form_add_roles'; + $form['#submit'][] = 'opigno_simple_ui_form_og_massadd_massadd_form_add_state'; + $form['#submit'] = array_merge(array('opigno_simple_ui_form_og_massadd_massadd_form_prepare'), $form['#submit']); +} + +/** + * Submit callback for hook_form_og_massadd_massadd_form_alter(). + * Prepare for submission by changing the format of the field value. + */ +function opigno_simple_ui_form_og_massadd_massadd_form_prepare($form, &$form_state) { + $csv = ''; + foreach ($form_state['values']['massadd'] as $uid) { + $csv .= $form['massadd']['#options'][$uid] . "\n"; + } + $form_state['values']['_massadd'] = $form_state['values']['massadd']; + $form_state['values']['massadd'] = $csv; +} + +/** + * Submit callback for hook_form_og_massadd_massadd_form_alter(). + * Add roles. + */ +function opigno_simple_ui_form_og_massadd_massadd_form_add_roles($form, $form_state) { + foreach ($form_state['values']['_massadd'] as $uid) { + foreach ($form_state['values']['roles'] as $rid => $selected) { + if ($selected) { + og_role_grant($form_state['values']['group_type'], current($form_state['values']['group_ids']), $uid, $rid); + } + } + } +} + +/** + * Submit callback for hook_form_og_massadd_massadd_form_alter(). + * Add state. + */ +function opigno_simple_ui_form_og_massadd_massadd_form_add_state($form, $form_state) { + foreach ($form_state['values']['_massadd'] as $uid) { + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'og_membership', '=') + ->propertyCondition('gid', current($form_state['values']['group_ids']), '=') + ->propertyCondition('entity_type', 'user', '=') + ->propertyCondition('etid', $uid, '='); + $result = $query->execute(); + + $og_membership = og_membership_load(current($result['og_membership'])->id); + $og_membership->state = $form_state['values']['state']; + og_membership_save($og_membership); + } +} + +/** + * Confirmation form for deleting an OG membership. + */ +function opigno_simple_ui_remove_member_confirm($form, $form_state, $gid, $og_membership_id) { + $og_membership = og_membership_load($og_membership_id); + $entity = current(entity_load($og_membership->entity_type, array($og_membership->etid))); + $form['og_membership_id'] = array( + '#type' => 'value', + '#value' => $og_membership_id, + ); + + $form['gid'] = array( + '#type' => 'value', + '#value' => $gid, + ); + + // @todo - we process the entity as a user (hence $entity->name), but this is a bit restrictive. + $form['entity'] = array( + '#type' => 'value', + '#value' => $entity->name, + ); + return confirm_form($form, t("Are you sure you want to remove %entity from this group ?", array('%entity' => $entity->name)), "node/$gid/group"); +} + +/** + * Submit callback for deleting an OG membership. + */ +function opigno_simple_ui_remove_member_confirm_submit($form, &$form_state) { + og_membership_delete($form_state['values']['og_membership_id']); + drupal_set_message(t("%entity was removed from the group", array('%entity' => $form_state['values']['entity']))); + $form_state['redirect'] = "node/{$form_state['values']['gid']}/group"; +} \ No newline at end of file diff --git a/modules/simple_ui/includes/opigno_simple_ui.quiz.inc b/modules/simple_ui/includes/opigno_simple_ui.quiz.inc new file mode 100755 index 0000000..d5d02ea --- /dev/null +++ b/modules/simple_ui/includes/opigno_simple_ui.quiz.inc @@ -0,0 +1,221 @@ + $theme_array) { + if (!empty($theme_array['#link']['path'])) { + if ($theme_array['#link']['path'] == 'node/add/quiz') { + $data['actions']['output'][$key]['#link']['title'] = t("Add new lesson"); + } + elseif ($theme_array['#link']['path'] == 'node/%/sort-quizzes') { + $data['actions']['output'][$key]['#link']['title'] = t("Sort lessons"); + } + } + } + } + } +} + +/** + * Implements hook_form_form_alter() for all question types. + */ +function opigno_simple_ui_question_typesform_alter(&$form, $form_state) { + // Remove the "Add to Quiz" options. This is for very advanced users, and rarely + // used. + unset($form['add_directly']['#group']); + $form['add_directly']['#access'] = FALSE; + $form['add_directly']['#attributes']['style'][] = 'display:none;'; + + // Change the order of the fields. Put the title on top, always. + $form['title']['#weight'] = -30; +} + +/** + * Implements hook_form_quiz_report_form_alter(). + */ +function opigno_simple_ui_form_quiz_report_form_alter(&$form, $form_state) { + $no_report = TRUE; + drupal_set_title(t("Lesson completed.")); + foreach ($form as $key => $value) { + if (is_numeric($key)) { + if (!$form[$key]['#no_report']) { + $no_report = FALSE; + } + } + } + + if ($no_report) { + $form = array('#markup' => t("There are no results to display.")); + drupal_set_message(t("You successfully completed this lesson.")); + } +} + +/** + * Implements hook_form_quiz_questions_form_alter(). + */ +function opigno_simple_ui_form_quiz_questions_form_alter(&$form, $form_state) { + $form['question_list']['browser']['#collapsible'] = TRUE; + $form['question_list']['browser']['#collapsed'] = TRUE; + + // If this quiz is a 'theory' quiz, hide questions. + $nid = arg(1); + if ($nid && is_numeric($nid)) { + $quiz = node_load($nid); + if (!empty($quiz) && !empty($quiz->quiz_type[LANGUAGE_NONE][0]['value']) && $quiz->quiz_type[LANGUAGE_NONE][0]['value'] == 'theory' && isset($form['additional_questions']['quiz_directions'])) { + $form['add_slide'] = array( + '#type' => 'fieldset', + '#title' => t("Add a slide to the lesson"), + '#description' => t("A lesson of type 'theory' can only have slides as content."), + '#weight' => -50, + ); + $form['add_slide']['add_slide'] = $form['additional_questions']['quiz_directions']; + + $form['question_list']['#title'] = t("Slides inside this lesson"); + $form['question_list']['browser']['#access'] = FALSE; + + $form['additional_questions']['#attributes']['class'][] = 'element-hidden'; + } + } +} + +/** + * Implements hook_form_quiz_node_form_alter(). + */ +function opigno_simple_ui_form_quiz_node_form_alter(&$form, $form_state) { + foreach ($form['resultoptions'] as $key => &$child) { + if (!empty($child['#type']) && $child['#type'] == 'fieldset') { + $child['#collapsed'] = TRUE; + } + } + $form['#validate'][] = 'opigno_simple_ui_form_quiz_node_form_alter_validate'; +} + +/** + * Validate callback for hook_form_quiz_node_form_alter(). + */ +function opigno_simple_ui_form_quiz_node_form_alter_validate($form, &$form_state) { + // If of type theory, deactivate Allow skipping questions". + if (!empty($form_state['values']['quiz_type'][LANGUAGE_NONE][0]['value']) && $form_state['values']['quiz_type'][LANGUAGE_NONE][0]['value'] == 'theory') { + $form_state['values']['allow_skipping'] = 0; + } +} + +/** + * Theme Quiz navigation. + */ +function theme_opigno_simple_ui_quiz_question_navigation_form($vars) { + $form = $vars['form']; + if (!isset($form['#last'])) { + return drupal_render_children($form); + } + else { + $submit = drupal_render($form['submit']) . drupal_render($form['op']); + return drupal_render_children($form) . $submit; + } +} + +/** + * Update the quiz fields and settings and name them "Lesson". + */ +function opigno_simple_ui_update_quiz_labels() { + variable_set('quiz_name', t("Lesson")); + + $instance = field_info_instance('node', 'quiz_weight', 'quiz'); + if (!empty($instance)) { + $instance['label'] = "Lesson weight"; + field_update_instance($instance); + } + + $instance = field_info_instance('node', 'quiz_type', 'quiz'); + if (!empty($instance)) { + $instance['label'] = "Lesson type"; + $instance['description'] = "Sets the type of this lesson. Can be theory (not shown in results by default), quiz (always shown in results) and mix."; + field_update_instance($instance); + } + + $instance = field_info_instance('node', 'body', 'quiz'); + if (!empty($instance)) { + $instance['label'] = "Introduction"; + field_update_instance($instance); + } + + foreach (array( + 'long_answer' => array( + 'body' => -1, + 'og_group_ref' => 1, + ), + 'matching' => array( + 'body' => -5, + 'og_group_ref' => 1, + ), + 'multichoice' => array( + 'body' => -5, + 'og_group_ref' => 1, + ), + 'quizfileupload' => array( + 'body' => -4, + ), + 'quiz_ddlines' => array( + 'body' => -4, + ), + 'quiz_directions' => array( + 'body' => -4, + ), + 'scale' => array( + 'body' => -4, + ), + 'short_answer' => array( + 'body' => -5, + ), + 'truefalse' => array( + 'body' => -5, + ), + ) as $type => $fields) { + foreach ($fields as $field => $weight) { + $instance = field_read_instance('node', $field, $type); + if (!empty($instance)) { + $instance['widget']['weight'] = $weight; + field_update_instance($instance); + } + } + } + + $settings = variable_get('field_bundle_settings_node__multichoice', NULL); + if ($settings) { + $settings['extra_fields']['form']['title']['weight'] = -10; + variable_set('field_bundle_settings_node__multichoice', $settings); + } + + /* + array('long-answer', 'quizfileupload', 'quiz-directions'); + foreach (array_keys(_quiz_question_get_implementations()) as $type) { + $instance = field_read_instance('node', 'body', $type); + if (!empty($instance)) { + $instance['widget']['weight'] = -6; + field_update_instance($instance); + } + } + */ + + $instance = field_info_instance('node', 'course_required_quiz_ref', OPIGNO_COURSE_BUNDLE); + if (!empty($instance)) { + $instance['label'] = "Lesson required for the course validation"; + field_update_instance($instance); + } +} diff --git a/modules/simple_ui/opigno_simple_ui.info b/modules/simple_ui/opigno_simple_ui.info new file mode 100644 index 0000000..302c38a --- /dev/null +++ b/modules/simple_ui/opigno_simple_ui.info @@ -0,0 +1,16 @@ +name = Opigno Simplified UI +description = Simplifies the UI to enhance the user experience +core = 7.x +package = Opigno +dependencies[] = opigno +dependencies[] = multiselect +dependencies[] = apps +; We depend on image so we are sure the Apps installation page shows logos and screenshots. +dependencies[] = image + +; Information added by Drupal.org packaging script on 2014-10-19 +version = "7.x-1.9" +core = "7.x" +project = "opigno" +datestamp = "1413751430" + diff --git a/modules/simple_ui/opigno_simple_ui.install b/modules/simple_ui/opigno_simple_ui.install new file mode 100755 index 0000000..799008f --- /dev/null +++ b/modules/simple_ui/opigno_simple_ui.install @@ -0,0 +1,24 @@ +fields(array('weight' => 10)) + ->condition('name', 'opigno_simple_ui') + ->execute(); + + module_load_include('inc', 'opigno', 'modules/simple_ui/includes/opigno_simple_ui.install'); + opigno_simple_ui_change_quiz_question_bundles_names(); + + if (module_exists('quiz')) { + module_load_include('inc', 'opigno', 'modules/simple_ui/includes/opigno_simple_ui.quiz'); + opigno_simple_ui_update_quiz_labels(); + } +} diff --git a/modules/simple_ui/opigno_simple_ui.module b/modules/simple_ui/opigno_simple_ui.module new file mode 100755 index 0000000..0a7f955 --- /dev/null +++ b/modules/simple_ui/opigno_simple_ui.module @@ -0,0 +1,411 @@ + "Manage users", + 'description' => "Manage platform users", + 'page callback' => 'opigno_simple_ui_admin_redirect', + 'page arguments' => array('admin/people'), + 'access arguments' => array('administer users'), + ); + + $items['admin/opigno/appearance/settings'] = array( + 'title' => "Manage display settings", + 'description' => "Manage platform theme settings", + 'page callback' => 'opigno_simple_ui_admin_redirect', + 'page arguments' => array('admin/appearance/settings/' . variable_get('theme_default', 'platon')), + 'access arguments' => array('administer themes'), + ); + + if (variable_get('theme_default', 'platon') === 'platon') { + $default_theme_settings = variable_get('theme_platon_settings', array()); + if (!empty($default_theme_settings['platon_menu_source'])) { + $items['admin/opigno/appearance/menu'] = array( + 'title' => "Manage menu", + 'description' => "Manage the main menu items", + 'page callback' => 'opigno_simple_ui_admin_redirect', + 'page arguments' => array('admin/structure/menu/manage/' . $default_theme_settings['platon_menu_source']), + 'access arguments' => array('administer menu'), + ); + } + } + + $items['admin/opigno/content/default-tools'] = array( + 'title' => "Set default course tools", + 'description' => "Manage the tools that are activated by default for new courses.", + 'page callback' => 'opigno_simple_ui_admin_redirect', + 'page arguments' => array('admin/structure/types/manage/course/fields/opigno_course_tools'), + 'access arguments' => array('administer content types'), + ); + + $items['admin/opigno/content/forums'] = array( + 'title' => "Manage forums", + 'description' => "Manage the different forums and forum containers.", + 'page callback' => 'opigno_simple_ui_admin_redirect', + 'page arguments' => array('admin/structure/forum'), + 'access arguments' => array('administer taxonomy'), + ); + + if (module_exists('apps')) { + $items['admin/opigno/system/apps'] = array( + 'title' => "Apps", + 'description' => "Manage and install apps for your platform", + 'page callback' => 'opigno_simple_ui_apps_page', + 'access arguments' => array('administer apps'), + ); + } + + return $items; +} + +/** + * Menu callback: provide a redirect functionality for creating shortcuts to system admin pages + * via the admin/opigno section. + */ +function opigno_simple_ui_admin_redirect($path) { + drupal_goto($path); + return 'Redirecting...'; +} + +/** + * Implements hook_views_api(). + */ +function opigno_simple_ui_views_api() { + return array( + 'api' => '3.0', + ); +} + +/** + * Implements of hook_views_default_views_alter(). + */ +function opigno_simple_ui_views_default_views_alter(&$views) { + // Group the courses by class. + if (array_key_exists('opigno_quizzes', $views)) { + $display = $views['opigno_quizzes']->display['default']; + $display->display_options['title'] = t("Lessons"); + } +} + +/** + * Implements hook_form_BASE_FORM_ID_alter() for node_form. + */ +function opigno_simple_ui_form_node_form_alter(&$form, $form_state) { + if (in_array($form['type']['#value'], array_keys(_quiz_question_get_implementations()))) { + module_load_include('inc', 'opigno_simple_ui', 'includes/opigno_simple_ui.quiz'); + opigno_simple_ui_question_typesform_alter($form, $form_state); + } +} + +/** + * Implements hook_form_alter(). + */ +function opigno_simple_ui_form_alter(&$form, &$form_state, $form_id) { + if ($form_id == 'user_login_block') { + $form['user_login_fieldset'] = array( + '#type' => 'fieldset', + '#title' => t('User login'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + ); + $form['user_login_fieldset']['name'] = $form['name']; + $form['user_login_fieldset']['pass'] = $form['pass']; + $form['user_login_fieldset']['actions'] = $form['actions']; + $form['user_login_fieldset']['links'] = $form['links']; + unset($form['name']); + unset($form['pass']); + unset($form['actions']); + unset($form['links']); + } +} + +/** + * Implements hook_form_FORM_ID_alter() for user_admin_permissions. + */ +function opigno_simple_ui_form_user_admin_permissions_alter(&$form, $form_state) { + // If in distribution context, order the roles by importance. + if (function_exists('opigno_lms_get_platform_role_id')) { + $admin_rid = opigno_lms_get_platform_role_id(OPIGNO_LMS_ADMIN_ROLE); + $student_manager_rid = opigno_lms_get_platform_role_id(OPIGNO_LMS_STUDENT_MANAGER_ROLE); + + if (isset($form['role_names'][$admin_rid]) && count($form['role_names']) > 3) { + if (isset($form['role_names'][$student_manager_rid])) { + // This will add it to the end of the list. + $student_manager_header = $form['role_names'][$student_manager_rid]; + unset($form['role_names'][$student_manager_rid]); + $form['role_names'][$student_manager_rid] = $student_manager_header; + + $student_manager_perms = $form['checkboxes'][$student_manager_rid]; + unset($form['checkboxes'][$student_manager_rid]); + $form['checkboxes'][$student_manager_rid] = $student_manager_perms; + } + + // This will add it to the end of the list. + $admin_header = $form['role_names'][$admin_rid]; + unset($form['role_names'][$admin_rid]); + $form['role_names'][$admin_rid] = $admin_header; + + $admin_perms = $form['checkboxes'][$admin_rid]; + unset($form['checkboxes'][$admin_rid]); + $form['checkboxes'][$admin_rid] = $admin_perms; + } + } +} + +/** + * Implements hook_menu_alter(). + */ +function opigno_simple_ui_menu_alter(&$items) { + // Change the default node tabs title on a course node. + unset($items['node/%node/view']['title']); + $items['node/%node/view']['title callback'] = 'opigno_simple_ui_course_view_tab_title'; + $items['node/%node/view']['title arguments'] = array(1); + unset($items['node/%node/edit']['title']); + $items['node/%node/edit']['title callback'] = 'opigno_simple_ui_course_edit_tab_title'; + $items['node/%node/edit']['title arguments'] = array(1); + unset($items['node/%/group']['title']); + $items['node/%/group']['title callback'] = 'opigno_simple_ui_course_group_tab_title'; + $items['node/%/group']['title arguments'] = array(1); + + // Add a new secondary tab "Members". + $items['node/%/group/users'] = $items['group/%/%/admin/people']; + $items['node/%/group/users']['title'] = "Members"; + $items['node/%/group/users']['title callback'] = 't'; + unset($items['node/%/group/users']['title arguments']); + $items['node/%/group/users']['page arguments'][0] = 'node'; + $items['node/%/group/users']['page arguments'][1] = 1; + $items['node/%/group/users']['access arguments'][1] = 'node'; + $items['node/%/group/users']['access arguments'][2] = 1; + $items['node/%/group/users']['type'] = MENU_DEFAULT_LOCAL_TASK; + + // Make parent inherit this access control. + $items['node/%/group']['access callback'] = $items['node/%/group/users']['access callback']; + $items['node/%/group']['access arguments'] = $items['node/%/group/users']['access arguments']; + + // Add a new secondary tab "Add members". + $items['node/%/group/add'] = $items['group/%/%/admin/people/add-user']; + $items['node/%/group/add']['page arguments'][1] = 'node'; + $items['node/%/group/add']['page arguments'][2] = 1; + $items['node/%/group/add']['access arguments'][1] = 'node'; + $items['node/%/group/add']['access arguments'][2] = 1; + $items['node/%/group/add']['type'] = MENU_LOCAL_TASK; + + if (isset($items['group/%/%/admin/people/mass-add-user'])) { + // Add a new secondary tab "Add multiple members". + $items['node/%/group/mass-add'] = $items['group/%/%/admin/people/mass-add-user']; + $items['node/%/group/mass-add']['title'] = "Add multiple members"; + $items['node/%/group/mass-add']['page arguments'][1] = 'node'; + $items['node/%/group/mass-add']['page arguments'][2] = 1; + $items['node/%/group/mass-add']['access arguments'][1] = 'node'; + $items['node/%/group/mass-add']['access arguments'][2] = 1; + $items['node/%/group/mass-add']['type'] = MENU_LOCAL_TASK; + } + + // Make parent tab point to "Members" page. + $items['node/%/group']['page callback'] = $items['node/%/group/users']['page callback']; + $items['node/%/group']['page arguments'] = $items['node/%/group/users']['page arguments']; + + // Correct sort quizzes page title. + $items['node/%node/sort-quizzes']['title'] = t("Sort lessons"); + + if (isset($items['group/%/%/admin/permissions'])) { + // Per group og permissions. + $items['node/%/group/permissions'] = $items['group/%/%/admin/permissions']; + $items['node/%/group/permissions']['title arguments'][1] = 'node'; + $items['node/%/group/permissions']['title arguments'][2] = 1; + $items['node/%/group/permissions']['access arguments'][1] = 'node'; + $items['node/%/group/permissions']['access arguments'][2] = 1; + $items['node/%/group/permissions']['page arguments'][1] = 'node'; + $items['node/%/group/permissions']['page arguments'][2] = 1; + $items['node/%/group/permissions']['type'] = MENU_LOCAL_TASK; + $items['node/%/group/permissions']['weight'] = 10; + } +} + +/** + * Implements hook_theme_registry_alter(). + */ +function opigno_simple_ui_theme_registry_alter(&$theme_registry) { + if (isset($theme_registry['menu_local_task'])) { + $theme_registry['menu_local_task']['function'] = 'theme_opigno_simple_ui_local_task'; + } + + if (module_exists('quiz')) { + opigno_simple_ui_quiz_theme_registry_alter($theme_registry); + } +} + +/** + * Implements hook_opigno_tool_alter(). + */ +function opigno_simple_ui_opigno_tool_alter(&$tools, $node = NULL) { + if (isset($tools['quiz'])) { + $tools['quiz']['name'] = t("Lessons"); + $tools['quiz']['weight'] = -20; + $tools['quiz']['description'] = t("Lessons allow teachers to assess students and provide slideshows with course content and/or questions."); + } +} + +/** + * Implements hook_modules_installed(). + */ +function opigno_simple_ui_modules_installed($modules) { + module_load_include('inc', 'opigno_simple_ui', 'includes/opigno_simple_ui.install'); + opigno_simple_ui_change_quiz_question_bundles_names(); +} + +/** + * Implements hook_menu_local_tasks_alter(). + */ +function opigno_simple_ui_menu_local_tasks_alter(&$data, $router_item, $root_path) { + if ($root_path == 'admin/opigno/content/course-administration' || $root_path == 'my-courses' || $root_path == 'course-catalogue') { + $item = menu_get_item('node/add/' . OPIGNO_COURSE_BUNDLE); + if ($item['access']) { + $item['title'] = t("Add a new course"); + $item['options']['attributes']['class'][] = $item['localized_options']['attributes']['class'][] = 'add-course'; + $item['options']['attributes']['class'][] = $item['localized_options']['attributes']['class'][] = 'opigno-simple-ui-add-course'; + $item['options']['attributes']['class'][] = $item['localized_options']['attributes']['class'][] = 'opigno-simple-ui-course-administration-add-course-link'; + $data['actions']['output'][] = array( + '#theme' => 'menu_local_action', + '#link' => $item, + ); + } + } + + module_load_include('inc', 'opigno_simple_ui', 'includes/opigno_simple_ui.quiz'); + opigno_simple_ui_quiz_menu_local_tasks_alter($data, $router_item, $root_path); +} + +/** + * Implements hook_modules_enabled(). + */ +function opigno_simple_ui_modules_enabled($modules) { + $handle = FALSE; + // We cannot use _quiz_question_implementations(), as the modules + // are not enabled yet... Update this list if we find new question + // types. + foreach (array( + 'quiz', + 'matching', + 'multichoice', + 'quizfileupload', + 'quiz_ddlines', + 'quiz_directions', + 'scale', + 'short_answer', + 'truefalse', + 'long_answer', + ) as $module) { + if (in_array($module, $modules)) { + $handle = TRUE; + break; + } + } + + if ($handle) { + module_load_include('inc', 'opigno_simple_ui', 'includes/opigno_simple_ui.quiz'); + opigno_simple_ui_update_quiz_labels(); + } +} + +/** + * Change the "View" tab title on course node pages. + * + * @param stdClass $node + * + * @return string + */ +function opigno_simple_ui_course_view_tab_title($node) { + if ($node->type == OPIGNO_COURSE_BUNDLE || ($node->type == 'class')) { + return t('Home'); + } + return t('View'); +} + +/** + * Change the "Edit" tab title on course node pages. + * + * @param stdClass $node + * + * @return string + */ +function opigno_simple_ui_course_edit_tab_title($node) { + if ($node->type == OPIGNO_COURSE_BUNDLE || ($node->type == 'class')) { + return t('Settings'); + } + return t('Edit'); +} + +/** + * Change the "Group" tab title on course node pages. + * + * @param stdClass $node + * + * @return string + */ +function opigno_simple_ui_course_group_tab_title($node) { + return t('Users'); +} + +/** + * Alter the way tabs are rendered to add custom classes. + * + * @param array $vars + * + * @return string + */ +function theme_opigno_simple_ui_local_task(&$vars) { + $class = 'node-tab'; + + switch ($vars['element']['#link']['path']) { + case 'node/%/view': + $class .= ' node-view-tab'; + break; + + case 'node/%/edit': + $class .= ' node-edit-tab'; + break; + + case 'node/%/tools': + $class .= ' node-tools-tab'; + break; + + case 'node/%/group': + $class .= ' node-group-tab'; + break; + + case 'node/%/certificate': + $class .= ' node-certificate-tab'; + break; + } + + $vars['element']['#link']['localized_options']['attributes']['class'][] = $class; + return theme_menu_local_task($vars); +} + +/** + * Page callback for the apps page. + * Fetches the apps administration page from the apps module and returns it. + */ +function opigno_simple_ui_apps_page() { + module_load_include('inc', 'apps' ,'apps.pages'); + return apps_install_page('opigno'); +} diff --git a/modules/simple_ui/opigno_simple_ui.views_default.inc b/modules/simple_ui/opigno_simple_ui.views_default.inc new file mode 100755 index 0000000..a402eda --- /dev/null +++ b/modules/simple_ui/opigno_simple_ui.views_default.inc @@ -0,0 +1,305 @@ +name = 'opigno_simple_ui_course_administration'; + $view->description = 'Administer courses on the platform.'; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'Course administration'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['title'] = 'Course administration'; + $handler->display->display_options['use_more_always'] = FALSE; + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['access']['perm'] = 'create course content'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '20'; + $handler->display->display_options['style_plugin'] = 'table'; + $handler->display->display_options['style_options']['columns'] = array( + 'nid' => 'nid', + 'timestamp' => 'timestamp', + 'views_bulk_operations' => 'views_bulk_operations', + 'title' => 'title', + 'status' => 'status', + 'edit_node' => 'edit_node', + ); + $handler->display->display_options['style_options']['default'] = '-1'; + $handler->display->display_options['style_options']['info'] = array( + 'nid' => array( + 'sortable' => 0, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'timestamp' => array( + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'views_bulk_operations' => array( + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'title' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'status' => array( + 'sortable' => 0, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'edit_node' => array( + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + ); + /* Relationship: Content: Author */ + $handler->display->display_options['relationships']['uid']['id'] = 'uid'; + $handler->display->display_options['relationships']['uid']['table'] = 'node'; + $handler->display->display_options['relationships']['uid']['field'] = 'uid'; + $handler->display->display_options['relationships']['uid']['label'] = 'Author'; + /* Field: Content: Nid */ + $handler->display->display_options['fields']['nid']['id'] = 'nid'; + $handler->display->display_options['fields']['nid']['table'] = 'node'; + $handler->display->display_options['fields']['nid']['field'] = 'nid'; + $handler->display->display_options['fields']['nid']['label'] = ''; + $handler->display->display_options['fields']['nid']['exclude'] = TRUE; + $handler->display->display_options['fields']['nid']['element_label_colon'] = FALSE; + /* Field: Content: Has new content */ + $handler->display->display_options['fields']['timestamp']['id'] = 'timestamp'; + $handler->display->display_options['fields']['timestamp']['table'] = 'history'; + $handler->display->display_options['fields']['timestamp']['field'] = 'timestamp'; + $handler->display->display_options['fields']['timestamp']['label'] = ''; + $handler->display->display_options['fields']['timestamp']['exclude'] = TRUE; + $handler->display->display_options['fields']['timestamp']['element_label_colon'] = FALSE; + /* Field: Bulk operations: Content */ + $handler->display->display_options['fields']['views_bulk_operations']['id'] = 'views_bulk_operations'; + $handler->display->display_options['fields']['views_bulk_operations']['table'] = 'node'; + $handler->display->display_options['fields']['views_bulk_operations']['field'] = 'views_bulk_operations'; + $handler->display->display_options['fields']['views_bulk_operations']['label'] = ''; + $handler->display->display_options['fields']['views_bulk_operations']['element_label_colon'] = FALSE; + $handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['display_type'] = '0'; + $handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['enable_select_all_pages'] = 1; + $handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['force_single'] = 0; + $handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['entity_load_capacity'] = '10'; + $handler->display->display_options['fields']['views_bulk_operations']['vbo_operations'] = array( + 'rules_component::rules_activate_group_membership' => array( + 'selected' => 0, + 'postpone_processing' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + ), + 'action::node_assign_owner_action' => array( + 'selected' => 0, + 'postpone_processing' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + ), + 'action::views_bulk_operations_delete_item' => array( + 'selected' => 1, + 'postpone_processing' => 1, + 'skip_confirmation' => 0, + 'override_label' => 1, + 'label' => 'Delete course(s)', + ), + 'action::views_bulk_operations_script_action' => array( + 'selected' => 0, + 'postpone_processing' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + ), + 'action::node_make_sticky_action' => array( + 'selected' => 0, + 'postpone_processing' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + ), + 'action::node_make_unsticky_action' => array( + 'selected' => 0, + 'postpone_processing' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + ), + 'action::views_bulk_operations_modify_action' => array( + 'selected' => 0, + 'postpone_processing' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + 'settings' => array( + 'show_all_tokens' => 1, + 'display_values' => array( + '_all_' => '_all_', + ), + ), + ), + 'action::views_bulk_operations_argument_selector_action' => array( + 'selected' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + 'settings' => array( + 'url' => '', + ), + ), + 'action::node_promote_action' => array( + 'selected' => 0, + 'postpone_processing' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + ), + 'action::node_publish_action' => array( + 'selected' => 1, + 'postpone_processing' => 1, + 'skip_confirmation' => 0, + 'override_label' => 1, + 'label' => 'Publish course(s)', + ), + 'action::node_unpromote_action' => array( + 'selected' => 0, + 'postpone_processing' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + ), + 'action::node_save_action' => array( + 'selected' => 0, + 'postpone_processing' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + ), + 'action::system_send_email_action' => array( + 'selected' => 0, + 'postpone_processing' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + ), + 'action::node_unpublish_action' => array( + 'selected' => 1, + 'postpone_processing' => 1, + 'skip_confirmation' => 0, + 'override_label' => 1, + 'label' => 'Unpublish course(s)', + ), + 'action::node_unpublish_by_keyword_action' => array( + 'selected' => 0, + 'postpone_processing' => 0, + 'skip_confirmation' => 0, + 'override_label' => 0, + 'label' => '', + ), + ); + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['alter']['alter_text'] = TRUE; + $handler->display->display_options['fields']['title']['alter']['text'] = '[title] [timestamp]'; + $handler->display->display_options['fields']['title']['alter']['word_boundary'] = FALSE; + $handler->display->display_options['fields']['title']['alter']['ellipsis'] = FALSE; + /* Field: User: Name */ + $handler->display->display_options['fields']['name']['id'] = 'name'; + $handler->display->display_options['fields']['name']['table'] = 'users'; + $handler->display->display_options['fields']['name']['field'] = 'name'; + $handler->display->display_options['fields']['name']['relationship'] = 'uid'; + $handler->display->display_options['fields']['name']['label'] = 'Author'; + /* Field: Content: Published */ + $handler->display->display_options['fields']['status']['id'] = 'status'; + $handler->display->display_options['fields']['status']['table'] = 'node'; + $handler->display->display_options['fields']['status']['field'] = 'status'; + $handler->display->display_options['fields']['status']['not'] = 0; + /* Field: Content: Edit link */ + $handler->display->display_options['fields']['edit_node']['id'] = 'edit_node'; + $handler->display->display_options['fields']['edit_node']['table'] = 'views_entity_node'; + $handler->display->display_options['fields']['edit_node']['field'] = 'edit_node'; + $handler->display->display_options['fields']['edit_node']['label'] = 'Actions'; + /* Sort criterion: Content: Title */ + $handler->display->display_options['sorts']['title']['id'] = 'title'; + $handler->display->display_options['sorts']['title']['table'] = 'node'; + $handler->display->display_options['sorts']['title']['field'] = 'title'; + $handler->display->display_options['sorts']['title']['order'] = 'DESC'; + /* Filter criterion: Content: Type */ + $handler->display->display_options['filters']['type']['id'] = 'type'; + $handler->display->display_options['filters']['type']['table'] = 'node'; + $handler->display->display_options['filters']['type']['field'] = 'type'; + $handler->display->display_options['filters']['type']['value'] = array( + 'course' => 'course', + ); + /* Filter criterion: Content: Title */ + $handler->display->display_options['filters']['title']['id'] = 'title'; + $handler->display->display_options['filters']['title']['table'] = 'node'; + $handler->display->display_options['filters']['title']['field'] = 'title'; + $handler->display->display_options['filters']['title']['operator'] = 'contains'; + $handler->display->display_options['filters']['title']['exposed'] = TRUE; + $handler->display->display_options['filters']['title']['expose']['operator_id'] = 'title_op'; + $handler->display->display_options['filters']['title']['expose']['label'] = 'Title'; + $handler->display->display_options['filters']['title']['expose']['operator'] = 'title_op'; + $handler->display->display_options['filters']['title']['expose']['identifier'] = 'title'; + $handler->display->display_options['filters']['title']['expose']['remember_roles'] = array( + 2 => '2', + 1 => 0, + ); + /* Filter criterion: Content: Published */ + $handler->display->display_options['filters']['status']['id'] = 'status'; + $handler->display->display_options['filters']['status']['table'] = 'node'; + $handler->display->display_options['filters']['status']['field'] = 'status'; + $handler->display->display_options['filters']['status']['value'] = 'All'; + $handler->display->display_options['filters']['status']['exposed'] = TRUE; + $handler->display->display_options['filters']['status']['expose']['operator_id'] = ''; + $handler->display->display_options['filters']['status']['expose']['label'] = 'Published'; + $handler->display->display_options['filters']['status']['expose']['operator'] = 'status_op'; + $handler->display->display_options['filters']['status']['expose']['identifier'] = 'status'; + $handler->display->display_options['filters']['status']['expose']['remember_roles'] = array( + 2 => '2', + 1 => 0, + ); + + /* Display: Page */ + $handler = $view->new_display('page', 'Page', 'page'); + $handler->display->display_options['path'] = 'admin/opigno/content/course-administration'; + $handler->display->display_options['menu']['type'] = 'normal'; + $handler->display->display_options['menu']['title'] = 'Course administration'; + $handler->display->display_options['menu']['description'] = 'Administer all courses on the platform.'; + $handler->display->display_options['menu']['weight'] = '0'; + $handler->display->display_options['menu']['name'] = 'management'; + $handler->display->display_options['menu']['context'] = 0; + + $export['opigno_simple_ui_course_administration'] = $view; + + return $export; +} diff --git a/og_add_role_rules_2076125.patch b/og_add_role_rules_2076125.patch new file mode 100755 index 0000000..5f46d7a --- /dev/null +++ b/og_add_role_rules_2076125.patch @@ -0,0 +1,236 @@ +diff --git a/og.module b/og.module +old mode 100644 +new mode 100755 +index da90f90..b8177d8 +--- a/og.module ++++ b/og.module +@@ -826,6 +826,48 @@ function og_entity_insert($entity, $entity_type) { + } + + /** ++ * Implements hook_og_role_grant(). ++ */ ++function og_og_role_grant($entity_type, $gid, $uid, $rid) { ++ if (module_exists('rules')) { ++ $query = new EntityFieldQuery(); ++ $query->entityCondition('entity_type', 'og_membership', '=') ++ ->propertyCondition('gid', $gid, '=') ++ ->propertyCondition('entity_type', 'user', '=') ++ ->propertyCondition('etid', $uid, '='); ++ $result = $query->execute(); ++ ++ if (!empty($result['og_membership'])) { ++ $info = current($result['og_membership']); ++ $og_membership = og_membership_load($info->id); ++ $account = user_load($uid); ++ rules_invoke_event('og_user_role_granted', $og_membership, $account); ++ } ++ } ++} ++ ++/** ++ * Implements hook_og_role_revoke(). ++ */ ++function og_og_role_revoke($entity_type, $gid, $uid, $rid) { ++ if (module_exists('rules')) { ++ $query = new EntityFieldQuery(); ++ $query->entityCondition('entity_type', 'og_membership', '=') ++ ->propertyCondition('gid', $gid, '=') ++ ->propertyCondition('entity_type', 'user', '=') ++ ->propertyCondition('etid', $uid, '='); ++ $result = $query->execute(); ++ ++ if (!empty($result['og_membership'])) { ++ $info = current($result['og_membership']); ++ $og_membership = og_membership_load($info->id); ++ $account = user_load($uid); ++ rules_invoke_event('og_user_role_revoked', $og_membership, $account); ++ } ++ } ++} ++ ++/** + * Implements hook_entity_update(). + */ + function og_entity_update($entity, $entity_type) { +diff --git a/og.rules.inc b/og.rules.inc +old mode 100644 +new mode 100755 +index ccfa6e3..afc30ed +--- a/og.rules.inc ++++ b/og.rules.inc +@@ -41,7 +41,15 @@ function og_rules_event_info() { + 'og_user_delete' => $defaults + array( + 'label' => t('User has been removed from group'), + 'help' => t("A user has been removed from group and is no longer a group member."), +- ), ++ ), ++ 'og_user_role_revoked' => $defaults + array( ++ 'label' => t('User role has been revoked'), ++ 'help' => t('A user has had a role removed within a group.'), ++ ), ++ 'og_user_role_granted' => $defaults + array( ++ 'label' => t('User was granted a role'), ++ 'help' => t('A user was granted a role within a group.'), ++ ), + ); + } + +@@ -199,10 +207,74 @@ function og_rules_action_info() { + 'access callback' => 'og_rules_integration_access', + ); + ++ $items['og_revoke_user_role'] = array( ++ 'label' => t('Revoke a role from a user'), ++ 'group' => t('Organic groups'), ++ 'parameter' => array( ++ 'role' => array( ++ 'type' => 'integer', ++ 'label' => t('Role'), ++ 'description' => t('The role to remove'), ++ 'options list' => 'og_rules_roles_options_list', ++ ), ++ 'user' => array( ++ 'type' => 'user', ++ 'label' => t('User'), ++ 'description' => t('The user to revoke the role from'), ++ ), ++ 'group' => array( ++ 'type' => array_keys(og_get_all_group_entity()), ++ 'label' => t('Group'), ++ 'wrapped' => TRUE, ++ ), ++ ), ++ 'base' => 'og_rules_revoke_role_from_user', ++ 'access callback' => 'og_rules_integration_access', ++ ); ++ ++ $items['og_give_og_role_to_user'] = array( ++ 'label' => t('Give user a og role'), ++ 'group' => t('Organic groups'), ++ 'parameter' => array( ++ 'role' => array( ++ 'type' => 'integer', ++ 'label' => t('Role'), ++ 'description' => t('The role to add'), ++ 'options list' => 'og_rules_roles_options_list', ++ ), ++ 'user' => array( ++ 'type' => 'user', ++ 'label' => t('User'), ++ 'description' => t('The user to grant the role to'), ++ ), ++ 'group' => array( ++ 'type' => array_keys(og_get_all_group_entity()), ++ 'label' => t('Group'), ++ 'wrapped' => TRUE, ++ ), ++ ), ++ 'base' => 'og_rules_add_role_to_user', ++ 'access callback' => 'og_rules_integration_access', ++ ); ++ + return $items; + } + + /** ++ * Action: grant a role to a user. ++ */ ++function og_rules_add_role_to_user($rid, $user, EntityDrupalWrapper $group) { ++ og_role_grant($group->type(), $group->getIdentifier(), $user->uid, $rid); ++} ++ ++/** ++ * Action: revoke a role from a user. ++ */ ++function og_rules_revoke_role_from_user($rid, $user, EntityDrupalWrapper $group) { ++ og_role_revoke($group->type(), $group->getIdentifier(), $user->uid, $rid); ++} ++ ++/** + * Action: Get group members from a group content. + */ + function og_rules_get_members($group_content) { +@@ -404,10 +476,62 @@ function og_rules_condition_info() { + 'base' => 'og_rules_entity_is_group_content', + 'access callback' => 'og_rules_integration_access', + ); ++ $items['og_user_has_role'] = array( ++ 'label' => t('User has role'), ++ 'group' => t('Organic groups'), ++ 'parameter' => array( ++ 'role' => array( ++ 'type' => 'integer', ++ 'label' => t('Role'), ++ 'description' => t('The role to check for.'), ++ 'options list' => 'og_rules_roles_options_list', ++ 'restriction' => 'input', ++ ), ++ 'group' => array( ++ 'type' => array_keys(og_get_all_group_entity()), ++ 'label' => t('Group'), ++ 'description' => t('The group for which permission should be checked.'), ++ 'wrapped' => TRUE, ++ ), ++ 'account' => array( ++ 'type' => 'user', ++ 'label' => t('User'), ++ ), ++ ), ++ 'base' => 'og_rules_user_has_roles', ++ 'access callback' => 'og_rules_integration_access', ++ ); + return $items; + } + + /** ++ * List possible roles accross all groups. ++ * ++ * @return array ++ */ ++function og_rules_roles_options_list() { ++ $groups = og_get_all_group_bundle(); ++ $roles = array(); ++ foreach ($groups as $entity_type => $entity_groups) { ++ foreach ($entity_groups as $bundle_machine_name => $bundle_name) { ++ $all_roles = og_roles($entity_type, $bundle_machine_name, 0, TRUE, FALSE); ++ foreach ($all_roles as $rid => $role) { ++ $roles["$bundle_name ($entity_type)"][$rid] = $role; ++ } ++ } ++ } ++ return $roles; ++} ++ ++/** ++ * Condition: user has role. ++ */ ++function og_rules_user_has_roles($rid, EntityDrupalWrapper $group, $account) { ++ $roles = og_get_user_roles($group->type(), $group->getIdentifier(), $account->uid, FALSE); ++ return in_array($rid, array_keys($roles)); ++} ++ ++/** + * Condition: User has group permisison. + */ + function og_rules_user_has_permission($permission, EntityDrupalWrapper $group, $account) { +@@ -428,6 +552,19 @@ function og_rules_user_has_permission_options_list() { + } + + /** ++ * Condition user has role within group. ++ */ ++function og_rules_user_has_role($has_role, $group, $account) { ++ $all_roles = og_get_user_roles('node', $group->nid, $account->uid, FALSE); ++ foreach ($all_roles as $role) { ++ if ($has_role == $role) { ++ return TRUE; ++ } ++ } ++ return FALSE; ++} ++ ++/** + * Condition: Entity is in group. + */ + function og_rules_condition_entity_in_group(EntityDrupalWrapper $entity, EntityDrupalWrapper $group) { diff --git a/opigno.api.php b/opigno.api.php new file mode 100755 index 0000000..400a695 --- /dev/null +++ b/opigno.api.php @@ -0,0 +1,71 @@ + array( + // The human-readable name of the tool. + 'name' => t("Quiz import"), + // The path of the tool. + 'path' => isset($node) ? "node/{$node->nid}/import-quiz" : 'admin/quiz/import/xls', + // Access arguments for this tool. Defaults to array('access content'). + 'access_arguments' => isset($node) ? array('node', $node->nid, 'create quiz content') : array('create quiz content'), + // Custom access callback. Note that it is advised to use OG access control and permissions whenever possible. + 'access_callback' => isset($node) ? 'og_user_access' : 'user_access', + // Description of the tool. This is shown to the end users. + 'description' => t("Import Excel files containing multiple choice questions as a quiz."), + /** + * You may provide a list of actions authorized users can take for this tool. + * Actions are listed as links inside an "Actions" block Opigno core provides. + * Each action link is passed as-is to the theme_link() function. For more information + * on formatting a link object, see: https://api.drupal.org/api/drupal/includes%21theme.inc/function/theme_links/7. + * The only exception are the access_arguments and access_callback keys, which gives + * you fine-grained access control. + */ + 'actions' => array( + // Keyed by the action name. + 'import_quiz' => array( + 'title' => t("Import a new Quiz"), + 'href' => isset($node) ? "node/{$node->nid}/import-quiz" : 'admin/quiz/import/xls', + // Access control is done in the same way as the parent level. + 'access_arguments' => array('import quiz questions xls'), + ), + ), + ), + ); +} + +/** + * Implements hook_opigno_tool_alter(). + * + * @param array $tools + * An array of all available tools. These can be updated here. + */ +function hook_opigno_tool_alter(&$tools) { + $tools['tool_machine_name']['name'] = t("Updated Quiz name"); +} diff --git a/opigno.info b/opigno.info new file mode 100644 index 0000000..badc024 --- /dev/null +++ b/opigno.info @@ -0,0 +1,21 @@ +name = Opigno Core +description = Opigno LMS Core +core = 7.x +package = Opigno + +dependencies[] = og +dependencies[] = options +dependencies[] = og_ui +dependencies[] = rules +dependencies[] = rules_conditional +dependencies[] = views +dependencies[] = views_php +dependencies[] = better_exposed_filters + +files[] = tests/OpignoWebTestCase.test +; Information added by Drupal.org packaging script on 2014-10-19 +version = "7.x-1.9" +core = "7.x" +project = "opigno" +datestamp = "1413751430" + diff --git a/opigno.install b/opigno.install new file mode 100755 index 0000000..96ace48 --- /dev/null +++ b/opigno.install @@ -0,0 +1,200 @@ +fields(array( + 'weight' => 20 + )) + ->condition('name', 'opigno') + ->execute(); + + // Disable comments by default. + if (module_exists('comment')) { + variable_set('comment_' . OPIGNO_COURSE_BUNDLE, COMMENT_NODE_CLOSED); + } + + $type = node_type_load(OPIGNO_COURSE_BUNDLE); + if (empty($type)) { + $type = node_type_set_defaults(array( + 'type' => OPIGNO_COURSE_BUNDLE, + 'name' => st('Course'), + 'base' => 'node_content', + 'description' => st("A course entity. This is the fundamental building bloc for Opigno. It can contain students, teachers, quizzes, files and many other ressources."), + 'custom' => 1, + 'modified' => 1, + 'locked' => 0, + 'promoted' => 0, + )); + node_type_save($type); + node_add_body_field($type); + } + + include_once drupal_get_path('module', 'og') . '/og_ui/og_ui.module'; + if (function_exists('og_ui_node_type_save')) { + variable_set('og_group_type_' . OPIGNO_COURSE_BUNDLE, TRUE); + og_ui_node_type_save(OPIGNO_COURSE_BUNDLE); + } + + // Add the OG content access field. + if (module_exists('og_access')) { + og_create_field(OG_ACCESS_FIELD, 'node', OPIGNO_COURSE_BUNDLE); + } + opigno_update_7101(); +} + +/** + * Implements hook_field_schema(). + */ +function opigno_field_schema($field) { + if ($field['type'] == 'opigno_tools') { + return array( + 'columns' => array( + 'tool' => array( + 'type' => + 'varchar', + 'length' => 50, + 'not null' => TRUE + ), + ), + 'indexes' => array( + 'tool' => array('tool'), + ), + ); + } +} + + +/** + * Enable the course image field + */ +function opigno_update_7101() +{ + $type = node_type_load(OPIGNO_COURSE_BUNDLE); + if (!empty($type)) { + $field = field_info_field('opigno_course_image'); + if (empty($field)) { + field_create_field(array( + 'active' => 1, + 'cardinality' => 1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'opigno_course_image', + 'foreign keys' => array( + 'fid' => array( + 'columns' => array( + 'fid' => 'fid', + ), + 'table' => 'file_managed', + ), + ), + 'indexes' => array( + 'fid' => array( + 0 => 'fid', + ), + ), + 'locked' => 0, + 'module' => 'image', + 'settings' => array( + 'default_image' => 47, + 'uri_scheme' => 'public', + ), + 'translatable' => 0, + 'type' => 'image', + )); + } + + $instance = field_info_instance('node', 'opigno_course_image', 'course'); + if (empty($instance)) { + field_create_instance(array( + 'bundle' => 'course', + 'deleted' => 0, + 'description' => '', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'module' => 'image', + 'settings' => array( + 'image_link' => '', + 'image_style' => 'thumbnail', + ), + 'type' => 'image', + 'weight' => 19, + ), + 'teaser' => array( + 'label' => 'above', + 'settings' => array(), + 'type' => 'hidden', + 'weight' => 0, + ), + ), + 'entity_type' => 'node', + 'field_name' => 'opigno_course_image', + 'label' => 'course_image', + 'required' => 0, + 'settings' => array( + 'alt_field' => 0, + 'default_image' => 0, + 'file_directory' => '', + 'file_extensions' => 'png gif jpg jpeg', + 'max_filesize' => '', + 'max_resolution' => '', + 'min_resolution' => '', + 'title_field' => 0, + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 1, + 'module' => 'image', + 'settings' => array( + 'preview_image_style' => 'apps_logo_small', + 'progress_indicator' => 'throbber', + ), + 'type' => 'image_image', + 'weight' => 42, + ), + )); + } + $source_dir=drupal_get_path('profile','opigno_lms')."/img"; + $filename="opigno_lms.png"; + $source = $source_dir . '/' . $filename; + $destination = $filename; + $field = field_info_field('opigno_course_image'); + opigno_content_set_default_image($field,$filename, $source, $destination); + } +} + + +function opigno_content_set_default_image(&$field, $filename, $source, $destination) { + // See if a default image hasn't been set for this field yet + // Dynamically set the user default image on the field + $destination = file_default_scheme() . '://' . $destination; + // Check to see if it exists already + $result = db_select('file_managed', 'f') + ->fields('f', array('fid')) + ->condition('f.uri', $destination) + ->execute(); + $fid = $result->fetchField(); + // Simulate an upload of the default user image + $file = new stdClass; + $file->filename = $filename; + $file->timestamp = REQUEST_TIME; + $file->uri = $source; + $file->filemime = file_get_mimetype($source); + $file->uid = 1; + $file->status = 1; + $file = file_copy($file, 'public://', FILE_EXISTS_REPLACE); + $fid = $file->fid; + // field_config key no longer seems to exist. + // $field['field_config']['settings']['default_image'] = (string) $fid; + // Use this instead. + $field['settings']['default_image'] = (string) $fid; + field_update_field($field); +} \ No newline at end of file diff --git a/opigno.module b/opigno.module new file mode 100755 index 0000000..2f112f8 --- /dev/null +++ b/opigno.module @@ -0,0 +1,815 @@ + array( + 'title' => 'Tools', + 'access callback' => 'opigno_access_tools', + 'access arguments' => array(1), + 'page callback' => 'opigno_tools_page', + 'page arguments' => array(1), + 'type' => MENU_LOCAL_TASK, + 'weight' => 10, + ), + 'admin/opigno' => array( + 'title' => "Opigno", + 'page callback' => 'opigno_admin_overview_page', + 'access arguments' => array('access opigno administration pages'), + ), + 'admin/opigno/students' => array( + 'title' => "Student management", + 'description' => "Tools for managing the platform students", + 'position' => 'left', + 'weight' => -10, + 'page callback' => 'system_admin_menu_block_page', + 'access arguments' => array('access administration pages'), + 'file' => 'system.admin.inc', + 'file path' => drupal_get_path('module', 'system'), + ), + 'admin/opigno/system' => array( + 'title' => "Administration", + 'description' => "Manage Opigno settings and users", + 'position' => 'left', + 'weight' => -5, + 'page callback' => 'system_admin_menu_block_page', + 'access arguments' => array('access administration pages'), + 'file' => 'system.admin.inc', + 'file path' => drupal_get_path('module', 'system'), + ), + 'admin/opigno/appearance' => array( + 'title' => "Appearance", + 'description' => "Manage Opigno theme settings and appearance", + 'position' => 'right', + 'weight' => -5, + 'page callback' => 'system_admin_menu_block_page', + 'access arguments' => array('access administration pages'), + 'file' => 'system.admin.inc', + 'file path' => drupal_get_path('module', 'system'), + ), + 'admin/opigno/content' => array( + 'title' => "Content", + 'description' => "Manage Opigno courses, quizzes, etc", + 'position' => 'right', + 'weight' => -10, + 'page callback' => 'system_admin_menu_block_page', + 'access arguments' => array('access administration pages'), + 'file' => 'system.admin.inc', + 'file path' => drupal_get_path('module', 'system'), + ), + ); + + return $items; +} + +/** + * Implements hook_permission(). + */ +function opigno_permission() { + return array( + 'access opigno administration pages' => array( + 'title' => t("Access the Opigno administration pages"), + 'description' => t("This permission is necessary for some global tools, like exporting user lists."), + ), + ); +} + +/** + * Implements hook_init(). + */ +function opigno_init() { + _opigno_install_custom_fields(); +} + +/** + * Implements hook_hook_info(). + */ +function opigno_hook_info() { + return array( + 'opigno_tool' => array( + 'group' => 'opigno', + ), + 'opigno_tool_alter' => array( + 'group' => 'opigno', + ), + ); +} + +/** + * Implements hook_og_context_negotiation_info + */ +function opigno_og_context_negotiation_info() { + $providers = array(); + + $providers['opigno_tool'] = array( + 'name' => t('Opigno tool urls'), + 'description' => t("Determine context by checking node/%/tool-page."), + 'callback' => 'opigno_og_context_handler', + 'menu path' => array('node/%'), + ); + $providers['opigno_entity_reference'] = array( + 'name' => t('Opigno entity reference urls'), + 'description' => t("Determine context by checking entityreference/autocomplete/single/%/%/%."), + 'callback' => 'opigno_og_context_handler', + 'menu path' => array('entityreference/autocomplete/single/%/%/%'), + ); + return $providers; +} + +/** + * Implements hook_block_info(). + */ +function opigno_block_info() { + return array( + 'opigno_tools_block' => array( + 'info' => t("Opigno Course Tools"), + ), + 'opigno_tool_actions_block' => array( + 'info' => t("Opigno Course Tool Actions"), + ), + ); +} + +/** + * Implements hook_block_view(). + */ +function opigno_block_view($delta) { + global $user; + $node = menu_get_object(); + switch ($delta) { + case 'opigno_tool_actions_block': + if (!empty($node) && opigno_access_tools($node)) { + $links = array(); + foreach (opigno_get_node_tools($node, $user) as $tool) { + if (!empty($tool['actions'])) { + foreach ($tool['actions'] as $action_id => $action) { + $defaults = array( + 'access_arguments' => array('access content'), + 'access_callback' => 'user_access', + ); + $action += $defaults; + + if (opigno_tool_action_access($action, $user)) { + $links[$action_id] = $action; + } + } + } + } + } + return array( + 'subject' => t("Tool actions"), + 'content' => empty($links) ? '' : theme('links', array('links' => $links)), + ); + + case 'opigno_tools_block': + if (!empty($node) && opigno_access_tools($node)) { + $elements = array(); + foreach (opigno_get_node_tools($node, $user) as $tool) { + $elements[$delta] = array( + '#type' => 'html_tag', + '#tag' => 'p', + '#value' => check_plain($tool['name']), + ); + } + } + return array( + 'subject' => t("Tool actions"), + 'content' => empty($elements) ? '' : $elements, + ); + } +} + +/** + * Implements hook_apps_servers_info() + */ +function opigno_apps_servers_info() { + return array( + 'opigno' => array( + 'title' => 'Opigno', + 'description' => t("Apps for Opigno"), + 'manifest' => 'http://www.opigno.org/app/query/opigno', + ), + ); +} + +/** + * Implements hook_theme(). + */ +function opigno_theme() { + return array( + 'opigno_tool' => array( + 'variables' => array('tool' => NULL, 'course' => NULL), + 'template' => 'templates/opigno--tool', + ), + 'opigno_tools' => array( + 'variables' => array('tools' => NULL, 'course' => NULL), + 'template' => 'templates/opigno--tools', + ), + ); +} + +/** + * Implements hook_field_info(). + */ +function opigno_field_info() { + return array( + 'opigno_tools' => array( + 'label' => t('Opigno tools'), + 'description' => t("This field stores tools that can be activated/deactivated per course."), + 'settings' => array('allowed_values' => array(), 'allowed_values_function' => ''), + 'default_widget' => 'options_buttons', + 'default_formatter' => 'opigno_tools_name', + ), + ); +} + +/** + * Implements hook_field_widget_info_alter(). + */ +function opigno_field_widget_info_alter(&$info) { + $info['options_buttons']['field types'] = array_merge($info['options_buttons']['field types'], array('opigno_tools')); +} + +/** + * Implements hook_options_list(). + */ +function opigno_options_list($field, $instance, $entity_type, $entity) { + $options = array(); + foreach (opigno_get_tools() as $tool) { + $options[$tool['machine_name']] = $tool['name']; + } + return $options; +} + +/** + * Implements hook_field_is_empty(). + */ +function opigno_field_is_empty($item, $field) { + return empty($item['tool']); +} + + +/** + * Implements hook_field_formatter_info(). + */ +function opigno_field_formatter_info() { + return array( + 'opigno_tools_name' => array( + 'label' => t('Only display tool name'), + 'field types' => array('opigno_tools'), + ), + 'opigno_tools_tool' => array( + 'label' => t('Display the tool "block" (opigno--tool.tpl.php)'), + 'field types' => array('opigno_tools'), + ), + ); +} + +/** + * Implements hook_field_formatter_view(). + */ +function opigno_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { + global $user; + $element = array(); + + switch ($display['type']) { + case 'opigno_tools_name': + foreach ($items as $delta => $item) { + $info = opigno_get_tool($item['tool'], $entity, $user); + if (!empty($info)) { + $element[$delta] = array( + '#type' => 'html_tag', + '#tag' => 'p', + '#value' => check_plain($info['name']), + ); + } + } + break; + + case 'opigno_tools_tool': + foreach ($items as $delta => $item) { + $info = opigno_get_tool($item['tool'], $entity, $user); + if (!empty($info)) { + $element[$delta] = array( + '#theme' => 'opigno_tool', + '#tool' => $info, + '#course' => $entity, + ); + } + } + break; + } + + return $element; +} + +/** + * Implements hook_form_alter(). + */ +function opigno_form_alter(&$form, &$form_state, $form_id) { + // UX: Add a confirmation to the permissions form to ask the user whether to + // auto-enable the 'access opigno administration pages' permission along with + // 'access administration pages'. + if ($form_id == 'user_admin_permissions') { + $form['#attached']['js'][] = drupal_get_path('module', 'opigno') . '/js/opigno.admin.js'; + } + if ($form_id=="og_ui_confirm_subscribe") + { + $form['#submit'][]="opigno_set_user_as_student"; + } +} + +function opigno_set_user_as_student($form, &$form_state) { + Global $user; + $gid = $form['gid']['#value']; + $node = node_load($gid); + $roles = og_roles("node", $node->type, $node->nid, $force_group = FALSE, $include_all = TRUE); + foreach ($roles as $index => $role) { + if ($role == 'student') { + og_role_grant("node", $gid, $user->uid, $index); + } + } +} + +/** + * Implements hook_preprocess_opigno_tool(). + */ +function opigno_preprocess_opigno_tool(&$vars) { + $vars['name'] = check_plain($vars['tool']['name']); + $vars['machine_name'] = check_plain($vars['tool']['machine_name']); + $vars['description'] = check_plain($vars['tool']['description']); + $vars['path'] = check_plain($vars['tool']['path']); +} + +/** + * Implements hook_views_api(). + */ +function opigno_views_api() { + return array( + 'api' => '3.0', + ); +} + +/** + * Implements hook_views_query_alter(). + */ +function opigno_views_query_alter(&$view, &$query) { + global $user; + if (preg_match('/exclude_own_groups/', $query->options['query_comment'])) { + $nids = array(); + + $nid_query = new EntityFieldQuery(); + $nid_query->entityCondition('entity_type', 'og_membership') + ->entityCondition('bundle', 'og_membership_type_default') + ->propertyCondition('entity_type', 'user', '=') + ->propertyCondition('etid', $user->uid, '=') + ->addMetaData('account', user_load(1)); // Run the query as user 1. + + $result = $nid_query->execute(); + if (!empty($result['og_membership'])) { + foreach($result['og_membership'] as $id => $item) { + $og_membership = og_membership_load($id); + $nids[] = $og_membership->gid; + } + } + + foreach ($query->where as &$conditions) { + if (is_array($conditions)) { + foreach ($conditions['conditions'] as &$condition) { + if ($condition['field'] == 'node.nid') { + if (!empty($nids)) { + $condition['value'] = $nids; + $condition['operator'] = 'NOT IN'; + } + else { + $condition['value'] = 0; + $condition['operator'] = '<>'; + } + } + } + } + } + } +} + +/** + * Determines the context from a url. + */ +function opigno_og_context_handler() { + if (preg_match('/^node\/[0-9]+\/./', current_path())) { + if (og_is_group('node', node_load(arg(1)))) { + return array('node' => array(arg(1))); + } + } + elseif (preg_match('/^entityreference\/autocomplete\/single/', current_path())) { + if (is_numeric(arg(6))&&og_is_group('node', node_load(arg(6)))) { + return array('node' => array(arg(6))); + } + } +} + +/** + * Fetches the list of tools for the platform. + * + * @param stdClass $node = NULL + * + * @return array + */ +function opigno_get_tools($node = NULL) { + $tools = &drupal_static(__FUNCTION__); + $group = isset($node->nid) ? $node->nid : 'global'; + + if (empty($tools[$group])) { + $tools[$group] = module_invoke_all('opigno_tool', $node); + foreach ($tools[$group] as $key => &$tool) { + $tool['machine_name'] = $key; + if (!isset($tools['weight'])) { + $tool['weight'] = 0; + } + } + drupal_alter('opigno_tool', $tools[$group], $node); + usort($tools[$group], 'drupal_sort_weight'); + + // Key them by machine name again, as the sort loses them. + $temp = array(); + foreach ($tools[$group] as $key => $ordered_tool) { + $temp[$ordered_tool['machine_name']] = $ordered_tool; + } + $tools[$group] = $temp; + } + + return $tools[$group]; +} + +/** + * Fetch information for a specific tool. + * Optionally, can be filtered by access rights. + * + * @param string $name + * @param stdClass $node = NULL + * @param stdClass $account = NULL + * + * @return array|false + */ +function opigno_get_tool($name, $node = NULL, $account = NULL) { + // Add defaults to prevent Notices. + $defaults = array( + 'machine_name' => $name, + 'description' => '', + 'path' => '', + 'actions' => array(), + 'access_arguments' => array('access content'), + 'access_callback' => 'user_access', + 'weight' => 0, + ); + + $tools = opigno_get_tools($node); + + if (isset($tools[$name])) { + $tool = $tools[$name] + $defaults; + + // If an account was given, check user access. + if (isset($account)) { + if (!opigno_tool_access($tool, $account)) { + return FALSE; + } + } + return $tool; + } + return FALSE; +} + +/** + * Check user access for the tool. + * + * @param array $tool + * @param stdClass $account + * + * @return bool + */ +function opigno_tool_access($tool, $user=NULL) { + if ($user==null) + { + Global $user; + } + return call_user_func_array($tool['access_callback'], array_merge($tool['access_arguments'], array($user))); +} + +/** + * Check user access for the tool action. + * + * @param array $action + * @param stdClass $account + * + * @return bool + */ +function opigno_tool_action_access($action, $account) { + return call_user_func_array($action['access_callback'], array_merge($action['access_arguments'], array($account))); +} + +/** + * Custom access callback for the tools tab on course nodes, or the rendered + * opigno_tools field. + * + * @param stdClass $node + * + * @return bool + */ +function opigno_access_tools($node) { + global $user; + + if ($node->type === OPIGNO_COURSE_BUNDLE) { + if ($user->uid === '1') { + return TRUE; + } + + $tools = opigno_get_tools($node); + + // If there are some tools, we must do some more checking. + if (!empty($tools)) { + // Only allow tool access when the membership is in a certain state. + $allowed_states = variable_get('opigno_access_tools_only_when_state', array(OG_STATE_ACTIVE)); // @todo this is hardcoded for now + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'og_membership', '=') + ->propertyCondition('gid', $node->nid, '=') + ->propertyCondition('entity_type', 'user', '=') + ->propertyCondition('etid', $user->uid, '='); + $result = $query->execute(); + + if (!empty($result['og_membership'])) { + $info = current($result['og_membership']); // There's only one membership. + $og_membership = og_membership_load($info->id); + return in_array((int) $og_membership->state, $allowed_states, TRUE); + } + } + } + + return FALSE; +} + +/** + * Get tools for the node. + * Optionally filter by access permissions. + * + * @param stdClass $node + * @param stdClass $account = NULL + * + * @return array + */ +function opigno_get_node_tools($node, $account = NULL) { + $tools = array(); + foreach ($node->opigno_course_tools[LANGUAGE_NONE] as $item) { + $tool = opigno_get_tool($item['tool'], $node, $account); + if (!empty($tool)) { + $tools[$item['tool']] = $tool; + } + } + return $tools; +} + +/** + * Page callback for the tools page. + */ +function opigno_tools_page($node) { + global $user; + foreach (opigno_get_node_tools($node, $user) as $tool) { + $tools[] = theme('opigno_tool', array('tool' => $tool, 'course' => $node)); + } + return theme('opigno_tools', array('tools' => $tools, 'course' => $node)); +} + +/** + * Returns an array of all course node NIDs for the platform. + * + * @param bool $only_published + * + * @return array + */ +function opigno_get_courses($only_published = FALSE) { + $query = db_select('node', 'n') + ->fields('n', array('nid')) + ->condition('n.type', OPIGNO_COURSE_BUNDLE); + + if ($only_published) { + $query->condition('n.status', NODE_PUBLISHED); + } + + $nids = array(); + $result = $query->execute(); + while ($nid = $result->fetchField()) { + $nids[] = $nid; + } + + return $nids; +} + +/** + * This is a hack to circumvent the fact that we cannot easily install custom field types + * within the install hook. This is because the hook_field_info() is not called at that point, + * triggering a FieldException: Attempt to create a field of unknown type. + * This function installs the custom fields and sets a variable, making sure we only install it + * once. It is called in opigno_init(). + */ +function _opigno_install_custom_fields() { + if (!variable_get('opigno_installed_fields', FALSE)) { + // Add the activate tools field. + $field = field_info_field('opigno_course_tools'); + if (empty($field)) { + field_create_field(array( + 'active' => 1, + 'cardinality' => -1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'opigno_course_tools', + 'foreign keys' => array(), + 'indexes' => array( + 'tool' => array( + 0 => 'tool', + ), + ), + 'locked' => 0, + 'module' => 'opigno', + 'settings' => array( + 'allowed_values' => array(), + 'allowed_values_function' => '', + ), + 'translatable' => 0, + 'type' => 'opigno_tools', + )); + } + + $instance = field_info_instance('node', 'opigno_course_tools', OPIGNO_COURSE_BUNDLE); + if (empty($instance)) { + field_create_instance(array( + 'field_name' => 'opigno_course_tools', + 'entity_type' => 'node', + 'bundle' => OPIGNO_COURSE_BUNDLE, + 'label' => "Course tools", + 'description' => "Activate tools for this course. Deactivated tools will be hidden from users.", + 'required' => FALSE, + )); + } + variable_set('opigno_installed_fields', TRUE); + } +} + +/** + * Helper function to get all users from a course. + * + * @param int $gid + * @param int $state = OG_STATE_ACTIVE + * + * @return array + */ +function opigno_get_users_in_group($gid, $state = OG_STATE_ACTIVE) { + $users = array(); + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'og_membership') + ->propertyCondition('group_type', 'node', '=') + ->propertyCondition('gid', $gid, '=') + ->propertyCondition('entity_type', 'user', '=') + ->propertyCondition('state', $state, '='); + + $result = $query->execute(); + if (!empty($result['og_membership'])) { + // Use a temporary array for sorting. + $temp = array(); + foreach ($result['og_membership'] as $membership_info) { + $og_membership = og_membership_load($membership_info->id); + $account = user_load($og_membership->etid); + $temp[$account->name] = $account; + } + + // Sort by name. + ksort($temp); + + foreach ($temp as $account) { + $users[$account->uid] = $account; + } + } + return $users; +} + +/** + * Menu callback; Provide the administration overview page. + */ +function opigno_admin_overview_page() { + $blocks = array(); + $admin = db_query("SELECT menu_name, mlid FROM {menu_links} WHERE link_path = 'admin/opigno' AND module = 'system'")->fetchAssoc(); + if ($admin) { + $result = db_query(" + SELECT m.*, ml.* + FROM {menu_links} ml + INNER JOIN {menu_router} m ON ml.router_path = m.path + WHERE menu_name = :menu_name AND ml.plid = :mlid AND hidden = 0", $admin, array('fetch' => PDO::FETCH_ASSOC)); + foreach ($result as $item) { + _menu_link_translate($item); + if (!$item['access']) { + continue; + } + // The link description, either derived from 'description' in hook_menu() + // or customized via menu module is used as title attribute. + if (!empty($item['localized_options']['attributes']['title'])) { + $item['description'] = $item['localized_options']['attributes']['title']; + unset($item['localized_options']['attributes']['title']); + } + $block = $item; + $block['content'] = ''; + $block['content'] .= theme('admin_block_content', array('content' => system_admin_menu_block($item))); + if (!empty($block['content'])) { + $block['show'] = TRUE; + } + + // Prepare for sorting as in function _menu_tree_check_access(). + // The weight is offset so it is always positive, with a uniform 5-digits. + $blocks[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $block; + } + } + if ($blocks) { + ksort($blocks); + return theme('admin_page', array('blocks' => $blocks)); + } + else { + return t('You do not have any administrative items.'); + } +} + +// @deprecated +function _opigno_course_students_info($node) { + $node_id = $node->nid; + $group_members = opigno_get_users_in_group($node_id); + $students_number = 0; + foreach ($group_members AS $member) { + $user = user_load($member->uid); + $user_roles = og_get_user_roles('node', $node_id, $user->uid, FALSE); + foreach ($user_roles as $roleid => $rolename) { + if (($roleid == 5) && (sizeof($user_roles) == 1)) { + $students_number++; + } + } + } + return array( + 'places' => $node->field_course_places[LANGUAGE_NONE][0]['value'], + 'available_places' => $node->field_course_places[LANGUAGE_NONE][0]['value'] - $students_number, + 'students' => $students_number + ); +} + + +/** + * Implements hook_og_role_grant(). + */ +function opigno_og_role_grant($entity_type, $gid, $uid, $rid) { + if (module_exists('rules')) { + rules_invoke_event('og_user_was_granted_role', $entity_type, $gid, $uid, $rid); + } +} + +/** + * Implements hook_og_role_revoke(). + */ +function opigno_og_role_revoke($entity_type, $gid, $uid, $rid) { + if (module_exists('rules')) { + rules_invoke_event('og_user_was_revoked_role', $entity_type, $gid, $uid, $rid); + } +} + +function opigno_get_course_teachers($gid) +{ + $users=opigno_get_users_in_group($gid,OG_STATE_ACTIVE); + $teachers=array(); + foreach($users as $user) + { + $roles=og_get_user_roles('node',$gid, $user->uid, $include = TRUE); + foreach($roles as $index => $role) + { + if ($role=="teacher") + { + $teachers[]=$user; + } + } + } + return $teachers; +} + +function opigno_get_teacher_html($gid) { + $teachers = opigno_get_course_teachers($gid); + $html = ""; + foreach ($teachers as $teacher) { + $user = user_load($teacher->uid); + $html .= '
    '; + $html .= theme('user_picture', array('account' => $user)); + $html .= theme('username', array('account' => $user)); + $html .= "
    "; + } + return $html; +} \ No newline at end of file diff --git a/opigno.rules.inc b/opigno.rules.inc new file mode 100755 index 0000000..abb0e38 --- /dev/null +++ b/opigno.rules.inc @@ -0,0 +1,233 @@ +getBundle(), $group->getIdentifier(), TRUE, TRUE); + foreach ($all_roles as $role_id => $a_role) { + if ($role == $role_id) { + og_role_grant('node', $group->getIdentifier(), $user->uid, $role_id); + } + } +} + +function opigno_rules_revoke_role_to_user($role, $user, EntityDrupalWrapper $group) { +// Load the user we want to add to the group + $all_roles = og_roles('node', $group->getBundle(), $group->getIdentifier(), TRUE, TRUE); + foreach ($all_roles as $role_id => $a_role) { + if ($role == $role_id) { + og_role_revoke('node', $group->getIdentifier(), $user->uid, $role_id); + } + } +} + +/*function opigno_rules_user_has_roles_options_list() { + $all_bundles = og_get_all_group_bundle(); + foreach ($all_bundles['node'] as $bundle_machine_name => $bundle_name) { + $all_roles = og_roles('node', $bundle_machine_name, 0, TRUE, TRUE); + foreach ($all_roles as $role_id => $role) { + $roles[$bundle_name][$role_id] = $role; + } + } + return $roles; +}*/ + + +function opigno_rules_user_has_roles($role, $group, $account) { + $all_roles = og_get_user_roles('node', $group->nid, $account->uid, TRUE); + + foreach ($all_roles as $a_role => $a_rolename) { + if ($role == $a_role) { + return TRUE; + } + } + return FALSE; +} + +function opigno_rules_action_info() { + $items = array(); + $items['opigno_load_og_membership'] = array( + 'label' => t("Load OG Membership"), + 'group' => t('Organic groups'), + 'parameter' => array( + 'user' => array( + 'type' => 'user', + 'label' => t('User'), + 'description' => t('The user who get the role'), + ), + 'group' => array( + 'type' => array_keys(og_get_all_group_entity()), + 'label' => t('Group'), + 'wrapped' => TRUE, + ), + ), + 'provides' => array( + 'og_membership' => array( + 'type' => 'og_membership', + 'label' => t('OG Membership'), + ), + ), + 'base' => 'opigno_load_og_membership', + ); + + $items['og_revoke_og_role_to_user'] = array( + 'label' => t('Revoke user a og role'), + 'group' => t('Organic groups'), + 'parameter' => array( + 'role' => array( + 'type' => 'text', + 'label' => t('Role'), + 'description' => t('The role to be removed'), + //'options list' => 'opigno_rules_user_has_roles_options_list', + ), + 'user' => array( + 'type' => 'user', + 'label' => t('User'), + 'description' => t('The user who get the role'), + ), + 'group' => array( + 'type' => array_keys(og_get_all_group_entity()), + 'label' => t('Group'), + 'wrapped' => TRUE, + ), + // @todo: Add membership-type setting + add in the membership-entity + // fields via the info_alter callback + reload the form once the + // membership-type has been chosen. + // Then, we probably also want to provide the newly created membership + // entity. + ), + 'base' => 'opigno_rules_revoke_role_to_user', + 'access callback' => 'og_rules_integration_access', + ); + + $items['og_give_og_role_to_user'] = array( + 'label' => t('Give user a og role'), + 'group' => t('Organic groups'), + 'parameter' => array( + 'role' => array( + 'type' => 'text', + 'label' => t('Role'), + 'description' => t('The role to add'), + //'options list' => 'opigno_rules_user_has_roles_options_list', + ), + 'user' => array( + 'type' => 'user', + 'label' => t('User'), + 'description' => t('The user who get the role'), + ), + 'group' => array( + 'type' => array_keys(og_get_all_group_entity()), + 'label' => t('Group'), + 'wrapped' => TRUE, + ), + // @todo: Add membership-type setting + add in the membership-entity + // fields via the info_alter callback + reload the form once the + // membership-type has been chosen. + // Then, we probably also want to provide the newly created membership + // entity. + ), + 'base' => 'opigno_rules_add_role_to_user', + 'access callback' => 'og_rules_integration_access', + ); + return $items; +} + +/*function opigno_rules_condition_info() { + $items = array(); + $items['og_user_has_role'] = array( + 'label' => t('User has role'), + 'group' => t('Organic groups'), + 'parameter' => array( + 'role' => array( + 'type' => 'text', + 'label' => t('Role'), + 'description' => t('The role to check for.'), + 'options list' => 'opigno_rules_user_has_roles_options_list', + 'restriction' => 'input', + ), + 'group' => array( + 'type' => array_keys(og_get_all_group_entity()), + 'label' => t('Group'), + 'description' => t('The group for which permission should be checked.'), + ), + 'account' => array( + 'type' => 'user', + 'label' => t('User'), + ), + ), + 'base' => 'opigno_rules_user_has_roles', + 'access callback' => 'og_rules_integration_access', + ); + + return $items; +}*/ + +function opigno_rules_event_info() { + return array( + 'og_user_was_granted_role' => array( + 'variables' => array( + 'entity_type' => array( + 'type' => 'token', + 'label' => t("The type of the entity the user is getting role to"), + ), + 'node_id' => array( + 'type' => 'integer', + 'label' => t('Node id'), + ), + 'user_id' => array( + 'type' => 'integer', + 'label' => t('User id'), + ), + 'role_id' => array( + 'type' => 'integer', + 'label' => t('Role id'), + ), + ), + 'group' => t('OG membership'), + 'label' => t('User has been granted a role'), + 'help' => t("A user has been granted a role"), + ), + 'og_user_was_revoked_role' => array( + 'variables' => array( + 'entity_type' => array( + 'type' => 'token', + 'label' => t("The type of the entity the user is getting the role taken from"), + ), + 'node_id' => array( + 'type' => 'integer', + 'label' => t('Node id'), + ), + 'user_id' => array( + 'type' => 'integer', + 'label' => t('User id'), + ), + 'role_id' => array( + 'type' => 'integer', + 'label' => t('Role id'), + ), + ), + 'group' => t('OG membership'), + 'label' => t('User has been revoked a role'), + 'help' => t("A user has been revoked a role"), + ), + ); +} +//*/ + +function opigno_load_og_membership($user, EntityDrupalWrapper $group, $settings) { + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'og_membership', '=') + ->propertyCondition('gid', $group->getIdentifier(), '=') + ->propertyCondition('group_type', $group->type(), '=') + ->propertyCondition('entity_type', 'user', '=') + ->propertyCondition('etid', $user->uid, '='); + $result = $query->execute(); + + if (!empty($result['og_membership'])) { + $og_membership = og_membership_load(current($result['og_membership'])->id); + } + else { + $og_membership = NULL; + } + return array('og_membership' => $og_membership); +} diff --git a/opigno.rules_defaults.inc b/opigno.rules_defaults.inc new file mode 100755 index 0000000..0868de1 --- /dev/null +++ b/opigno.rules_defaults.inc @@ -0,0 +1,42 @@ +name = 'opigno_my_courses'; + $view->description = ''; + $view->tag = 'default'; + $view->base_table = 'og_membership'; + $view->human_name = 'My courses'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['title'] = 'My courses'; + $handler->display->display_options['use_more_always'] = FALSE; + $handler->display->display_options['access']['type'] = 'role'; + $handler->display->display_options['access']['role'] = array( + 2 => '2', + ); + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['disable_sql_rewrite'] = TRUE; + $handler->display->display_options['query']['options']['distinct'] = TRUE; + $handler->display->display_options['query']['options']['pure_distinct'] = TRUE; + $handler->display->display_options['exposed_form']['type'] = 'better_exposed_filters'; + $handler->display->display_options['exposed_form']['options']['autosubmit'] = TRUE; + $handler->display->display_options['exposed_form']['options']['bef'] = array( + 'general' => array( + 'allow_secondary' => 0, + 'secondary_label' => 'Advanced options', + ), + 'opigno_course_categories_tid' => array( + 'bef_format' => 'default', + 'more_options' => array( + 'bef_select_all_none' => FALSE, + 'bef_collapsible' => 0, + 'is_secondary' => 0, + 'any_label' => '', + 'bef_filter_description' => '', + 'tokens' => array( + 'available' => array( + 0 => 'global_types', + ), + ), + 'rewrite' => array( + 'filter_rewrite_values' => '', + ), + ), + ), + ); + $handler->display->display_options['exposed_form']['options']['input_required'] = 0; + $handler->display->display_options['exposed_form']['options']['text_input_required_format'] = 'html'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '30'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['style_options']['grouping'] = array( + 0 => array( + 'field' => 'title_1', + 'rendered' => 1, + 'rendered_strip' => 0, + ), + ); + $handler->display->display_options['row_plugin'] = 'fields'; + /* Relationship: OG membership: Group Node from OG membership */ + $handler->display->display_options['relationships']['og_membership_related_node_group']['id'] = 'og_membership_related_node_group'; + $handler->display->display_options['relationships']['og_membership_related_node_group']['table'] = 'og_membership'; + $handler->display->display_options['relationships']['og_membership_related_node_group']['field'] = 'og_membership_related_node_group'; + /* Relationship: Entity Reference: Referencing entity */ + $handler->display->display_options['relationships']['reverse_opigno_class_courses_node']['id'] = 'reverse_opigno_class_courses_node'; + $handler->display->display_options['relationships']['reverse_opigno_class_courses_node']['table'] = 'node'; + $handler->display->display_options['relationships']['reverse_opigno_class_courses_node']['field'] = 'reverse_opigno_class_courses_node'; + $handler->display->display_options['relationships']['reverse_opigno_class_courses_node']['relationship'] = 'og_membership_related_node_group'; + $handler->display->display_options['relationships']['reverse_opigno_class_courses_node']['label'] = 'Class referencing the course'; + + + /* Field: Content: course_image */ + $handler->display->display_options['fields']['opigno_course_image']['id'] = 'opigno_course_image'; + $handler->display->display_options['fields']['opigno_course_image']['table'] = 'field_data_opigno_course_image'; + $handler->display->display_options['fields']['opigno_course_image']['field'] = 'opigno_course_image'; + $handler->display->display_options['fields']['opigno_course_image']['relationship'] = 'og_membership_related_node_group'; + $handler->display->display_options['fields']['opigno_course_image']['label'] = ''; + $handler->display->display_options['fields']['opigno_course_image']['element_label_colon'] = FALSE; + $handler->display->display_options['fields']['opigno_course_image']['click_sort_column'] = 'fid'; + $handler->display->display_options['fields']['opigno_course_image']['settings'] = array( + 'image_style' => 'thumbnail', + 'image_link' => '', + ); + + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['relationship'] = 'og_membership_related_node_group'; + $handler->display->display_options['fields']['title']['label'] = ''; + $handler->display->display_options['fields']['title']['element_label_colon'] = FALSE; + + /* Field: Content: Description */ + $handler->display->display_options['fields']['body']['id'] = 'body'; + $handler->display->display_options['fields']['body']['table'] = 'field_data_body'; + $handler->display->display_options['fields']['body']['field'] = 'body'; + $handler->display->display_options['fields']['body']['relationship'] = 'og_membership_related_node_group'; + $handler->display->display_options['fields']['body']['label'] = 'Description'; + $handler->display->display_options['fields']['body']['type'] = 'text_summary_or_trimmed'; + $handler->display->display_options['fields']['body']['settings'] = array( + 'trim_length' => '600', + ); + + /* Field: Global: PHP */ + $handler->display->display_options['fields']['php_teachers']['id'] = 'php_teachers'; + $handler->display->display_options['fields']['php_teachers']['table'] = 'views'; + $handler->display->display_options['fields']['php_teachers']['field'] = 'php'; + $handler->display->display_options['fields']['php_teachers']['label'] = 'Teachers'; + $handler->display->display_options['fields']['php_teachers']['hide_empty'] = TRUE; + $handler->display->display_options['fields']['php_teachers']['use_php_setup'] = 0; + $handler->display->display_options['fields']['php_teachers']['php_output'] = 'node_og_membership_nid); +?>'; + $handler->display->display_options['fields']['php_teachers']['use_php_click_sortable'] = '0'; + $handler->display->display_options['fields']['php_teachers']['php_click_sortable'] = ''; + + /* Field: OG membership: Created */ + $handler->display->display_options['fields']['created']['id'] = 'created'; + $handler->display->display_options['fields']['created']['table'] = 'og_membership'; + $handler->display->display_options['fields']['created']['field'] = 'created'; + $handler->display->display_options['fields']['created']['label'] = 'Member since'; + $handler->display->display_options['fields']['created']['date_format'] = 'raw time ago'; + /* Field: Content: Nid */ + $handler->display->display_options['fields']['nid']['id'] = 'nid'; + $handler->display->display_options['fields']['nid']['table'] = 'node'; + $handler->display->display_options['fields']['nid']['field'] = 'nid'; + $handler->display->display_options['fields']['nid']['relationship'] = 'og_membership_related_node_group'; + $handler->display->display_options['fields']['nid']['label'] = ''; + $handler->display->display_options['fields']['nid']['exclude'] = TRUE; + $handler->display->display_options['fields']['nid']['element_label_colon'] = FALSE; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title_1']['id'] = 'title_1'; + $handler->display->display_options['fields']['title_1']['table'] = 'node'; + $handler->display->display_options['fields']['title_1']['field'] = 'title'; + $handler->display->display_options['fields']['title_1']['relationship'] = 'reverse_opigno_class_courses_node'; + $handler->display->display_options['fields']['title_1']['label'] = ''; + $handler->display->display_options['fields']['title_1']['exclude'] = TRUE; + $handler->display->display_options['fields']['title_1']['element_label_colon'] = FALSE; + /* Contextual filter: OG membership: Entity id */ + $handler->display->display_options['arguments']['etid']['id'] = 'etid'; + $handler->display->display_options['arguments']['etid']['table'] = 'og_membership'; + $handler->display->display_options['arguments']['etid']['field'] = 'etid'; + $handler->display->display_options['arguments']['etid']['default_action'] = 'default'; + $handler->display->display_options['arguments']['etid']['default_argument_type'] = 'current_user'; + $handler->display->display_options['arguments']['etid']['summary']['number_of_records'] = '0'; + $handler->display->display_options['arguments']['etid']['summary']['format'] = 'default_summary'; + $handler->display->display_options['arguments']['etid']['summary_options']['items_per_page'] = '25'; + $handler->display->display_options['arguments']['etid']['specify_validation'] = TRUE; + $handler->display->display_options['arguments']['etid']['validate']['type'] = 'user'; + /* Filter criterion: Content: Type */ + $handler->display->display_options['filters']['type']['id'] = 'type'; + $handler->display->display_options['filters']['type']['table'] = 'node'; + $handler->display->display_options['filters']['type']['field'] = 'type'; + $handler->display->display_options['filters']['type']['relationship'] = 'og_membership_related_node_group'; + $handler->display->display_options['filters']['type']['value'] = array( + 'course' => 'course', + ); + /* Filter criterion: OG membership: State */ + $handler->display->display_options['filters']['state']['id'] = 'state'; + $handler->display->display_options['filters']['state']['table'] = 'og_membership'; + $handler->display->display_options['filters']['state']['field'] = 'state'; + $handler->display->display_options['filters']['state']['value'] = array( + 1 => '1', + ); + /* Filter criterion: Content: Course categories (opigno_course_categories) */ + $handler->display->display_options['filters']['opigno_course_categories_tid']['id'] = 'opigno_course_categories_tid'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['table'] = 'field_data_opigno_course_categories'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['field'] = 'opigno_course_categories_tid'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['relationship'] = 'og_membership_related_node_group'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['exposed'] = TRUE; + $handler->display->display_options['filters']['opigno_course_categories_tid']['expose']['operator_id'] = 'opigno_course_categories_tid_op'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['expose']['label'] = 'Course category'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['expose']['operator'] = 'opigno_course_categories_tid_op'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['expose']['identifier'] = 'opigno_course_categories_tid'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['expose']['remember_roles'] = array( + 2 => '2', + 1 => 0, + 3 => 0, + 4 => 0, + ); + $handler->display->display_options['filters']['opigno_course_categories_tid']['type'] = 'select'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['vocabulary'] = 'course_categories'; + /* Filter criterion: OG membership: Entity_type */ + $handler->display->display_options['filters']['entity_type']['id'] = 'entity_type'; + $handler->display->display_options['filters']['entity_type']['table'] = 'og_membership'; + $handler->display->display_options['filters']['entity_type']['field'] = 'entity_type'; + $handler->display->display_options['filters']['entity_type']['value'] = 'user'; + + /* Display: My courses */ + $handler = $view->new_display('page', 'My courses', 'page_my_courses'); + $handler->display->display_options['path'] = 'my-courses'; + + /* Display: Entity Reference */ + $handler = $view->new_display('entityreference', 'Entity Reference', 'entityreference'); + $handler->display->display_options['defaults']['title'] = FALSE; + $handler->display->display_options['pager']['type'] = 'some'; + $handler->display->display_options['defaults']['style_plugin'] = FALSE; + $handler->display->display_options['style_plugin'] = 'entityreference_style'; + $handler->display->display_options['style_options']['search_fields'] = array( + 'nid' => 'nid', + ); + $handler->display->display_options['defaults']['style_options'] = FALSE; + $handler->display->display_options['defaults']['row_plugin'] = FALSE; + $handler->display->display_options['row_plugin'] = 'entityreference_fields'; + $handler->display->display_options['defaults']['row_options'] = FALSE; + $translatables['opigno_my_courses'] = array( + t('Master'), + t('My courses'), + t('more'), + t('Apply'), + t('Reset'), + t('Sort by'), + t('Asc'), + t('Desc'), + t('Items per page'), + t('- All -'), + t('Offset'), + t('« first'), + t('‹ previous'), + t('next ›'), + t('last »'), + t('Group node from OG membership'), + t('Class referencing the course'), + t('Title'), + t('Member since'), + t('All'), + t('Course category'), + t('Entity Reference'), + ); + + + + $export['opigno_my_courses'] = $view; + + /** + * Course catalogue + */ + $view = new view(); + $view->name = 'opigno_course_catalgue'; + $view->description = ''; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'Training catalogue'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['title'] = 'Training catalogue'; + $handler->display->display_options['use_more_always'] = FALSE; + $handler->display->display_options['access']['type'] = 'role'; + $handler->display->display_options['access']['role'] = array( + 2 => '2', + ); + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['disable_sql_rewrite'] = TRUE; + $handler->display->display_options['query']['options']['distinct'] = TRUE; + $handler->display->display_options['query']['options']['query_comment'] = 'exclude_own_groups'; + $handler->display->display_options['exposed_form']['type'] = 'better_exposed_filters'; + $handler->display->display_options['exposed_form']['options']['autosubmit'] = TRUE; + $handler->display->display_options['exposed_form']['options']['bef'] = array( + 'general' => array( + 'allow_secondary' => 0, + 'secondary_label' => 'Advanced options', + ), + 'opigno_course_categories_tid' => array( + 'bef_format' => 'default', + 'more_options' => array( + 'bef_select_all_none' => FALSE, + 'bef_collapsible' => 0, + 'is_secondary' => 0, + 'any_label' => '', + 'bef_filter_description' => '', + 'tokens' => array( + 'available' => array( + 0 => 'global_types', + ), + ), + 'rewrite' => array( + 'filter_rewrite_values' => '', + ), + ), + ), + ); + $handler->display->display_options['exposed_form']['options']['input_required'] = 0; + $handler->display->display_options['exposed_form']['options']['text_input_required_format'] = 'html'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '30'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['style_options']['grouping'] = array( + 0 => array( + 'field' => 'group_group_1', + 'rendered' => 1, + 'rendered_strip' => 0, + ), + ); + $handler->display->display_options['row_plugin'] = 'fields'; + /* Relationship: Entity Reference: Referencing entity */ + $handler->display->display_options['relationships']['reverse_opigno_class_courses_node']['id'] = 'reverse_opigno_class_courses_node'; + $handler->display->display_options['relationships']['reverse_opigno_class_courses_node']['table'] = 'node'; + $handler->display->display_options['relationships']['reverse_opigno_class_courses_node']['field'] = 'reverse_opigno_class_courses_node'; + $handler->display->display_options['relationships']['reverse_opigno_class_courses_node']['relationship'] = 'og_membership_related_node_group'; + $handler->display->display_options['relationships']['reverse_opigno_class_courses_node']['label'] = 'Class referencing the course'; + + /* Field: Content: Group */ + $handler->display->display_options['fields']['group_group']['id'] = 'group_group'; + $handler->display->display_options['fields']['group_group']['table'] = 'field_data_group_group'; + $handler->display->display_options['fields']['group_group']['field'] = 'group_group'; + $handler->display->display_options['fields']['group_group']['label'] = ''; + $handler->display->display_options['fields']['group_group']['element_label_colon'] = FALSE; + $handler->display->display_options['fields']['group_group']['type'] = 'og_group_subscribe'; + $handler->display->display_options['fields']['group_group']['settings'] = array( + 'field_name' => '0', + ); + + /* Field: Content: course_image */ + $handler->display->display_options['fields']['opigno_course_image']['id'] = 'opigno_course_image'; + $handler->display->display_options['fields']['opigno_course_image']['table'] = 'field_data_opigno_course_image'; + $handler->display->display_options['fields']['opigno_course_image']['field'] = 'opigno_course_image'; + $handler->display->display_options['fields']['opigno_course_image']['label'] = ''; + $handler->display->display_options['fields']['opigno_course_image']['element_label_colon'] = FALSE; + $handler->display->display_options['fields']['opigno_course_image']['click_sort_column'] = 'fid'; + $handler->display->display_options['fields']['opigno_course_image']['settings'] = array( + 'image_style' => 'thumbnail', + 'image_link' => '', + ); + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['label'] = ''; + $handler->display->display_options['fields']['title']['alter']['word_boundary'] = FALSE; + $handler->display->display_options['fields']['title']['alter']['ellipsis'] = FALSE; + $handler->display->display_options['fields']['title']['element_label_colon'] = FALSE; + + /* Field: Content: Body */ + $handler->display->display_options['fields']['body']['id'] = 'body'; + $handler->display->display_options['fields']['body']['table'] = 'field_data_body'; + $handler->display->display_options['fields']['body']['field'] = 'body'; + $handler->display->display_options['fields']['body']['label'] = 'Description'; + $handler->display->display_options['fields']['body']['type'] = 'text_summary_or_trimmed'; + $handler->display->display_options['fields']['body']['settings'] = array( + 'trim_length' => '600', + ); + + if (module_exists("opigno_course_quota_app")) + { + /* Field: Global: PHP */ + $handler->display->display_options['fields']['php_available_places']['id'] = 'php_available_places'; + $handler->display->display_options['fields']['php_available_places']['table'] = 'views'; + $handler->display->display_options['fields']['php_available_places']['field'] = 'php'; + $handler->display->display_options['fields']['php_available_places']['label'] = 'Available places'; + $handler->display->display_options['fields']['php_available_places']['hide_empty'] = TRUE; + $handler->display->display_options['fields']['php_available_places']['use_php_setup'] = 0; + $handler->display->display_options['fields']['php_available_places']['php_output'] = 'nid); +$info=opigno_course_quota_app_students_info($node); +if ((isset($info[\'places\']))&&($info[\'places\']!=-1)) +{ + print $info[\'available_places\']; +} +?>'; + $handler->display->display_options['fields']['php_available_places']['use_php_click_sortable'] = '0'; + $handler->display->display_options['fields']['php_available_places']['php_click_sortable'] = ''; + } + + /* Field: Global: PHP Teachers */ + $handler->display->display_options['fields']['php_course_teachers']['id'] = 'php_course_teachers'; + $handler->display->display_options['fields']['php_course_teachers']['table'] = 'views'; + $handler->display->display_options['fields']['php_course_teachers']['field'] = 'php'; + $handler->display->display_options['fields']['php_course_teachers']['label'] = 'Teachers'; + $handler->display->display_options['fields']['php_course_teachers']['hide_empty'] = TRUE; + $handler->display->display_options['fields']['php_course_teachers']['use_php_setup'] = 0; + $handler->display->display_options['fields']['php_course_teachers']['php_output'] = 'nid); +?>'; + $handler->display->display_options['fields']['php_course_teachers']['use_php_click_sortable'] = '0'; + $handler->display->display_options['fields']['php_course_teachers']['php_click_sortable'] = ''; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title_1']['id'] = 'title_1'; + $handler->display->display_options['fields']['title_1']['table'] = 'node'; + $handler->display->display_options['fields']['title_1']['field'] = 'title'; + $handler->display->display_options['fields']['title_1']['relationship'] = 'reverse_opigno_class_courses_node'; + $handler->display->display_options['fields']['title_1']['label'] = ''; + $handler->display->display_options['fields']['title_1']['exclude'] = TRUE; + $handler->display->display_options['fields']['title_1']['element_label_colon'] = FALSE; + + if (module_exists("opigno_in_house_training_app")) + { + /* Field: Global: PHP */ + $handler->display->display_options['fields']['php_in_house_trainings']['id'] = 'php_in_house_trainings'; + $handler->display->display_options['fields']['php_in_house_trainings']['table'] = 'views'; + $handler->display->display_options['fields']['php_in_house_trainings']['field'] = 'php'; + $handler->display->display_options['fields']['php_in_house_trainings']['label'] = 'In house training'; + $handler->display->display_options['fields']['php_in_house_trainings']['hide_empty'] = TRUE; + $handler->display->display_options['fields']['php_in_house_trainings']['use_php_setup'] = 0; + $handler->display->display_options['fields']['php_in_house_trainings']['php_output'] = 'nid); +?>'; + $handler->display->display_options['fields']['php_in_house_trainings']['use_php_click_sortable'] = '0'; + $handler->display->display_options['fields']['php_in_house_trainings']['php_click_sortable'] = ''; + } + + if (module_exists("opigno_webex_app")) + { + /* Field: Global: PHP */ + $handler->display->display_options['fields']['php_webex']['id'] = 'php_webex'; + $handler->display->display_options['fields']['php_webex']['table'] = 'views'; + $handler->display->display_options['fields']['php_webex']['field'] = 'php'; + $handler->display->display_options['fields']['php_webex']['label'] = 'Webex meetings'; + $handler->display->display_options['fields']['php_webex']['hide_empty'] = TRUE; + $handler->display->display_options['fields']['php_webex']['use_php_setup'] = 0; + $handler->display->display_options['fields']['php_webex']['php_output'] = 'nid); +?>'; + $handler->display->display_options['fields']['php_webex']['use_php_click_sortable'] = '0'; + $handler->display->display_options['fields']['php_webex']['php_click_sortable'] = ''; + } + + /* Filter criterion: Content: Published */ + $handler->display->display_options['filters']['status']['id'] = 'status'; + $handler->display->display_options['filters']['status']['table'] = 'node'; + $handler->display->display_options['filters']['status']['field'] = 'status'; + $handler->display->display_options['filters']['status']['value'] = 1; + $handler->display->display_options['filters']['status']['group'] = 1; + $handler->display->display_options['filters']['status']['expose']['operator'] = FALSE; + /* Filter criterion: Content: Type */ + $handler->display->display_options['filters']['type']['id'] = 'type'; + $handler->display->display_options['filters']['type']['table'] = 'node'; + $handler->display->display_options['filters']['type']['field'] = 'type'; + $handler->display->display_options['filters']['type']['value'] = array( + 'course' => 'course', + ); + /* Filter criterion: Content: Nid */ + $handler->display->display_options['filters']['nid']['id'] = 'nid'; + $handler->display->display_options['filters']['nid']['table'] = 'node'; + $handler->display->display_options['filters']['nid']['field'] = 'nid'; + $handler->display->display_options['filters']['nid']['value']['value'] = '0'; + /* Filter criterion: Content: Course categories (opigno_course_categories) */ + $handler->display->display_options['filters']['opigno_course_categories_tid']['id'] = 'opigno_course_categories_tid'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['table'] = 'field_data_opigno_course_categories'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['field'] = 'opigno_course_categories_tid'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['exposed'] = TRUE; + $handler->display->display_options['filters']['opigno_course_categories_tid']['expose']['operator_id'] = 'opigno_course_categories_tid_op'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['expose']['label'] = 'Course category'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['expose']['operator'] = 'opigno_course_categories_tid_op'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['expose']['identifier'] = 'opigno_course_categories_tid'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['expose']['remember_roles'] = array( + 2 => '2', + 1 => 0, + 3 => 0, + 4 => 0, + ); + $handler->display->display_options['filters']['opigno_course_categories_tid']['type'] = 'select'; + $handler->display->display_options['filters']['opigno_course_categories_tid']['vocabulary'] = 'course_categories'; + + /* Display: Page */ + $handler = $view->new_display('page', 'Page', 'page'); + $handler->display->display_options['path'] = 'course-catalogue'; + $translatables['opigno_course_catalgue'] = array( + t('Master'), + t('Training catalogue'), + t('more'), + t('Apply'), + t('Reset'), + t('Sort by'), + t('Asc'), + t('Desc'), + t('Items per page'), + t('- All -'), + t('Offset'), + t('« first'), + t('‹ previous'), + t('next ›'), + t('last »'), + t('Class referencing the course'), + t('Title'), + t('Subscribe'), + t('Course category'), + t('Page'), + ); + + $export['opigno_course_catalgue'] = $view; + + return $export; +} diff --git a/templates/opigno--tool.tpl.php b/templates/opigno--tool.tpl.php new file mode 100755 index 0000000..5deedf7 --- /dev/null +++ b/templates/opigno--tool.tpl.php @@ -0,0 +1,24 @@ + +
    +
    +

    + + array('class' => array('opigno-tool-link')))); ?> + + + +

    + +

    +
    \ No newline at end of file diff --git a/templates/opigno--tools.tpl.php b/templates/opigno--tools.tpl.php new file mode 100755 index 0000000..b09f254 --- /dev/null +++ b/templates/opigno--tools.tpl.php @@ -0,0 +1,11 @@ + +
    + + + +
    \ No newline at end of file diff --git a/tests/OpignoWebTestCase.test b/tests/OpignoWebTestCase.test new file mode 100644 index 0000000..6605b9a --- /dev/null +++ b/tests/OpignoWebTestCase.test @@ -0,0 +1,128 @@ + array('manager', 'teacher')) + * + * @return object + */ + protected function createCourse($title = NULL, $creator = NULL, $private = NULL, $members = array()) { + $settings = array( + 'type' => OPIGNO_COURSE_BUNDLE, + 'title' => $title ? $title : $this->randomName(8), + 'body' => array( + LANGUAGE_NONE => array( + array('value' => $this->randomName(16)), + ), + ), + ); + if (!empty($creator->uid)) { + $settings['uid'] = $creator->uid; + } + if (isset($private)) { + $settings['group_access'][LANGUAGE_NONE][0]['value'] = $private; + } + $node = $this->drupalCreateNode($settings); + + $this->assertTrue(!empty($node->nid), 'Created a new course.'); + + if (!empty($members)) { + foreach ($members as $uid => $roles) { + $this->addMemberToCourse($node, $uid, $roles); + } + } + + return $node; + } + + /** + * Add member to course. + * + * @param object $node + * @param int $uid + * @param array $roles + */ + protected function addMemberToCourse($node, $uid, $roles = array('member')) { + og_membership_create('node', $node->nid, 'user', $uid, 'og_user_node'); + foreach ($roles as $role) { + $rid = $this->getRoleId($role); + if (!empty($rid)) { + og_role_grant('node', $node->nid, $uid, $rid); + } + else { + $this->fail("Could not find the role '$role'."); + } + } + } + + /** + * Create a role and set the permissions. + * + * @param string $role_name + * @param array $permissions = array() + * + * @return object + */ + protected function createRole($role_name, $permissions = array()) { + $role = og_role_create($role_name, 'node', 0, OPIGNO_COURSE_BUNDLE); + og_role_save($role); + og_role_grant_permissions($role->rid, $permissions); + return $role; + } + + + /** + * Fetch the role ID by name. + * + * @param string $role_name + * + * @return int + */ + protected function getRoleId($role_name) { + $rid = db_select('og_role', 'r') + ->fields('r', array('rid')) + ->condition('r.name', $role_name) + ->condition('group_bundle', OPIGNO_COURSE_BUNDLE) + ->execute() + ->fetchField(); + return !empty($rid) ? $rid : 0; + } + + /** + * Helper function to enable a block. + */ + protected function enableBlock($module, $delta, $region = 'sidebar_first', $pages_visible = array(), $pages_unvisible = array()) { + $block = array( + 'module' => $module, + 'delta' => $delta, + 'theme' => variable_get('theme_default', 'bartik'), + 'status' => 1, + 'weight' => 0, + 'region' => $region, + 'visibility' => (int) empty($pages_visible), + 'pages' => implode("\n", !empty($pages_visible) ? $pages_visible : $pages_unvisible), + 'cache' => -1, + ); + + $query = db_insert('block')->fields(array('module', 'delta', 'theme', 'status', 'weight', 'region', 'visibility', 'pages', 'cache')); + $query->values($block); + $query->execute(); + } +}