<commit_before>import { Meteor } from 'meteor/meteor';

import { Minutes } from '/imports/minutes'
import { UserRoles } from '/imports/userroles'
import { AttachmentsCollection } from '/imports/collections/attachments_private'
import { Attachment } from '/imports/attachment'

let _minutesID; // the ID of these minutes

Template.minutesAttachments.onCreated(function() {
    this.currentUpload = new ReactiveVar(false);
    _minutesID = this.data._id;
    console.log("minutesAttachment on minutes.id:"+_minutesID);

    // Calculate initial expanded/collapsed state
    Session.set("attachments.expand", true);
    if (Attachment.countForMinutes(_minutesID) == 0) {
        Session.set("attachments.expand", false);
    }
});

Template.minutesAttachments.onRendered(function() {
    //add your statement here
});

Template.minutesAttachments.onDestroyed(function() {
    //add your statement here
});

Template.minutesAttachments.helpers({
    attachmentsEnabled() {
        return Meteor.settings.public.attachments.enabled;
    },

    isAttachmentsExpanded() {
        return Session.get("attachments.expand");
    },

    attachments() {
        return Attachment.findForMinutes(_minutesID);
    },

    attachmentsCount() {
        const count = Attachment.countForMinutes(_minutesID);
        return count == 1 ? count + " file" : count + " files";
    },

    currentUpload() {
        return Template.instance().currentUpload.get();
    },

    // some responsive CSS tweaking
    useClassWell() {
        if (! Session.get("global.isMobileWidth")) {
            return "well";
        }
    },
    useStylePadding() {
        if (! Session.get("global.isMobileWidth")) {
            return "padding-left: 1.5em;";
        }
    },

    showUploadButton() {
        let min = new Minutes(_minutesID);
        let ur = new UserRoles();
        if (! min.isFinalized && ur.isUploaderFor(min.parentMeetingSeriesID())) {
            return true;
        }
        return false;
    },

    showAttachmentRemoveButton() {
        let min = new Minutes(_minutesID);
        let file = this.fetch()[0]; // this is an attachment cursor in this context, so get "first" object of array
        try {
            let attachment = new Attachment(file._id);
            if (attachment.mayRemove()) {
                return true;
            }
            return false;
        } catch(err) {
            // when attachment is in the process of being removed
            // new Attachment(id) may fail...
            // we handle this situation gracefully
            return false;
        }
    },

    uploadSpeed() {
        if (Template.instance().currentUpload.get()
            && Template.instance().currentUpload.get().state.get() == "active") {
            let speed = Template.instance().currentUpload.get().estimateSpeed.get();
            speed = speed / 1024 / 1024;
            return "@"+speed.toFixed(2)+" MB/s";
        }
        return "";
    },

    timeToFinish() {
        if (Template.instance().currentUpload.get()
            && Template.instance().currentUpload.get().state.get() == "active") {
            let time = Template.instance().currentUpload.get().estimateTime.get();
            time = global.msToHHMMSS(time);
            return time+ " remaining";
        }
        return "";
    },

    uploaderUsername() {
        let file = this.fetch()[0]; // this is an attachment cursor in this context, so get "first" object of array
        let usr = Meteor.users.findOne(file.userId);
        return usr.username;
    },

    uploadTimestamp() {
        let file = this.fetch()[0]; // this is an attachment cursor in this context, so get "first" object of array
        return (global.formatDateISO8601Time(file.meta.timestamp));
    }
});

Template.minutesAttachments.events({
    'change #btnUploadAttachment': function (e, template) {
        if (e.currentTarget.files && e.currentTarget.files[0]) {
            // We upload only one file, in case
            // multiple files were selected
            const uploadFilename = e.currentTarget.files[0];
            console.log("Uploading... "+uploadFilename);
            let minObj = new Minutes(_minutesID);
            Attachment.uploadFile(uploadFilename, minObj, template.currentUpload);
        }
    },

    "click #btnDelAttachment": function (evt, tmpl) {
        evt.preventDefault();
        console.log("Remove Attachment: "+this._id);
        confirmationDialog(
            /* callback called if user wants to continue */
            () => {
                Meteor.call("attachments.remove", this._id);
            },
            /* Dialog content */
            "Do you really want to delete the attachment
"+this.name+"?"
        );
    },

    "click #btnToggleUpload": function (e) {
        e.preventDefault();
        this.toggle();
        return false;
    },

    "click #btnAbortUpload": function (e) {
        e.preventDefault();
        this.abort();
        return false;
    },

    "click #btnAttachmentsExpand" () {
        Session.set("attachments.expand", !Session.get("attachments.expand"));
    }
});
<commit_msg>Enh: Some low-hanging 0.8 UX improvements #168<commit_after>import { Meteor } from 'meteor/meteor';

import { Minutes } from '/imports/minutes'
import { UserRoles } from '/imports/userroles'
import { AttachmentsCollection } from '/imports/collections/attachments_private'
import { Attachment } from '/imports/attachment'

let _minutesID; // the ID of these minutes

Template.minutesAttachments.onCreated(function() {
    this.currentUpload = new ReactiveVar(false);
    _minutesID = this.data._id;
    console.log("minutesAttachment on minutes.id:"+_minutesID);

    // Calculate initial expanded/collapsed state
    Session.set("attachments.expand", true);
    if (Attachment.countForMinutes(_minutesID) == 0) {
        Session.set("attachments.expand", false);
    }
});

Template.minutesAttachments.onRendered(function() {
    //add your statement here
});

Template.minutesAttachments.onDestroyed(function() {
    //add your statement here
});

Template.minutesAttachments.helpers({
    attachmentsEnabled() {
        return Meteor.settings.public.attachments.enabled;
    },

    isAttachmentsExpanded() {
        let min = new Minutes(_minutesID);
        if (min.isFinalized && Attachment.countForMinutes(_minutesID) == 0) {
            return false;
        }

        return Session.get("attachments.expand");
    },

    attachments() {
        return Attachment.findForMinutes(_minutesID);
    },

    attachmentsCount() {
        const count = Attachment.countForMinutes(_minutesID);
        return count == 1 ? count + " file" : count + " files";
    },

    currentUpload() {
        return Template.instance().currentUpload.get();
    },

    // some responsive CSS tweaking
    useClassWell() {
        if (! Session.get("global.isMobileWidth")) {
            return "well";
        }
    },
    useStylePadding() {
        if (! Session.get("global.isMobileWidth")) {
            return "padding-left: 1.5em;";
        }
    },

    showUploadButton() {
        let min = new Minutes(_minutesID);
        let ur = new UserRoles();
        if (! min.isFinalized && ur.isUploaderFor(min.parentMeetingSeriesID())) {
            return true;
        }
        return false;
    },

    showAttachmentRemoveButton() {
        let min = new Minutes(_minutesID);
        let file = this.fetch()[0]; // this is an attachment cursor in this context, so get "first" object of array
        try {
            let attachment = new Attachment(file._id);
            if (attachment.mayRemove()) {
                return true;
            }
            return false;
        } catch(err) {
            // when attachment is in the process of being removed
            // new Attachment(id) may fail...
            // we handle this situation gracefully
            return false;
        }
    },

    uploadSpeed() {
        if (Template.instance().currentUpload.get()
            && Template.instance().currentUpload.get().state.get() == "active") {
            let speed = Template.instance().currentUpload.get().estimateSpeed.get();
            speed = speed / 1024 / 1024;
            return "@"+speed.toFixed(2)+" MB/s";
        }
        return "";
    },

    timeToFinish() {
        if (Template.instance().currentUpload.get()
            && Template.instance().currentUpload.get().state.get() == "active") {
            let time = Template.instance().currentUpload.get().estimateTime.get();
            time = global.msToHHMMSS(time);
            return time+ " remaining";
        }
        return "";
    },

    uploaderUsername() {
        let file = this.fetch()[0]; // this is an attachment cursor in this context, so get "first" object of array
        let usr = Meteor.users.findOne(file.userId);
        return usr.username;
    },

    uploadTimestamp() {
        let file = this.fetch()[0]; // this is an attachment cursor in this context, so get "first" object of array
        return (global.formatDateISO8601Time(file.meta.timestamp));
    }
});

Template.minutesAttachments.events({
    'change #btnUploadAttachment': function (e, template) {
        if (e.currentTarget.files && e.currentTarget.files[0]) {
            // We upload only one file, in case
            // multiple files were selected
            const uploadFilename = e.currentTarget.files[0];
            console.log("Uploading... "+uploadFilename);
            let minObj = new Minutes(_minutesID);
            Attachment.uploadFile(uploadFilename, minObj, template.currentUpload);
        }
    },

    "click #btnDelAttachment": function (evt, tmpl) {
        evt.preventDefault();
        console.log("Remove Attachment: "+this._id);
        confirmationDialog(
            /* callback called if user wants to continue */
            () => {
                Meteor.call("attachments.remove", this._id);
            },
            /* Dialog content */
            "Do you really want to delete the attachment
"+this.name+"?"
        );
    },

    "click #btnToggleUpload": function (e) {
        e.preventDefault();
        this.toggle();
        return false;
    },

    "click #btnAbortUpload": function (e) {
        e.preventDefault();
        this.abort();
        return false;
    },

    "click #btnAttachmentsExpand" () {
        Session.set("attachments.expand", !Session.get("attachments.expand"));
    }
});
<|endoftext|>


<commit_before>Router.map(function () {
	this.route('showCourse', {
		path: 'course/:_id/:slug?',
		template: 'coursedetails',
		waitOn: function () {
			return subs.subscribe('courseDetails', this.params._id);
		},
		data: function() {
			var self = this;
			var course = Courses.findOne({_id: this.params._id});

			if (!course) return false;

			var userId = Meteor.userId();
			var member = getMember(course.members, userId);
			var data = {
				edit: !!this.params.query.edit,
				roles_details: loadroles(course),
				course: course,
				member: member
			};
			if (course.editableBy(Meteor.user())) {
				data.editableName = makeEditable(
					course.name,
					true,
					function(newName) {
						Meteor.call("save_course", course._id, { name: newName }, function(err, courseId) {
							if (err) {
								addMessage(mf('course.saving.error', { ERROR: err }, 'Saving the course went wrong! Sorry about this. We encountered the following error: {ERROR}'), 'danger');
							} else {
								addMessage(mf('course.saving.name.editable.success', { NAME: course.name }), 'success');
							}
						});
					},
					mf('course.title.placeholder')
				);
				data.editableDescription = makeEditable(
					course.description,
					false,
					function(newDescription) {
						Meteor.call("save_course", course._id, { description: newDescription }, function(err, courseId) {
							if (err) {
								addMessage(mf('course.saving.error', { ERROR: err }, 'Saving the course went wrong! Sorry about this. We encountered the following error: {ERROR}'), 'danger');
							} else {
								addMessage(mf('course.saving.desc.editable.success', { NAME: course.name }), 'success');
							}
						});
					},
					mf('course.description.placeholder')
				);
			}
			return data;
		},
		onAfterAction: function() {
			var data = this.data();
			if (data) {
				var course = data.course;
				document.title = webpagename + 'Course: ' + course.name;
			}
		}
	});

	this.route('showCourseDocs', {
		path: 'course/:_id/:slug/docs',
		//template: 'coursedocs',
		waitOn: function () {
			return [
				Meteor.subscribe('courseDetails', this.params._id),
			];
		},
		data: function () {
			var course = Courses.findOne({_id: this.params._id});
			return {
				course: course
			};
		}
	});
	this.route('showCourseHistory', {
		path: 'course/:_id/:slug/History',
		//template: 'coursehistory',
		waitOn: function () {
			return [
				Meteor.subscribe('courseDetails', this.params._id)
			];
		},
		data: function () {
			var course = Courses.findOne({_id: this.params._id});
			return {
				course: course
			};
		}
	});
});

