Merge branch 'MDL-59890-master' of git://github.com/andrewnicols/moodle

This commit is contained in:
Jun Pataleta 2017-10-04 11:19:33 +08:00
commit cd7cd9d9c1
66 changed files with 1410 additions and 123 deletions

View file

@ -38,40 +38,37 @@ class block_calendar_month extends block_base {
public function get_content() {
global $CFG;
$calm = optional_param('cal_m', 0, PARAM_INT);
$caly = optional_param('cal_y', 0, PARAM_INT);
$time = optional_param('time', 0, PARAM_INT);
require_once($CFG->dirroot.'/calendar/lib.php');
if ($this->content !== null) {
return $this->content;
}
// If a day, month and year were passed then convert it to a timestamp. If these were passed then we can assume
// the day, month and year are passed as Gregorian, as no where in core should we be passing these values rather
// than the time. This is done for BC.
if (!empty($calm) && (!empty($caly))) {
$time = make_timestamp($caly, $calm, 1);
} else if (empty($time)) {
$time = time();
}
$this->content = new stdClass;
$this->content->text = '';
$this->content->footer = '';
// [pj] To me it looks like this if would never be needed, but Penny added it
// when committing the /my/ stuff. Reminder to discuss and learn what it's about.
// It definitely needs SOME comment here!
$courseid = $this->page->course->id;
$issite = ($courseid == SITEID);
$course = null;
$courses = null;
$categories = null;
if ($issite) {
// Being displayed at site level. This will cause the filter to fall back to auto-detecting
// the list of courses it will be grabbing events from.
$course = get_site();
$courses = calendar_get_default_courses();
if ($this->page->context->contextlevel === CONTEXT_COURSECAT) {
// Restrict to categories, and their parents, and the courses that the user is enrolled in within those
// categories.
$categories = array_keys($this->page->categories);
$courses = array_filter($courses, function($course) use ($categories) {
return array_search($course->category, $categories) !== false;
});
}
} else {
// Forcibly filter events to include only those from the particular course we are in.
$course = $this->page->course;
@ -80,8 +77,8 @@ class block_calendar_month extends block_base {
$renderer = $this->page->get_renderer('core_calendar');
$calendar = new calendar_information(0, 0, 0, $time);
$calendar->prepare_for_view($course, $courses);
$calendar = new calendar_information();
$calendar->set_sources($course, $courses, $this->page->category);
list($data, $template) = calendar_get_view($calendar, 'mini');
$this->content->text .= $renderer->render_from_template($template, $data);

View file

@ -1 +1 @@
define(["jquery","core/ajax","core/str","core/templates","core/notification","core/custom_interaction_events","core/modal_events","core/modal_factory","core_calendar/modal_event_form","core_calendar/summary_modal","core_calendar/repository","core_calendar/events","core_calendar/view_manager"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){var n={ROOT:"[data-region='calendar']",DAY:"[data-region='day']",EVENT_ITEM:"[data-region='event-item']",EVENT_LINK:"[data-action='view-event']",NEW_EVENT_BUTTON:"[data-action='new-event-button']",DAY_CONTENT:"[data-region='day-content']",LOADING_ICON:".loading-icon",VIEW_DAY_LINK:"[data-action='view-day-link']",CALENDAR_MONTH_WRAPPER:".calendarwrapper",COURSE_SELECTOR:'select[name="course"]',TODAY:".today"},o=function(a){var b="type"+a;return c.get_string(b,"core_calendar").then(function(a){return a})},p=function(a){k.getEventById(a).then(function(b){if(!b.event)throw new Error("Error encountered while trying to fetch calendar event with ID: "+a);var c=b.event;return o(c.eventtype).then(function(a){return c.eventtype=a,c})}).then(function(a){var b={title:a.name,type:j.TYPE,body:d.render("core_calendar/event_summary_body",a),templateContext:{canedit:a.canedit,candelete:a.candelete,isactionevent:a.isactionevent,url:a.url}};return h.create(b)}).done(function(a){a.getRoot().on(g.hidden,function(){a.destroy()}),a.show()}).fail(e.exception)},q=function(b,c,f,g){var h=null,i=g.attr("data-day-timestamp");f&&(h=f.attr("data-day-timestamp")),f&&h==i||d.render("core/loading",{}).then(function(a,b){g.find(n.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(g,a,b),f&&(f.find(n.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(f,a,b))}).then(function(){return k.updateEventStartDay(c,i)}).then(function(){a("body").trigger(l.eventMoved,[c,f,g])}).always(function(){var a=g.find(n.LOADING_ICON);if(g.find(n.DAY_CONTENT).removeClass("hidden"),d.replaceNode(a,"",""),f){var b=f.find(n.LOADING_ICON);f.find(n.DAY_CONTENT).removeClass("hidden"),d.replaceNode(b,"","")}}).fail(e.exception)},r=function(a){var b=a.find(n.NEW_EVENT_BUTTON),c=b.attr("data-context-id");return h.create({type:i.TYPE,large:!0,templateContext:{contextid:c}})},s=function(b,c){var d=a("body"),f=a(b).find(n.CALENDAR_MONTH_WRAPPER).data("courseid");d.on(l.created,function(){m.reloadCurrentMonth(b)}),d.on(l.deleted,function(){m.reloadCurrentMonth(b)}),d.on(l.updated,function(){m.reloadCurrentMonth(b)}),d.on(l.editActionEvent,function(a,b){window.location.assign(b)}),d.on(l.moveEvent,q),d.on(l.eventMoved,function(){m.reloadCurrentMonth(b)}),c.then(function(a){d.on(l.editEvent,function(b,c){a.setEventId(c),a.show()}),a.setCourseId(f)}).fail(e.exception)},t=function(b){b.on("click",n.EVENT_ITEM,function(b){b.preventDefault(),b.stopPropagation();var c=a(b.target),d=null;d=c.is(n.EVENT_LINK)?c.attr("data-event-id"):c.find(n.EVENT_LINK).attr("data-event-id"),p(d)}),b.on("change",n.COURSE_SELECTOR,function(){var c=a(this),d=c.val();m.reloadCurrentMonth(b,d).then(function(){return b.find(n.COURSE_SELECTOR).val(d)}).fail(e.exception)});var c=r(b);s(b,c),b.on("click",n.NEW_EVENT_BUTTON,function(a){c.then(function(a){var c=b.find(n.TODAY);c.length||a.setStartTime(b.find(n.DAY).attr("data-new-event-timestamp")),a.show()}).fail(e.exception),a.preventDefault()}),b.on("click",n.DAY,function(b){var d=a(b.target);if(!d.is(n.VIEW_DAY_LINK)){var f=a(this).attr("data-new-event-timestamp");c.then(function(a){a.setStartTime(f),a.show()}).fail(e.exception),b.preventDefault()}})};return{init:function(b){b=a(b),m.init(b),t(b)}}});
define(["jquery","core/ajax","core/str","core/templates","core/notification","core/custom_interaction_events","core/modal_events","core/modal_factory","core_calendar/modal_event_form","core_calendar/summary_modal","core_calendar/repository","core_calendar/events","core_calendar/view_manager"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){var n={ROOT:"[data-region='calendar']",DAY:"[data-region='day']",EVENT_ITEM:"[data-region='event-item']",EVENT_LINK:"[data-action='view-event']",NEW_EVENT_BUTTON:"[data-action='new-event-button']",DAY_CONTENT:"[data-region='day-content']",LOADING_ICON:".loading-icon",VIEW_DAY_LINK:"[data-action='view-day-link']",CALENDAR_MONTH_WRAPPER:".calendarwrapper",COURSE_SELECTOR:'select[name="course"]',TODAY:".today"},o=function(a){var b="type"+a;return c.get_string(b,"core_calendar").then(function(a){return a})},p=function(a){k.getEventById(a).then(function(b){if(!b.event)throw new Error("Error encountered while trying to fetch calendar event with ID: "+a);var c=b.event;return o(c.eventtype).then(function(a){return c.eventtype=a,c})}).then(function(a){var b={title:a.name,type:j.TYPE,body:d.render("core_calendar/event_summary_body",a),templateContext:{canedit:a.canedit,candelete:a.candelete,isactionevent:a.isactionevent,url:a.url}};return h.create(b)}).done(function(a){a.getRoot().on(g.hidden,function(){a.destroy()}),a.show()}).fail(e.exception)},q=function(b,c,f,g){var h=null,i=g.attr("data-day-timestamp");f&&(h=f.attr("data-day-timestamp")),f&&h==i||d.render("core/loading",{}).then(function(a,b){g.find(n.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(g,a,b),f&&(f.find(n.DAY_CONTENT).addClass("hidden"),d.appendNodeContents(f,a,b))}).then(function(){return k.updateEventStartDay(c,i)}).then(function(){a("body").trigger(l.eventMoved,[c,f,g])}).always(function(){var a=g.find(n.LOADING_ICON);if(g.find(n.DAY_CONTENT).removeClass("hidden"),d.replaceNode(a,"",""),f){var b=f.find(n.LOADING_ICON);f.find(n.DAY_CONTENT).removeClass("hidden"),d.replaceNode(b,"","")}}).fail(e.exception)},r=function(a){var b=a.find(n.NEW_EVENT_BUTTON),c=b.attr("data-context-id");return h.create({type:i.TYPE,large:!0,templateContext:{contextid:c}})},s=function(b,c){var d=a("body");d.on(l.created,function(){m.reloadCurrentMonth(b)}),d.on(l.deleted,function(){m.reloadCurrentMonth(b)}),d.on(l.updated,function(){m.reloadCurrentMonth(b)}),d.on(l.editActionEvent,function(a,b){window.location.assign(b)}),d.on(l.moveEvent,q),d.on(l.eventMoved,function(){m.reloadCurrentMonth(b)}),c.then(function(a){d.on(l.editEvent,function(b,c){a.setEventId(c),a.show()})}).fail(e.exception)},t=function(b){b.on("click",n.EVENT_ITEM,function(b){b.preventDefault(),b.stopPropagation();var c=a(b.target),d=null;d=c.is(n.EVENT_LINK)?c.attr("data-event-id"):c.find(n.EVENT_LINK).attr("data-event-id"),p(d)}),b.on("change",n.COURSE_SELECTOR,function(){var c=a(this),d=c.val();m.reloadCurrentMonth(b,d,null).then(function(){return b.find(n.COURSE_SELECTOR).val(d)}).fail(e.exception)});var c=r(b);s(b,c),f.define(b,[f.events.activate]),b.on("click",n.NEW_EVENT_BUTTON,function(a){c.then(function(a){var c=b.find(n.CALENDAR_MONTH_WRAPPER);a.setCourseId(c.data("courseid"));var d=c.data("categoryid");"undefined"!=typeof d&&a.setCategoryId(d);var e=b.find(n.TODAY);e.length||a.setStartTime(b.find(n.DAY).attr("data-new-event-timestamp")),a.show()}).fail(e.exception),a.preventDefault()}),b.on("click",n.DAY,function(b){var d=a(b.target);if(!d.is(n.VIEW_DAY_LINK)){var f=a(this).attr("data-new-event-timestamp");c.then(function(a){var b=d.closest(n.CALENDAR_MONTH_WRAPPER);a.setCourseId(b.data("courseid"));var c=b.data("categoryid");"undefined"!=typeof c&&a.setCategoryId(c),a.setStartTime(f),a.show()}).fail(e.exception),b.preventDefault()}})};return{init:function(b){b=a(b),m.init(b),t(b)}}});

View file

@ -1 +1 @@
define(["jquery","core_calendar/selectors","core_calendar/events","core/templates","core_calendar/view_manager"],function(a,b,c,d,e){var f=function(d){var f=a("body");f.on(c.monthChanged,function(a,b,c,e){d.queue(function(d){return g(a,b,c,e).then(function(){return d()})})});var g=function(c,f,g,h){var i=d.find('[data-year="'+f+'"][data-month="'+g+'"]'),j=i.closest(b.calendarPeriods.month),k=d.find(b.calendarPeriods.month),l=a(k[0]),m=a(k[2]),n=a("<span>");n.attr("data-template","core_calendar/threemonth_month"),n.attr("data-includenavigation",!1);var o=a("<div>");o.hide(),o.append(n);var p,q,r;return j.is(l)?(o.insertBefore(l),p=l.data("previousYear"),q=l.data("previousMonth"),r=m):j.is(m)&&(o.insertAfter(m),p=m.data("nextYear"),q=m.data("nextMonth"),r=l),e.refreshMonthContent(n,p,q,h,n).then(function(){var b=a.Deferred(),c=a.Deferred();return r.slideUp("fast",function(){a(this).remove(),b.resolve()}),o.slideDown("fast",function(){c.resolve()}),a.when(b,c)})}};return{init:function(b){b=a(b),f(b)}}});
define(["jquery","core/notification","core_calendar/selectors","core_calendar/events","core/templates","core_calendar/view_manager"],function(a,b,c,d,e,f){var g=function(e){var g=a("body");g.on(d.monthChanged,function(a,c,d,f,g){e.queue(function(e){return h(a,c,d,f,g).then(function(){return e()}).fail(b.exception)})});var h=function(b,d,g,h,i){var j=e.find('[data-year="'+d+'"][data-month="'+g+'"]'),k=j.closest(c.calendarPeriods.month),l=e.find(c.calendarPeriods.month),m=a(l[0]),n=a(l[2]),o=a("<span>");o.attr("data-template","core_calendar/threemonth_month"),o.attr("data-includenavigation",!1);var p=a("<div>");p.hide(),p.append(o);var q,r,s;return k.is(m)?(p.insertBefore(m),q=m.data("previousYear"),r=m.data("previousMonth"),s=n):k.is(n)&&(p.insertAfter(n),q=n.data("nextYear"),r=n.data("nextMonth"),s=m),f.refreshMonthContent(o,q,r,h,i,o).then(function(){var b=a.Deferred(),c=a.Deferred();return s.slideUp("fast",function(){a(this).remove(),b.resolve()}),p.slideDown("fast",function(){c.resolve()}),a.when(b,c)})}};return{init:function(b){b=a(b),g(b)}}});

View file

@ -1 +1 @@
define(["jquery","core/event","core/str","core/notification","core/templates","core/custom_interaction_events","core/modal","core/modal_registry","core/fragment","core_calendar/events","core_calendar/repository"],function(a,b,c,d,e,f,g,h,i,j,k){var l=!1,m={SAVE_BUTTON:'[data-action="save"]',LOADING_ICON_CONTAINER:'[data-region="loading-icon-container"]'},n=function(a){g.call(this,a),this.eventId=null,this.startTime=null,this.courseId=null,this.reloadingBody=!1,this.reloadingTitle=!1,this.saveButton=this.getFooter().find(m.SAVE_BUTTON)};return n.TYPE="core_calendar-modal_event_form",n.prototype=Object.create(g.prototype),n.prototype.constructor=n,n.prototype.setCourseId=function(a){this.courseId=a},n.prototype.getCourseId=function(){return this.courseId},n.prototype.hasCourseId=function(){return null!==this.courseId},n.prototype.setEventId=function(a){this.eventId=a},n.prototype.getEventId=function(){return this.eventId},n.prototype.hasEventId=function(){return null!==this.eventId},n.prototype.setStartTime=function(a){this.startTime=a},n.prototype.getStartTime=function(){return this.startTime},n.prototype.hasStartTime=function(){return null!==this.startTime},n.prototype.getForm=function(){return this.getBody().find("form")},n.prototype.disableButtons=function(){this.saveButton.prop("disabled",!0)},n.prototype.enableButtons=function(){this.saveButton.prop("disabled",!1)},n.prototype.reloadTitleContent=function(){return this.reloadingTitle?this.titlePromise:(this.reloadingTitle=!0,this.hasEventId()?this.titlePromise=c.get_string("editevent","calendar"):this.titlePromise=c.get_string("newevent","calendar"),this.titlePromise.then(function(a){return this.setTitle(a),a}.bind(this)).always(function(){this.reloadingTitle=!1}.bind(this)),this.titlePromise)},n.prototype.reloadBodyContent=function(a){if(this.reloadingBody)return this.bodyPromise;this.reloadingBody=!0,this.disableButtons();var b=this.saveButton.attr("data-context-id"),c={};return this.hasEventId()&&(c.eventid=this.getEventId()),this.hasStartTime()&&(c.starttime=this.getStartTime()),this.hasCourseId()&&(c.courseid=this.getCourseId()),"undefined"!=typeof a&&(c.formdata=a),this.bodyPromise=i.loadFragment("calendar","event_form",b,c),this.setBody(this.bodyPromise),this.bodyPromise.then(function(){this.enableButtons()}.bind(this))["catch"](d.exception).always(function(){this.reloadingBody=!1}.bind(this)),this.bodyPromise},n.prototype.reloadAllContent=function(){return a.when(this.reloadTitleContent(),this.reloadBodyContent())},n.prototype.show=function(){this.reloadAllContent(),g.prototype.show.call(this)},n.prototype.hide=function(){g.prototype.hide.call(this),this.setEventId(null),this.setStartTime(null)},n.prototype.getFormData=function(){return this.getForm().serialize()},n.prototype.save=function(){var b=this.saveButton.find(m.LOADING_ICON_CONTAINER);b.removeClass("hidden"),this.disableButtons();var c=this.getFormData();return k.submitCreateUpdateForm(c).then(function(b){return b.validationerror?this.reloadBodyContent(c):(this.hide(),void(this.hasEventId()?a("body").trigger(j.updated,[b.event]):a("body").trigger(j.created,[b.event])))}.bind(this)).always(function(){b.addClass("hidden"),this.enableButtons()}.bind(this))["catch"](d.exception)},n.prototype.registerEventListeners=function(){g.prototype.registerEventListeners.call(this),this.getModal().on(f.events.activate,m.SAVE_BUTTON,function(a,b){this.getForm().submit(),b.originalEvent.preventDefault(),a.stopPropagation()}.bind(this)),this.getModal().on("submit",function(a){this.save(),a.preventDefault(),a.stopPropagation()}.bind(this))},l||(h.register(n.TYPE,n,"calendar/modal_event_form"),l=!0),n});
define(["jquery","core/event","core/str","core/notification","core/templates","core/custom_interaction_events","core/modal","core/modal_registry","core/fragment","core_calendar/events","core_calendar/repository"],function(a,b,c,d,e,f,g,h,i,j,k){var l=!1,m={SAVE_BUTTON:'[data-action="save"]',LOADING_ICON_CONTAINER:'[data-region="loading-icon-container"]'},n=function(a){g.call(this,a),this.eventId=null,this.startTime=null,this.courseId=null,this.categoryId=null,this.reloadingBody=!1,this.reloadingTitle=!1,this.saveButton=this.getFooter().find(m.SAVE_BUTTON)};return n.TYPE="core_calendar-modal_event_form",n.prototype=Object.create(g.prototype),n.prototype.constructor=n,n.prototype.setCourseId=function(a){this.courseId=a},n.prototype.getCourseId=function(){return this.courseId},n.prototype.setCategoryId=function(a){this.categoryId=a},n.prototype.getCategoryId=function(){return this.categoryId},n.prototype.hasCourseId=function(){return null!==this.courseId},n.prototype.hasCategoryId=function(){return null!==this.categoryId},n.prototype.setEventId=function(a){this.eventId=a},n.prototype.getEventId=function(){return this.eventId},n.prototype.hasEventId=function(){return null!==this.eventId},n.prototype.setStartTime=function(a){this.startTime=a},n.prototype.getStartTime=function(){return this.startTime},n.prototype.hasStartTime=function(){return null!==this.startTime},n.prototype.getForm=function(){return this.getBody().find("form")},n.prototype.disableButtons=function(){this.saveButton.prop("disabled",!0)},n.prototype.enableButtons=function(){this.saveButton.prop("disabled",!1)},n.prototype.reloadTitleContent=function(){return this.reloadingTitle?this.titlePromise:(this.reloadingTitle=!0,this.hasEventId()?this.titlePromise=c.get_string("editevent","calendar"):this.titlePromise=c.get_string("newevent","calendar"),this.titlePromise.then(function(a){return this.setTitle(a),a}.bind(this)).always(function(){this.reloadingTitle=!1}.bind(this)).fail(d.exception),this.titlePromise)},n.prototype.reloadBodyContent=function(a){if(this.reloadingBody)return this.bodyPromise;this.reloadingBody=!0,this.disableButtons();var b=this.saveButton.attr("data-context-id"),c={};return this.hasEventId()&&(c.eventid=this.getEventId()),this.hasStartTime()&&(c.starttime=this.getStartTime()),this.hasCourseId()&&(c.courseid=this.getCourseId()),this.hasCategoryId()&&(c.categoryid=this.getCategoryId()),"undefined"!=typeof a&&(c.formdata=a),this.bodyPromise=i.loadFragment("calendar","event_form",b,c),this.setBody(this.bodyPromise),this.bodyPromise.then(function(){this.enableButtons()}.bind(this)).fail(d.exception).always(function(){this.reloadingBody=!1}.bind(this)).fail(d.exception),this.bodyPromise},n.prototype.reloadAllContent=function(){return a.when(this.reloadTitleContent(),this.reloadBodyContent())},n.prototype.show=function(){this.reloadAllContent(),g.prototype.show.call(this)},n.prototype.hide=function(){g.prototype.hide.call(this),this.setEventId(null),this.setStartTime(null),this.setCourseId(null),this.setCategoryId(null)},n.prototype.getFormData=function(){return this.getForm().serialize()},n.prototype.save=function(){var b=this.saveButton.find(m.LOADING_ICON_CONTAINER);b.removeClass("hidden"),this.disableButtons();var c=this.getFormData();return k.submitCreateUpdateForm(c).then(function(b){return b.validationerror?void this.reloadBodyContent(c):(this.hide(),void(this.hasEventId()?a("body").trigger(j.updated,[b.event]):a("body").trigger(j.created,[b.event])))}.bind(this)).always(function(){b.addClass("hidden"),this.enableButtons()}.bind(this)).fail(d.exception)},n.prototype.registerEventListeners=function(){g.prototype.registerEventListeners.call(this),this.getModal().on(f.events.activate,m.SAVE_BUTTON,function(a,b){this.getForm().submit(),b.originalEvent.preventDefault(),a.stopPropagation()}.bind(this)),this.getModal().on("submit",function(a){this.save(),a.preventDefault(),a.stopPropagation()}.bind(this))},l||(h.register(n.TYPE,n,"calendar/modal_event_form"),l=!0),n});

View file

@ -1 +1 @@
define(["jquery","core/ajax"],function(a,b){var c=function(a,c){"undefined"==typeof c&&(c=!1);var d={methodname:"core_calendar_delete_calendar_events",args:{events:[{eventid:a,repeat:c}]}};return b.call([d])[0]},d=function(a){var c={methodname:"core_calendar_get_calendar_event_by_id",args:{eventid:a}};return b.call([c])[0]},e=function(a){var c={methodname:"core_calendar_submit_create_update_form",args:{formdata:a}};return b.call([c])[0]},f=function(a,c,d,e){var f={methodname:"core_calendar_get_calendar_monthly_view",args:{year:a,month:c,courseid:d,includenavigation:e}};return b.call([f])[0]},g=function(a,c){var d={methodname:"core_calendar_update_event_start_day",args:{eventid:a,daytimestamp:c}};return b.call([d])[0]};return{getEventById:d,deleteEvent:c,updateEventStartDay:g,submitCreateUpdateForm:e,getCalendarMonthData:f}});
define(["jquery","core/ajax"],function(a,b){var c=function(a,c){"undefined"==typeof c&&(c=!1);var d={methodname:"core_calendar_delete_calendar_events",args:{events:[{eventid:a,repeat:c}]}};return b.call([d])[0]},d=function(a){var c={methodname:"core_calendar_get_calendar_event_by_id",args:{eventid:a}};return b.call([c])[0]},e=function(a){var c={methodname:"core_calendar_submit_create_update_form",args:{formdata:a}};return b.call([c])[0]},f=function(a,c,d,e,f){var g={methodname:"core_calendar_get_calendar_monthly_view",args:{year:a,month:c,courseid:d,categoryid:e,includenavigation:f}};return b.call([g])[0]},g=function(a,c){var d={methodname:"core_calendar_update_event_start_day",args:{eventid:a,daytimestamp:c}};return b.call([d])[0]};return{getEventById:d,deleteEvent:c,updateEventStartDay:g,submitCreateUpdateForm:e,getCalendarMonthData:f}});

View file

@ -1 +1 @@
define([],function(){return{eventFilterItem:"[data-action='filter-event-type']",eventType:{site:"[data-eventtype-site]",course:"[data-eventtype-course]",group:"[data-eventtype-group]",user:"[data-eventtype-user]"},popoverType:{site:"[data-popover-eventtype-site]",course:"[data-popover-eventtype-course]",group:"[data-popover-eventtype-group]",user:"[data-popover-eventtype-user]"},calendarPeriods:{month:"[data-period='month']"}}});
define([],function(){return{eventFilterItem:"[data-action='filter-event-type']",eventType:{site:"[data-eventtype-site]",category:"[data-eventtype-category]",course:"[data-eventtype-course]",group:"[data-eventtype-group]",user:"[data-eventtype-user]"},popoverType:{site:"[data-popover-eventtype-site]",category:"[data-popover-eventtype-category]",course:"[data-popover-eventtype-course]",group:"[data-popover-eventtype-group]",user:"[data-popover-eventtype-user]"},calendarPeriods:{month:"[data-period='month']"}}});

View file

@ -1 +1 @@
define(["jquery","core/templates","core/notification","core_calendar/repository","core_calendar/events"],function(a,b,c,d,e){var f={ROOT:"[data-region='calendar']",CALENDAR_NAV_LINK:".calendarwrapper .arrow_link",CALENDAR_MONTH_WRAPPER:".calendarwrapper",LOADING_ICON_CONTAINER:'[data-region="overlay-icon-container"]'},g=function(b){b=a(b),b.on("click",f.CALENDAR_NAV_LINK,function(c){var d=a(b).find(f.CALENDAR_MONTH_WRAPPER).data("courseid"),e=a(c.currentTarget);i(b,e.attr("href"),e.data("year"),e.data("month"),d),c.preventDefault()})},h=function(g,h,i,j,m){k(g),m=m||g.find(f.CALENDAR_MONTH_WRAPPER),M.util.js_pending([g.get("id"),h,i,j].join("-"));var n=g.data("includenavigation");return d.getCalendarMonthData(h,i,j,n).then(function(a){return b.render(g.attr("data-template"),a)}).then(function(a,c){return b.replaceNode(m,a,c)}).then(function(){a("body").trigger(e.viewUpdated)}).always(function(){return M.util.js_complete([g.get("id"),h,i,j].join("-")),l(g)}).fail(c.exception)},i=function(b,c,d,f,g){return h(b,d,f,g).then(function(){return c.length&&"#"!==c&&window.history.pushState({},"",c),arguments}).then(function(){return a("body").trigger(e.monthChanged,[d,f,g]),arguments})},j=function(a,b){var c=a.find(f.CALENDAR_MONTH_WRAPPER).data("year"),d=a.find(f.CALENDAR_MONTH_WRAPPER).data("month");return b||(b=a.find(f.CALENDAR_MONTH_WRAPPER).data("courseid")),h(a,c,d,b)},k=function(a){var b=a.find(f.LOADING_ICON_CONTAINER);b.removeClass("hidden")},l=function(a){var b=a.find(f.LOADING_ICON_CONTAINER);b.addClass("hidden")};return{init:function(a){g(a)},reloadCurrentMonth:j,changeMonth:i,refreshMonthContent:h}});
define(["jquery","core/templates","core/notification","core_calendar/repository","core_calendar/events"],function(a,b,c,d,e){var f={ROOT:"[data-region='calendar']",CALENDAR_NAV_LINK:".calendarwrapper .arrow_link",CALENDAR_MONTH_WRAPPER:".calendarwrapper",LOADING_ICON_CONTAINER:'[data-region="overlay-icon-container"]'},g=function(b){b=a(b),b.on("click",f.CALENDAR_NAV_LINK,function(c){var d=b.find(f.CALENDAR_MONTH_WRAPPER),e=d.data("courseid"),g=d.data("categoryid"),h=a(c.currentTarget);i(b,h.attr("href"),h.data("year"),h.data("month"),e,g),c.preventDefault()})},h=function(g,h,i,j,m,n){k(g),n=n||g.find(f.CALENDAR_MONTH_WRAPPER),M.util.js_pending([g.get("id"),h,i,j].join("-"));var o=g.data("includenavigation");return d.getCalendarMonthData(h,i,j,m,o).then(function(a){return b.render(g.attr("data-template"),a)}).then(function(a,c){return b.replaceNode(n,a,c)}).then(function(){a("body").trigger(e.viewUpdated)}).always(function(){return M.util.js_complete([g.get("id"),h,i,j].join("-")),l(g)}).fail(c.exception)},i=function(b,c,d,f,g,i){return h(b,d,f,g,i).then(function(){return c.length&&"#"!==c&&window.history.pushState({},"",c),arguments}).then(function(){return a("body").trigger(e.monthChanged,[d,f,g,i]),arguments})},j=function(a,b,c){var d=a.find(f.CALENDAR_MONTH_WRAPPER).data("year"),e=a.find(f.CALENDAR_MONTH_WRAPPER).data("month");return"undefined"==typeof b&&(b=a.find(f.CALENDAR_MONTH_WRAPPER).data("courseid")),"undefined"==typeof c&&(c=a.find(f.CALENDAR_MONTH_WRAPPER).data("categoryid")),h(a,d,e,b,c)},k=function(a){var b=a.find(f.LOADING_ICON_CONTAINER);b.removeClass("hidden")},l=function(a){var b=a.find(f.LOADING_ICON_CONTAINER);b.addClass("hidden")};return{init:function(a){g(a)},reloadCurrentMonth:j,changeMonth:i,refreshMonthContent:h}});

View file

@ -223,8 +223,7 @@ define([
* @param {object} eventFormModalPromise A promise reolved with the event form modal
*/
var registerCalendarEventListeners = function(root, eventFormModalPromise) {
var body = $('body'),
courseId = $(root).find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid');
var body = $('body');
body.on(CalendarEvents.created, function() {
CalendarViewManager.reloadCurrentMonth(root);
@ -246,14 +245,14 @@ define([
CalendarViewManager.reloadCurrentMonth(root);
});
eventFormModalPromise.then(function(modal) {
eventFormModalPromise
.then(function(modal) {
// When something within the calendar tells us the user wants
// to edit an event then show the event form modal.
body.on(CalendarEvents.editEvent, function(e, eventId) {
modal.setEventId(eventId);
modal.show();
});
modal.setCourseId(courseId);
return;
})
.fail(Notification.exception);
@ -287,7 +286,7 @@ define([
root.on('change', SELECTORS.COURSE_SELECTOR, function() {
var selectElement = $(this);
var courseId = selectElement.val();
CalendarViewManager.reloadCurrentMonth(root, courseId)
CalendarViewManager.reloadCurrentMonth(root, courseId, null)
.then(function() {
// We need to get the selector again because the content has changed.
return root.find(SELECTORS.COURSE_SELECTOR).val(courseId);
@ -299,8 +298,16 @@ define([
registerCalendarEventListeners(root, eventFormPromise);
// Bind click event on the new event button.
CustomEvents.define(root, [CustomEvents.events.activate]);
root.on('click', SELECTORS.NEW_EVENT_BUTTON, function(e) {
eventFormPromise.then(function(modal) {
var wrapper = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER);
modal.setCourseId(wrapper.data('courseid'));
var categoryId = wrapper.data('categoryid');
if (typeof categoryId !== 'undefined') {
modal.setCategoryId(categoryId);
}
// Attempt to find the cell for today.
// If it can't be found, then use the start time of the first day on the calendar.
var today = root.find(SELECTORS.TODAY);
@ -323,6 +330,15 @@ define([
if (!target.is(SELECTORS.VIEW_DAY_LINK)) {
var startTime = $(this).attr('data-new-event-timestamp');
eventFormPromise.then(function(modal) {
var wrapper = target.closest(SELECTORS.CALENDAR_MONTH_WRAPPER);
modal.setCourseId(wrapper.data('courseid'));
var categoryId = wrapper.data('categoryid');
if (typeof categoryId !== 'undefined') {
modal.setCategoryId(categoryId);
}
modal.setStartTime(startTime);
modal.show();
return;

View file

@ -24,6 +24,7 @@
*/
define([
'jquery',
'core/notification',
'core_calendar/selectors',
'core_calendar/events',
'core/templates',
@ -31,6 +32,7 @@ define([
],
function(
$,
Notification,
CalendarSelectors,
CalendarEvents,
Templates,
@ -45,18 +47,20 @@ function(
*/
var registerCalendarEventListeners = function(root) {
var body = $('body');
body.on(CalendarEvents.monthChanged, function(e, year, month, courseId) {
body.on(CalendarEvents.monthChanged, function(e, year, month, courseId, categoryId) {
// We have to use a queue here because the calling code is decoupled from these listeners.
// It's possible for the event to be called multiple times before one call is fully resolved.
root.queue(function(next) {
return processRequest(e, year, month, courseId)
return processRequest(e, year, month, courseId, categoryId)
.then(function() {
return next();
});
})
.fail(Notification.exception)
;
});
});
var processRequest = function(e, year, month, courseId) {
var processRequest = function(e, year, month, courseId, categoryId) {
var newCurrentMonth = root.find('[data-year="' + year + '"][data-month="' + month + '"]');
var newParent = newCurrentMonth.closest(CalendarSelectors.calendarPeriods.month);
var allMonths = root.find(CalendarSelectors.calendarPeriods.month);
@ -95,6 +99,7 @@ function(
requestYear,
requestMonth,
courseId,
categoryId,
placeHolder
)
.then(function() {

View file

@ -65,6 +65,7 @@ define([
this.eventId = null;
this.startTime = null;
this.courseId = null;
this.categoryId = null;
this.reloadingBody = false;
this.reloadingTitle = false;
this.saveButton = this.getFooter().find(SELECTORS.SAVE_BUTTON);
@ -94,6 +95,26 @@ define([
return this.courseId;
};
/**
* Set the category id to the given value.
*
* @method setCategoryId
* @param {int} id The event id
*/
ModalEventForm.prototype.setCategoryId = function(id) {
this.categoryId = id;
};
/**
* Retrieve the current category id, if any.
*
* @method getCategoryId
* @return {int|null} The event id
*/
ModalEventForm.prototype.getCategoryId = function() {
return this.categoryId;
};
/**
* Check if the modal has an course id.
*
@ -104,6 +125,16 @@ define([
return this.courseId !== null;
};
/**
* Check if the modal has an category id.
*
* @method hasCategoryId
* @return {bool}
*/
ModalEventForm.prototype.hasCategoryId = function() {
return this.categoryId !== null;
};
/**
* Set the event id to the given value.
*
@ -220,7 +251,8 @@ define([
.always(function() {
this.reloadingTitle = false;
return;
}.bind(this));
}.bind(this))
.fail(Notification.exception);
return this.titlePromise;
};
@ -261,6 +293,10 @@ define([
args.courseid = this.getCourseId();
}
if (this.hasCategoryId()) {
args.categoryid = this.getCategoryId();
}
if (typeof formData !== 'undefined') {
args.formdata = formData;
}
@ -273,11 +309,12 @@ define([
this.enableButtons();
return;
}.bind(this))
.catch(Notification.exception)
.fail(Notification.exception)
.always(function() {
this.reloadingBody = false;
return;
}.bind(this));
}.bind(this))
.fail(Notification.exception);
return this.bodyPromise;
};
@ -321,6 +358,8 @@ define([
Modal.prototype.hide.call(this);
this.setEventId(null);
this.setStartTime(null);
this.setCourseId(null);
this.setCategoryId(null);
};
/**
@ -361,7 +400,8 @@ define([
// If there was a server side validation error then
// we need to re-request the rendered form from the server
// in order to display the error for the user.
return this.reloadBodyContent(formData);
this.reloadBodyContent(formData);
return;
} else {
// No problemo! Our work here is done.
this.hide();
@ -382,8 +422,10 @@ define([
// the loading icon and re-enable the buttons.
loadingContainer.addClass('hidden');
this.enableButtons();
return;
}.bind(this))
.catch(Notification.exception);
.fail(Notification.exception);
};
/**

View file

@ -29,7 +29,7 @@ define(['jquery', 'core/ajax'], function($, Ajax) {
*
* @method deleteEvent
* @param {int} eventId The event id.
* @arapm {bool} deleteSeries Whether to delete all events in the series
* @param {bool} deleteSeries Whether to delete all events in the series
* @return {promise} Resolved with requested calendar event
*/
var deleteEvent = function(eventId, deleteSeries) {
@ -93,16 +93,18 @@ define(['jquery', 'core/ajax'], function($, Ajax) {
* @param {Number} year Year
* @param {Number} month Month
* @param {Number} courseid The course id.
* @param {Number} categoryid The category id.
* @param {Bool} includenavigation Whether to include navigation.
* @return {promise} Resolved with the month view data.
*/
var getCalendarMonthData = function(year, month, courseid, includenavigation) {
var getCalendarMonthData = function(year, month, courseid, categoryid, includenavigation) {
var request = {
methodname: 'core_calendar_get_calendar_monthly_view',
args: {
year: year,
month: month,
courseid: courseid,
categoryid: categoryid,
includenavigation: includenavigation,
}
};

View file

@ -26,12 +26,14 @@ define([], function() {
eventFilterItem: "[data-action='filter-event-type']",
eventType: {
site: "[data-eventtype-site]",
category: "[data-eventtype-category]",
course: "[data-eventtype-course]",
group: "[data-eventtype-group]",
user: "[data-eventtype-user]",
},
popoverType: {
site: "[data-popover-eventtype-site]",
category: "[data-popover-eventtype-category]",
course: "[data-popover-eventtype-course]",
group: "[data-popover-eventtype-group]",
user: "[data-popover-eventtype-user]",

View file

@ -40,9 +40,11 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
root = $(root);
root.on('click', SELECTORS.CALENDAR_NAV_LINK, function(e) {
var courseId = $(root).find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid');
var wrapper = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER);
var courseId = wrapper.data('courseid');
var categoryId = wrapper.data('categoryid');
var link = $(e.currentTarget);
changeMonth(root, link.attr('href'), link.data('year'), link.data('month'), courseId);
changeMonth(root, link.attr('href'), link.data('year'), link.data('month'), courseId, categoryId);
e.preventDefault();
});
@ -55,17 +57,18 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
* @param {Number} year Year
* @param {Number} month Month
* @param {Number} courseid The id of the course whose events are shown
* @param {Number} categoryid The id of the category whose events are shown
* @param {object} target The element being replaced. If not specified, the calendarwrapper is used.
* @return {promise}
*/
var refreshMonthContent = function(root, year, month, courseid, target) {
var refreshMonthContent = function(root, year, month, courseid, categoryid, target) {
startLoading(root);
target = target || root.find(SELECTORS.CALENDAR_MONTH_WRAPPER);
M.util.js_pending([root.get('id'), year, month, courseid].join('-'));
var includenavigation = root.data('includenavigation');
return CalendarRepository.getCalendarMonthData(year, month, courseid, includenavigation)
return CalendarRepository.getCalendarMonthData(year, month, courseid, categoryid, includenavigation)
.then(function(context) {
return Templates.render(root.attr('data-template'), context);
})
@ -86,15 +89,16 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
/**
* Handle changes to the current calendar view.
*
* @param {object} root The root element.
* @param {object} root The container element
* @param {String} url The calendar url to be shown
* @param {Number} year Year
* @param {Number} month Month
* @param {Number} courseid The id of the course whose events are shown
* @param {Number} categoryid The id of the category whose events are shown
* @return {promise}
*/
var changeMonth = function(root, url, year, month, courseid) {
return refreshMonthContent(root, year, month, courseid)
var changeMonth = function(root, url, year, month, courseid, categoryid) {
return refreshMonthContent(root, year, month, courseid, categoryid)
.then(function() {
if (url.length && url !== '#') {
window.history.pushState({}, '', url);
@ -102,7 +106,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
return arguments;
})
.then(function() {
$('body').trigger(CalendarEvents.monthChanged, [year, month, courseid]);
$('body').trigger(CalendarEvents.monthChanged, [year, month, courseid, categoryid]);
return arguments;
});
};
@ -112,16 +116,23 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
*
* @param {object} root The container element.
* @param {Number} courseId The course id.
* @param {Number} categoryId The id of the category whose events are shown
* @return {promise}
*/
var reloadCurrentMonth = function(root, courseId) {
var reloadCurrentMonth = function(root, courseId, categoryId) {
var year = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('year');
var month = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('month');
if (!courseId) {
if (typeof courseId === 'undefined') {
courseId = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid');
}
return refreshMonthContent(root, year, month, courseId);
if (typeof categoryId === 'undefined') {
categoryId = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('categoryid');
}
return refreshMonthContent(root, year, month, courseId, categoryId);
};
/**

View file

@ -82,6 +82,8 @@ class calendar_event_exporter extends event_exporter_base {
$params = array('update' => $moduleid, 'return' => true, 'sesskey' => sesskey());
$editurl = new \moodle_url('/course/mod.php', $params);
$values['editurl'] = $editurl->out(false);
} else if ($event->get_type() == 'category') {
$url = $event->get_category()->get_proxied_instance()->get_view_link();
} else if ($event->get_type() == 'course') {
$url = course_get_url($event->get_course()->get('id') ?: SITEID);
} else {
@ -126,6 +128,17 @@ class calendar_event_exporter extends event_exporter_base {
}
}
// Include category name into the event name, if applicable.
$proxy = $this->event->get_category();
if ($proxy && $proxy->get('id')) {
$category = $proxy->get_proxied_instance();
$eventnameparams = (object) [
'name' => $values['popupname'],
'category' => $category->get_formatted_name(),
];
$values['popupname'] = get_string('eventnameandcategory', 'calendar', $eventnameparams);
}
// Include course's shortname into the event name, if applicable.
$course = $this->event->get_course();
if ($course && $course->get('id') && $course->get('id') !== SITEID) {

View file

@ -59,11 +59,20 @@ class day_exporter extends exporter {
*/
public function __construct(\calendar_information $calendar, $data, $related) {
$this->calendar = $calendar;
$this->url = new moodle_url('/calendar/view.php', [
'view' => 'day',
'time' => $calendar->time,
'course' => $this->calendar->course->id,
]);
$url = new moodle_url('/calendar/view.php', [
'view' => 'day',
'time' => $calendar->time,
]);
if ($this->calendar->course && SITEID !== $this->calendar->course->id) {
$url->param('course', $this->calendar->course->id);
} else if ($this->calendar->categoryid) {
$url->param('category', $this->calendar->categoryid);
}
$this->url = $url;
parent::__construct($data, $related);
}
@ -179,9 +188,9 @@ class day_exporter extends exporter {
'navigation' => $this->get_navigation(),
'filter_selector' => $this->get_course_filter_selector($output),
'new_event_button' => $this->get_new_event_button(),
'viewdaylink' => $this->url->out(false),
];
$return['viewdaylink'] = $this->url->out(false);
$cache = $this->related['cache'];
$eventexporters = array_map(function($event) use ($cache, $output) {

View file

@ -48,17 +48,13 @@ class event_exporter extends event_exporter_base {
* @return array
*/
protected static function define_other_properties() {
$values = parent::define_other_properties();
$values['url'] = ['type' => PARAM_URL];
$values['action'] = [
'type' => event_action_exporter::read_properties_definition(),
'optional' => true,
];
$values['editurl'] = [
'type' => PARAM_URL,
'optional' => true,
];
return $values;
}
@ -86,6 +82,8 @@ class event_exporter extends event_exporter_base {
$params = array('update' => $moduleid, 'return' => true, 'sesskey' => sesskey());
$editurl = new \moodle_url('/course/mod.php', $params);
$values['editurl'] = $editurl->out(false);
} else if ($event->get_type() == 'category') {
$url = $event->get_category()->get_proxied_instance()->get_view_link();
} else if ($event->get_type() == 'course') {
$url = \course_get_url($this->related['course'] ?: SITEID);
} else {

View file

@ -34,6 +34,7 @@ use \core_calendar\local\event\container;
use \core_calendar\local\event\entities\event_interface;
use \core_calendar\local\event\entities\action_event_interface;
use \core_course\external\course_summary_exporter;
use \core\external\coursecat_summary_exporter;
use \renderer_base;
use moodle_url;
@ -64,6 +65,7 @@ class event_exporter_base extends exporter {
$endtimestamp = $event->get_times()->get_end_time()->getTimestamp();
$groupid = $event->get_group() ? $event->get_group()->get('id') : null;
$userid = $event->get_user() ? $event->get_user()->get('id') : null;
$categoryid = $event->get_category() ? $event->get_category()->get('id') : null;
$data = new \stdClass();
$data->id = $event->get_id();
@ -79,6 +81,7 @@ class event_exporter_base extends exporter {
$data->descriptionformat = $event->get_description()->get_format();
$data->groupid = $groupid;
$data->userid = $userid;
$data->categoryid = $categoryid;
$data->eventtype = $event->get_type();
$data->timestart = $starttimestamp;
$data->timeduration = $endtimestamp - $starttimestamp;
@ -120,6 +123,12 @@ class event_exporter_base extends exporter {
'default' => null,
'null' => NULL_ALLOWED
],
'categoryid' => [
'type' => PARAM_INT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'groupid' => [
'type' => PARAM_INT,
'optional' => true,
@ -175,6 +184,10 @@ class event_exporter_base extends exporter {
'icon' => [
'type' => event_icon_exporter::read_properties_definition(),
],
'category' => [
'type' => coursecat_summary_exporter::read_properties_definition(),
'optional' => true,
],
'course' => [
'type' => course_summary_exporter::read_properties_definition(),
'optional' => true,
@ -204,6 +217,9 @@ class event_exporter_base extends exporter {
'iscourseevent' => [
'type' => PARAM_BOOL
],
'iscategoryevent' => [
'type' => PARAM_BOOL
],
'groupname' => [
'type' => PARAM_RAW,
'optional' => true,
@ -226,10 +242,13 @@ class event_exporter_base extends exporter {
$context = $this->related['context'];
$values['isactionevent'] = false;
$values['iscourseevent'] = false;
$values['iscategoryevent'] = false;
if ($moduleproxy = $event->get_course_module()) {
$values['isactionevent'] = true;
} else if ($event->get_type() == 'course') {
$values['iscourseevent'] = true;
} else if ($event->get_type() == 'category') {
$values['iscategoryevent'] = true;
}
$timesort = $event->get_times()->get_sort_time()->getTimestamp();
$iconexporter = new event_icon_exporter($event, ['context' => $context]);
@ -239,6 +258,13 @@ class event_exporter_base extends exporter {
$subscriptionexporter = new event_subscription_exporter($event);
$values['subscription'] = $subscriptionexporter->export($output);
$proxy = $this->event->get_category();
if ($proxy && $proxy->get('id')) {
$category = $proxy->get_proxied_instance();
$categorysummaryexporter = new coursecat_summary_exporter($category, ['context' => $context]);
$values['category'] = $categorysummaryexporter->export($output);
}
if ($course = $this->related['course']) {
$coursesummaryexporter = new course_summary_exporter($course, ['context' => $context]);
$values['course'] = $coursesummaryexporter->export($output);

View file

@ -46,6 +46,8 @@ class event_icon_exporter extends exporter {
*/
public function __construct(event_interface $event, $related = []) {
$coursemodule = $event->get_course_module();
$category = $event->get_category();
$categoryid = $category ? $category->get('id') : null;
$course = $event->get_course();
$courseid = $course ? $course->get('id') : null;
$group = $event->get_group();
@ -54,6 +56,7 @@ class event_icon_exporter extends exporter {
$userid = $user ? $user->get('id') : null;
$isactivityevent = !empty($coursemodule);
$isglobalevent = ($course && $courseid == SITEID);
$iscategoryevent = ($category && !empty($categoryid));
$iscourseevent = ($course && !empty($courseid) && $courseid != SITEID && empty($groupid));
$isgroupevent = ($group && !empty($groupid));
$isuserevent = ($user && !empty($userid));
@ -70,24 +73,28 @@ class event_icon_exporter extends exporter {
} else if ($isglobalevent) {
$key = 'i/siteevent';
$component = 'core';
$alttext = get_string('globalevent', 'calendar');
$alttext = get_string('typesite', 'calendar');
} else if ($iscategoryevent) {
$key = 'i/categoryevent';
$component = 'core';
$alttext = get_string('typecategory', 'calendar');
} else if ($iscourseevent) {
$key = 'i/courseevent';
$component = 'core';
$alttext = get_string('courseevent', 'calendar');
$alttext = get_string('typecourse', 'calendar');
} else if ($isgroupevent) {
$key = 'i/groupevent';
$component = 'core';
$alttext = get_string('groupevent', 'calendar');
$alttext = get_string('typegroup', 'calendar');
} else if ($isuserevent) {
$key = 'i/userevent';
$component = 'core';
$alttext = get_string('userevent', 'calendar');
$alttext = get_string('typeuser', 'calendar');
} else {
// Default to site event icon?
$key = 'i/siteevent';
$component = 'core';
$alttext = get_string('globalevent', 'calendar');
$alttext = get_string('typesite', 'calendar');
}
$data = new \stdClass();

View file

@ -75,8 +75,10 @@ class month_exporter extends exporter {
'time' => $calendar->time,
]);
if ($this->calendar->courseid) {
$this->url->param('course', $this->calendar->courseid);
if ($this->calendar->course && SITEID !== $this->calendar->course->id) {
$this->url->param('course', $this->calendar->course->id);
} else if ($this->calendar->categoryid) {
$this->url->param('category', $this->calendar->categoryid);
}
$related['type'] = $type;
@ -106,6 +108,11 @@ class month_exporter extends exporter {
'courseid' => [
'type' => PARAM_INT,
],
'categoryid' => [
'type' => PARAM_INT,
'optional' => true,
'default' => 0,
],
'filter_selector' => [
'type' => PARAM_RAW,
],
@ -209,6 +216,10 @@ class month_exporter extends exporter {
$return['defaulteventcontext'] = $context->id;
}
if ($this->calendar->categoryid) {
$return['categoryid'] = $this->calendar->categoryid;
}
return $return;
}

View file

@ -118,10 +118,16 @@ class week_day_exporter extends day_exporter {
$url = new moodle_url('/calendar/view.php', [
'view' => 'day',
'time' => $timestamp,
'course' => $this->calendar->course->id,
]);
]);
if ($this->calendar->course && SITEID !== $this->calendar->course->id) {
$url->param('course', $this->calendar->course->id);
} else if ($this->calendar->categoryid) {
$url->param('category', $this->calendar->categoryid);
}
$return['viewdaylink'] = $url->out(false);
if ($popovertitle = $this->get_popover_title()) {
$return['popovertitle'] = $popovertitle;
}

View file

@ -71,6 +71,7 @@ class api {
array $usersfilter = null,
array $groupsfilter = null,
array $coursesfilter = null,
array $categoriesfilter = null,
$withduration = true,
$ignorehidden = true,
callable $filter = null
@ -102,6 +103,7 @@ class api {
$usersfilter,
$groupsfilter,
$coursesfilter,
$categoriesfilter,
$withduration,
$ignorehidden,
$filter

View file

@ -117,6 +117,15 @@ class container {
[self::class, 'apply_component_provide_event_action'],
[self::class, 'apply_component_is_event_visible'],
function ($dbrow) {
if (!empty($dbrow->categoryid)) {
// This is a category event. Check that the category is visible to this user.
$category = \coursecat::get($dbrow->categoryid, IGNORE_MISSING, true);
if (empty($category) || !$category->is_uservisible()) {
return true;
}
}
// At present we only have a bail-out check for events in course modules.
if (empty($dbrow->modulename)) {
return false;

View file

@ -33,6 +33,8 @@ use core_calendar\local\event\factories\action_factory_interface;
use core_calendar\local\event\factories\event_factory_interface;
use core_calendar\local\event\strategies\raw_event_retrieval_strategy_interface;
require_once($CFG->libdir . '/coursecatlib.php');
/**
* Event vault class.
*
@ -95,6 +97,7 @@ class event_vault implements event_vault_interface {
array $usersfilter = null,
array $groupsfilter = null,
array $coursesfilter = null,
array $categoriesfilter = null,
$withduration = true,
$ignorehidden = true,
callable $filter = null
@ -162,6 +165,7 @@ class event_vault implements event_vault_interface {
$usersfilter,
$groupsfilter,
$coursesfilter,
$categoriesfilter,
$where,
$params,
"COALESCE(e.timesort, e.timestart) ASC, e.id ASC",
@ -197,6 +201,10 @@ class event_vault implements event_vault_interface {
event_interface $afterevent = null,
$limitnum = 20
) {
$categoryids = array_map(function($category) {
return $category->id;
}, \coursecat::get_all());
$courseids = array_map(function($course) {
return $course->id;
}, enrol_get_all_users_courses($user->id));
@ -219,6 +227,7 @@ class event_vault implements event_vault_interface {
[$user->id],
$groupids ? $groupids : null,
$courseids ? $courseids : null,
$categoryids ? $categoryids : null,
true,
true,
function ($event) {
@ -249,6 +258,7 @@ class event_vault implements event_vault_interface {
[$user->id],
$groupings[0] ? $groupings[0] : null,
[$course->id],
[],
true,
true,
function ($event) use ($course) {
@ -375,6 +385,7 @@ class event_vault implements event_vault_interface {
[$userid],
null,
null,
null,
$whereconditions,
$whereparams,
$ordersql,

View file

@ -76,6 +76,7 @@ interface event_vault_interface {
array $usersfilter = null,
array $groupsfilter = null,
array $coursesfilter = null,
array $categoriesfilter = null,
$withduration = true,
$ignorehidden = true,
callable $filter = null

View file

@ -50,6 +50,11 @@ class action_event implements action_event_interface {
*/
protected $action;
/**
* @var proxy_interface $category Category for this event.
*/
protected $category;
/**
* Constructor.
*
@ -73,6 +78,10 @@ class action_event implements action_event_interface {
return $this->event->get_description();
}
public function get_category() {
return $this->event->get_category();
}
public function get_course() {
return $this->event->get_course();
}

View file

@ -52,6 +52,11 @@ class event implements event_interface {
*/
protected $description;
/**
* @var proxy_interface $category Category for this event.
*/
protected $category;
/**
* @var proxy_interface $course Course for this event.
*/
@ -103,6 +108,7 @@ class event implements event_interface {
* @param int $id The event's ID in the database.
* @param string $name The event's name.
* @param description_interface $description The event's description.
* @param proxy_interface $category The category associated with the event.
* @param proxy_interface $course The course associated with the event.
* @param proxy_interface $group The group associated with the event.
* @param proxy_interface $user The user associated with the event.
@ -117,6 +123,7 @@ class event implements event_interface {
$id,
$name,
description_interface $description,
proxy_interface $category = null,
proxy_interface $course = null,
proxy_interface $group = null,
proxy_interface $user = null,
@ -130,6 +137,7 @@ class event implements event_interface {
$this->id = $id;
$this->name = $name;
$this->description = $description;
$this->category = $category;
$this->course = $course;
$this->group = $group;
$this->user = $user;
@ -153,6 +161,10 @@ class event implements event_interface {
return $this->description;
}
public function get_category() {
return $this->category;
}
public function get_course() {
return $this->course;
}

View file

@ -56,6 +56,13 @@ interface event_interface {
*/
public function get_description();
/**
* Get the category object associated with the event.
*
* @return proxy_interface
*/
public function get_category();
/**
* Get the course object associated with the event.
*

View file

@ -30,11 +30,14 @@ use core_calendar\local\event\entities\event;
use core_calendar\local\event\entities\repeat_event_collection;
use core_calendar\local\event\exceptions\invalid_callback_exception;
use core_calendar\local\event\proxies\cm_info_proxy;
use core_calendar\local\event\proxies\coursecat_proxy;
use core_calendar\local\event\proxies\std_proxy;
use core_calendar\local\event\value_objects\event_description;
use core_calendar\local\event\value_objects\event_times;
use core_calendar\local\event\entities\event_interface;
require_once($CFG->libdir . '/coursecatlib.php');
/**
* Abstract factory for creating calendar events.
*
@ -126,6 +129,7 @@ abstract class event_abstract_factory implements event_factory_interface {
return null;
}
$category = null;
$course = null;
$group = null;
$user = null;
@ -136,6 +140,8 @@ abstract class event_abstract_factory implements event_factory_interface {
$module = new cm_info_proxy($dbrow->modulename, $dbrow->instance, $dbrow->courseid);
}
$category = new coursecat_proxy($dbrow->categoryid);
$course = new std_proxy($dbrow->courseid, function($id) {
return calendar_get_course_cached($this->coursecachereference, $id);
});
@ -163,6 +169,7 @@ abstract class event_abstract_factory implements event_factory_interface {
$dbrow->id,
$dbrow->name,
new event_description($dbrow->description, $dbrow->format),
$category,
$course,
$group,
$user,

View file

@ -55,7 +55,7 @@ class create extends \moodleform {
/**
* The form definition
*/
public function definition () {
public function definition() {
global $PAGE;
$mform = $this->_form;
@ -203,6 +203,9 @@ class create extends \moodleform {
if (isset($eventtypes['course'])) {
$options['course'] = get_string('course');
}
if (isset($eventtypes['category'])) {
$options['category'] = get_string('category');
}
if (isset($eventtypes['site'])) {
$options['site'] = get_string('site');
}
@ -223,6 +226,16 @@ class create extends \moodleform {
$mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $options);
}
if (isset($eventtypes['category'])) {
$categoryoptions = [];
foreach ($eventtypes['category'] as $id => $category) {
$categoryoptions[$id] = $category;
}
$mform->addElement('select', 'categoryid', get_string('category'), $categoryoptions);
$mform->hideIf('categoryid', 'eventtype', 'noteq', 'category');
}
if (isset($eventtypes['course'])) {
$courseoptions = [];
foreach ($eventtypes['course'] as $course) {

View file

@ -69,6 +69,7 @@ class event_mapper implements event_mapper_interface {
'name' => $coalesce('name'),
'description' => $coalesce('description'),
'format' => $coalesce('format'),
'categoryid' => $coalesce('categoryid'),
'courseid' => $coalesce('courseid'),
'groupid' => $coalesce('groupid'),
'userid' => $coalesce('userid'),

View file

@ -0,0 +1,92 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Course category proxy.
*
* @package core_calendar
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_calendar\local\event\proxies;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/coursecatlib.php');
/**
* Course category proxy.
*
* This returns an instance of a coursecat rather than a stdClass.
*
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class coursecat_proxy implements proxy_interface {
/**
* @var int $id The ID of the database record.
*/
protected $id;
/**
* @var \stdClass $base Base class to get members from.
*/
protected $base;
/**
* @var \coursecat $category The proxied instance.
*/
protected $category;
/**
* coursecat_proxy constructor.
*
* @param int $id The ID of the record in the database.
*/
public function __construct($id) {
$this->id = $id;
$this->base = (object) [
'id' => $id,
];
}
/**
* Retrieve a member of the proxied class.
*
* @param string $member The name of the member to retrieve
* @return mixed The member.
*/
public function get($member) {
if ($this->base && property_exists($this->base, $member)) {
return $this->base->{$member};
}
return $this->get_proxied_instance()->{$member};
}
/**
* Get the full instance of the proxied class.
*
* @return \coursecat
*/
public function get_proxied_instance() : \coursecat {
if (!$this->category) {
$this->category = \coursecat::get($this->id, IGNORE_MISSING, true);
}
return $this->category;
}
}

View file

@ -40,6 +40,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
array $usersfilter = null,
array $groupsfilter = null,
array $coursesfilter = null,
array $categoriesfilter = null,
array $whereconditions = null,
array $whereparams = null,
$ordersql = null,
@ -51,6 +52,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
!is_null($usersfilter) ? $usersfilter : true, // True means no filter in old implementation.
!is_null($groupsfilter) ? $groupsfilter : true,
!is_null($coursesfilter) ? $coursesfilter : true,
!is_null($categoriesfilter) ? $categoriesfilter : true,
$whereconditions,
$whereparams,
$ordersql,
@ -78,6 +80,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
$users,
$groups,
$courses,
$categories,
$whereconditions,
$whereparams,
$ordersql,
@ -89,7 +92,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
$params = array();
// Quick test.
if (empty($users) && empty($groups) && empty($courses)) {
if (empty($users) && empty($groups) && empty($courses) && empty($categories)) {
return array();
}
@ -100,11 +103,11 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
if ((is_array($users) && !empty($users)) or is_numeric($users)) {
// Events from a number of users.
list($insqlusers, $inparamsusers) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED);
$filters[] = "(e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0)";
$filters[] = "(e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)";
$params = array_merge($params, $inparamsusers);
} else if ($users === true) {
// Events from ALL users.
$filters[] = "(e.userid != 0 AND e.courseid = 0 AND e.groupid = 0)";
$filters[] = "(e.userid != 0 AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)";
}
// Boolean false (no users at all): We don't need to do anything.
@ -130,6 +133,16 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
$filters[] = "(e.groupid = 0 AND e.courseid != 0)";
}
// Category filter.
if ((is_array($categories) && !empty($categories)) or is_numeric($categories)) {
list($insqlcategories, $inparamscategories) = $DB->get_in_or_equal($categories, SQL_PARAMS_NAMED);
$filters[] = "(e.groupid = 0 AND e.courseid = 0 AND e.categoryid $insqlcategories)";
$params = array_merge($params, $inparamscategories);
} else if ($categories === true) {
// Events from ALL categories.
$filters[] = "(e.groupid = 0 AND e.courseid = 0 AND e.categoryid != 0)";
}
// Security check: if, by now, we have NOTHING in $whereclause, then it means
// that NO event-selecting clauses were defined. Thus, we won't be returning ANY
// events no matter what. Allowing the code to proceed might return a completely
@ -168,7 +181,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
if ($user) {
// Set filter condition for the user's events.
$subqueryconditions[] = "(ev.userid = :user AND ev.courseid = 0 AND ev.groupid = 0)";
$subqueryconditions[] = "(ev.userid = :user AND ev.courseid = 0 AND ev.groupid = 0 AND ev.categoryid = 0)";
$subqueryparams['user'] = $user;
foreach ($usercourses as $courseid) {
@ -210,10 +223,19 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
// Set subquery filter condition for the courses.
if (!empty($subquerycourses)) {
list($incourses, $incoursesparams) = $DB->get_in_or_equal($subquerycourses, SQL_PARAMS_NAMED);
$subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid $incourses)";
$subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid $incourses AND ev.categoryid = 0)";
$subqueryparams = array_merge($subqueryparams, $incoursesparams);
}
// Set subquery filter condition for the categories.
if ($categories === true) {
$subqueryconditions[] = "(ev.categoryid != 0 AND ev.eventtype = 'category')";
} else if (!empty($categories)) {
list($incategories, $incategoriesparams) = $DB->get_in_or_equal($categories, SQL_PARAMS_NAMED);
$subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid = 0 AND ev.categoryid $incategories)";
$subqueryparams = array_merge($subqueryparams, $incategoriesparams);
}
// Build the WHERE condition for the sub-query.
if (!empty($subqueryconditions)) {
$subquerywhere = 'WHERE ' . implode(" OR ", $subqueryconditions);

View file

@ -39,6 +39,7 @@ interface raw_event_retrieval_strategy_interface {
* @param array|null $usersfilter Array of users to retrieve events for.
* @param array|null $groupsfilter Array of groups to retrieve events for.
* @param array|null $coursesfilter Array of courses to retrieve events for.
* @param array|null $categoriesfilter Array of categories to retrieve events for.
* @param array|null $whereconditions Array of where conditions to restrict results.
* @param array|null $whereparams Array of parameters for $whereconditions.
* @param string|null $ordersql SQL to order results.
@ -51,6 +52,7 @@ interface raw_event_retrieval_strategy_interface {
array $usersfilter = null,
array $groupsfilter = null,
array $coursesfilter = null,
array $categoriesfilter = null,
array $whereconditions = null,
array $whereparams = null,
$ordersql = null,

View file

@ -108,7 +108,7 @@ if ($action === 'delete' && $eventid > 0) {
}
$calendar = new calendar_information(0, 0, 0, $time);
$calendar->prepare_for_view($course, $courses);
$calendar->set_sources($course, $courses);
$formoptions = new stdClass;
if ($eventid !== 0) {

View file

@ -100,7 +100,7 @@ if ($course !== NULL) {
$PAGE->set_url($url);
$calendar = new calendar_information(0, 0, 0, $time);
$calendar->prepare_for_view($course, $courses);
$calendar->set_sources($course, $courses);
$pagetitle = get_string('export', 'calendar');

View file

@ -882,10 +882,11 @@ class core_calendar_external extends external_api {
* @param int $year The year to be shown
* @param int $month The month to be shown
* @param int $courseid The course to be included
* @param int $categoryid The category to be included
* @param bool $includenavigation Whether to include navigation
* @return array
*/
public static function get_calendar_monthly_view($year, $month, $courseid, $includenavigation) {
public static function get_calendar_monthly_view($year, $month, $courseid, $categoryid, $includenavigation) {
global $CFG, $DB, $USER, $PAGE;
require_once($CFG->dirroot."/calendar/lib.php");
@ -894,27 +895,45 @@ class core_calendar_external extends external_api {
'year' => $year,
'month' => $month,
'courseid' => $courseid,
'categoryid' => $categoryid,
'includenavigation' => $includenavigation,
]);
// TODO: Copy what we do in calendar/view.php.
$context = \context_user::instance($USER->id);
self::validate_context($context);
$PAGE->set_url('/calendar/');
if ($courseid != SITEID && !empty($courseid)) {
// Course ID must be valid and existing.
$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
$courses = [$course->id => $course];
$coursecat = \coursecat::get($course->category);
$category = $coursecat->get_db_record();
} else {
$course = get_site();
$courses = calendar_get_default_courses();
}
$category = null;
// TODO: Copy what we do in calendar/view.php.
$context = \context_user::instance($USER->id);
self::validate_context($context);
if ($categoryid) {
self::validate_context(context_coursecat::instance($categoryid));
$ids = [$categoryid];
$category = \coursecat::get($categoryid);
$ids += $category->get_parents();
$categories = \coursecat::get_many($ids);
$courses = array_filter($courses, function($course) use ($categories) {
return array_search($course->category, $categories) !== false;
});
$category = $category->get_db_record();
}
}
$type = \core_calendar\type_factory::get_calendar_instance();
$time = $type->convert_to_timestamp($year, $month, 1);
$calendar = new calendar_information(0, 0, 0, $time);
$calendar->prepare_for_view($course, $courses);
$calendar->set_sources($course, $courses, $category);
list($data, $template) = calendar_get_view($calendar, 'month', $params['includenavigation']);
@ -932,6 +951,7 @@ class core_calendar_external extends external_api {
'year' => new external_value(PARAM_INT, 'Month to be viewed', VALUE_REQUIRED),
'month' => new external_value(PARAM_INT, 'Year to be viewed', VALUE_REQUIRED),
'courseid' => new external_value(PARAM_INT, 'Course being viewed', VALUE_DEFAULT, SITEID, NULL_ALLOWED),
'categoryid' => new external_value(PARAM_INT, 'Category being viewed', VALUE_DEFAULT, null, NULL_ALLOWED),
'includenavigation' => new external_value(
PARAM_BOOL,
'Whether to show course navigation',

View file

@ -27,6 +27,8 @@ if (!defined('MOODLE_INTERNAL')) {
die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
}
require_once($CFG->libdir . '/coursecatlib.php');
/**
* These are read by the administration component to provide default values
*/
@ -89,6 +91,10 @@ define('CALENDAR_EVENT_GROUP', 4);
*/
define('CALENDAR_EVENT_USER', 8);
/**
* CALENDAR_EVENT_COURSECAT - Course category calendar event types
*/
define('CALENDAR_EVENT_COURSECAT', 16);
/**
* CALENDAR_IMPORT_FROM_FILE - import the calendar from a file
@ -312,7 +318,9 @@ class calendar_event {
global $USER, $DB;
$context = null;
if (isset($this->properties->courseid) && $this->properties->courseid > 0) {
if (isset($this->properties->categoryid) && $this->properties->categoryid > 0) {
$context = \context_coursecat::instance($this->properties->categoryid);
} else if (isset($this->properties->courseid) && $this->properties->courseid > 0) {
$context = \context_course::instance($this->properties->courseid);
} else if (isset($this->properties->course) && $this->properties->course > 0) {
$context = \context_course::instance($this->properties->course);
@ -360,8 +368,7 @@ class calendar_event {
if ($this->editorcontext === null) {
// Switch on the event type to decide upon the appropriate context to use for this event.
$this->editorcontext = $this->properties->context;
if ($this->properties->eventtype != 'user' && $this->properties->eventtype != 'course'
&& $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') {
if (!calendar_is_valid_eventtype($this->properties->eventtype)) {
return clean_text($this->properties->description, $this->properties->format);
}
}
@ -461,6 +468,11 @@ class calendar_event {
$this->properties->groupid = 0;
$this->properties->userid = $USER->id;
break;
case 'category':
$this->properties->groupid = 0;
$this->properties->category = 0;
$this->properties->userid = $USER->id;
break;
case 'group':
$this->properties->userid = $USER->id;
break;
@ -752,6 +764,14 @@ class calendar_event {
// We have a course and are within the course context so we had
// better use the courses max bytes value.
$this->editoroptions['maxbytes'] = $course->maxbytes;
} else if ($properties->eventtype === 'category') {
// First check the course is valid.
\coursecat::get($properties->categoryid, MUST_EXIST, true);
// Course context.
$this->editorcontext = $this->properties->context;
// We have a course and are within the course context so we had
// better use the courses max bytes value.
$this->editoroptions['maxbytes'] = $course->maxbytes;
} else {
// If we get here we have a custom event type as used by some
// modules. In this case the event will have been added by
@ -896,8 +916,7 @@ class calendar_event {
// Switch on the event type to decide upon the appropriate context to use for this event.
$this->editorcontext = $this->properties->context;
if ($this->properties->eventtype != 'user' && $this->properties->eventtype != 'course'
&& $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') {
if (!calendar_is_valid_eventtype($this->properties->eventtype)) {
// We don't have a context here, do a normal format_text.
return external_format_text($this->properties->description, $this->properties->format, $this->editorcontext->id);
}
@ -939,6 +958,12 @@ class calendar_information {
/** @var int A course id */
public $courseid = null;
/** @var array An array of categories */
public $categories = array();
/** @var int The current category */
public $categoryid = null;
/** @var array An array of courses */
public $courses = array();
@ -989,7 +1014,7 @@ class calendar_information {
* @return $this
*/
public function set_time($time = null) {
if ($time === null) {
if (empty($time)) {
$this->time = time();
} else {
$this->time = $time;
@ -1001,17 +1026,71 @@ class calendar_information {
/**
* Initialize calendar information
*
* @deprecated 3.4
* @param stdClass $course object
* @param array $coursestoload An array of courses [$course->id => $course]
* @param bool $ignorefilters options to use filter
*/
public function prepare_for_view(stdClass $course, array $coursestoload, $ignorefilters = false) {
$this->courseid = $course->id;
debugging('The prepare_for_view() function has been deprecated. Please update your code to use set_sources()',
DEBUG_DEVELOPER);
$this->set_sources($course, $coursestoload);
}
/**
* Set the sources for events within the calendar.
*
* If no category is provided, then the category path for the current
* course will be used.
*
* @param stdClass $course The current course being viewed.
* @param int[] $courses The list of all courses currently accessible.
* @param stdClass $category The current category to show.
*/
public function set_sources(stdClass $course, array $courses, stdClass $category = null) {
// A cousre must always be specified.
$this->course = $course;
list($courses, $group, $user) = calendar_set_filters($coursestoload, $ignorefilters);
$this->courses = $courses;
$this->courseid = $course->id;
list($courseids, $group, $user) = calendar_set_filters($courses);
$this->courses = $courseids;
$this->groups = $group;
$this->users = $user;
// Do not show category events by default.
$this->categoryid = null;
$this->categories = null;
if (null !== $category && $category->id > 0) {
// A specific category was requested - set the current category, and include all parents of that category.
$category = \coursecat::get($category->id);
$this->categoryid = $category->id;
$this->categories = $category->get_parents();
$this->categories[] = $category->id;
} else if (SITEID === $this->courseid) {
// This is the site.
// Show categories for all courses the user has access to.
$this->categories = true;
$categories = [];
foreach ($courses as $course) {
$category = \coursecat::get($course->category);
$categories = array_merge($categories, $category->get_parents());
$categories[] = $category->id;
}
// And all categories that the user can manage.
foreach (\coursecat::get_all() as $category) {
if (!$category->has_manage_capability()) {
continue;
}
$categories = array_merge($categories, $category->get_parents());
$categories[] = $category->id;
}
$this->categories = array_unique($categories);
}
}
/**
@ -1087,15 +1166,17 @@ class calendar_information {
* @param boolean $withduration whether only events starting within time range selected
* or events in progress/already started selected as well
* @param boolean $ignorehidden whether to select only visible events or all events
* @param array|int|boolean $categories array of categories, category id or boolean for all/no course events
* @return array $events of selected events or an empty array if there aren't any (or there was an error)
*/
function calendar_get_events($tstart, $tend, $users, $groups, $courses, $withduration=true, $ignorehidden=true) {
function calendar_get_events($tstart, $tend, $users, $groups, $courses,
$withduration = true, $ignorehidden = true, $categories = []) {
global $DB;
$whereclause = '';
$params = array();
// Quick test.
if (empty($users) && empty($groups) && empty($courses)) {
if (empty($users) && empty($groups) && empty($courses) && empty($categories)) {
return array();
}
@ -1103,12 +1184,12 @@ function calendar_get_events($tstart, $tend, $users, $groups, $courses, $withdur
// Events from a number of users
if(!empty($whereclause)) $whereclause .= ' OR';
list($insqlusers, $inparamsusers) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED);
$whereclause .= " (e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0)";
$whereclause .= " (e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)";
$params = array_merge($params, $inparamsusers);
} else if($users === true) {
// Events from ALL users
if(!empty($whereclause)) $whereclause .= ' OR';
$whereclause .= ' (e.userid != 0 AND e.courseid = 0 AND e.groupid = 0)';
$whereclause .= ' (e.userid != 0 AND e.courseid = 0 AND e.groupid = 0 AND e.categoryid = 0)';
} else if($users === false) {
// No user at all, do nothing
}
@ -1137,6 +1218,21 @@ function calendar_get_events($tstart, $tend, $users, $groups, $courses, $withdur
$whereclause .= ' (e.groupid = 0 AND e.courseid != 0)';
}
if ((is_array($categories) && !empty($categories)) || is_numeric($categories)) {
if (!empty($whereclause)) {
$whereclause .= ' OR';
}
list($insqlcategories, $inparamscategories) = $DB->get_in_or_equal($categories, SQL_PARAMS_NAMED);
$whereclause .= " (e.groupid = 0 AND e.courseid = 0 AND e.categoryid $insqlcategories)";
$params = array_merge($params, $inparamscategories);
} else if ($categories === true) {
// Events from ALL categories.
if (!empty($whereclause)) {
$whereclause .= ' OR';
}
$whereclause .= ' (e.groupid = 0 AND e.courseid = 0 AND e.categoryid != 0)';
}
// Security check: if, by now, we have NOTHING in $whereclause, then it means
// that NO event-selecting clauses were defined. Thus, we won't be returning ANY
// events no matter what. Allowing the code to proceed might return a completely
@ -2050,19 +2146,27 @@ function calendar_edit_event_allowed($event, $manualedit = false) {
// Allow users to add/edit group events if -
// 1) They have manageentries for the course OR
// 2) They have managegroupentries AND are in the group.
$eventcontext = \context_course::instance($event->courseid);
$group = $DB->get_record('groups', array('id' => $event->groupid));
return $group && (
has_capability('moodle/calendar:manageentries', $event->context) ||
(has_capability('moodle/calendar:managegroupentries', $event->context)
has_capability('moodle/calendar:manageentries', $eventcontext) ||
(has_capability('moodle/calendar:managegroupentries', $eventcontext)
&& groups_is_member($event->groupid)));
} else if (!empty($event->courseid)) {
// If groupid is not set, but course is set, it's definiely a course event.
return has_capability('moodle/calendar:manageentries', $event->context);
// If groupid is not set, but course is set, it's definitely a course event.
$eventcontext = \context_course::instance($event->courseid);
return has_capability('moodle/calendar:manageentries', $eventcontext);
} else if (!empty($event->categoryid)) {
// If groupid is not set, but category is set, it's definitely a category event.
$eventcontext = \context_coursecat::instance($event->categoryid);
return has_capability('moodle/calendar:manageentries', $eventcontext);
} else if (!empty($event->userid) && $event->userid == $USER->id) {
// If course is not set, but userid id set, it's a user event.
return (has_capability('moodle/calendar:manageownentries', $event->context));
$eventcontext = \context_user::instance($event->userid);
return (has_capability('moodle/calendar:manageownentries', $eventcontext));
} else if (!empty($event->userid)) {
return (has_capability('moodle/calendar:manageentries', $event->context));
$eventcontext = \context_user::instance($event->userid);
return (has_capability('moodle/calendar:manageentries', $eventcontext));
}
return false;
@ -2237,7 +2341,8 @@ function calendar_show_event_type($type, $user = null) {
*/
function calendar_set_event_type_display($type, $display = null, $user = null) {
$persist = get_user_preferences('calendar_persistflt', 0, $user);
$default = CALENDAR_EVENT_GLOBAL + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP + CALENDAR_EVENT_USER;
$default = CALENDAR_EVENT_GLOBAL + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP
+ CALENDAR_EVENT_USER + CALENDAR_EVENT_COURSECAT;
if ($persist === 0) {
global $SESSION;
if (!isset($SESSION->calendarshoweventtype)) {
@ -2273,14 +2378,16 @@ function calendar_set_event_type_display($type, $display = null, $user = null) {
* @param stdClass $allowed list of allowed edit for event type
* @param stdClass|int $course object of a course or course id
* @param array $groups array of groups for the given course
* @param stdClass|int $category object of a category
*/
function calendar_get_allowed_types(&$allowed, $course = null, $groups = null) {
function calendar_get_allowed_types(&$allowed, $course = null, $groups = null, $category = null) {
global $USER, $DB;
$allowed = new \stdClass();
$allowed->user = has_capability('moodle/calendar:manageownentries', \context_system::instance());
$allowed->groups = false;
$allowed->courses = false;
$allowed->categories = false;
$allowed->site = has_capability('moodle/calendar:manageentries', \context_course::instance(SITEID));
$getgroupsfunc = function($course, $context, $user) use ($groups) {
if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
@ -2316,6 +2423,13 @@ function calendar_get_allowed_types(&$allowed, $course = null, $groups = null) {
}
}
}
if (!empty($category)) {
$catcontext = \context_coursecat::instance($category->id);
if (has_capability('moodle/category:manage', $catcontext)) {
$allowed->categories = [$category->id => 1];
}
}
}
/**
@ -2325,6 +2439,7 @@ function calendar_get_allowed_types(&$allowed, $course = null, $groups = null) {
* The returned array will optionally have 5 keys:
* 'user' : true if the logged in user can create user events
* 'site' : true if the logged in user can create site events
* 'category' : array of course categories that the user can create events for
* 'course' : array of courses that the user can create events for
* 'group': array of groups that the user can create events for
* 'groupcourses' : array of courses that the groups belong to (can
@ -2333,7 +2448,7 @@ function calendar_get_allowed_types(&$allowed, $course = null, $groups = null) {
* @return array The array of allowed types.
*/
function calendar_get_all_allowed_types() {
global $CFG, $USER;
global $CFG, $USER, $DB;
require_once($CFG->libdir . '/enrollib.php');
@ -2349,6 +2464,10 @@ function calendar_get_all_allowed_types() {
$types['site'] = true;
}
if (coursecat::has_manage_capability_on_any()) {
$types['category'] = coursecat::make_categories_list('moodle/category:manage');
}
// This function warms the context cache for the course so the calls
// to load the course context in calendar_get_allowed_types don't result
// in additional DB queries.
@ -2401,7 +2520,7 @@ function calendar_user_can_add_event($course) {
calendar_get_allowed_types($allowed, $course);
return (bool)($allowed->user || $allowed->groups || $allowed->courses || $allowed->site);
return (bool)($allowed->user || $allowed->groups || $allowed->courses || $allowed->category || $allowed->site);
}
/**
@ -2426,6 +2545,8 @@ function calendar_add_event_allowed($event) {
}
switch ($event->eventtype) {
case 'category':
return has_capability('moodle/category:manage', $event->context);
case 'course':
return has_capability('moodle/calendar:manageentries', $event->context);
case 'group':
@ -2485,6 +2606,9 @@ function calendar_get_eventtype_choices($courseid) {
if (!empty($allowed->courses)) {
$choices['course'] = get_string('courseevents', 'calendar');
}
if (!empty($allowed->categories)) {
$choices['category'] = get_string('categoryevents', 'calendar');
}
if (!empty($allowed->groups) and is_array($allowed->groups)) {
$choices['group'] = get_string('group');
}
@ -2505,6 +2629,8 @@ function calendar_add_subscription($sub) {
$sub->courseid = $SITE->id;
} else if ($sub->eventtype === 'group' || $sub->eventtype === 'course') {
$sub->courseid = $sub->course;
} else if ($sub->eventtype === 'category') {
$sub->categoryid = $sub->category;
} else {
// User events.
$sub->courseid = 0;
@ -2956,11 +3082,12 @@ function core_calendar_user_preferences() {
* @param boolean $ignorehidden whether to select only visible events or all events
* @return array $events of selected events or an empty array if there aren't any (or there was an error)
*/
function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses, $withduration = true, $ignorehidden = true) {
function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses,
$withduration = true, $ignorehidden = true, $categories = []) {
// Normalise the users, groups and courses parameters so that they are compliant with \core_calendar\local\api::get_events().
// Existing functions that were using the old calendar_get_events() were passing a mixture of array, int, boolean for these
// parameters, but with the new API method, only null and arrays are accepted.
list($userparam, $groupparam, $courseparam) = array_map(function($param) {
list($userparam, $groupparam, $courseparam, $categoryparam) = array_map(function($param) {
// If parameter is true, return null.
if ($param === true) {
return null;
@ -2978,7 +3105,7 @@ function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses, $
// No normalisation required.
return $param;
}, [$users, $groups, $courses]);
}, [$users, $groups, $courses, $categories]);
$mapper = \core_calendar\local\event\container::get_event_mapper();
$events = \core_calendar\local\api::get_events(
@ -2993,6 +3120,7 @@ function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses, $
$userparam,
$groupparam,
$courseparam,
$categoryparam,
$withduration,
$ignorehidden
);
@ -3043,7 +3171,7 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig
}
}
list($userparam, $groupparam, $courseparam) = array_map(function($param) {
list($userparam, $groupparam, $courseparam, $categoryparam) = array_map(function($param) {
// If parameter is true, return null.
if ($param === true) {
return null;
@ -3061,7 +3189,7 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig
// No normalisation required.
return $param;
}, [$calendar->users, $calendar->groups, $calendar->courses]);
}, [$calendar->users, $calendar->groups, $calendar->courses, $calendar->categories]);
$events = \core_calendar\local\api::get_events(
$tstart,
@ -3075,13 +3203,19 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig
$userparam,
$groupparam,
$courseparam,
$categoryparam,
true,
true,
function ($event) {
if ($proxy = $event->get_course_module()) {
$cminfo = $proxy->get_proxied_instance();
return $cminfo->uservisible;
}
if ($proxy = $event->get_category()) {
$category = $proxy->get_proxied_instance();
return $category->is_uservisible();
}
return true;
@ -3123,6 +3257,7 @@ function calendar_output_fragment_event_form($args) {
$eventid = isset($args['eventid']) ? clean_param($args['eventid'], PARAM_INT) : null;
$starttime = isset($args['starttime']) ? clean_param($args['starttime'], PARAM_INT) : null;
$courseid = isset($args['courseid']) ? clean_param($args['courseid'], PARAM_INT) : null;
$categoryid = isset($args['categoryid']) ? clean_param($args['categoryid'], PARAM_INT) : null;
$event = null;
$hasformdata = isset($args['formdata']) && !empty($args['formdata']);
$context = \context_user::instance($USER->id);
@ -3155,6 +3290,9 @@ function calendar_output_fragment_event_form($args) {
$data['eventtype'] = 'course';
$data['courseid'] = $courseid;
$data['groupcourseid'] = $courseid;
} else if (!empty($categoryid)) {
$data['eventtype'] = 'category';
$data['categoryid'] = $categoryid;
}
$mform->set_data($data);
} else {
@ -3262,6 +3400,7 @@ function calendar_get_footer_options($calendar) {
function calendar_get_filter_types() {
$types = [
'site',
'category',
'course',
'group',
'user',
@ -3274,3 +3413,20 @@ function calendar_get_filter_types() {
];
}, $types);
}
/**
* Check whether the specified event type is valid.
*
* @param string $type
* @return bool
*/
function calendar_is_valid_eventtype($type) {
$validtypes = [
'user',
'group',
'course',
'category',
'site',
];
return in_array($type, $validtypes);
}

View file

@ -46,6 +46,9 @@
{{/description}}
<h4>{{#str}} eventtype, core_calendar {{/str}}</h4>
{{eventtype}}
{{#iscategoryevent}}
<div>{{{category.nestedname}}}</div>
{{/iscategoryevent}}
{{#iscourseevent}}
<div><a href="{{url}}">{{course.fullname}}</a></div>
{{/iscourseevent}}

View file

@ -33,7 +33,8 @@
}}
<div{{!
}} class="calendarwrapper"{{!
}} data-courseid="{{courseid}}"{{!
}}{{#courseid}} data-courseid="{{courseid}}"{{/courseid}}{{!
}}{{#categoryid}} data-categoryid="{{categoryid}}"{{/categoryid}}{{!
}} data-month="{{date.mon}}"{{!
}} data-year="{{date.year}}"{{!
}}>

View file

@ -35,6 +35,7 @@
}} id="month-mini-{{date.year}}-{{date.month}}-{{uniqid}}"{{!
}} class="calendarwrapper"{{!
}} data-courseid="{{courseid}}"{{!
}} data-categoryid="{{categoryid}}"{{!
}} data-month="{{date.mon}}"{{!
}} data-year="{{date.year}}"{{!
}}>

View file

@ -108,6 +108,10 @@ class core_calendar_action_event_test_event implements event_interface {
return new event_description('asdf', 1);
}
public function get_category() {
return new \stdClass();
}
public function get_course() {
return new \stdClass();
}

View file

@ -0,0 +1,96 @@
@core @core_calendar
Feature: Course Category Events
In order to inform multiple courses of shared events
As a manager
I need to create catgory events
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| managera | Manager | A | managera@example.com |
| managera1 | Manager | A1 | managera1@example.com |
| managera2 | Manager | A2 | managera2@example.com |
| teachera1i | Teacher | A1i | teachera1i@example.com |
| managerb | Manager | B | managerb@example.com |
| managerb1 | Manager | B1 | managerb1@example.com |
| managerb2 | Manager | B2 | managerb2@example.com |
| teacherb1i | Teacher | B1i | teacherb1i@example.com |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
And the following "categories" exist:
| name | idnumber | category |
| Year | year | |
| Faculty A | faculty-a | year |
| Faculty B | faculty-b | year |
| Department A1 | department-a1 | faculty-a |
| Department A2 | department-a2 | faculty-a |
| Department B1 | department-b1 | faculty-b |
| Department B2 | department-b2 | faculty-b |
And the following "courses" exist:
| fullname | shortname | idnumber | format | category |
| Course A1i | A1i | A1i | topics | department-a1 |
| Course A2i | A2i | A2i | topics | department-a2 |
| Course B1i | B1i | B1i | topics | department-b1 |
| Course B2i | B2i | B2i | topics | department-b2 |
And the following "role assigns" exist:
| user | role | contextlevel | reference |
| managera | manager | Category | faculty-a |
| managera1 | manager | Category | department-a1 |
| managerb | manager | Category | faculty-b |
| managerb1 | manager | Category | department-b1 |
And the following "course enrolments" exist:
| user | course | role |
| teachera1i | A1i | editingteacher |
| teacherb1i | B1i | editingteacher |
| student1 | A1i | student |
| student1 | A2i | student |
| student2 | B1i | student |
| student2 | B2i | student |
And the following "events" exist:
| name | eventtype | category | course |
| Site event | global | | |
| FA event | category | faculty-a | |
| DA1 event | category | department-a1 | |
| DA2 event | category | department-a1 | |
| FB event | category | faculty-b | |
| DB1 event | category | department-b1 | |
| DB2 event | category | department-b1 | |
| CA1i event | course | | A1i |
| CA2i event | course | | A2i |
| CB1i event | course | | B1i |
| CB2i event | course | | B2i |
@javascript
Scenario: Manager of a Category can see all child and parent events in their category
Given I log in as "managera"
When I navigate to "Calendar" node in "Site pages"
Then I should see "FA event"
And I should see "DA1 event"
And I should see "DA2 event"
And I should not see "FB event"
And I should not see "DB1 event"
And I should not see "DB2 event"
And I log out
Given I log in as "managerb"
When I navigate to "Calendar" node in "Site pages"
Then I should see "FB event"
And I should see "DB1 event"
And I should see "DB2 event"
And I should not see "FA event"
And I should not see "DA1 event"
And I should not see "DA2 event"
@javascript
Scenario: Users enrolled in a course can see all child and parent events in their category
Given I log in as "student1"
When I navigate to "Calendar" node in "Site pages"
Then I should see "FA event"
And I should see "DA1 event"
And I should see "DA2 event"
And I should see "CA1i event"
And I should see "CA2i event"
And I should not see "FB event"
And I should not see "DB1 event"
And I should not see "DB2 event"
And I should not see "CB1i event"
And I should not see "CB2i event"

View file

@ -240,6 +240,84 @@ class core_calendar_container_testcase extends advanced_testcase {
$this->assertNull($event);
}
/**
* Test that the event factory deals with invisible categorys as an admin.
*/
public function test_event_factory_when_category_visibility_is_toggled_as_admin() {
// Create a hidden category.
$category = $this->getDataGenerator()->create_category(['visible' => 0]);
$eventdata = [
'categoryid' => $category->id,
'eventtype' => 'category',
];
$legacyevent = $this->create_event($eventdata);
$dbrow = $this->get_dbrow_from_skeleton((object) $eventdata);
$dbrow->id = $legacyevent->id;
$factory = \core_calendar\local\event\container::get_event_factory();
$event = $factory->create_instance($dbrow);
// Module is still visible to admins even if the category is invisible.
$this->assertInstanceOf(event_interface::class, $event);
}
/**
* Test that the event factory deals with invisible categorys as an user.
*/
public function test_event_factory_when_category_visibility_is_toggled_as_user() {
// Create a hidden category.
$category = $this->getDataGenerator()->create_category(['visible' => 0]);
$eventdata = [
'categoryid' => $category->id,
'eventtype' => 'category',
];
$legacyevent = $this->create_event($eventdata);
$dbrow = $this->get_dbrow_from_skeleton((object) $eventdata);
$dbrow->id = $legacyevent->id;
// Use a standard user.
$user = $this->getDataGenerator()->create_user();
// Set the user to the student.
$this->setUser($user);
$factory = \core_calendar\local\event\container::get_event_factory();
$event = $factory->create_instance($dbrow);
// Module is invisible to non-privileged users.
$this->assertNull($event);
}
/**
* Test that the event factory deals with invisible categorys as an guest.
*/
public function test_event_factory_when_category_visibility_is_toggled_as_guest() {
// Create a hidden category.
$category = $this->getDataGenerator()->create_category(['visible' => 0]);
$eventdata = [
'categoryid' => $category->id,
'eventtype' => 'category',
];
$legacyevent = $this->create_event($eventdata);
$dbrow = $this->get_dbrow_from_skeleton((object) $eventdata);
$dbrow->id = $legacyevent->id;
// Set the user to the student.
$this->setGuestUser();
$factory = \core_calendar\local\event\container::get_event_factory();
$event = $factory->create_instance($dbrow);
// Module is invisible to guests.
$this->assertNull($event);
}
/**
* Test that the event factory deals with completion related events properly.
*/
@ -264,6 +342,7 @@ class core_calendar_container_testcase extends advanced_testcase {
$event->userid = 1;
$event->modulename = 'assign';
$event->instance = $assign->id;
$event->categoryid = 0;
$event->courseid = $course->id;
$event->groupid = 0;
$event->timestart = time();
@ -312,6 +391,7 @@ class core_calendar_container_testcase extends advanced_testcase {
$event->userid = $user->id;
$event->modulename = 'lesson';
$event->instance = $lesson->id;
$event->categoryid = 0;
$event->courseid = $course->id;
$event->groupid = 0;
$event->timestart = time();
@ -397,6 +477,7 @@ class core_calendar_container_testcase extends advanced_testcase {
'name' => 'Test event',
'description' => 'Hello',
'format' => 1,
'categoryid' => 0,
'courseid' => 1,
'groupid' => 0,
'userid' => 1,
@ -418,6 +499,7 @@ class core_calendar_container_testcase extends advanced_testcase {
'name' => 'Test event',
'description' => 'Hello',
'format' => 1,
'categoryid' => 0,
'courseid' => 1,
'groupid' => 1,
'userid' => 1,
@ -459,4 +541,38 @@ class core_calendar_container_testcase extends advanced_testcase {
$event = new calendar_event($record);
return $event->create($record, false);
}
/**
* Pad out a basic DB row with basic information.
*
* @param \stdClass $skeleton the current skeleton
* @return \stdClass
*/
protected function get_dbrow_from_skeleton($skeleton) {
$dbrow = (object) [
'name' => 'Name',
'description' => 'Description',
'format' => 1,
'categoryid' => 0,
'courseid' => 0,
'groupid' => 0,
'userid' => 0,
'repeatid' => 0,
'modulename' => '',
'instance' => 0,
'eventtype' => 'user',
'timestart' => 1486396800,
'timeduration' => 0,
'timesort' => 1486396800,
'visible' => 1,
'timemodified' => 1485793098,
'subscriptionid' => null
];
foreach ((array) $skeleton as $key => $value) {
$dbrow->$key = $value;
}
return $dbrow;
}
}

View file

@ -0,0 +1,64 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* coursecat_proxy tests.
*
* @package core_calendar
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use core_calendar\local\event\proxies\coursecat_proxy;
/**
* coursecat_proxy testcase.
*
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_calendar_coursecat_proxy_testcase extends advanced_testcase {
public function test_valid_coursecat() {
global $DB;
$this->resetAfterTest();
$name = '2027-2028 Academic Year';
$generator = $this->getDataGenerator();
$category = $generator->create_category([
'name' => $name,
]);
cache_helper::purge_by_event('changesincoursecat');
// Fetch the proxy.
$startreads = $DB->perf_get_reads();
$proxy = new coursecat_proxy($category->id);
$this->assertInstanceOf(coursecat_proxy::class, $proxy);
$this->assertEquals(0, $DB->perf_get_reads() - $startreads);
// Fetch the ID - this is known and doesn't require a cache read.
$this->assertEquals($category->id, $proxy->get('id'));
$this->assertEquals(0, $DB->perf_get_reads() - $startreads);
// Fetch the name - not known, and requires a read.
$this->assertEquals($name, $proxy->get('name'));
$this->assertEquals(1, $DB->perf_get_reads() - $startreads);
$this->assertInstanceOf('coursecat', $proxy->get_proxied_instance());
}
}

View file

@ -114,6 +114,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
'name' => 'test',
'description' => 'Test description',
'format' => 2,
'categoryid' => 0,
'courseid' => 1,
'groupid' => 1,
'userid' => 1,
@ -162,6 +163,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
'name' => 'test',
'description' => 'Test description',
'format' => 2,
'categoryid' => 0,
'courseid' => 1,
'groupid' => 1,
'userid' => 1,
@ -210,6 +212,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
'name' => 'test',
'description' => 'Test description',
'format' => 2,
'categoryid' => 0,
'courseid' => 1,
'groupid' => 1,
'userid' => 1,
@ -258,6 +261,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
'name' => 'test',
'description' => 'Test description',
'format' => 2,
'categoryid' => 0,
'courseid' => $course->id,
'groupid' => 1,
'userid' => 1,
@ -312,6 +316,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
'name' => 'test',
'description' => 'Test description',
'format' => 2,
'categoryid' => 0,
'courseid' => 0,
'groupid' => 1,
'userid' => 1,
@ -345,6 +350,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
'name' => 'Test event',
'description' => 'Hello',
'format' => 1,
'categoryid' => 0,
'courseid' => 1,
'groupid' => 1,
'userid' => 1,
@ -378,6 +384,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
'name' => 'Test event',
'description' => 'Hello',
'format' => 1,
'categoryid' => 0,
'courseid' => 1,
'groupid' => 1,
'userid' => 1,
@ -411,6 +418,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
'name' => 'Test event',
'description' => 'Hello',
'format' => 1,
'categoryid' => 0,
'courseid' => 1,
'groupid' => 1,
'userid' => 1,

View file

@ -202,6 +202,10 @@ class event_mapper_test_action_event implements action_event_interface {
return $this->event->get_description();
}
public function get_category() {
return $this->event->get_category();
}
public function get_course() {
return $this->event->get_course();
}
@ -255,6 +259,11 @@ class event_mapper_test_action_event implements action_event_interface {
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class event_mapper_test_event implements event_interface {
/**
* @var proxy_interface $categoryproxy Category proxy.
*/
protected $categoryproxy;
/**
* @var proxy_interface $courseproxy Course proxy.
*/
@ -312,6 +321,10 @@ class event_mapper_test_event implements event_interface {
return new event_description('asdf', 1);
}
public function get_category() {
return $this->categoryproxy;
}
public function get_course() {
return $this->courseproxy;
}

View file

@ -26,6 +26,7 @@ defined('MOODLE_INTERNAL') || die();
use core_calendar\local\event\entities\event;
use core_calendar\local\event\proxies\std_proxy;
use core_calendar\local\event\proxies\coursecat_proxy;
use core_calendar\local\event\value_objects\event_description;
use core_calendar\local\event\value_objects\event_times;
use core_calendar\local\event\entities\event_collection_interface;
@ -48,6 +49,7 @@ class core_calendar_event_testcase extends advanced_testcase {
$constructorparams['id'],
$constructorparams['name'],
$constructorparams['description'],
$constructorparams['category'],
$constructorparams['course'],
$constructorparams['group'],
$constructorparams['user'],
@ -82,6 +84,7 @@ class core_calendar_event_testcase extends advanced_testcase {
'id' => 1,
'name' => 'Test event 1',
'description' => new event_description('asdf', 1),
'category' => new coursecat_proxy(0),
'course' => new std_proxy(1, $lamecallable),
'group' => new std_proxy(1, $lamecallable),
'user' => new std_proxy(1, $lamecallable),

View file

@ -1600,7 +1600,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
public function test_submit_create_update_form_create_site_event() {
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$context = context_course::instance(SITEID);
$context = context_system::instance();
$roleid = $generator->create_role();
$timestart = new DateTime();
$interval = new DateInterval("P1D"); // One day.

View file

@ -32,6 +32,7 @@ use core_calendar\local\event\entities\action_event;
use core_calendar\local\event\entities\event;
use core_calendar\local\event\entities\repeat_event_collection;
use core_calendar\local\event\proxies\std_proxy;
use core_calendar\local\event\proxies\coursecat_proxy;
use core_calendar\local\event\proxies\cm_info_proxy;
use core_calendar\local\event\value_objects\action;
use core_calendar\local\event\value_objects\event_description;
@ -108,6 +109,7 @@ class action_event_test_factory implements event_factory_interface {
$record->id,
$record->name,
new event_description($record->description, $record->format),
new coursecat_proxy($record->categoryid),
new std_proxy($record->courseid, function($id) {
$course = new \stdClass();
$course->id = $id;

View file

@ -277,4 +277,69 @@ class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testc
$events = $retrievalstrategy->get_raw_events();
$this->assertCount(3, $events);
}
/**
* Test retrieval strategy with category specifications.
*/
public function test_get_raw_events_category() {
global $DB;
$this->resetAfterTest();
$retrievalstrategy = new raw_event_retrieval_strategy();
$generator = $this->getDataGenerator();
$category1 = $generator->create_category();
$category2 = $generator->create_category();
$events = [
[
'name' => 'E1',
'eventtype' => 'category',
'description' => '',
'format' => 1,
'categoryid' => $category1->id,
'userid' => 2,
'timestart' => time(),
],
[
'name' => 'E2',
'eventtype' => 'category',
'description' => '',
'format' => 1,
'categoryid' => $category2->id,
'userid' => 2,
'timestart' => time() + 1,
],
];
foreach ($events as $event) {
calendar_event::create($event, false);
}
// Get all events.
$events = $retrievalstrategy->get_raw_events(null, null, null, null);
$this->assertCount(2, $events);
$event = array_shift($events);
$this->assertEquals('E1', $event->name);
$event = array_shift($events);
$this->assertEquals('E2', $event->name);
// Get events for C1 events.
$events = $retrievalstrategy->get_raw_events(null, null, null, [$category1->id]);
$this->assertCount(1, $events);
$event = array_shift($events);
$this->assertEquals('E1', $event->name);
// Get events for C2 events.
$events = $retrievalstrategy->get_raw_events(null, null, null, [$category2->id]);
$this->assertCount(1, $events);
$event = array_shift($events);
$this->assertEquals('E2', $event->name);
// Get events for several categories.
$events = $retrievalstrategy->get_raw_events(null, null, null, [$category1->id, $category2->id]);
$this->assertCount(2, $events);
}
}

View file

@ -30,6 +30,7 @@ require_once($CFG->dirroot . '/calendar/lib.php');
use core_calendar\local\event\entities\event;
use core_calendar\local\event\entities\repeat_event_collection;
use core_calendar\local\event\proxies\coursecat_proxy;
use core_calendar\local\event\proxies\std_proxy;
use core_calendar\local\event\value_objects\event_description;
use core_calendar\local\event\value_objects\event_times;
@ -161,6 +162,7 @@ class core_calendar_repeat_event_collection_event_test_factory implements event_
$dbrow->id,
$dbrow->name,
new event_description($dbrow->description, $dbrow->format),
new coursecat_proxy($dbrow->categoryid),
new std_proxy($dbrow->courseid, $identity),
new std_proxy($dbrow->groupid, $identity),
new std_proxy($dbrow->userid, $identity),

View file

@ -49,6 +49,7 @@ require_once('../config.php');
require_once($CFG->dirroot.'/course/lib.php');
require_once($CFG->dirroot.'/calendar/lib.php');
$categoryid = optional_param('category', null, PARAM_INT);
$courseid = optional_param('course', SITEID, PARAM_INT);
$view = optional_param('view', 'upcoming', PARAM_ALPHA);
$time = optional_param('time', 0, PARAM_INT);
@ -63,6 +64,10 @@ if ($courseid != SITEID) {
$url->param('course', $courseid);
}
if ($categoryid) {
$url->param('categoryid', $categoryid);
}
if ($view !== 'upcoming') {
$time = usergetmidnight($time);
$url->param('view', $view);
@ -76,18 +81,30 @@ if ($courseid != SITEID && !empty($courseid)) {
// Course ID must be valid and existing.
$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
$courses = array($course->id => $course);
$issite = false;
navigation_node::override_active_url(new moodle_url('/course/view.php', array('id' => $course->id)));
} else {
$course = get_site();
$courses = calendar_get_default_courses();
$issite = true;
if ($categoryid) {
$PAGE->set_category_by_id($categoryid);
} else {
$PAGE->set_context(context_system::instance());
}
if ($PAGE->context->contextlevel === CONTEXT_COURSECAT) {
// Restrict to categories, and their parents, and the courses that the user is enrolled in within those
// categories.
$categories = array_keys($PAGE->categories);
$courses = array_filter($courses, function($course) use ($categories) {
return array_search($course->category, $categories) !== false;
});
navigation_node::override_active_url(new moodle_url('/course/index.php', array('categoryid' => $categoryid)));
}
}
require_login($course, false);
$calendar = new calendar_information(0, 0, 0, $time);
$calendar->prepare_for_view($course, $courses);
$calendar->set_sources($course, $courses, $PAGE->category);
$pagetitle = '';

View file

@ -32,6 +32,7 @@ $string['calendarheading'] = '{$a} Calendar';
$string['calendarpreferences'] = 'Calendar preferences';
$string['calendartypes'] = 'Calendar types';
$string['calendarurl'] = 'Calendar URL: {$a}';
$string['categoryevent'] = 'Category event';
$string['clickhide'] = 'click to hide';
$string['clickshow'] = 'click to show';
$string['colcalendar'] = 'Calendar';
@ -44,6 +45,7 @@ $string['confirmeventseriesdelete'] = 'The "{$a->name}" event is part of a serie
$string['course'] = 'Course';
$string['courseevent'] = 'Course event';
$string['courseevents'] = 'Course events';
$string['categoryevents'] = 'Category events';
$string['courses'] = 'Courses';
$string['customexport'] = 'Custom range ({$a->timestart} - {$a->timeend})';
$string['daily'] = 'Daily';
@ -97,6 +99,7 @@ $string['eventendtimewrapped'] = '{$a} (End time)';
$string['eventinstanttime'] = 'Time';
$string['eventkind'] = 'Type of event';
$string['eventname'] = 'Event title';
$string['eventnameandcategory'] = '{$a->category}: {$a->name}';
$string['eventnameandcourse'] = '{$a->course}: {$a->name}';
$string['eventnone'] = 'No events';
$string['eventrepeat'] = 'Repeats';
@ -141,6 +144,7 @@ $string['groupevent'] = 'Group event';
$string['groupevents'] = 'Group events';
$string['eventtypeglobal'] = 'global';
$string['eventtypesite'] = 'global';
$string['eventtypecategory'] = 'category';
$string['eventtypecourse'] = 'course';
$string['eventtypemodule'] = 'module';
$string['eventtypegroup'] = 'group';
@ -227,6 +231,7 @@ $string['tue'] = 'Tue';
$string['tuesday'] = 'Tuesday';
$string['typeclose'] = 'Close event';
$string['typecourse'] = 'Course event';
$string['typecategory'] = 'Category event';
$string['typedue'] = 'Due event';
$string['typegradingdue'] = 'Grading due event';
$string['typegroup'] = 'Group event';

View file

@ -0,0 +1,133 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Class for exporting summary information for a course category.
*
* @package core
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\external;
defined('MOODLE_INTERNAL') || die();
use renderer_base;
use moodle_url;
/**
* Class for exporting a course summary from an stdClass.
*
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class coursecat_summary_exporter extends \core\external\exporter {
/**
* @var \coursecat $category
*/
protected $category;
public function __construct(\coursecat $category, $related) {
$this->category = $category;
$data = [];
// Specify some defaults.
foreach ($category as $key => $value) {
$data[$key] = $value;
}
return parent::__construct($data, $related);
}
protected static function define_related() {
return [
'context' => 'context',
];
}
public static function define_other_properties() {
return [
'nestedname' => [
'type' => PARAM_RAW,
],
'url' => [
'type' => PARAM_URL,
],
];
}
protected function get_other_values(renderer_base $output) {
$return = [
'nestedname' => $this->category->get_nested_name(),
'url' => (new moodle_url('/course/index.php', [
'categoryid' => $this->category->id,
]))->out(false),
];
return $return;
}
public static function define_properties() {
return [
'id' => [
'type' => PARAM_INT,
],
'name' => [
'type' => PARAM_TEXT,
'default' => '',
],
'idnumber' => [
'type' => PARAM_RAW,
'null' => NULL_ALLOWED,
],
'description' => [
'type' => PARAM_RAW,
'optional' => true,
],
'parent' => [
'type' => PARAM_INT,
],
'coursecount' => [
'type' => PARAM_INT,
'default' => 0,
],
'visible' => [
'type' => PARAM_INT,
'default' => 1,
],
'timemodified' => [
'type' => PARAM_INT,
'default' => 0,
],
'depth' => [
'type' => PARAM_INT,
'default' => 0,
],
];
}
/**
* Get the formatting parameters for the summary.
*
* @return array
*/
protected function get_format_parameters_for_description() {
return [
'component' => 'coursecat',
'filearea' => 'description',
];
}
}

View file

@ -203,6 +203,7 @@ class icon_system_fontawesome extends icon_system_font {
'core:i/completion_self' => 'fa-user-o',
'core:i/dashboard' => 'fa-tachometer',
'core:i/lock' => 'fa-lock',
'core:i/categoryevent' => 'fa-users',
'core:i/courseevent' => 'fa-calendar',
'core:i/db' => 'fa-database',
'core:i/delete' => 'fa-trash',

View file

@ -291,6 +291,48 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
return $categories;
}
/**
* Load all coursecat objects.
*
* @param array $options Options:
* @param bool $options.returnhidden Return categories even if they are hidden
* @return coursecat[]
*/
public static function get_all($options = []) {
global $DB;
$coursecatrecordcache = cache::make('core', 'coursecatrecords');
$catcontextsql = \context_helper::get_preload_record_columns_sql('ctx');
$catsql = "SELECT cc.*, {$catcontextsql}
FROM {course_categories} cc
JOIN {context} ctx ON cc.id = ctx.instanceid";
$catsqlwhere = "WHERE ctx.contextlevel = :contextlevel";
$catsqlorder = "ORDER BY cc.depth ASC, cc.sortorder ASC";
$catrs = $DB->get_recordset_sql("{$catsql} {$catsqlwhere} {$catsqlorder}", [
'contextlevel' => CONTEXT_COURSECAT,
]);
$types['categories'] = [];
$categories = [];
$toset = [];
foreach ($catrs as $record) {
$category = new coursecat($record);
$toset[$category->id] = $category;
if (!empty($options['returnhidden']) || $category->is_uservisible()) {
$categories[$record->id] = $category;
}
}
$catrs->close();
$coursecatrecordcache->set_many($toset);
return $categories;
}
/**
* Returns the first found category
*
@ -1258,6 +1300,17 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
array($this->id));
}
/**
* Get the link used to view this course category.
*
* @return \moodle_url
*/
public function get_view_link() {
return new \moodle_url('/course/index.php', [
'categoryid' => $this->id,
]);
}
/**
* Searches courses
*
@ -1703,6 +1756,9 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
throw new moodle_exception('cannotdeletecategoryquestions', '', '', $this->get_formatted_name());
}
// Delete all events in the category.
$DB->delete_records('event', array('categoryid' => $this->id));
// Finally delete the category and it's context.
$DB->delete_records('course_categories', array('id' => $this->id));
@ -2158,6 +2214,32 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
}
}
/**
* Get the nested name of this category, with all of it's parents.
*
* @param bool $includelinks Whether to wrap each name in the view link for that category.
* @param string $separator The string between each name.
* @param array $options Formatting options.
* @return string
*/
public function get_nested_name($includelinks = true, $separator = ' / ', $options = []) {
// Get the name of hierarchical name of this category.
$parents = $this->get_parents();
$categories = static::get_many($parents);
$categories[] = $this;
$names = array_map(function($category) use ($options, $includelinks) {
if ($includelinks) {
return html_writer::link($category->get_view_link(), $category->get_formatted_name($options));
} else {
return $category->get_formatted_name($options);
}
}, $categories);
return implode($separator, $names);
}
/**
* Returns ids of all parents of the category. Last element in the return array is the direct parent
*

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20170904" COMMENT="XMLDB file for core Moodle tables"
<XMLDB PATH="lib/db" VERSION="20170921" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
@ -426,6 +426,7 @@
<FIELD NAME="name" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="description" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="format" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="categoryid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="courseid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="groupid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
@ -446,6 +447,7 @@
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="categoryid" TYPE="foreign" FIELDS="categoryid" REFTABLE="course_categories" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="courseid" UNIQUE="false" FIELDS="courseid"/>
@ -453,7 +455,7 @@
<INDEX NAME="timestart" UNIQUE="false" FIELDS="timestart"/>
<INDEX NAME="timeduration" UNIQUE="false" FIELDS="timeduration"/>
<INDEX NAME="type-timesort" UNIQUE="false" FIELDS="type, timesort"/>
<INDEX NAME="groupid-courseid-visible-userid" UNIQUE="false" FIELDS="groupid, courseid, visible, userid" COMMENT="used for calendar view"/>
<INDEX NAME="groupid-courseid-categoryid-visible-userid" UNIQUE="false" FIELDS="groupid, courseid, categoryid, visible, userid" COMMENT="used for calendar view"/>
</INDEXES>
</TABLE>
<TABLE NAME="cache_filters" COMMENT="For keeping information about cached data">
@ -3719,4 +3721,4 @@
</INDEXES>
</TABLE>
</TABLES>
</XMLDB>
</XMLDB>

View file

@ -2570,5 +2570,36 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2017092700.00);
}
if ($oldversion < 2017092900.00) {
// Define field categoryid to be added to event.
$table = new xmldb_table('event');
$field = new xmldb_field('categoryid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'format');
// Conditionally launch add field categoryid.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Add the categoryid key.
$key = new xmldb_key('categoryid', XMLDB_KEY_FOREIGN, array('categoryid'), 'course_categories', array('id'));
$dbman->add_key($table, $key);
// Add a new index for groupid/courseid/categoryid/visible/userid.
// Do this before we remove the old index.
$index = new xmldb_index('groupid-courseid-categoryid-visible-userid', XMLDB_INDEX_NOTUNIQUE, array('groupid', 'courseid', 'categoryid', 'visible', 'userid'));
if (!$dbman->index_exists($table, $index)) {
$dbman->add_index($table, $index);
}
// Drop the old index.
$index = new xmldb_index('groupid-courseid-visible-userid', XMLDB_INDEX_NOTUNIQUE, array('groupid', 'courseid', 'visible', 'userid'));
if ($dbman->index_exists($table, $index)) {
$dbman->drop_index($table, $index);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2017092900.00);
}
return true;
}

View file

@ -1103,4 +1103,59 @@ EOD;
// Get the tool associated with this instance.
return $DB->get_record('enrol_lti_tools', array('enrolid' => $instanceid));
}
/**
* Helper function used to create an event.
*
* @param array $data
* @return stdClass
*/
public function create_event($data = []) {
global $CFG;
require_once($CFG->dirroot . '/calendar/lib.php');
$record = new \stdClass();
$record->name = 'event name';
$record->eventtype = 'global';
$record->repeat = 0;
$record->repeats = 0;
$record->timestart = time();
$record->timeduration = 0;
$record->timesort = 0;
$record->eventtype = 'user';
$record->courseid = 0;
foreach ($data as $key => $value) {
$record->$key = $value;
}
switch ($record->eventtype) {
case 'user':
unset($record->categoryid);
unset($record->courseid);
unset($record->groupid);
break;
case 'group':
unset($record->categoryid);
break;
case 'course':
unset($record->categoryid);
unset($record->groupid);
break;
case 'category':
unset($record->courseid);
unset($record->groupid);
break;
case 'global':
unset($record->categoryid);
unset($record->courseid);
unset($record->groupid);
break;
}
$event = new calendar_event($record);
$event->create($record);
return $event->properties();
}
}

View file

@ -174,6 +174,15 @@ class behat_data_generators extends behat_base {
'datagenerator' => 'tag',
'required' => array('name')
),
'events' => array(
'datagenerator' => 'event',
'required' => array('name', 'eventtype'),
'switchids' => array(
'user' => 'userid',
'course' => 'courseid',
'category' => 'categoryid',
)
),
);
/**
@ -218,7 +227,7 @@ class behat_data_generators extends behat_base {
$methodname = 'get_' . $element . '_id';
// Not all the switch fields are required, default vars will be assigned by data generators.
if (isset($elementdata[$element])) {
if (!empty($elementdata[$element])) {
// Temp $id var to avoid problems when $element == $field.
$id = $this->{$methodname}($elementdata[$element]);
unset($elementdata[$element]);

View file

@ -750,6 +750,22 @@ class core_coursecatlib_testcase extends advanced_testcase {
$this->assertEquals(1, count($courses[$c5->id]->get_course_overviewfiles()));
}
public function test_get_nested_name() {
$cat1name = 'Cat1';
$cat2name = 'Cat2';
$cat3name = 'Cat3';
$cat4name = 'Cat4';
$category1 = coursecat::create(array('name' => $cat1name));
$category2 = coursecat::create(array('name' => $cat2name, 'parent' => $category1->id));
$category3 = coursecat::create(array('name' => $cat3name, 'parent' => $category2->id));
$category4 = coursecat::create(array('name' => $cat4name, 'parent' => $category2->id));
$this->assertEquals($cat1name, $category1->get_nested_name(false));
$this->assertEquals("{$cat1name} / {$cat2name}", $category2->get_nested_name(false));
$this->assertEquals("{$cat1name} / {$cat2name} / {$cat3name}", $category3->get_nested_name(false));
$this->assertEquals("{$cat1name} / {$cat2name} / {$cat4name}", $category4->get_nested_name(false));
}
/**
* Creates a draft area for current user and fills it with fake files
*

3
pix/i/categoryevent.svg Normal file
View file

@ -0,0 +1,3 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M8 4c1.6 0 2.9 1.7 2.9 3.8 0 1.4-.9 2.5-.9 2.5-.3.4-.2 1 .3 1.2l3.6 1.7c.5.3 1.1.9 1.1 1.5V16H1v-1.2c0-.5.6-1.2 1.1-1.5l3.6-1.8c.5-.3.6-.8.3-1.3 0 0-.9-1.1-.9-2.5C5.1 5.7 6.4 4 8 4zm3.9 3.8c0 1.1-.4 2.1-.4 2.1-.2.5 0 1.1.5 1.4l1.5.7H16V8.4l-1.7-.8c-.5-.3-.6-.8-.3-1.2 0 0 .9-1.1.9-2.5C14.9 1.7 13.6 0 12 0c-1.5 0-2.6 1.4-2.8 3.2 1.5.7 2.7 2.4 2.7 4.6zM1.7 7.6L0 8.4V12h2.6l1.4-.7c.5-.3.8-.9.5-1.4 0 0-.4-1-.4-2.1 0-2.1 1.2-3.9 2.8-4.6C6.6 1.4 5.5 0 4 0 2.4 0 1.1 1.7 1.1 3.8c0 1.4.9 2.5.9 2.5.3.4.2 1-.3 1.3z" fill="#999"/></svg>

After

Width:  |  Height:  |  Size: 833 B

View file

@ -1,12 +1,16 @@
/* calendar.less */
// Calendar colour variables defined.
$calendarEventCategoryColor: #d8bfd8 !default; // Pale purple.
$calendarEventCourseColor: #ffd3bd !default; // Pale red.
$calendarEventGlobalColor: #d6f8cd !default; // Pale green.
$calendarEventGroupColor: #fee7ae !default; // Pale yellow.
$calendarEventUserColor: #dce7ec !default; // Pale blue.
// Calendar event background colours defined.
.calendar_event_category {
background-color: $calendarEventCategoryColor;
}
.calendar_event_course {
background-color: $calendarEventCourseColor;
}
@ -129,6 +133,7 @@ $calendarEventUserColor: #dce7ec !default; // Pale blue.
margin: 10px auto;
}
.calendar_event_category,
.calendar_event_course,
.calendar_event_site,
.calendar_event_group,
@ -141,6 +146,9 @@ $calendarEventUserColor: #dce7ec !default; // Pale blue.
}
}
.calendar_event_category {
border-color: $calendarEventCategoryColor;
}
.calendar_event_course {
border-color: $calendarEventCourseColor;
}
@ -231,6 +239,15 @@ $calendarEventUserColor: #dce7ec !default; // Pale blue.
}
}
&.duration_category {
border-top: 1px solid $calendarEventCategoryColor;
border-bottom: 1px solid $calendarEventCategoryColor;
&.duration_finish {
background-color: $calendarEventCategoryColor;
}
}
&.duration_course {
border-top: 1px solid $calendarEventCourseColor;
border-bottom: 1px solid $calendarEventCourseColor;

View file

@ -1,12 +1,16 @@
/* calendar.less */
// Calendar colour variables defined.
@calendarEventCategoryColor: #d8bfd8; // Pale purple.
@calendarEventCourseColor: #ffd3bd; // Pale red.
@calendarEventGlobalColor: #d6f8cd; // Pale green.
@calendarEventGroupColor: #fee7ae; // Pale yellow.
@calendarEventUserColor: #dce7ec; // Pale blue.
// Calendar event background colours defined.
.calendar_event_category {
background-color: @calendarEventCategoryColor;
}
.calendar_event_course {
background-color: @calendarEventCourseColor;
}
@ -110,6 +114,7 @@
width: 98%;
margin: 10px auto;
}
.calendar_event_category,
.calendar_event_course,
.calendar_event_site,
.calendar_event_group,
@ -121,6 +126,9 @@
}
}
}
.calendar_event_category {
border-color: @calendarEventCategoryColor;
}
.calendar_event_course {
border-color: @calendarEventCourseColor;
}
@ -244,6 +252,13 @@
background-color: @calendarEventGlobalColor;
}
}
&.duration_category {
border-top: 1px solid @calendarEventCategoryColor;
border-bottom: 1px solid @calendarEventCategoryColor;
&.duration_finish {
background-color: @calendarEventCategoryColor;
}
}
&.duration_course {
border-top: 1px solid @calendarEventCourseColor;
border-bottom: 1px solid @calendarEventCourseColor;

View file

@ -5563,6 +5563,9 @@ img.iconsmall {
background-color: #fcf8e3;
}
/* calendar.less */
.calendar_event_category {
background-color: #d8bfd8;
}
.calendar_event_course {
background-color: #ffd3bd;
}
@ -5655,6 +5658,7 @@ img.iconsmall {
width: 98%;
margin: 10px auto;
}
.path-calendar .maincalendar .calendar_event_category:hover a,
.path-calendar .maincalendar .calendar_event_course:hover a,
.path-calendar .maincalendar .calendar_event_site:hover a,
.path-calendar .maincalendar .calendar_event_group:hover a,
@ -5662,6 +5666,9 @@ img.iconsmall {
color: #003d5c;
text-decoration: underline;
}
.path-calendar .maincalendar .calendar_event_category {
border-color: #d8bfd8;
}
.path-calendar .maincalendar .calendar_event_course {
border-color: #ffd3bd;
}
@ -5768,6 +5775,13 @@ img.iconsmall {
.block .minicalendar td.duration_global.duration_finish {
background-color: #d6f8cd;
}
.block .minicalendar td.duration_category {
border-top: 1px solid #d8bfd8;
border-bottom: 1px solid #d8bfd8;
}
.block .minicalendar td.duration_category.duration_finish {
background-color: #d8bfd8;
}
.block .minicalendar td.duration_course {
border-top: 1px solid #ffd3bd;
border-bottom: 1px solid #ffd3bd;

View file

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2017092800.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2017092900.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.