To ensure security and privacy, RSS feed URLs contain a special token that identifies the user they are for. This prevents other users from accessing areas of the site where they are not allowed.
The token is automatically created the first time you access an area that produces an RSS feed. If you think that your RSS feed token has been compromised, you can request a new one by clicking the reset link. Please note that your current RSS feed URLs will then become invalid.
'; $string['rsstype'] = 'RSS feed for this activity'; +$string['save'] = 'Save'; $string['saveandnext'] = 'Save and show next'; $string['savedat'] = 'Saved at:'; $string['savechanges'] = 'Save changes'; diff --git a/lib/amd/build/modal.min.js b/lib/amd/build/modal.min.js index a469ddad71c..d1b6f8e91e8 100644 --- a/lib/amd/build/modal.min.js +++ b/lib/amd/build/modal.min.js @@ -1 +1 @@ -define(["jquery","core/templates","core/notification","core/key_codes","core/custom_interaction_events","core/modal_backdrop","core/event","core/modal_events"],function(a,b,c,d,e,f,g,h){var i,j={CONTAINER:'[data-region="modal-container"]',MODAL:'[data-region="modal"]',HEADER:'[data-region="header"]',TITLE:'[data-region="title"]',BODY:'[data-region="body"]',FOOTER:'[data-region="footer"]',HIDE:'[data-action="hide"]',DIALOG:"[role=dialog]",MENU_BAR:"[role=menubar]",HAS_Z_INDEX:".moodle-has-zindex",CAN_RECEIVE_FOCUS:'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]'},k={LOADING:"core/loading",BACKDROP:"core/modal_backdrop"},l=function(b){this.root=a(b),this.modal=this.root.find(j.MODAL),this.header=this.modal.find(j.HEADER),this.title=this.header.find(j.TITLE),this.body=this.modal.find(j.BODY),this.footer=this.modal.find(j.FOOTER),this.hiddenSiblings=[],this.isAttached=!1,this.bodyJS=null,this.footerJS=null,this.root.is(j.CONTAINER)||c.exception({message:"Element is not a modal container"}),this.modal.length||c.exception({message:"Container does not contain a modal"}),this.header.length||c.exception({message:"Modal is missing a header region"}),this.title.length||c.exception({message:"Modal header is missing a title region"}),this.body.length||c.exception({message:"Modal is missing a body region"}),this.footer.length||c.exception({message:"Modal is missing a footer region"}),this.registerEventListeners()};return l.prototype.attachToDOM=function(){this.isAttached||(a("body").append(this.root),this.bodyJS&&(b.runTemplateJS(this.bodyJS),this.bodyJS=null),this.footerJS&&(b.runTemplateJS(this.footerJS),this.footerJS=null),this.isAttached=!0)},l.prototype.countOtherVisibleModals=function(){var b=0;return a("body").find(j.CONTAINER).each(function(c,d){d=a(d),!this.root.is(d)&&d.hasClass("show")&&b++}.bind(this)),b},l.prototype.getBackdrop=function(){return i||(i=b.render(k.BACKDROP,{}).then(function(b){var c=a(b);return new f(c)}).fail(c.exception)),i},l.prototype.getRoot=function(){return this.root},l.prototype.getModal=function(){return this.modal},l.prototype.getTitle=function(){return this.title},l.prototype.getBody=function(){return this.body},l.prototype.getFooter=function(){return this.footer},l.prototype.setTitle=function(a){var b=this.getTitle();b.html(a)},l.prototype.setBody=function(a){var c=this.getBody();"string"==typeof a?(c.html(a),g.notifyFilterContentUpdated(c),this.getRoot().trigger(h.bodyRendered,this)):b.render(k.LOADING,{}).done(function(d){c.html(d),a.done(function(a,d){c.html(a),d&&(this.isAttached?b.runTemplateJS(d):this.bodyJS=d),g.notifyFilterContentUpdated(c),this.getRoot().trigger(h.bodyRendered,this)}.bind(this))}.bind(this))},l.prototype.setFooter=function(a){var c=this.getFooter();"string"==typeof a?c.html(a):b.render(k.LOADING,{}).done(function(d){c.html(d),a.done(function(a,d){c.html(a),d&&(this.isAttached?b.runTemplateJS(d):this.footerJS=d)}.bind(this))}.bind(this))},l.prototype.setLarge=function(){this.isLarge()||this.getRoot().addClass("large")},l.prototype.isLarge=function(){return this.getRoot().hasClass("large")},l.prototype.setSmall=function(){this.isSmall()||this.getRoot().removeClass("large")},l.prototype.isSmall=function(){return!this.getRoot().hasClass("large")},l.prototype.calculateZIndex=function(){var b=a(j.DIALOG+", "+j.MENU_BAR+", "+j.HAS_Z_INDEX),c=parseInt(this.root.css("z-index"));return b.each(function(b,d){d=a(d);var e=d.css("z-index")?parseInt(d.css("z-index")):0;e>c&&(c=e)}),c},l.prototype.isVisible=function(){return this.root.hasClass("show")},l.prototype.hasFocus=function(){var b=a(document.activeElement);return this.root.is(b)||this.root.has(b).length},l.prototype.hasTransitions=function(){return this.getRoot().hasClass("fade")},l.prototype.show=function(){this.isVisible()||(this.isAttached||this.attachToDOM(),this.getBackdrop().done(function(b){var c=this.calculateZIndex(),d=c+2,e=d-1;this.root.css("z-index",d),b.setZIndex(e),b.show(),this.root.removeClass("hide").addClass("show"),this.accessibilityShow(),this.getTitle().focus(),a("body").addClass("modal-open"),this.root.trigger(h.shown,this)}.bind(this)))},l.prototype.hide=function(){this.isVisible()&&this.getBackdrop().done(function(b){this.countOtherVisibleModals()||(b.hide(),a("body").removeClass("modal-open"));var c=parseInt(this.root.css("z-index"));this.root.css("z-index",""),b.setZIndex(c-3),this.accessibilityHide(),this.hasTransitions()?this.getRoot().one("transitionend webkitTransitionEnd oTransitionEnd",function(){this.getRoot().removeClass("show").addClass("hide")}.bind(this)):this.getRoot().removeClass("show").addClass("hide"),this.root.trigger(h.hidden,this)}.bind(this))},l.prototype.destroy=function(){this.root.remove(),this.root.trigger(h.destroyed,this)},l.prototype.accessibilityShow=function(){a("body").children().each(function(b,c){if(!this.root.is(c)){c=a(c);var d=c.attr("aria-hidden");"true"!==d&&(c.data("previous-aria-hidden",d),this.hiddenSiblings.push(c),c.attr("aria-hidden","true"))}}.bind(this)),this.root.attr("aria-hidden","false")},l.prototype.accessibilityHide=function(){this.root.attr("aria-hidden","true"),a.each(this.hiddenSiblings,function(b,c){c=a(c);var d=c.data("previous-aria-hidden");"undefined"==typeof d?c.removeAttr("aria-hidden"):c.attr("aria-hidden",d)}),this.hiddenSiblings=[]},l.prototype.handleTabLock=function(b){if(this.hasFocus()){var c=a(document.activeElement),d=this.modal.find(j.CAN_RECEIVE_FOCUS),e=d.first(),f=d.last();c.is(e)&&b.shiftKey?(f.focus(),b.preventDefault()):c.is(f)&&!b.shiftKey&&(e.focus(),b.preventDefault())}},l.prototype.registerEventListeners=function(){this.getRoot().on("keydown",function(a){this.isVisible()&&(a.keyCode==d.tab?this.handleTabLock(a):a.keyCode==d.escape&&this.hide())}.bind(this)),e.define(this.getModal(),[e.events.activate]),this.getModal().on(e.events.activate,j.HIDE,function(a,b){this.hide(),b.originalEvent.preventDefault()}.bind(this))},l}); \ No newline at end of file +define(["jquery","core/templates","core/notification","core/key_codes","core/custom_interaction_events","core/modal_backdrop","core/event","core/modal_events"],function(a,b,c,d,e,f,g,h){var i,j={CONTAINER:'[data-region="modal-container"]',MODAL:'[data-region="modal"]',HEADER:'[data-region="header"]',TITLE:'[data-region="title"]',BODY:'[data-region="body"]',FOOTER:'[data-region="footer"]',HIDE:'[data-action="hide"]',DIALOG:"[role=dialog]",MENU_BAR:"[role=menubar]",HAS_Z_INDEX:".moodle-has-zindex",CAN_RECEIVE_FOCUS:'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]'},k={LOADING:"core/loading",BACKDROP:"core/modal_backdrop"},l=function(b){this.root=a(b),this.modal=this.root.find(j.MODAL),this.header=this.modal.find(j.HEADER),this.title=this.header.find(j.TITLE),this.body=this.modal.find(j.BODY),this.footer=this.modal.find(j.FOOTER),this.hiddenSiblings=[],this.isAttached=!1,this.bodyJS=null,this.footerJS=null,this.root.is(j.CONTAINER)||c.exception({message:"Element is not a modal container"}),this.modal.length||c.exception({message:"Container does not contain a modal"}),this.header.length||c.exception({message:"Modal is missing a header region"}),this.title.length||c.exception({message:"Modal header is missing a title region"}),this.body.length||c.exception({message:"Modal is missing a body region"}),this.footer.length||c.exception({message:"Modal is missing a footer region"}),this.registerEventListeners()};return l.prototype.attachToDOM=function(){this.isAttached||(a("body").append(this.root),this.bodyJS&&(b.runTemplateJS(this.bodyJS),this.bodyJS=null),this.footerJS&&(b.runTemplateJS(this.footerJS),this.footerJS=null),this.isAttached=!0)},l.prototype.countOtherVisibleModals=function(){var b=0;return a("body").find(j.CONTAINER).each(function(c,d){d=a(d),!this.root.is(d)&&d.hasClass("show")&&b++}.bind(this)),b},l.prototype.getBackdrop=function(){return i||(i=b.render(k.BACKDROP,{}).then(function(b){var c=a(b);return new f(c)}).fail(c.exception)),i},l.prototype.getRoot=function(){return this.root},l.prototype.getModal=function(){return this.modal},l.prototype.getTitle=function(){return this.title},l.prototype.getBody=function(){return this.body},l.prototype.getFooter=function(){return this.footer},l.prototype.setTitle=function(a){var b=this.getTitle();b.html(a)},l.prototype.setBody=function(a){var c=this.getBody();"string"==typeof a?(c.html(a),g.notifyFilterContentUpdated(c),this.getRoot().trigger(h.bodyRendered,this)):b.render(k.LOADING,{}).done(function(d){c.html(d),a.done(function(a,d){c.html(a),d&&(this.isAttached?b.runTemplateJS(d):this.bodyJS=d),g.notifyFilterContentUpdated(c),this.getRoot().trigger(h.bodyRendered,this)}.bind(this))}.bind(this))},l.prototype.setFooter=function(a){var c=this.getFooter();"string"==typeof a?c.html(a):b.render(k.LOADING,{}).done(function(d){c.html(d),a.done(function(a,d){c.html(a),d&&(this.isAttached?b.runTemplateJS(d):this.footerJS=d)}.bind(this))}.bind(this))},l.prototype.setLarge=function(){this.isLarge()||this.getModal().addClass("modal-lg")},l.prototype.isLarge=function(){return this.getModal().hasClass("modal-lg")},l.prototype.setSmall=function(){this.isSmall()||this.getModal().removeClass("modal-lg")},l.prototype.isSmall=function(){return!this.getModal().hasClass("modal-lg")},l.prototype.calculateZIndex=function(){var b=a(j.DIALOG+", "+j.MENU_BAR+", "+j.HAS_Z_INDEX),c=parseInt(this.root.css("z-index"));return b.each(function(b,d){d=a(d);var e=d.css("z-index")?parseInt(d.css("z-index")):0;e>c&&(c=e)}),c},l.prototype.isVisible=function(){return this.root.hasClass("show")},l.prototype.hasFocus=function(){var b=a(document.activeElement);return this.root.is(b)||this.root.has(b).length},l.prototype.hasTransitions=function(){return this.getRoot().hasClass("fade")},l.prototype.show=function(){this.isVisible()||(this.isAttached||this.attachToDOM(),this.getBackdrop().done(function(b){var c=this.calculateZIndex(),d=c+2,e=d-1;this.root.css("z-index",d),b.setZIndex(e),b.show(),this.root.removeClass("hide").addClass("show"),this.accessibilityShow(),this.getTitle().focus(),a("body").addClass("modal-open"),this.root.trigger(h.shown,this)}.bind(this)))},l.prototype.hide=function(){this.isVisible()&&this.getBackdrop().done(function(b){this.countOtherVisibleModals()||(b.hide(),a("body").removeClass("modal-open"));var c=parseInt(this.root.css("z-index"));this.root.css("z-index",""),b.setZIndex(c-3),this.accessibilityHide(),this.hasTransitions()?this.getRoot().one("transitionend webkitTransitionEnd oTransitionEnd",function(){this.getRoot().removeClass("show").addClass("hide")}.bind(this)):this.getRoot().removeClass("show").addClass("hide"),this.root.trigger(h.hidden,this)}.bind(this))},l.prototype.destroy=function(){this.root.remove(),this.root.trigger(h.destroyed,this)},l.prototype.accessibilityShow=function(){a("body").children().each(function(b,c){if(!this.root.is(c)){c=a(c);var d=c.attr("aria-hidden");"true"!==d&&(c.data("previous-aria-hidden",d),this.hiddenSiblings.push(c),c.attr("aria-hidden","true"))}}.bind(this)),this.root.attr("aria-hidden","false")},l.prototype.accessibilityHide=function(){this.root.attr("aria-hidden","true"),a.each(this.hiddenSiblings,function(b,c){c=a(c);var d=c.data("previous-aria-hidden");"undefined"==typeof d?c.removeAttr("aria-hidden"):c.attr("aria-hidden",d)}),this.hiddenSiblings=[]},l.prototype.handleTabLock=function(b){if(this.hasFocus()){var c=a(document.activeElement),d=this.modal.find(j.CAN_RECEIVE_FOCUS),e=d.first(),f=d.last();c.is(e)&&b.shiftKey?(f.focus(),b.preventDefault()):c.is(f)&&!b.shiftKey&&(e.focus(),b.preventDefault())}},l.prototype.registerEventListeners=function(){this.getRoot().on("keydown",function(a){this.isVisible()&&(a.keyCode==d.tab?this.handleTabLock(a):a.keyCode==d.escape&&this.hide())}.bind(this)),e.define(this.getModal(),[e.events.activate]),this.getModal().on(e.events.activate,j.HIDE,function(a,b){this.hide(),b.originalEvent.preventDefault()}.bind(this))},l}); \ No newline at end of file diff --git a/lib/amd/build/modal_factory.min.js b/lib/amd/build/modal_factory.min.js index 715247b49a4..06f8a5f41d9 100644 --- a/lib/amd/build/modal_factory.min.js +++ b/lib/amd/build/modal_factory.min.js @@ -1 +1 @@ -define(["jquery","core/modal_events","core/modal_registry","core/modal","core/modal_save_cancel","core/modal_confirm","core/modal_cancel","core/templates","core/notification","core/custom_interaction_events"],function(a,b,c,d,e,f,g,h,i,j){var k={DEFAULT:"core/modal",SAVE_CANCEL:"core/modal_save_cancel",CONFIRM:"core/modal_confirm",CANCEL:"core/modal_cancel"},l={DEFAULT:"DEFAULT",SAVE_CANCEL:"SAVE_CANCEL",CONFIRM:"CONFIRM",CANCEL:"CANCEL"};c.register(l.DEFAULT,d,k.DEFAULT),c.register(l.SAVE_CANCEL,e,k.SAVE_CANCEL),c.register(l.CONFIRM,f,k.CONFIRM),c.register(l.CANCEL,g,k.CANCEL);var m=function(a,c){"undefined"!=typeof c&&(j.define(c,[j.events.activate]),c.on(j.events.activate,function(b,c){a.show(),c.originalEvent.preventDefault()}),a.getRoot().on(b.hidden,function(){c.focus()}))},n=function(b,c,d){c=a(c);var e=b.module,f=new e(c);return m(f,d),f},o=function(b,c){var d=b.template;return h.render(d,{}).then(function(d){var e=a(d);return n(b,e,c)}).fail(i.exception)},p=function(a,b){var d=a.type||l.DEFAULT,e=!!a.large,f=null;return f=c.get(d),f||i.exception({message:"Unable to find modal of type: "+d}),o(f,b).then(function(b){return"undefined"!=typeof a.title&&b.setTitle(a.title),"undefined"!=typeof a.body&&b.setBody(a.body),"undefined"!=typeof a.footer&&b.setFooter(a.footer),e&&b.setLarge(),b})};return{create:p,types:l}}); \ No newline at end of file +define(["jquery","core/modal_events","core/modal_registry","core/modal","core/modal_save_cancel","core/modal_confirm","core/modal_cancel","core/templates","core/notification","core/custom_interaction_events"],function(a,b,c,d,e,f,g,h,i,j){var k={DEFAULT:"core/modal",SAVE_CANCEL:"core/modal_save_cancel",CONFIRM:"core/modal_confirm",CANCEL:"core/modal_cancel"},l={DEFAULT:"DEFAULT",SAVE_CANCEL:"SAVE_CANCEL",CONFIRM:"CONFIRM",CANCEL:"CANCEL"};c.register(l.DEFAULT,d,k.DEFAULT),c.register(l.SAVE_CANCEL,e,k.SAVE_CANCEL),c.register(l.CONFIRM,f,k.CONFIRM),c.register(l.CANCEL,g,k.CANCEL);var m=function(a,c){"undefined"!=typeof c&&(j.define(c,[j.events.activate]),c.on(j.events.activate,function(b,c){a.show(),c.originalEvent.preventDefault()}),a.getRoot().on(b.hidden,function(){c.focus()}))},n=function(b,c,d){c=a(c);var e=b.module,f=new e(c);return m(f,d),f},o=function(b,c,d){var e=b.template;return h.render(e,c).then(function(c){var e=a(c);return n(b,e,d)}).fail(i.exception)},p=function(a,b){var d=a.type||l.DEFAULT,e=!!a.large,f=null,g={};return f=c.get(d),f||i.exception({message:"Unable to find modal of type: "+d}),"undefined"!=typeof a.templateContext&&(g=a.templateContext),o(f,g,b).then(function(b){return"undefined"!=typeof a.title&&b.setTitle(a.title),"undefined"!=typeof a.body&&b.setBody(a.body),"undefined"!=typeof a.footer&&b.setFooter(a.footer),e&&b.setLarge(),b})};return{create:p,types:l}}); \ No newline at end of file diff --git a/lib/amd/src/modal.js b/lib/amd/src/modal.js index 6cca43b0f4e..53ae0b77b40 100644 --- a/lib/amd/src/modal.js +++ b/lib/amd/src/modal.js @@ -315,7 +315,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes', return; } - this.getRoot().addClass('large'); + this.getModal().addClass('modal-lg'); }; /** @@ -325,7 +325,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes', * @return {bool} */ Modal.prototype.isLarge = function() { - return this.getRoot().hasClass('large'); + return this.getModal().hasClass('modal-lg'); }; /** @@ -338,7 +338,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes', return; } - this.getRoot().removeClass('large'); + this.getModal().removeClass('modal-lg'); }; /** @@ -348,7 +348,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes', * @return {bool} */ Modal.prototype.isSmall = function() { - return !this.getRoot().hasClass('large'); + return !this.getModal().hasClass('modal-lg'); }; /** diff --git a/lib/amd/src/modal_factory.js b/lib/amd/src/modal_factory.js index 68dedf2c832..52839b138c3 100644 --- a/lib/amd/src/modal_factory.js +++ b/lib/amd/src/modal_factory.js @@ -101,10 +101,10 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal', * @param {object} triggerElement The trigger HTML jQuery object * @return {promise} Resolved with a Modal instance */ - var createFromType = function(registryConf, triggerElement) { + var createFromType = function(registryConf, templateContext, triggerElement) { var templateName = registryConf.template; - return Templates.render(templateName, {}) + return Templates.render(templateName, templateContext) .then(function(html) { var modalElement = $(html); return createFromElement(registryConf, modalElement, triggerElement); @@ -124,6 +124,7 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal', var type = modalConfig.type || TYPES.DEFAULT; var isLarge = modalConfig.large ? true : false; var registryConf = null; + var templateContext = {}; registryConf = ModalRegistry.get(type); @@ -131,7 +132,11 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal', Notification.exception({message: 'Unable to find modal of type: ' + type}); } - return createFromType(registryConf, triggerElement) + if (typeof modalConfig.templateContext != 'undefined') { + templateContext = modalConfig.templateContext; + } + + return createFromType(registryConf, templateContext, triggerElement) .then(function(modal) { if (typeof modalConfig.title != 'undefined') { modal.setTitle(modalConfig.title); diff --git a/lib/db/services.php b/lib/db/services.php index 14385ae6628..909b7fafccd 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -126,6 +126,15 @@ $functions = array( 'ajax' => true, 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), + 'core_calendar_submit_create_update_form' => array( + 'classname' => 'core_calendar_external', + 'methodname' => 'submit_create_update_form', + 'description' => 'Submit form data for event form', + 'classpath' => 'calendar/externallib.php', + 'type' => 'write', + 'capabilities' => 'moodle/calendar:manageentries, moodle/calendar:manageownentries, moodle/calendar:managegroupentries', + 'ajax' => true, + ), 'core_cohort_add_cohort_members' => array( 'classname' => 'core_cohort_external', 'methodname' => 'add_cohort_members', diff --git a/lib/form/templatable_form_element.php b/lib/form/templatable_form_element.php index 7d8c2abb804..beb7173f7f9 100644 --- a/lib/form/templatable_form_element.php +++ b/lib/form/templatable_form_element.php @@ -78,6 +78,7 @@ trait templatable_form_element { $otherattributes[] = $attr . '="' . s($value) . '"'; } } + $context['name'] = $context['name'] ?: $this->getName(); $context['extraclasses'] = $extraclasses; $context['type'] = $this->getType(); $context['attributes'] = implode(' ', $otherattributes); diff --git a/lib/grouplib.php b/lib/grouplib.php index 08bee9a2be1..8893d132200 100644 --- a/lib/grouplib.php +++ b/lib/grouplib.php @@ -294,6 +294,151 @@ function groups_get_all_groups($courseid, $userid=0, $groupingid=0, $fields='g.* return $results; } +/** + * Gets array of all groups in a set of course. + * + * @category group + * @param array $courses Array of course objects or course ids. + * @return array Array of groups indexed by course id. + */ +function groups_get_all_groups_for_courses($courses) { + global $DB; + + if (empty($courses)) { + return []; + } + + $groups = []; + $courseids = []; + + foreach ($courses as $course) { + $courseid = is_object($course) ? $course->id : $course; + $groups[$courseid] = []; + $courseids[] = $courseid; + } + + $groupfields = [ + 'g.id as gid', + 'g.courseid', + 'g.idnumber', + 'g.name', + 'g.description', + 'g.descriptionformat', + 'g.enrolmentkey', + 'g.picture', + 'g.hidepicture', + 'g.timecreated', + 'g.timemodified' + ]; + + $groupsmembersfields = [ + 'gm.id as gmid', + 'gm.groupid', + 'gm.userid', + 'gm.timeadded', + 'gm.component', + 'gm.itemid' + ]; + + $concatidsql = $DB->sql_concat_join("'-'", ['g.id', 'COALESCE(gm.id, 0)']) . ' AS uniqid'; + list($courseidsql, $params) = $DB->get_in_or_equal($courseids); + $groupfieldssql = implode(',', $groupfields); + $groupmembersfieldssql = implode(',', $groupsmembersfields); + $sql = "SELECT {$concatidsql}, {$groupfieldssql}, {$groupmembersfieldssql} + FROM {groups} g + LEFT JOIN {groups_members} gm + ON gm.groupid = g.id + WHERE g.courseid {$courseidsql}"; + + $results = $DB->get_records_sql($sql, $params); + + // The results will come back as a flat dataset thanks to the left + // join so we will need to do some post processing to blow it out + // into a more usable data structure. + // + // This loop will extract the distinct groups from the result set + // and add it's list of members to the object as a property called + // 'members'. Then each group will be added to the result set indexed + // by it's course id. + // + // The resulting data structure for $groups should be: + // $groups = [ + // '1' = [ + // '1' => (object) [ + // 'id' => 1, + //