function loadroles(course) {
	var userId = Meteor.userId();
	return _.reduce(Roles.find({}, {sort: {type: 1} }).fetch(), function(goodroles, roletype) {
		var role = roletype.type;
		var sub = hasRoleUser(course.members, role, userId);
		if (course.roles && course.roles.indexOf(role) !== -1) {
			goodroles.push({
				roletype: roletype,
				role: role,
				subscribed: !!sub,
				anonsub: sub == 'anon',
				course: course
			});
		}
		return goodroles;
	}, []);
}

Template.coursedetails.helpers({    // more helpers in course.roles.js
	currentUserMayEdit: function() {
		return this.editableBy(Meteor.user());
	},
	coursestate: function() {
		var today = new Date();
		var upcoming = Events.find({course_id: this._id, start: {$gt:today}}).count() > 0;
		if (upcoming) return 'hasupcomingevents';

		var past = Events.find({course_id: this._id, start: {$lt:today}}).count() > 0;
		if (past) return 'haspastevents';

		return 'proposal';
	},
	mobileViewport: function() {
		return Session.get('screenSize') <= 480; // @screen-xs
	},
	isProposal: function() {
		return Events.find({course_id: this.course._id}).count() === 0;
	}
});

Template.show_course_submenu.helpers({
	hasFiles: function() {
		var withFiles = {course_id: this.course._id, files: {$exists: 1, $not: {$size: 0}}};
		return !!Events.findOne(withFiles);
	},
});

Template.coursedetails.events({
	'click button.del': function () {
		var self = this;
		if (pleaseLogin()) return;
		if (confirm(mf("course.detail.remove", "Remove course and all its events?"))) {
			Meteor.call('remove_course', this._id, function(error) {
				if (error) {
					addMessage(mf('course.detail.remove.error', { ERROR: error, NAME: self.name }, 'Sorry but removing the proposal "{NAME}" went wrong. We encountered the following error: {ERROR}'), 'danger');
				} else {
					addMessage(mf('course.detail.remove.success', { NAME: self.name }, 'The proposal "{NAME}" was obliterated!'), 'success');
				}
			});
			Router.go('/');
		}
	},

	'click button.edit': function () {
		if (pleaseLogin()) return;
		Router.go('showCourse', this, { query: {edit: 'course'} });
	}
});

Template.coursedetails.rendered = function() {
	this.$("[data-toggle='tooltip']").tooltip();
	var currentPath = Router.current().route.path(this);
	$('a[href!="' + currentPath + '"].nav_link').removeClass('active');
	$('#nav_courses').addClass('active');
};

Template.courseGroupList.helpers({
	'isOrganizer': function() {
		return Template.instance().data.groupOrganizers.indexOf(_id(this)) >= 0;
	},
	'tools': function() {
		var tools = [];
		var user = Meteor.user();
		var groupId = String(this);
		var course = Template.parentData();
		if (user && user.mayPromoteWith(groupId)) {
			tools.push({
				toolTemplate: Template.courseGroupRemove,
				groupId: groupId,
				course: course,
			});
		}
		if (user && course.editableBy(user)) {
			var hasOrgRights = course.groupOrganizers.indexOf(groupId) > -1;
			tools.push({
				toolTemplate: hasOrgRights ? Template.courseGroupRemoveOrganizer : Template.courseGroupMakeOrganizer,
				groupId: groupId,
				course: course,
			});
		}
		return tools;
	},
});

var expandible = function(template) {
	template.onCreated(function() {
		var expander = Random.id(); // Token to keep track of which Expandible is open
		this.expander = expander;
		this.collapse = function() {
			if (Session.equals('verify', expander)) {
				Session.set('verify', false);
			}
		};
	});
	template.helpers({
		'expanded': function() {
			return Session.equals('verify', Template.instance().expander);
		}
	});
	template.events({
		'click .js-expand': function(event, instance) {
			Session.set('verify', instance.expander);
			event.stopPropagation();
		},
		'click .js-collapse': function(event, instance) {
			Session.set('verify', false);
		},
	});
};

expandible(Template.courseGroupAdd);
Template.courseGroupAdd.helpers(groupNameHelpers);
Template.courseGroupAdd.helpers({
	'groupsToAdd': function() {
		var user = Meteor.user();
		return _.difference(user.groups, this.groups);
	}
});

Template.courseGroupAdd.events({
	'click .js-add': function(event, instance) {
		Meteor.call('groupPromotesCourse', instance.data._id, event.target.value, true, function(error) {
			if (error) {
				addMessage(mf('course.group.addFailed', "Failed to add group to course"), 'danger');
			} else {
				addMessage(mf('course.group.addedGroup', "Added your group to the list of promoters"), 'success');
				instance.collapse();
			}
		});
	}
});

expandible(Template.courseGroupRemove);
Template.courseGroupRemove.helpers(groupNameHelpers);
Template.courseGroupRemove.events({
	'click .js-remove': function(event, instance) {
		Meteor.call('groupPromotesCourse', instance.data.course._id, instance.data.groupId, false, function(error) {
			if (error) {
				addMessage(mf('course.group.removeFailed', "Failed to remove group from course"), 'danger');
			} else {
				addMessage(mf('course.group.removedGroup', "Removed group from the list of promoters"), 'success');
				instance.collapse();
			}
		});
	}
});

expandible(Template.courseGroupMakeOrganizer);
Template.courseGroupMakeOrganizer.helpers(groupNameHelpers);
Template.courseGroupMakeOrganizer.events({
	'click .js-makeOrganizer': function(event, instance) {
		Meteor.call('groupEditing', instance.data.course._id, instance.data.groupId, true, function(error) {
			if (error) {
				addMessage(mf('course.group.makeOrganizerFailed', "Failed to give group editing rights"), 'danger');
			} else {
				addMessage(mf('course.group.groupMadeOrganizer', "Group members can now edit the course"), 'success');
				instance.collapse();
			}
		});
	}
});

expandible(Template.courseGroupRemoveOrganizer);
Template.courseGroupRemoveOrganizer.helpers(groupNameHelpers);
Template.courseGroupRemoveOrganizer.events({
	'click .js-removeOrganizer': function(event, instance) {
		Meteor.call('groupEditing', instance.data.course._id, instance.data.groupId, false, function(error) {
			if (error) {
				addMessage(mf('course.group.removeOrganizerFailed', "Failed to remove organizer status"), 'danger');
			} else {
				addMessage(mf('course.group.removedOrganizer', "Removed editing rights"), 'success');
				instance.collapse();
			}
		});
	}
});
<commit_msg>don't fail when no user is logged-in<commit_after>Router.map(function () {
	this.route('showCourse', {
		path: 'course/:_id/:slug?',
		template: 'coursedetails',
		waitOn: function () {
			return subs.subscribe('courseDetails', this.params._id);
		},
		data: function() {
			var self = this;
			var course = Courses.findOne({_id: this.params._id});

			if (!course) return false;

			var userId = Meteor.userId();
			var member = getMember(course.members, userId);
			var data = {
				edit: !!this.params.query.edit,
				roles_details: loadroles(course),
				course: course,
				member: member
			};
			if (course.editableBy(Meteor.user())) {
				data.editableName = makeEditable(
					course.name,
					true,
					function(newName) {
						Meteor.call("save_course", course._id, { name: newName }, function(err, courseId) {
							if (err) {
								addMessage(mf('course.saving.error', { ERROR: err }, 'Saving the course went wrong! Sorry about this. We encountered the following error: {ERROR}'), 'danger');
							} else {
								addMessage(mf('course.saving.name.editable.success', { NAME: course.name }), 'success');
							}
						});
					},
					mf('course.title.placeholder')
				);
				data.editableDescription = makeEditable(
					course.description,
					false,
					function(newDescription) {
						Meteor.call("save_course", course._id, { description: newDescription }, function(err, courseId) {
							if (err) {
								addMessage(mf('course.saving.error', { ERROR: err }, 'Saving the course went wrong! Sorry about this. We encountered the following error: {ERROR}'), 'danger');
							} else {
								addMessage(mf('course.saving.desc.editable.success', { NAME: course.name }), 'success');
							}
						});
					},
					mf('course.description.placeholder')
				);
			}
			return data;
		},
		onAfterAction: function() {
			var data = this.data();
			if (data) {
				var course = data.course;
				document.title = webpagename + 'Course: ' + course.name;
			}
		}
	});

	this.route('showCourseDocs', {
		path: 'course/:_id/:slug/docs',
		//template: 'coursedocs',
		waitOn: function () {
			return [
				Meteor.subscribe('courseDetails', this.params._id),
			];
		},
		data: function () {
			var course = Courses.findOne({_id: this.params._id});
			return {
				course: course
			};
		}
	});
	this.route('showCourseHistory', {
		path: 'course/:_id/:slug/History',
		//template: 'coursehistory',
		waitOn: function () {
			return [
				Meteor.subscribe('courseDetails', this.params._id)
			];
		},
		data: function () {
			var course = Courses.findOne({_id: this.params._id});
			return {
				course: course
			};
		}
	});
});

function loadroles(course) {
	var userId = Meteor.userId();
	return _.reduce(Roles.find({}, {sort: {type: 1} }).fetch(), function(goodroles, roletype) {
		var role = roletype.type;
		var sub = hasRoleUser(course.members, role, userId);
		if (course.roles && course.roles.indexOf(role) !== -1) {
			goodroles.push({
				roletype: roletype,
				role: role,
				subscribed: !!sub,
				anonsub: sub == 'anon',
				course: course
			});
		}
		return goodroles;
	}, []);
}

Template.coursedetails.helpers({    // more helpers in course.roles.js
	currentUserMayEdit: function() {
		return this.editableBy(Meteor.user());
	},
	coursestate: function() {
		var today = new Date();
		var upcoming = Events.find({course_id: this._id, start: {$gt:today}}).count() > 0;
		if (upcoming) return 'hasupcomingevents';

		var past = Events.find({course_id: this._id, start: {$lt:today}}).count() > 0;
		if (past) return 'haspastevents';

		return 'proposal';
	},
	mobileViewport: function() {
		return Session.get('screenSize') <= 480; // @screen-xs
	},
	isProposal: function() {
		return Events.find({course_id: this.course._id}).count() === 0;
	}
});

Template.show_course_submenu.helpers({
	hasFiles: function() {
		var withFiles = {course_id: this.course._id, files: {$exists: 1, $not: {$size: 0}}};
		return !!Events.findOne(withFiles);
	},
});

Template.coursedetails.events({
	'click button.del': function () {
		var self = this;
		if (pleaseLogin()) return;
		if (confirm(mf("course.detail.remove", "Remove course and all its events?"))) {
			Meteor.call('remove_course', this._id, function(error) {
				if (error) {
					addMessage(mf('course.detail.remove.error', { ERROR: error, NAME: self.name }, 'Sorry but removing the proposal "{NAME}" went wrong. We encountered the following error: {ERROR}'), 'danger');
				} else {
					addMessage(mf('course.detail.remove.success', { NAME: self.name }, 'The proposal "{NAME}" was obliterated!'), 'success');
				}
			});
			Router.go('/');
		}
	},

	'click button.edit': function () {
		if (pleaseLogin()) return;
		Router.go('showCourse', this, { query: {edit: 'course'} });
	}
});

Template.coursedetails.rendered = function() {
	this.$("[data-toggle='tooltip']").tooltip();
	var currentPath = Router.current().route.path(this);
	$('a[href!="' + currentPath + '"].nav_link').removeClass('active');
	$('#nav_courses').addClass('active');
};

Template.courseGroupList.helpers({
	'isOrganizer': function() {
		return Template.instance().data.groupOrganizers.indexOf(_id(this)) >= 0;
	},
	'tools': function() {
		var tools = [];
		var user = Meteor.user();
		var groupId = String(this);
		var course = Template.parentData();
		if (user && user.mayPromoteWith(groupId)) {
			tools.push({
				toolTemplate: Template.courseGroupRemove,
				groupId: groupId,
				course: course,
			});
		}
		if (user && course.editableBy(user)) {
			var hasOrgRights = course.groupOrganizers.indexOf(groupId) > -1;
			tools.push({
				toolTemplate: hasOrgRights ? Template.courseGroupRemoveOrganizer : Template.courseGroupMakeOrganizer,
				groupId: groupId,
				course: course,
			});
		}
		return tools;
	},
});

var expandible = function(template) {
	template.onCreated(function() {
		var expander = Random.id(); // Token to keep track of which Expandible is open
		this.expander = expander;
		this.collapse = function() {
			if (Session.equals('verify', expander)) {
				Session.set('verify', false);
			}
		};
	});
	template.helpers({
		'expanded': function() {
			return Session.equals('verify', Template.instance().expander);
		}
	});
	template.events({
		'click .js-expand': function(event, instance) {
			Session.set('verify', instance.expander);
			event.stopPropagation();
		},
		'click .js-collapse': function(event, instance) {
			Session.set('verify', false);
		},
	});
};

expandible(Template.courseGroupAdd);
Template.courseGroupAdd.helpers(groupNameHelpers);
Template.courseGroupAdd.helpers({
	'groupsToAdd': function() {
		var user = Meteor.user();
		return user && _.difference(user.groups, this.groups);
	}
});

Template.courseGroupAdd.events({
	'click .js-add': function(event, instance) {
		Meteor.call('groupPromotesCourse', instance.data._id, event.target.value, true, function(error) {
			if (error) {
				addMessage(mf('course.group.addFailed', "Failed to add group to course"), 'danger');
			} else {
				addMessage(mf('course.group.addedGroup', "Added your group to the list of promoters"), 'success');
				instance.collapse();
			}
		});
	}
});

expandible(Template.courseGroupRemove);
Template.courseGroupRemove.helpers(groupNameHelpers);
Template.courseGroupRemove.events({
	'click .js-remove': function(event, instance) {
		Meteor.call('groupPromotesCourse', instance.data.course._id, instance.data.groupId, false, function(error) {
			if (error) {
				addMessage(mf('course.group.removeFailed', "Failed to remove group from course"), 'danger');
			} else {
				addMessage(mf('course.group.removedGroup', "Removed group from the list of promoters"), 'success');
				instance.collapse();
			}
		});
	}
});

expandible(Template.courseGroupMakeOrganizer);
Template.courseGroupMakeOrganizer.helpers(groupNameHelpers);
Template.courseGroupMakeOrganizer.events({
	'click .js-makeOrganizer': function(event, instance) {
		Meteor.call('groupEditing', instance.data.course._id, instance.data.groupId, true, function(error) {
			if (error) {
				addMessage(mf('course.group.makeOrganizerFailed', "Failed to give group editing rights"), 'danger');
			} else {
				addMessage(mf('course.group.groupMadeOrganizer', "Group members can now edit the course"), 'success');
				instance.collapse();
			}
		});
	}
});

expandible(Template.courseGroupRemoveOrganizer);
Template.courseGroupRemoveOrganizer.helpers(groupNameHelpers);
Template.courseGroupRemoveOrganizer.events({
	'click .js-removeOrganizer': function(event, instance) {
		Meteor.call('groupEditing', instance.data.course._id, instance.data.groupId, false, function(error) {
			if (error) {
				addMessage(mf('course.group.removeOrganizerFailed', "Failed to remove organizer status"), 'danger');
			} else {
				addMessage(mf('course.group.removedOrganizer', "Removed editing rights"), 'success');
				instance.collapse();
			}
		});
	}
});
<|endoftext|>


<commit_before>'use strict';

function LoginController($scope, $mdDialog, message) {
    $scope.message = message;
    $scope.user = {
        'username': '',
        'password': '',
        'tfa':''
    };
    $scope.cancel = function() {
        $mdDialog.cancel();
    };
    $scope.login = function() {
        $mdDialog.hide($scope.user);
    };
}

function LogoutController($scope, $mdDialog) {
    $scope.cancel = function() {
        $mdDialog.cancel();
    };
    $scope.logout = function() {
        $mdDialog.hide(true);
    };
}

angular.module('quimby.directives')
    .directive("navbar", ['$location', '$auth', '$mdDialog', '$mdSidenav', function($location, $auth, $mdDialog, $mdSidenav) {
        return {
            restrict: "E",
            replace: true,
            transclude: true,
            templateUrl: "/components/navbar/navbar.html?t=" + new Date().getTime(),
            controller: function($scope, $rootScope, $timeout, $mdDialog) {
                var originatorEv;
                $scope.loggedIn = false;
                $rootScope.user = {};
                $scope.openMenu = function($mdOpenMenu, ev) {
                    originatorEv = ev;
                    $mdOpenMenu(ev);
                };
                $scope.showLogout = function(ev) {
                    $mdDialog.show({
                        controller: LogoutController,
                        templateUrl: '/components/navbar/logout.html?t=' + new Date().getTime(),
                        targetEvent: ev,
                    }).then(function(result) {
                        if (result) {
                            $auth.logout(function() {
                                $rootScope.user = {};
                                $scope.user = {};
                                $scope.loggedIn = false;
                                $location.path("/admin");
                            });
                        }
                    });
                }
                $scope.showLogin = function(ev) {
                    $mdDialog.show({
                        controller: LoginController,
                        templateUrl: 'components/navbar/login.html?t=' + new Date().getTime(),
                        locals: {
                            message: $scope.message
                        },
                        targetEvent: ev
                    }).then(function(result) {
                        if (result == "forgot") {
                            $location.path("/reset-password");
                        } else if (result) {
                            $auth.login(result.username, result.password, result.tfa, function(user) {
                                $scope.message = "";
                                $scope.user = user;
                                $scope.loggedIn = true;
                                $location.path("/gadgets");
                            }, function(data) {
                                console.log("not logged in", data);
                                $scope.message = "the username or password is not correct, please try again";
                                $scope.showLogin();
                            });
                        }
                    });
                };

                $scope.admin = function() {
                    $location.path('/admin');
                }

                $auth.getUser(function(user) {
                    if (user) {
                        $rootScope.user = user;
                        $scope.user = user;
                        $scope.loggedIn = true;
                    } else {
                        $scope.user = {};
                    }
                });
            }
        }
    }])
<commit_msg>fixed logout location<commit_after>'use strict';

function LoginController($scope, $mdDialog, message) {
    $scope.message = message;
    $scope.user = {
        'username': '',
        'password': '',
        'tfa':''
    };
    $scope.cancel = function() {
        $mdDialog.cancel();
    };
    $scope.login = function() {
        $mdDialog.hide($scope.user);
    };
}

function LogoutController($scope, $mdDialog) {
    $scope.cancel = function() {
        $mdDialog.cancel();
    };
    $scope.logout = function() {
        $mdDialog.hide(true);
    };
}

angular.module('quimby.directives')
    .directive("navbar", ['$location', '$auth', '$mdDialog', '$mdSidenav', function($location, $auth, $mdDialog, $mdSidenav) {
        return {
            restrict: "E",
            replace: true,
            transclude: true,
            templateUrl: "/components/navbar/navbar.html?t=" + new Date().getTime(),
            controller: function($scope, $rootScope, $timeout, $mdDialog) {
                var originatorEv;
                $scope.loggedIn = false;
                $rootScope.user = {};
                $scope.openMenu = function($mdOpenMenu, ev) {
                    originatorEv = ev;
                    $mdOpenMenu(ev);
                };
                $scope.showLogout = function(ev) {
                    $mdDialog.show({
                        controller: LogoutController,
                        templateUrl: '/components/navbar/logout.html?t=' + new Date().getTime(),
                        targetEvent: ev,
                    }).then(function(result) {
                        if (result) {
                            $auth.logout(function() {
                                $rootScope.user = {};
                                $scope.user = {};
                                $scope.loggedIn = false;
                                $location.path("/gadgets");
                            });
                        }
                    });
                }
                $scope.showLogin = function(ev) {
                    $mdDialog.show({
                        controller: LoginController,
                        templateUrl: 'components/navbar/login.html?t=' + new Date().getTime(),
                        locals: {
                            message: $scope.message
                        },
                        targetEvent: ev
                    }).then(function(result) {
                        if (result == "forgot") {
                            $location.path("/reset-password");
                        } else if (result) {
                            $auth.login(result.username, result.password, result.tfa, function(user) {
                                $scope.message = "";
                                $scope.user = user;
                                $scope.loggedIn = true;
                                $location.path("/gadgets");
                            }, function(data) {
                                console.log("not logged in", data);
                                $scope.message = "the username or password is not correct, please try again";
                                $scope.showLogin();
                            });
                        }
                    });
                };

                $scope.admin = function() {
                    $location.path('/admin');
                }

                $auth.getUser(function(user) {
                    if (user) {
                        $rootScope.user = user;
                        $scope.user = user;
                        $scope.loggedIn = true;
                    } else {
                        $scope.user = {};
                    }
                });
            }
        }
    }])
<|endoftext|>


<commit_before>import React from 'react';
import { createMockProvidedChart, uuidRegex } from '../../test-utils'
import Annotation from '../../../src/components/Annotation/Annotation';
import ChartContext from '../../../src/components/ChartContext';

describe('<Annotation />', () => {
  let testContext;
  let ProvidedAnnotation;

  beforeEach(() => {
    testContext = {};

    const { chartStubs, needsRedraw } = createMockProvidedChart();
    chartStubs.addAnnotation = jest.fn();
    chartStubs.removeAnnotation = jest.fn();
    testContext.chartStubs = chartStubs;
    ProvidedAnnotation = (props) => (
      <ChartContext.Provider value={ chartStubs }>
        <Annotation {...props} />
      </ChartContext.Provider>
    )
  });

  describe('when mounted', () => {
    it('adds an annotation using the chart addAnnotation method', () => {
      mount(<ProvidedAnnotation id="My Annotation" />);
      expect(testContext.chartStubs.addAnnotation).toHaveBeenCalledWith(expect.objectContaining(
        { id: 'My Annotation' }
      ));
    });

    it('should pass additional props through to chart addAnnotation method', () => {
      mount(<ProvidedAnnotation
        id="My Other Annotation"
        labels={ [{ text: "label", point: { x: 200, y: 200} }] }
        labelOptions={ { borderColor: 'red' }}
        />);
      expect(testContext.chartStubs.addAnnotation).toHaveBeenCalledWith(expect.objectContaining({
        id: 'My Other Annotation',
        labelOptions: { borderColor: 'red' },
        labels: [{ text: "label", point: { x: 200, y: 200} }]
      }));
    });

    it('uses the provided ID if id prop is a string', () => {
      mount(
        <ProvidedAnnotation id="myPlotLineIdStr" value={2} />
      );
      expect(testContext.chartStubs.addAnnotation.mock.calls[0][0].id).toBe('myPlotLineIdStr');
    });

    it('resolves the ID if id prop is a function', () => {
      const idFunc = () => 'myPlotLineIdFromFunc'
      mount(
        <ProvidedAnnotation id={idFunc} value={2} />
      );
      expect(testContext.chartStubs.addAnnotation.mock.calls[0][0].id).toBe('myPlotLineIdFromFunc');
    });

    it('uses a uuid as an ID if no id prop provided', () => {
      mount(
        <ProvidedAnnotation />
      );
      expect(testContext.chartStubs.addAnnotation.mock.calls[0][0].id).toMatch(uuidRegex);
    });
  });

  describe('when unmounted', () => {
    it('removes the annotation by id (if the parent chart still exists)', () => {
      const wrapper = mount(
        <ProvidedAnnotation id="My Annotation" />
      );
      testContext.chartStubs.removeAnnotation.mockReset();
      wrapper.unmount();
      expect(testContext.chartStubs.removeAnnotation).toHaveBeenCalledWith('My Annotation');
    });
  });

  describe('when updated', () => {
    it('removes and re-adds annotation with new props', () => {
      const wrapper = mount(
        <ProvidedAnnotation id="My Annotation" labels={[{ text: "label", point: { x: 200, y: 200} }]} />
      );
      testContext.chartStubs.addAnnotation.mockReset();
      wrapper.setProps({ labels: [{ text: "label", point: { x: 100, y: 100} }]});
      expect(testContext.chartStubs.removeAnnotation).toHaveBeenCalledWith('My Annotation');

      expect(testContext.chartStubs.addAnnotation).toHaveBeenCalledWith(expect.objectContaining({
        id: 'My Annotation',
        labels: [{ text: "label", point: { x: 100, y: 100} }]
      }));
    });
  });

  describe('children', () => {
    it('should pass the ID of the plot band to the children', () => {
      const ChildComponent = props => (<div />);

      const wrapper = mount(
        <ProvidedAnnotation id="myId">
          <ChildComponent />
        </ProvidedAnnotation>
      ).children();
      expect(wrapper.find(ChildComponent)).toHaveProp('id', 'myId');
    });

  });
});
<commit_msg>Fix annotation test<commit_after>import React from 'react';
import { createMockProvidedChart, uuidRegex } from '../../test-utils'
import Annotation from '../../../src/components/Annotation/Annotation';
import ChartContext from '../../../src/components/ChartContext';

describe('<Annotation />', () => {
  let testContext;
  let ProvidedAnnotation;

  beforeEach(() => {
    testContext = {};

    const { chartStubs, needsRedraw } = createMockProvidedChart();
    chartStubs.addAnnotation = jest.fn();
    chartStubs.removeAnnotation = jest.fn();
    testContext.chartStubs = chartStubs;
    ProvidedAnnotation = (props) => (
      <ChartContext.Provider value={ chartStubs }>
        <Annotation {...props} />
      </ChartContext.Provider>
    )
  });

  describe('when mounted', () => {
    it('adds an annotation using the chart addAnnotation method', () => {
      mount(<ProvidedAnnotation id="My Annotation" />);
      expect(testContext.chartStubs.addAnnotation).toHaveBeenCalledWith(expect.objectContaining(
        { id: 'My Annotation' }
      ));
    });

    it('should pass additional props through to chart addAnnotation method', () => {
      mount(<ProvidedAnnotation
        id="My Other Annotation"
        labels={ [{ text: "label", point: { x: 200, y: 200} }] }
        labelOptions={ { borderColor: 'red' }}
        />);
      expect(testContext.chartStubs.addAnnotation).toHaveBeenCalledWith(expect.objectContaining({
        id: 'My Other Annotation',
        labelOptions: { borderColor: 'red' },
        labels: [{ text: "label", point: { x: 200, y: 200} }]
      }));
    });

    it('uses the provided ID if id prop is a string', () => {
      mount(
        <ProvidedAnnotation id="myPlotLineIdStr" value={2} />
      );
      expect(testContext.chartStubs.addAnnotation.mock.calls[0][0].id).toBe('myPlotLineIdStr');
    });

    it('resolves the ID if id prop is a function', () => {
      const idFunc = () => 'myPlotLineIdFromFunc'
      mount(
        <ProvidedAnnotation id={idFunc} value={2} />
      );
      expect(testContext.chartStubs.addAnnotation.mock.calls[0][0].id).toBe('myPlotLineIdFromFunc');
    });

    it('uses a uuid as an ID if no id prop provided', () => {
      mount(
        <ProvidedAnnotation />
      );
      expect(testContext.chartStubs.addAnnotation.mock.calls[0][0].id).toMatch(uuidRegex);
    });
  });

  describe('when unmounted', () => {
    it('removes the annotation by id (if the parent chart still exists)', () => {
      const wrapper = mount(
        <ProvidedAnnotation id="My Annotation" />
      );
      testContext.chartStubs.removeAnnotation.mockReset();
      wrapper.unmount();
      expect(testContext.chartStubs.removeAnnotation).toHaveBeenCalledWith('My Annotation');
    });
  });

  describe('when updated', () => {
    it('removes and re-adds annotation with new props', () => {
      const wrapper = mount(
        <ProvidedAnnotation id="My Annotation" labels={[{ text: "label", point: { x: 200, y: 200} }]} />
      );
      testContext.chartStubs.addAnnotation.mockReset();
      wrapper.setProps({ labels: [{ text: "label", point: { x: 100, y: 100} }]});
      expect(testContext.chartStubs.removeAnnotation).toHaveBeenCalledWith('My Annotation');

      expect(testContext.chartStubs.addAnnotation).toHaveBeenCalledWith(expect.objectContaining({
        id: 'My Annotation',
        labels: [{ text: "label", point: { x: 100, y: 100} }]
      }));
    });
  });
});
<|endoftext|>


<commit_before>const TimerMixin = require('react-timer-mixin');

let nextID = 1;

module.exports = (React, ReactNative, { constants, model, styles }) => {
  const {
    NativeModules: { UIManager },
    TouchableWithoutFeedback,
    ScrollView,
    View,
    BackAndroid
  } = ReactNative;
  const AnimatedOptionsContainer = require('./makeAnimatedOptionsContainer')(React, ReactNative);

  // Calls a function once, then never again.
  const once = (fn) => {
    let called = false;
    return (...args) => {
      if (!called) {
        called = true;
        fn(...args);
      }
    };
  };

  const defaultOptionsContainerRenderer = (options) => (
    <ScrollView>
      {options}
    </ScrollView>
  );

  const makeOptions = (options, { top, right }) => {
    const { optionsContainerStyle, renderOptionsContainer = defaultOptionsContainerRenderer} = options.props;
    return (
      <AnimatedOptionsContainer style={[ styles.optionsContainer, optionsContainerStyle, { top, right }]}>
        { renderOptionsContainer(options) }
      </AnimatedOptionsContainer>
    );
  };

  const NULL_HOOKS = {
    didOpen: () => {},
    didClose: () => {}
  }

  /*
   * The MenuContext provides a tunnel for descendant menu components to access
   * top-level methods. It also allows the <MenuOptions/> element to be placed
   * properly.
   */
  const MenuContext = React.createClass({
    displayName: 'MenuContext',
    propTypes: {
      detectBackAndroid: React.PropTypes.boolean,
    },
    getDefaultProps() {
      return {
        detectBackAndroid: true,
      };
    },
    mixins: [TimerMixin],

    // Public methods
    isMenuOpen() {
      return this.state.openedMenu
    },
    openMenu(name) {
      const handle = ReactNative.findNodeHandle(this._menus[name].ref);
      UIManager.measure(handle, (x, y, w, h, px, py) => {
        this._menus[name].measurements = { x, y, w, h, px, py };

        this.setState({
          openedMenu: name,
          menuOptions: this._makeAndPositionOptions(name, this._menus[name].measurements),
          backdropWidth: this._ownMeasurements.w
        });

        this._activeMenuHooks = this._menus[name];
        this._activeMenuHooks && this._activeMenuHooks.didOpen();
      });
    },
    closeMenu() {
      this.setState({
        openedMenu: '',
        menuOptions: null
      });

      this._activeMenuHooks && this._activeMenuHooks.didClose();
      this._activeMenuHooks = NULL_HOOKS;
    },
    toggleMenu(name) {
      if (this.state.openedMenu === name) {
        this.closeMenu(name);
      } else {
        this.openMenu(name);
      }
    },

    // Private and lifecycle methods
    getInitialState() {
      return {
        openedMenu: '',
        menuOptions: null,
        optionsTop: 0,
        optionsRight: 0,
        backdropWidth: 0
      };
    },
    childContextTypes: {
      menuController: model.IMenuController
    },
    getChildContext() {
      const menuController = {
        open: this.openMenu,
        close: this.closeMenu,
        toggle: this.toggleMenu,
        registerMenu: this._registerMenu,
        unregisterMenu: this._unregisterMenu,
        registerOptionsElement: this._registerOptionsElement,
        makeName: this._makeName
      };
      return { menuController };
    },
    componentWillMount() {
      this._menus = {};
      this._options = {};
      // Only do this once on initial layout.
      this.onLayout = once(this.onLayout);
    },
    handleBackAndroid() {
      if (this.isMenuOpen()){
        this.closeMenu();
        return true;
      }
      return false;
    },
    onLayout() {
      const handle = ReactNative.findNodeHandle(this.refs.Container);
      UIManager.measure(handle, (x, y, w, h, px, py) => {
        this._ownMeasurements = {x, y, w, h, px, py};
      });
    },
    _registerMenu(name, hooks) {
      if (this._menus[name]) {
        console.warn(`Menu ${name} has already been registered in this context. Please provide a different name.`);
      }

      if (this.props.detectBackAndroid){
        BackAndroid.removeEventListener('hardwareBackPress', this.handleBackAndroid.bind(this));  //Override previous listener
        BackAndroid.addEventListener('hardwareBackPress', this.handleBackAndroid.bind(this));
      }
      this._menus[name] = hooks;
    },
    _unregisterMenu(name) {
      delete this._menus[name];
      delete this._options[name];
    },
    _registerOptionsElement(name, options) {
      // If options are already set and visible, skip the update.
      if (this.state.menuOptions === options) {
        return;
      }
      this._options[name] = options;
      // If the menu is already open, re-render the options.
      this.setTimeout(() => {
        if (this.state.openedMenu === name) {
          this.setState({ menuOptions: this._makeAndPositionOptions(name, this._menus[name].measurements) });
        }
      }, 16);
    },
    _makeName() {
      return `menu-${nextID++}`;
    },
    _makeAndPositionOptions(name, menuMeasurements) {
      const options = this._options[name];
      const { w: menuWidth, px: menuPX, py: menuPY } = menuMeasurements;
      const { w: ownWidth, px: ownPX, py: ownPY } = this._ownMeasurements;
      const optionsTop = menuPY - ownPY;
      const optionsRight = ownWidth + ownPX - menuPX - menuWidth;
      return makeOptions(options, { top: optionsTop, right: optionsRight });
    },

    render() {
      return (
        <View ref="Container" onLayout={this.onLayout} style={{ flex: 1 }}>
          <View style={this.props.style}>
            { this.props.children }
          </View>
          <TouchableWithoutFeedback onPress={this.closeMenu}>
            <View style={[ styles.backdrop
                          , this.state.openedMenu ? { width: this.state.backdropWidth, top: 0, bottom: 0 }: null ]}/>
          </TouchableWithoutFeedback>
          { this.state.menuOptions }
        </View>
      )
    }
  });

  return MenuContext;
};
<commit_msg>fixed some warnings<commit_after>const TimerMixin = require('react-timer-mixin');

let nextID = 1;

module.exports = (React, ReactNative, { constants, model, styles }) => {
  const {
    NativeModules: { UIManager },
    TouchableWithoutFeedback,
    ScrollView,
    View,
    BackAndroid
  } = ReactNative;
  const AnimatedOptionsContainer = require('./makeAnimatedOptionsContainer')(React, ReactNative);

  // Calls a function once, then never again.
  const once = (fn) => {
    let called = false;
    return (...args) => {
      if (!called) {
        called = true;
        fn(...args);
      }
    };
  };

  const defaultOptionsContainerRenderer = (options) => (
    <ScrollView>
      {options}
    </ScrollView>
  );

  const makeOptions = (options, { top, right }) => {
    const { optionsContainerStyle, renderOptionsContainer = defaultOptionsContainerRenderer} = options.props;
    return (
      <AnimatedOptionsContainer style={[ styles.optionsContainer, optionsContainerStyle, { top, right }]}>
        { renderOptionsContainer(options) }
      </AnimatedOptionsContainer>
    );
  };

  const NULL_HOOKS = {
    didOpen: () => {},
    didClose: () => {}
  }

  /*
   * The MenuContext provides a tunnel for descendant menu components to access
   * top-level methods. It also allows the <MenuOptions/> element to be placed
   * properly.
   */
  const MenuContext = React.createClass({
    displayName: 'MenuContext',
    propTypes: {
      detectBackAndroid: React.PropTypes.bool,
    },
    getDefaultProps() {
      return {
        detectBackAndroid: true,
      };
    },
    mixins: [TimerMixin],

    // Public methods
    isMenuOpen() {
      return this.state.openedMenu
    },
    openMenu(name) {
      const handle = ReactNative.findNodeHandle(this._menus[name].ref);
      UIManager.measure(handle, (x, y, w, h, px, py) => {
        this._menus[name].measurements = { x, y, w, h, px, py };

        this.setState({
          openedMenu: name,
          menuOptions: this._makeAndPositionOptions(name, this._menus[name].measurements),
          backdropWidth: this._ownMeasurements.w
        });

        this._activeMenuHooks = this._menus[name];
        this._activeMenuHooks && this._activeMenuHooks.didOpen();
      });
    },
    closeMenu() {
      this.setState({
        openedMenu: '',
        menuOptions: null
      });

      this._activeMenuHooks && this._activeMenuHooks.didClose();
      this._activeMenuHooks = NULL_HOOKS;
    },
    toggleMenu(name) {
      if (this.state.openedMenu === name) {
        this.closeMenu(name);
      } else {
        this.openMenu(name);
      }
    },

    // Private and lifecycle methods
    getInitialState() {
      return {
        openedMenu: '',
        menuOptions: null,
        optionsTop: 0,
        optionsRight: 0,
        backdropWidth: 0
      };
    },
    childContextTypes: {
      menuController: model.IMenuController
    },
    getChildContext() {
      const menuController = {
        open: this.openMenu,
        close: this.closeMenu,
        toggle: this.toggleMenu,
        registerMenu: this._registerMenu,
        unregisterMenu: this._unregisterMenu,
        registerOptionsElement: this._registerOptionsElement,
        makeName: this._makeName
      };
      return { menuController };
    },
    componentWillMount() {
      this._menus = {};
      this._options = {};
      // Only do this once on initial layout.
      this.onLayout = once(this.onLayout);
    },
    handleBackAndroid() {
      if (this.isMenuOpen()){
        this.closeMenu();
        return true;
      }
      return false;
    },
    onLayout() {
      const handle = ReactNative.findNodeHandle(this.refs.Container);
      UIManager.measure(handle, (x, y, w, h, px, py) => {
        this._ownMeasurements = {x, y, w, h, px, py};
      });
    },
    _registerMenu(name, hooks) {
      if (this._menus[name]) {
        console.warn(`Menu ${name} has already been registered in this context. Please provide a different name.`);
      }

      if (this.props.detectBackAndroid){
        BackAndroid.removeEventListener('hardwareBackPress', this.handleBackAndroid);  //Override previous listener
        BackAndroid.addEventListener('hardwareBackPress', this.handleBackAndroid);
      }
      this._menus[name] = hooks;
    },
    _unregisterMenu(name) {
      delete this._menus[name];
      delete this._options[name];
    },
    _registerOptionsElement(name, options) {
      // If options are already set and visible, skip the update.
      if (this.state.menuOptions === options) {
        return;
      }
      this._options[name] = options;
      // If the menu is already open, re-render the options.
      this.setTimeout(() => {
        if (this.state.openedMenu === name) {
          this.setState({ menuOptions: this._makeAndPositionOptions(name, this._menus[name].measurements) });
        }
      }, 16);
    },
    _makeName() {
      return `menu-${nextID++}`;
    },
    _makeAndPositionOptions(name, menuMeasurements) {
      const options = this._options[name];
      const { w: menuWidth, px: menuPX, py: menuPY } = menuMeasurements;
      const { w: ownWidth, px: ownPX, py: ownPY } = this._ownMeasurements;
      const optionsTop = menuPY - ownPY;
      const optionsRight = ownWidth + ownPX - menuPX - menuWidth;
      return makeOptions(options, { top: optionsTop, right: optionsRight });
    },

    render() {
      return (
        <View ref="Container" onLayout={this.onLayout} style={{ flex: 1 }}>
          <View style={this.props.style}>
            { this.props.children }
          </View>
          <TouchableWithoutFeedback onPress={this.closeMenu}>
            <View style={[ styles.backdrop
                          , this.state.openedMenu ? { width: this.state.backdropWidth, top: 0, bottom: 0 }: null ]}/>
          </TouchableWithoutFeedback>
          { this.state.menuOptions }
        </View>
      )
    }
  });

  return MenuContext;
};
<|endoftext|>


<commit_before>// @flow

import type {Tag} from './TagStore';

import {ReduceStore} from 'flux/utils';
import ProjectSearchDispatcher from './ProjectSearchDispatcher.js';
import {List, Record} from 'immutable'

export type Project = {|
  +description: string,
  +issueArea: string,
  +location: string,
  +name: string,
|};

type ProjectAPIData = {|
  +project_description: string,
  +project_issue_area: $ReadOnlyArray<{|+name: string|}>,
  +project_location: string,
  +project_name: string,
|};

export type ProjectSearchActionType = {
  type: 'INIT',
} | {
  type: 'ADD_TAG',
  tag: Tag,
} | {
  type: 'REMOVE_TAG',
  tag: Tag,
} | {
  type: 'SET_KEYWORD',
  keyword: string,
} | {
  type: 'SET_PROJECTS_DO_NOT_CALL_OUTSIDE_OF_STORE',
  projects: List<Project>,
};

const DEFAULT_STATE = {
  keyword: '',
  projects: List(),
  tags: List(),
};

class State extends Record(DEFAULT_STATE) {
  keyword: string;
  projects: List<Project>;
}

class ProjectSearchStore extends ReduceStore<State> {
  constructor(): void {
    super(ProjectSearchDispatcher);
  }

  getInitialState(): State {
    return new State();
  }

  reduce(state: State, action: ProjectSearchActionType): State {
    let newState = new State();
    switch (action.type) {
      case 'INIT':
        newState = this._clearProjects(state);
        this._loadProjects(newState);
        return newState;
      case 'ADD_TAG':
        newState = this._clearProjects(
          state.set('tags', state.tags.push(action.tag)),
        );
        this._loadProjects(newState);
        return newState;
      case 'REMOVE_TAG':
        newState = this._clearProjects(
          state.set(
            'tags',
            state.tags.delete(
              /* $FlowFixMe I don't know why, but the action type isn't being
                subtyped here */
              state.tags.findIndex(tag => tag.id === action.tag.id),
            ),
          ),
        );
        this._loadProjects(newState);
        return newState;
      case 'SET_KEYWORD':
        newState = this._clearProjects(state.set('keyword', action.keyword));
        this._loadProjects(newState);
        return newState;
      case 'SET_PROJECTS_DO_NOT_CALL_OUTSIDE_OF_STORE':
        return state.set('projects', action.projects);
      default:
        (action: empty);
        return state;
    }
  }

  _clearProjects(state: State): State {
    return state.set('projects', null);
  }

  _loadProjects(state: State): void {
    const url = [
      '/api/projects?',
      state.keyword ? '&keyword=' + this.getState().keyword : null,
      state.tags
        ? '&tags='
          + state.tags.map(tag => tag.tagName).join(',').replace('-', '_')
        : null,
    ].join('');
    fetch(new Request(url))
      .then(response => response.json())
      .then(projects =>
        ProjectSearchDispatcher.dispatch({
          type: 'SET_PROJECTS_DO_NOT_CALL_OUTSIDE_OF_STORE',
          projects: List(projects.map(this._projectFromAPIData)),
        }),
      );
  }

  _projectFromAPIData(apiData: ProjectAPIData): Project {
    return {
      description: apiData.project_description,
      issueArea:
        apiData.project_issue_area.length != 0
          ? apiData.project_issue_area[0].name
          : 'None',
      location: apiData.project_location,
      name: apiData.project_name,
    };
  }

  getKeyword(): string {
    return this.getState().keyword;
  }

  getProjects(): List<Project> {
    return this.getState().projects;
  }

  getTags(): List<Tag> {
    return this.getState().tags;
  }
}

export default new ProjectSearchStore();
<commit_msg>refactor/simplify ProjectSearchStore logic<commit_after>// @flow

import type {Tag} from './TagStore';

import {ReduceStore} from 'flux/utils';
import ProjectSearchDispatcher from './ProjectSearchDispatcher.js';
import {List, Record} from 'immutable'

export type Project = {|
  +description: string,
  +issueArea: string,
  +location: string,
  +name: string,
|};

type ProjectAPIData = {|
  +project_description: string,
  +project_issue_area: $ReadOnlyArray<{|+name: string|}>,
  +project_location: string,
  +project_name: string,
|};

export type ProjectSearchActionType = {
  type: 'INIT',
} | {
  type: 'ADD_TAG',
  tag: Tag,
} | {
  type: 'REMOVE_TAG',
  tag: Tag,
} | {
  type: 'SET_KEYWORD',
  keyword: string,
} | {
  type: 'SET_PROJECTS_DO_NOT_CALL_OUTSIDE_OF_STORE',
  projects: List<Project>,
};

const DEFAULT_STATE = {
  keyword: '',
  projects: List(),
  tags: List(),
};

class State extends Record(DEFAULT_STATE) {
  keyword: string;
  projects: List<Project>;
}

class ProjectSearchStore extends ReduceStore<State> {
  constructor(): void {
    super(ProjectSearchDispatcher);
  }

  getInitialState(): State {
    return new State();
  }

  reduce(state: State, action: ProjectSearchActionType): State {
    switch (action.type) {
      case 'INIT':
        return this._loadProjects(new State());
      case 'ADD_TAG':
        return this._loadProjects(state.set('tags', state.tags.push(action.tag)));
      case 'REMOVE_TAG':
        return this._loadProjects(
          state.set(
            'tags',
            state.tags.delete(
              /* $FlowFixMe I don't know why, but the action type isn't being
                subtyped here */
              state.tags.findIndex(tag => tag.id === action.tag.id),
            ),
          ),
        );
      case 'SET_KEYWORD':
        return this._loadProjects(state.set('keyword', action.keyword));
      case 'SET_PROJECTS_DO_NOT_CALL_OUTSIDE_OF_STORE':
        return state.set('projects', action.projects);
      default:
        (action: empty);
        return state;
    }
  }

  _loadProjects(state: State): State {
    const url = [
      '/api/projects?',
      state.keyword ? '&keyword=' + this.getState().keyword : null,
      state.tags
        ? '&tags='
          + state.tags.map(tag => tag.tagName).join(',').replace('-', '_')
        : null,
    ].join('');
    fetch(new Request(url))
      .then(response => response.json())
      .then(projects =>
        ProjectSearchDispatcher.dispatch({
          type: 'SET_PROJECTS_DO_NOT_CALL_OUTSIDE_OF_STORE',
          projects: List(projects.map(this._projectFromAPIData)),
        }),
      );
    return state.set('projects', null);
  }

  _projectFromAPIData(apiData: ProjectAPIData): Project {
    return {
      description: apiData.project_description,
      issueArea:
        apiData.project_issue_area.length != 0
          ? apiData.project_issue_area[0].name
          : 'None',
      location: apiData.project_location,
      name: apiData.project_name,
    };
  }

  getKeyword(): string {
    return this.getState().keyword;
  }

  getProjects(): List<Project> {
    return this.getState().projects;
  }

  getTags(): List<Tag> {
    return this.getState().tags;
  }
}

export default new ProjectSearchStore();
<|endoftext|>


<commit_before>/**
 * foodItemModel.js
 *
 * This module contains the API for retrieving and storing data in the
 * current day's food log.
 *
 */

define('foodItemModel', ['db', 'util'], function( db, util ) {

    'use strict';

    var init,
        add,
        set,
        del,
        getAll,
        keys,
        getTotals,
        getTotalPercents,
        sanitize,
        keyPrefix;

    /**
     * Intialize the foodItemModel module
     * @return {void}
     */
    init = function(){
        keyPrefix = 'food-items-' + util.ymd();
        keys = ['fat', 'protein', 'carbs'];
    };

    /**
     * Adds a food item to the food log
     * @param {Object} item - object containing fat, protein, carbs
     */
    add = function( item ){
        var items = getAll();

        if ( items === null ) {
            items = [];
        }

        item.id = util.guid();
        items.push(sanitize(item));
        db.set(keyPrefix, JSON.stringify(items));
    };

    /**
     * Delete an item from the log by id
     * @param  {String} id - globally unique id
     * @return {void}
     */
    del = function( id ){
        var items = getAll();

        items = util.map(items, function( obj ) {
            return obj.id !== id;
        });
        db.set(keyPrefix, JSON.stringify(items));
    };

    /**
     * Get all items from the food log
     * @return {Object}
     */
    getAll = function(){
        return JSON.parse(db.get(keyPrefix));
    };

    /**
     * Get macronutrient totals from the food log
     * @return {Object}
     */
    getTotals = function(){
        var items = getAll(),
            keys = ['fat', 'protein', 'carbs'],
            output = {
                fat: 0,
                protein: 0,
                carbs: 0
            };

        keys.forEach(function( key ){
            output[key] = util.reduce(items || [], function( memo, obj ){
                return memo + parseInt(obj[key]);
            }, 0);
        });

        return output;
    };

    /**
     * Get macronutrient total percentages from the food log
     * @return {Object}
     */
    getTotalPercents = function(){
        var totals = getTotals(),
            sumTotal = 0,
            keys = ['fat', 'protein', 'carbs'],
            output = {
                fat: 0,
                protein: 0,
                carbs: 0
            };

        keys.forEach(function( key ){
            sumTotal += totals[key];
        });

        if ( sumTotal === 0 ) {
            return output;
        }

        keys.forEach(function( key ){
            output[key] = parseInt((totals[key]/sumTotal)*100);
        });

        return output;
    };

    /**
     * Remove the `new-food-` string from object keys
     * @param  {Object} obj
     * @return {Object}
     */
    sanitize = function( obj ){
        var key,
            output = {};

        for ( key in obj ) {
            output[key.replace(/new-food-/, '')] = obj[key];
        }

        return output;
    };

    init();

    return {
        add: add,
        set: set,
        del: del,
        getAll: getAll,
        getTotals: getTotals,
        getTotalPercents: getTotalPercents
    };

});
<commit_msg>make keyPrefix a function, so we’re always getting the correct date<commit_after>/**
 * foodItemModel.js
 *
 * This module contains the API for retrieving and storing data in the
 * current day's food log.
 *
 */

define('foodItemModel', ['db', 'util'], function( db, util ) {

    'use strict';

    var init,
        add,
        set,
        del,
        getAll,
        keys,
        getTotals,
        getTotalPercents,
        sanitize,
        keyPrefix;

    /**
     * Intialize the foodItemModel module
     * @return {void}
     */
    init = function(){
        console.log('init');
        keyPrefix = function() {
            return 'food-items-' + util.ymd();
        };
        keys = ['fat', 'protein', 'carbs'];
    };

    /**
     * Adds a food item to the food log
     * @param {Object} item - object containing fat, protein, carbs
     */
    add = function( item ){
        var items = getAll();

        if ( items === null ) {
            items = [];
        }

        item.id = util.guid();
        items.push(sanitize(item));
        db.set(keyPrefix(), JSON.stringify(items));
    };

    /**
     * Delete an item from the log by id
     * @param  {String} id - globally unique id
     * @return {void}
     */
    del = function( id ){
        var items = getAll();

        items = util.map(items, function( obj ) {
            return obj.id !== id;
        });
        db.set(keyPrefix(), JSON.stringify(items));
    };

    /**
     * Get all items from the food log
     * @return {Object}
     */
    getAll = function(){
        return JSON.parse(db.get(keyPrefix()));
    };

    /**
     * Get macronutrient totals from the food log
     * @return {Object}
     */
    getTotals = function(){
        var items = getAll(),
            keys = ['fat', 'protein', 'carbs'],
            output = {
                fat: 0,
                protein: 0,
                carbs: 0
            };

        keys.forEach(function( key ){
            output[key] = util.reduce(items || [], function( memo, obj ){
                return memo + parseInt(obj[key]);
            }, 0);
        });

        return output;
    };

    /**
     * Get macronutrient total percentages from the food log
     * @return {Object}
     */
    getTotalPercents = function(){
        var totals = getTotals(),
            sumTotal = 0,
            keys = ['fat', 'protein', 'carbs'],
            output = {
                fat: 0,
                protein: 0,
                carbs: 0
            };

        keys.forEach(function( key ){
            sumTotal += totals[key];
        });

        if ( sumTotal === 0 ) {
            return output;
        }

        keys.forEach(function( key ){
            output[key] = parseInt((totals[key]/sumTotal)*100);
        });

        return output;
    };

    /**
     * Remove the `new-food-` string from object keys
     * @param  {Object} obj
     * @return {Object}
     */
    sanitize = function( obj ){
        var key,
            output = {};

        for ( key in obj ) {
            output[key.replace(/new-food-/, '')] = obj[key];
        }

        return output;
    };

    init();

    return {
        add: add,
        set: set,
        del: del,
        getAll: getAll,
        getTotals: getTotals,
        getTotalPercents: getTotalPercents
    };

});
<|endoftext|>


<commit_before>const Module = require('module');
const path = require('path');
const crypto = require('crypto');
const vm = require('vm');

function computeHash(contents) {
  return crypto
    .createHash('sha1')
    .update(contents, 'utf8')
    .digest('hex');
}

class NativeCompileCache {
  constructor() {
    this.cacheStore = null;
    this.previousModuleCompile = null;
  }

  setCacheStore(store) {
    this.cacheStore = store;
  }

  setV8Version(v8Version) {
    this.v8Version = v8Version.toString();
  }

  install() {
    this.savePreviousModuleCompile();
    this.overrideModuleCompile();
  }

  uninstall() {
    this.restorePreviousModuleCompile();
  }

  savePreviousModuleCompile() {
    this.previousModuleCompile = Module.prototype._compile;
  }

  runInThisContext(code, filename) {
    // TodoElectronIssue:  produceCachedData is deprecated after Node 10.6, so we'll
    // will need to update this for Electron v4 to use script.createCachedData().
    const script = new vm.Script(code, { filename, produceCachedData: true });
    return {
      result: script.runInThisContext(),
      cacheBuffer: script.cachedData
    };
  }

  runInThisContextCached(code, filename, cachedData) {
    const script = new vm.Script(code, { filename, cachedData });
    return {
      result: script.runInThisContext(),
      wasRejected: script.cachedDataRejected
    };
  }

  overrideModuleCompile() {
    let self = this;
    // Here we override Node's module.js
    // (https://github.com/atom/node/blob/atom/lib/module.js#L378), changing
    // only the bits that affect compilation in order to use the cached one.
    Module.prototype._compile = function(content, filename) {
      let moduleSelf = this;
      // remove shebang
      content = content.replace(/^#!.*/, '');
      function require(path) {
        return moduleSelf.require(path);
      }
      require.resolve = function(request) {
        return Module._resolveFilename(request, moduleSelf);
      };
      require.main = process.mainModule;

      // Enable support to add extra extension types
      require.extensions = Module._extensions;
      require.cache = Module._cache;

      let dirname = path.dirname(filename);

      // create wrapper function
      let wrapper = Module.wrap(content);

      let cacheKey = computeHash(wrapper + self.v8Version);
      let compiledWrapper = null;
      if (self.cacheStore.has(cacheKey)) {
        let buffer = self.cacheStore.get(cacheKey);
        let compilationResult = self.runInThisContextCached(
          wrapper,
          filename,
          buffer
        );
        compiledWrapper = compilationResult.result;
        if (compilationResult.wasRejected) {
          self.cacheStore.delete(cacheKey);
        }
      } else {
        let compilationResult;
        try {
          compilationResult = self.runInThisContext(wrapper, filename);
        } catch (err) {
          console.error(`Error running script ${filename}`);
          throw err;
        }
        if (compilationResult.cacheBuffer !== null) {
          self.cacheStore.set(cacheKey, compilationResult.cacheBuffer);
        }
        compiledWrapper = compilationResult.result;
      }

      let args = [
        moduleSelf.exports,
        require,
        moduleSelf,
        filename,
        dirname,
        process,
        global,
        Buffer
      ];
      return compiledWrapper.apply(moduleSelf.exports, args);
    };
  }

  restorePreviousModuleCompile() {
    Module.prototype._compile = this.previousModuleCompile;
  }
}

module.exports = new NativeCompileCache();
<commit_msg>Do not store null buffers on the cacheStore<commit_after>const Module = require('module');
const path = require('path');
const crypto = require('crypto');
const vm = require('vm');

function computeHash(contents) {
  return crypto
    .createHash('sha1')
    .update(contents, 'utf8')
    .digest('hex');
}

class NativeCompileCache {
  constructor() {
    this.cacheStore = null;
    this.previousModuleCompile = null;
  }

  setCacheStore(store) {
    this.cacheStore = store;
  }

  setV8Version(v8Version) {
    this.v8Version = v8Version.toString();
  }

  install() {
    this.savePreviousModuleCompile();
    this.overrideModuleCompile();
  }

  uninstall() {
    this.restorePreviousModuleCompile();
  }

  savePreviousModuleCompile() {
    this.previousModuleCompile = Module.prototype._compile;
  }

  runInThisContext(code, filename) {
    // TodoElectronIssue:  produceCachedData is deprecated after Node 10.6, so we'll
    // will need to update this for Electron v4 to use script.createCachedData().
    const script = new vm.Script(code, { filename, produceCachedData: true });
    return {
      result: script.runInThisContext(),
      cacheBuffer: script.cachedDataProduced ? script.cachedData : null
    };
  }

  runInThisContextCached(code, filename, cachedData) {
    const script = new vm.Script(code, { filename, cachedData });
    return {
      result: script.runInThisContext(),
      wasRejected: script.cachedDataRejected
    };
  }

  overrideModuleCompile() {
    let self = this;
    // Here we override Node's module.js
    // (https://github.com/atom/node/blob/atom/lib/module.js#L378), changing
    // only the bits that affect compilation in order to use the cached one.
    Module.prototype._compile = function(content, filename) {
      let moduleSelf = this;
      // remove shebang
      content = content.replace(/^#!.*/, '');
      function require(path) {
        return moduleSelf.require(path);
      }
      require.resolve = function(request) {
        return Module._resolveFilename(request, moduleSelf);
      };
      require.main = process.mainModule;

      // Enable support to add extra extension types
      require.extensions = Module._extensions;
      require.cache = Module._cache;

      let dirname = path.dirname(filename);

      // create wrapper function
      let wrapper = Module.wrap(content);

      let cacheKey = computeHash(wrapper + self.v8Version);
      let compiledWrapper = null;
      if (self.cacheStore.has(cacheKey)) {
        let buffer = self.cacheStore.get(cacheKey);
        let compilationResult = self.runInThisContextCached(
          wrapper,
          filename,
          buffer
        );
        compiledWrapper = compilationResult.result;
        if (compilationResult.wasRejected) {
          self.cacheStore.delete(cacheKey);
        }
      } else {
        let compilationResult;
        try {
          compilationResult = self.runInThisContext(wrapper, filename);
        } catch (err) {
          console.error(`Error running script ${filename}`);
          throw err;
        }
        if (compilationResult.cacheBuffer) {
          self.cacheStore.set(cacheKey, compilationResult.cacheBuffer);
        }
        compiledWrapper = compilationResult.result;
      }

      let args = [
        moduleSelf.exports,
        require,
        moduleSelf,
        filename,
        dirname,
        process,
        global,
        Buffer
      ];
      return compiledWrapper.apply(moduleSelf.exports, args);
    };
  }

  restorePreviousModuleCompile() {
    Module.prototype._compile = this.previousModuleCompile;
  }
}

module.exports = new NativeCompileCache();
<|endoftext|>


<commit_before>import React, { Component, PropTypes } from 'react';

import Checkbox from '../Checkbox/Checkbox';
import noop from '../../../utils/noop';

export default class CheckboxGroup extends Component {
  static propTypes = {
    Input: PropTypes.oneOfType([
      PropTypes.func,
      PropTypes.string,
    ]),
    children: PropTypes.func.isRequired,
    value: PropTypes.arrayOf(PropTypes.string),
    onChange: PropTypes.func,
    optional: PropTypes.bool,
    name: PropTypes.string,
    className: PropTypes.string,
    id: PropTypes.string,
  };

  static defaultProps = {
    onChange: noop,
    Input: Checkbox,
  };

  focus = () => {
    this.input.focus();
  };

  blur = () => {
    this.input.blur();
  };

  handleChange = (e, name, changedValue) => {
    const { value, onChange } = this.props;

    const selected = new Set(value);

    if (selected.has(changedValue)) {
      selected.delete(changedValue);
    } else {
      selected.add(changedValue);
    }

    onChange(e, name, Array.from(selected));
  }

  render() {
    const {
      children,
      Input,
      onChange,
      optional,
      value,
      name,
      className,
      id,
    } = this.props;

    const setInputRef = (() => {
      let called = false;

      return (c) => {
        if (!called) {
          called = true;
          this.input = c;
        }
      };
    })();

    const selected = new Set(value);

    return (

        { children && children((childProps) => {
          const checked = selected.has(childProps.value);

          return (
            <Input
              id={ id }
              name={ name }
              onChange={ onChange }
              checked={ checked }
              required={ !optional }
              ref={ setInputRef }
              { ...childProps }
            />
          );
        }) }

    );
  }
}<commit_msg>Fix `<CheckboxGroup />` not passing the correct `onChange` fn down<commit_after>import React, { Component, PropTypes } from 'react';

import Checkbox from '../Checkbox/Checkbox';
import noop from '../../../utils/noop';

export default class CheckboxGroup extends Component {
  static propTypes = {
    Input: PropTypes.oneOfType([
      PropTypes.func,
      PropTypes.string,
    ]),
    children: PropTypes.func.isRequired,
    value: PropTypes.arrayOf(PropTypes.string),
    onChange: PropTypes.func,
    optional: PropTypes.bool,
    name: PropTypes.string,
    className: PropTypes.string,
    id: PropTypes.string,
  };

  static defaultProps = {
    onChange: noop,
    Input: Checkbox,
  };

  focus = () => {
    this.input.focus();
  };

  blur = () => {
    this.input.blur();
  };

  handleChange = (e, name, changedValue) => {
    const { value, onChange } = this.props;

    const selected = new Set(value);

    if (selected.has(changedValue)) {
      selected.delete(changedValue);
    } else {
      selected.add(changedValue);
    }

    onChange(e, name, Array.from(selected));
  }

  render() {
    const {
      children,
      Input,
      optional,
      value,
      name,
      className,
      id,
    } = this.props;

    const setInputRef = (() => {
      let called = false;

      return (c) => {
        if (!called) {
          called = true;
          this.input = c;
        }
      };
    })();

    const selected = new Set(value);

    return (

        { children && children((childProps) => {
          const checked = selected.has(childProps.value);

          return (
            <Input
              id={ id }
              name={ name }
              onChange={ this.handleChange }
              checked={ checked }
              required={ !optional }
              ref={ setInputRef }
              { ...childProps }
            />
          );
        }) }

    );
  }
}<|endoftext|>


<commit_before>/**
 * The Group Manager provides functions to manage groups in the database
 */

/*
 * 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var ERR = require("async-stacktrace");
var customError = require("../utils/customError");
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
var db = require("./DB").db;
var async = require("async");
var padManager = require("./PadManager");
var sessionManager = require("./SessionManager");

exports.listAllGroups = function(callback) {
  db.get("groups", function (err, groups) {
    if(ERR(err, callback)) return;

    // there are no groups
    if(groups == null) {
      callback(null, {groupIDs: []});
      return;
    }

    var groupIDs = [];
    for ( var groupID in groups ) {
      groupIDs.push(groupID);
    }
    callback(null, {groupIDs: groupIDs});
  });
}

exports.deleteGroup = function(groupID, callback)
{
  var group;

  async.series([
    //ensure group exists
    function (callback)
    {
      //try to get the group entry
      db.get("group:" + groupID, function (err, _group)
      {
        if(ERR(err, callback)) return;

        //group does not exist
        if(_group == null)
        {
          callback(new customError("groupID does not exist","apierror"));
        }
        //group exists, everything is fine
        else
        {
          group = _group;
          callback();
        }
      });
    },
    //iterate trough all pads of this groups and delete them
    function(callback)
    {
      //collect all padIDs in an array, that allows us to use async.forEach
      var padIDs = [];
      for(var i in group.pads)
      {
        padIDs.push(i);
      }

      //loop trough all pads and delete them
      async.forEach(padIDs, function(padID, callback)
      {
        padManager.getPad(padID, function(err, pad)
        {
          if(ERR(err, callback)) return;

          pad.remove(callback);
        });
      }, callback);
    },
    //iterate trough group2sessions and delete all sessions
    function(callback)
    {
      //try to get the group entry
      db.get("group2sessions:" + groupID, function (err, group2sessions)
      {
        if(ERR(err, callback)) return;

        //skip if there is no group2sessions entry
        if(group2sessions == null) {callback(); return}

        //collect all sessions in an array, that allows us to use async.forEach
        var sessions = [];
        for(var i in group2sessions.sessionsIDs)
        {
          sessions.push(i);
        }

        //loop trough all sessions and delete them
        async.forEach(sessions, function(session, callback)
        {
          sessionManager.deleteSession(session, callback);
        }, callback);
      });
    },
    //remove group and group2sessions entry
    function(callback)
    {
      db.remove("group2sessions:" + groupID);
      db.remove("group:" + groupID);
      callback();
    },
    //unlist the group
    function(callback)
    {
      exports.listAllGroups(function(err, groups) {
        if(ERR(err, callback)) return;
        groups = groups? groups.groupIDs : [];

        // it's not listed
        if(groups.indexOf(groupID) == -1) {
          callback();
          return;
        }

        groups.splice(groups.indexOf(groupID), 1);

        // store empty groupe list
        if(groups.length == 0) {
          db.set("groups", {});
          callback();
          return;
        }

        // regenerate group list
        var newGroups = {};
        async.forEach(groups, function(group, cb) {
          newGroups[group] = 1;
          cb();
        },function() {
          db.set("groups", newGroups);
          callback();
        });
      });
    }
  ], function(err)
  {
    if(ERR(err, callback)) return;
    callback();
  });
}

exports.doesGroupExist = function(groupID, callback)
{
  //try to get the group entry
  db.get("group:" + groupID, function (err, group)
  {
    if(ERR(err, callback)) return;
    callback(null, group != null);
  });
}

exports.createGroup = function(callback)
{
  //search for non existing groupID
  var groupID = "g." + randomString(16);

  //create the group
  db.set("group:" + groupID, {pads: {}});

  //list the group
  exports.listAllGroups(function(err, groups) {
    if(ERR(err, callback)) return;
    groups = groups? groups.groupIDs : [];

    groups.push(groupID);

    // regenerate group list
    var newGroups = {};
    async.forEach(groups, function(group, cb) {
      newGroups[group] = 1;
      cb();
    },function() {
      db.set("groups", newGroups);
      callback(null, {groupID: groupID});
    });
  });
}

exports.createGroupIfNotExistsFor = function(groupMapper, callback)
{
  //ensure mapper is optional
  if(typeof groupMapper != "string")
  {
    callback(new customError("groupMapper is no string","apierror"));
    return;
  }

  //try to get a group for this mapper
  db.get("mapper2group:"+groupMapper, function(err, groupID)
  {
    function createGroupForMapper(cb) {
      exports.createGroup(function(err, responseObj)
      {
        if(ERR(err, cb)) return;

        //create the mapper entry for this group
        db.set("mapper2group:"+groupMapper, responseObj.groupID);

        cb(null, responseObj);
      });
    }

    if(ERR(err, callback)) return;

    // there is a group for this mapper
    if(groupID) {
      exports.doesGroupExist(groupID, function(err, exists) {
        if(ERR(err, callback)) return;
        if(exists) return callback(null, {groupID: groupID});

        // hah, the returned group doesn't exist, let's create one
        createGroupForMapper(callback)
      })
    }
    //there is no group for this mapper, let's create a group
    else {
      createGroupForMapper(callback)
    }
  });
}

exports.createGroupPad = function(groupID, padName, text, callback)
{
  //create the padID
  var padID = groupID + "$" + padName;

  async.series([
    //ensure group exists
    function (callback)
    {
      exports.doesGroupExist(groupID, function(err, exists)
      {
        if(ERR(err, callback)) return;

        //group does not exist
        if(exists == false)
        {
          callback(new customError("groupID does not exist","apierror"));
        }
        //group exists, everything is fine
        else
        {
          callback();
        }
      });
    },
    //ensure pad does not exists
    function (callback)
    {
      padManager.doesPadExists(padID, function(err, exists)
      {
        if(ERR(err, callback)) return;

        //pad exists already
        if(exists == true)
        {
          callback(new customError("padName does already exist","apierror"));
        }
        //pad does not exist, everything is fine
        else
        {
          callback();
        }
      });
    },
    //create the pad
    function (callback)
    {
      padManager.getPad(padID, text, function(err)
      {
        if(ERR(err, callback)) return;
        callback();
      });
    },
    //create an entry in the group for this pad
    function (callback)
    {
      db.setSub("group:" + groupID, ["pads", padID], 1);
      callback();
    }
  ], function(err)
  {
    if(ERR(err, callback)) return;
    callback(null, {padID: padID});
  });
}

exports.listPads = function(groupID, callback)
{
  exports.doesGroupExist(groupID, function(err, exists)
  {
    if(ERR(err, callback)) return;

    //group does not exist
    if(exists == false)
    {
      callback(new customError("groupID does not exist","apierror"));
    }
    //group exists, let's get the pads
    else
    {
      db.getSub("group:" + groupID, ["pads"], function(err, result)
      {
        if(ERR(err, callback)) return;
        var pads = [];
        for ( var padId in result ) {
          pads.push(padId);
        }
        callback(null, {padIDs: pads});
      });
    }
  });
}
<commit_msg>db/GroupManager: early return, no functional changes<commit_after>/**
 * The Group Manager provides functions to manage groups in the database
 */

/*
 * 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var ERR = require("async-stacktrace");
var customError = require("../utils/customError");
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
var db = require("./DB").db;
var async = require("async");
var padManager = require("./PadManager");
var sessionManager = require("./SessionManager");

exports.listAllGroups = function(callback) {
  db.get("groups", function (err, groups) {
    if(ERR(err, callback)) return;

    // there are no groups
    if(groups == null) {
      callback(null, {groupIDs: []});
      return;
    }

    var groupIDs = [];
    for ( var groupID in groups ) {
      groupIDs.push(groupID);
    }
    callback(null, {groupIDs: groupIDs});
  });
}

exports.deleteGroup = function(groupID, callback)
{
  var group;

  async.series([
    //ensure group exists
    function (callback)
    {
      //try to get the group entry
      db.get("group:" + groupID, function (err, _group)
      {
        if(ERR(err, callback)) return;

        //group does not exist
        if(_group == null)
        {
          callback(new customError("groupID does not exist","apierror"));
        }
        //group exists, everything is fine
        else
        {
          group = _group;
          callback();
        }
      });
    },
    //iterate trough all pads of this groups and delete them
    function(callback)
    {
      //collect all padIDs in an array, that allows us to use async.forEach
      var padIDs = [];
      for(var i in group.pads)
      {
        padIDs.push(i);
      }

      //loop trough all pads and delete them
      async.forEach(padIDs, function(padID, callback)
      {
        padManager.getPad(padID, function(err, pad)
        {
          if(ERR(err, callback)) return;

          pad.remove(callback);
        });
      }, callback);
    },
    //iterate trough group2sessions and delete all sessions
    function(callback)
    {
      //try to get the group entry
      db.get("group2sessions:" + groupID, function (err, group2sessions)
      {
        if(ERR(err, callback)) return;

        //skip if there is no group2sessions entry
        if(group2sessions == null) {callback(); return}

        //collect all sessions in an array, that allows us to use async.forEach
        var sessions = [];
        for(var i in group2sessions.sessionsIDs)
        {
          sessions.push(i);
        }

        //loop trough all sessions and delete them
        async.forEach(sessions, function(session, callback)
        {
          sessionManager.deleteSession(session, callback);
        }, callback);
      });
    },
    //remove group and group2sessions entry
    function(callback)
    {
      db.remove("group2sessions:" + groupID);
      db.remove("group:" + groupID);
      callback();
    },
    //unlist the group
    function(callback)
    {
      exports.listAllGroups(function(err, groups) {
        if(ERR(err, callback)) return;
        groups = groups? groups.groupIDs : [];

        // it's not listed
        if(groups.indexOf(groupID) == -1) {
          callback();
          return;
        }

        groups.splice(groups.indexOf(groupID), 1);

        // store empty groupe list
        if(groups.length == 0) {
          db.set("groups", {});
          callback();
          return;
        }

        // regenerate group list
        var newGroups = {};
        async.forEach(groups, function(group, cb) {
          newGroups[group] = 1;
          cb();
        },function() {
          db.set("groups", newGroups);
          callback();
        });
      });
    }
  ], function(err)
  {
    if(ERR(err, callback)) return;
    callback();
  });
}

exports.doesGroupExist = function(groupID, callback)
{
  //try to get the group entry
  db.get("group:" + groupID, function (err, group)
  {
    if(ERR(err, callback)) return;
    callback(null, group != null);
  });
}

exports.createGroup = function(callback)
{
  //search for non existing groupID
  var groupID = "g." + randomString(16);

  //create the group
  db.set("group:" + groupID, {pads: {}});

  //list the group
  exports.listAllGroups(function(err, groups) {
    if(ERR(err, callback)) return;
    groups = groups? groups.groupIDs : [];

    groups.push(groupID);

    // regenerate group list
    var newGroups = {};
    async.forEach(groups, function(group, cb) {
      newGroups[group] = 1;
      cb();
    },function() {
      db.set("groups", newGroups);
      callback(null, {groupID: groupID});
    });
  });
}

exports.createGroupIfNotExistsFor = function(groupMapper, callback)
{
  //ensure mapper is optional
  if(typeof groupMapper != "string")
  {
    callback(new customError("groupMapper is no string","apierror"));
    return;
  }

  //try to get a group for this mapper
  db.get("mapper2group:"+groupMapper, function(err, groupID)
  {
    function createGroupForMapper(cb) {
      exports.createGroup(function(err, responseObj)
      {
        if(ERR(err, cb)) return;

        //create the mapper entry for this group
        db.set("mapper2group:"+groupMapper, responseObj.groupID);

        cb(null, responseObj);
      });
    }

    if(ERR(err, callback)) return;

    // there is a group for this mapper
    if(groupID) {
      exports.doesGroupExist(groupID, function(err, exists) {
        if(ERR(err, callback)) return;
        if(exists) return callback(null, {groupID: groupID});

        // hah, the returned group doesn't exist, let's create one
        createGroupForMapper(callback)
      })

      return;
    }

    //there is no group for this mapper, let's create a group
    createGroupForMapper(callback)
  });
}

exports.createGroupPad = function(groupID, padName, text, callback)
{
  //create the padID
  var padID = groupID + "$" + padName;

  async.series([
    //ensure group exists
    function (callback)
    {
      exports.doesGroupExist(groupID, function(err, exists)
      {
        if(ERR(err, callback)) return;

        //group does not exist
        if(exists == false)
        {
          callback(new customError("groupID does not exist","apierror"));
        }
        //group exists, everything is fine
        else
        {
          callback();
        }
      });
    },
    //ensure pad does not exists
    function (callback)
    {
      padManager.doesPadExists(padID, function(err, exists)
      {
        if(ERR(err, callback)) return;

        //pad exists already
        if(exists == true)
        {
          callback(new customError("padName does already exist","apierror"));
        }
        //pad does not exist, everything is fine
        else
        {
          callback();
        }
      });
    },
    //create the pad
    function (callback)
    {
      padManager.getPad(padID, text, function(err)
      {
        if(ERR(err, callback)) return;
        callback();
      });
    },
    //create an entry in the group for this pad
    function (callback)
    {
      db.setSub("group:" + groupID, ["pads", padID], 1);
      callback();
    }
  ], function(err)
  {
    if(ERR(err, callback)) return;
    callback(null, {padID: padID});
  });
}

exports.listPads = function(groupID, callback)
{
  exports.doesGroupExist(groupID, function(err, exists)
  {
    if(ERR(err, callback)) return;

    //group does not exist
    if(exists == false)
    {
      callback(new customError("groupID does not exist","apierror"));
    }
    //group exists, let's get the pads
    else
    {
      db.getSub("group:" + groupID, ["pads"], function(err, result)
      {
        if(ERR(err, callback)) return;
        var pads = [];
        for ( var padId in result ) {
          pads.push(padId);
        }
        callback(null, {padIDs: pads});
      });
    }
  });
}
<|endoftext|>