<commit_before>const { authenticate } = require('@feathersjs/authentication');
const { Forbidden, GeneralError, BadRequest, Conflict } = require('@feathersjs/errors');
const {	iff, isProvider } = require('feathers-hooks-common');

const globalHooks = require('../../../hooks');
const { equal: equalIds } = require('../../../helper/compare').ObjectId;

const filterRequestedSubmissions = async (context) => {
	const hasSchoolView = await globalHooks.hasPermissionNoHook(
		context, context.params.account.userId, 'SUBMISSIONS_SCHOOL_VIEW',
	);
	if (!hasSchoolView) {
		// todo: see team submissions
		context.params.query.studentId = context.params.account.userId;
	}
	return context;
};

const stringifyUserId = (context) => {
	if ((context.params.account || {}).userId) {
		context.params.account.userId = context.params.account.userId.toString();
	}
	return Promise.resolve(context);
};

const insertSubmissionData = (context) => {
	const submissionId = context.id || context.data.submissionId;
	if (submissionId) {
		const submissionService = context.app.service('/submissions');
		return submissionService.get(submissionId, { account: { userId: context.params.account.userId } })
			.then((submission) => {
				context.data = {
					...context.data,
					submission,
				};
				context.data = JSON.parse(JSON.stringify(context.data));
				context.data.isTeamMember = false;
				context.data.isOwner = false;
				if (context.data.submission.studentId === context.params.account.userId) {
					context.data.isOwner = true;
					context.data.isTeamMember = true;
				}

				if ((context.data.submission.teamMembers || []).includes(context.params.account.userId)) {
					context.data.isTeamMember = true;
				}

				if (context.data.submission.courseGroupId || context.data.courseGroupId) {
					context.data.isTeamMember = true;
					(context.data.submission.teamMembers || []).push(context.params.account.userId);
				}

				return Promise.resolve(context);
			})
			.catch(() => Promise.reject(new GeneralError({ message: "can't reach submission service" })));
	}
	return Promise.resolve(context);
};

const insertHomeworkData = (context) => {
	const homeworkId = context.data.homeworkId || (context.data.submission || {}).homeworkId;
	if (homeworkId) {
		const userId = context.params.account.userId.toString();
		const homeworkService = context.app.service('/homework');
		return homeworkService.get(homeworkId, {
			account: { userId },
			query: {
				$populate: ['courseId'],
			},
		})
			.then((homework) => {
				context.data.homework = homework;
				// isTeacher?
				context.data.isTeacher = false;
				if ((equalIds(context.data.homework.teacherId, userId))
					|| (context.data.homework.courseId.teacherIds || [])
						.some((teacherId) => equalIds(teacherId, userId))
					|| (context.data.homework.courseId.substitutionIds || [])
						.some((subsId) => equalIds(subsId, userId))
				) {
					context.data.isTeacher = true;
				}
				return Promise.resolve(context);
			})
			.catch(() => Promise.reject(new GeneralError({ message: "can't reach homework service" })));
	}
	return Promise.reject(new BadRequest());
};

const insertSubmissionsData = (context) => {
// get all the submissions for the homework
	const submissionService = context.app.service('/submissions');
	return submissionService.find({
		query: {
			homeworkId: context.data.homeworkId,
			$populate: ['studentId'],
		},
	}).then((submissions) => {
		context.data.submissions = submissions.data;
		return Promise.resolve(context);
	})
		.catch(() => Promise.reject(new GeneralError({ message: "can't reach submission service" })));
};

const preventNoTeamMember = (context) => {
	if (!(context.data.submission || {}).teamMembers) {
		context.data.teamMembers = [context.params.account.userId];
	}
	if (context.method == 'update' && (!context.data.teamMembers || context.data.teamMembers.length == 0)) {
		return Promise.reject(new Conflict({
			message: 'Abgaben ohne TeamMember sind nicht erlaubt!',
		}));
	}
	return Promise.resolve(context);
};

const setTeamMembers = (context) => {
	if (!context.data.teamMembers) { // if student (no grading) is going to submit without teamMembers set
		context.data.teamMembers = [context.params.account.userId];
	}
	if (!context.data.teamMembers.includes(context.params.account.userId)) {
		return Promise.reject(new Conflict({
			message: 'Du kannst nicht ausschließlich für andere Abgeben. Füge dich selbst zur Abgabe hinzu!',
		}));
	}
};

const noSubmissionBefore = (context) => {
// check that no one has already submitted for the current User
	const submissionsForMe = context.data.submissions.filter((submission) => // is there an submission for the current user?
		(submission.teamMembers.includes(context.params.account.userId))
		|| ((submission.studentId || {})._id == context.params.account.userId));
	if (submissionsForMe.length > 0) {
		return Promise.reject(new Conflict({
			message: `${submissionsForMe[0].studentId.firstName} ${submissionsForMe[0].studentId.lastName} hat bereits für dich abgegeben!`,
		}));
	}
	return Promise.resolve(context);
};

const noDuplicateSubmissionForTeamMembers = (context) => {
	if (!context.data.isTeacher && context.data.teamMembers) {
		// check if a teamMember submitted a solution on his own => display names
		let newTeamMembers = context.data.teamMembers;
		if (context.data.submission) {
			newTeamMembers = newTeamMembers.filter((teamMember) => !context.data.submission.teamMembers.includes(teamMember.toString()));
		}

		let toRemove = '';
		const submissionsForTeamMembers = context.data.submissions.filter((submission) => {
			for (let i = 0; i < newTeamMembers.length; i += 1) {
				const teamMember = newTeamMembers[i].toString();
				if (submission.teamMembers.includes(teamMember)
						|| (((submission.studentId || {})._id || {}).toString() == teamMember)
				) {
					toRemove += (toRemove == '') ? '' : ', ';
					toRemove += `${submission.studentId.firstName} ${submission.studentId.lastName}`;
					return true;
				}
			}
			return false;
		});
		if (submissionsForTeamMembers.length > 0) {
			return Promise.reject(new Conflict({
				message: `${toRemove + ((submissionsForTeamMembers.length == 1) ? ' hat' : ' haben')} bereits eine Lösung abgegeben! Entferne diese${(submissionsForTeamMembers.length == 1) ? 's Mitglied!' : ' Mitglieder!'}`,
			}));
		}
		return Promise.resolve(context);
	}
};

const populateCourseGroup = (context) => {
	if (!context.data.courseGroupId) {
		return Promise.resolve(context);
	}

	return context.app.service('/courseGroups/').get(context.data.courseGroupId).then((courseGroup) => {
		context.courseGroupTemp = courseGroup;
		return Promise.resolve(context);
	});
};

const maxTeamMembers = (context) => {
	if (!context.data.isTeacher && context.data.homework.teamSubmissions) {
		if (context.data.homework.maxTeamMembers) {
			// NOTE the following conditional is a bit hard to understand. To prevent side effects, I added a pre-conditional above.
			if ((context.data.homework.maxTeamMembers || 0) >= 1
				&& ((context.data.teamMembers || []).length > context.data.homework.maxTeamMembers)
				|| (context.courseGroupTemp && (context.courseGroupTemp.userIds || []).length > context.data.homework.maxTeamMembers)) {
				return Promise.reject(new Conflict({
					message: `Dein Team ist größer als erlaubt! ( maximal ${context.data.homework.maxTeamMembers} Teammitglieder erlaubt)`,
				}));
			}
		}
	} else if ((context.data.teamMembers || []).length > 1) {
		return Promise.reject(new Conflict({
			message: 'Teamabgaben sind nicht erlaubt!',
		}));
	}
	return Promise.resolve(context);
};

const canRemoveOwner = (context) => {
	if (context.data.teamMembers
&& !context.data.teamMembers.includes(context.data.submission.studentId)
&& !context.data.courseGroupId) {
		if (context.data.isOwner) {
			return Promise.reject(new Conflict({
				message: 'Du hast diese Abgabe erstellt. Du darfst dich nicht selbst von dieser löschen!',
			}));
		}
		return Promise.reject(new Conflict({
			message: 'Du darfst den Ersteller nicht von der Abgabe löschen!',
		}));
	}
	return Promise.resolve(context);
};

const canGrade = (context) => {
	if (!context.data.isTeacher
&& (Number.isInteger(context.data.grade) || typeof context.data.gradeComment === 'string')) { // students try to grade? BLOCK!
		return Promise.reject(new Forbidden());
	}
	return Promise.resolve(context);
};

const hasEditPermission = (context) => {
// may check that the teacher can't edit the submission itself (only grading allowed)
	if (context.data.isTeamMember || context.data.isTeacher) {
		return Promise.resolve(context);
	}
	return Promise.reject(new Forbidden());
};

const hasDeletePermission = (context) => {
	if (context.data.isTeamMember) {
		return Promise.resolve(context);
	}
	return Promise.reject(new Forbidden());
};

const hasViewPermission = async (context, submission, currentUserId) => {
	// user is submitter
	const submissionUserId = submission.studentId.toString();
	if (equalIds(submission.studentId, currentUserId) {
		return submission;
	}

	// user is team member (should only be available if team work for this homework is enabled)
	const teamMemberIds = submission.teamMembers ? submission.teamMembers.map((m) => m.toString()) : [];
	if (teamMemberIds.includes(currentUserId)) {
		return submission;
	}

	try {
		const homeworkService = context.app.service('/homework');
		const homework = await homeworkService.get(submission.homeworkId);
		// is private teacher homework
		if (equalIds(homework.teacherId, currentUserId)) {
			return submission;
		}

		const courseService = context.app.service('/courses');
		const course = await courseService.get(homework.courseId);
		const teacherIds = course.teacherIds.map((t) => t.toString());
		const substitutionIds = (course.substitutionIds || []).map((s) => s.toString())

		// user is course or substitution teacher
		if (teacherIds.includes(currentUserId) || substitutionIds.includes(currentUserId)) {
			return submission;
		}

		// user is course student and submissions are public
		const courseStudentIds = course.userIds.map((u) => u.toString());
		if (courseStudentIds.includes(currentUserId) && homework.publicSubmissions) {
			return submission;
		}

		// user is part of a courseGroup
		if (submission.courseGroupId) {
			const courseGroupService = context.app.service('/courseGroups');
			const courseGroup = await courseGroupService.get(submission.courseGroupId);
			const courseGroupStudents = courseGroup.userIds.map((u) => u.toString());
			if (courseGroupStudents.includes(currentUserId)) {
				return submission;
			}
		}
	} catch (err) {
		return Promise.reject(new GeneralError({ message: "can't reach homework service" }));
	}

	// user is someone else and not authorized
	return Promise.resolve();
};

const checkGetSubmissionViewPermission = async (context) => {
	const currentUserId = context.params.account.userId;

	const hasViewPermissionResult = await hasViewPermission(context, context.result, currentUserId);
	if (hasViewPermissionResult) {
		return context;
	}
	// user is someone else and not authorized
	throw new Forbidden();
};

const filterApplicableSubmissions = async (context) => {
	const currentUserId = context.params.account.userId;
	const submissions = context.result.data;

	const filterSubmission = await Promise.all(submissions.map((submission)=> hasViewPermission(context, submission, currentUserId)));
	const result = submissions.filter((_v, index) => filterSubmission[index]);

	context.result.total = result.length;
	context.result.data = result;
	return Promise.resolve(context);
};

exports.before = () => ({
	all: [authenticate('jwt'), stringifyUserId],
	find: [
		iff(isProvider('external'), globalHooks.restrictToCurrentSchool),
		globalHooks.hasPermission('SUBMISSIONS_VIEW'),
		iff(isProvider('external'), filterRequestedSubmissions),
		globalHooks.mapPaginationQuery.bind(this),
	],
	get: [globalHooks.hasPermission('SUBMISSIONS_VIEW')],
	create: [
		globalHooks.hasPermission('SUBMISSIONS_CREATE'),
		insertHomeworkData, insertSubmissionsData,
		setTeamMembers,
		noSubmissionBefore,
		noDuplicateSubmissionForTeamMembers,
		populateCourseGroup,
		maxTeamMembers,
		canGrade,
	],
	update: [
		globalHooks.hasPermission('SUBMISSIONS_EDIT'),
		insertSubmissionData,
		insertHomeworkData,
		insertSubmissionsData,
		hasEditPermission,
		preventNoTeamMember,
		canRemoveOwner,
		noDuplicateSubmissionForTeamMembers,
		populateCourseGroup,
		maxTeamMembers,
		canGrade],
	patch: [
		globalHooks.hasPermission('SUBMISSIONS_EDIT'),
		insertSubmissionData,
		insertHomeworkData,
		insertSubmissionsData,
		hasEditPermission,
		preventNoTeamMember,
		canRemoveOwner,
		noDuplicateSubmissionForTeamMembers,
		populateCourseGroup,
		maxTeamMembers,
		globalHooks.permitGroupOperation,
		canGrade,
	],
	remove: [
		globalHooks.hasPermission('SUBMISSIONS_CREATE'),
		insertSubmissionData,
		insertHomeworkData,
		insertSubmissionsData,
		globalHooks.permitGroupOperation,
		hasDeletePermission,
	],
});

exports.after = {
	all: [],
	find: [filterApplicableSubmissions],
	get: [checkGetSubmissionViewPermission],
	create: [],
	update: [],
	patch: [],
	remove: [],
};
<commit_msg>Revert "SC-4308 remove extern check"<commit_after>const { authenticate } = require('@feathersjs/authentication');
const { Forbidden, GeneralError, BadRequest, Conflict } = require('@feathersjs/errors');
const {	iff, isProvider } = require('feathers-hooks-common');

const globalHooks = require('../../../hooks');
const { equal: equalIds } = require('../../../helper/compare').ObjectId;

const filterRequestedSubmissions = async (context) => {
	const hasSchoolView = await globalHooks.hasPermissionNoHook(
		context, context.params.account.userId, 'SUBMISSIONS_SCHOOL_VIEW',
	);
	if (!hasSchoolView) {
		// todo: see team submissions
		context.params.query.studentId = context.params.account.userId;
	}
	return context;
};

const stringifyUserId = (context) => {
	if ((context.params.account || {}).userId) {
		context.params.account.userId = context.params.account.userId.toString();
	}
	return Promise.resolve(context);
};

const insertSubmissionData = (context) => {
	const submissionId = context.id || context.data.submissionId;
	if (submissionId) {
		const submissionService = context.app.service('/submissions');
		return submissionService.get(submissionId, { account: { userId: context.params.account.userId } })
			.then((submission) => {
				context.data = {
					...context.data,
					submission,
				};
				context.data = JSON.parse(JSON.stringify(context.data));
				context.data.isTeamMember = false;
				context.data.isOwner = false;
				if (context.data.submission.studentId === context.params.account.userId) {
					context.data.isOwner = true;
					context.data.isTeamMember = true;
				}

				if ((context.data.submission.teamMembers || []).includes(context.params.account.userId)) {
					context.data.isTeamMember = true;
				}

				if (context.data.submission.courseGroupId || context.data.courseGroupId) {
					context.data.isTeamMember = true;
					(context.data.submission.teamMembers || []).push(context.params.account.userId);
				}

				return Promise.resolve(context);
			})
			.catch(() => Promise.reject(new GeneralError({ message: "can't reach submission service" })));
	}
	return Promise.resolve(context);
};

const insertHomeworkData = (context) => {
	const homeworkId = context.data.homeworkId || (context.data.submission || {}).homeworkId;
	if (homeworkId) {
		const userId = context.params.account.userId.toString();
		const homeworkService = context.app.service('/homework');
		return homeworkService.get(homeworkId, {
			account: { userId },
			query: {
				$populate: ['courseId'],
			},
		})
			.then((homework) => {
				context.data.homework = homework;
				// isTeacher?
				context.data.isTeacher = false;
				if ((equalIds(context.data.homework.teacherId, userId))
					|| (context.data.homework.courseId.teacherIds || [])
						.some((teacherId) => equalIds(teacherId, userId))
					|| (context.data.homework.courseId.substitutionIds || [])
						.some((subsId) => equalIds(subsId, userId))
				) {
					context.data.isTeacher = true;
				}
				return Promise.resolve(context);
			})
			.catch(() => Promise.reject(new GeneralError({ message: "can't reach homework service" })));
	}
	return Promise.reject(new BadRequest());
};

const insertSubmissionsData = (context) => {
// get all the submissions for the homework
	const submissionService = context.app.service('/submissions');
	return submissionService.find({
		query: {
			homeworkId: context.data.homeworkId,
			$populate: ['studentId'],
		},
	}).then((submissions) => {
		context.data.submissions = submissions.data;
		return Promise.resolve(context);
	})
		.catch(() => Promise.reject(new GeneralError({ message: "can't reach submission service" })));
};

const preventNoTeamMember = (context) => {
	if (!(context.data.submission || {}).teamMembers) {
		context.data.teamMembers = [context.params.account.userId];
	}
	if (context.method == 'update' && (!context.data.teamMembers || context.data.teamMembers.length == 0)) {
		return Promise.reject(new Conflict({
			message: 'Abgaben ohne TeamMember sind nicht erlaubt!',
		}));
	}
	return Promise.resolve(context);
};

const setTeamMembers = (context) => {
	if (!context.data.teamMembers) { // if student (no grading) is going to submit without teamMembers set
		context.data.teamMembers = [context.params.account.userId];
	}
	if (!context.data.teamMembers.includes(context.params.account.userId)) {
		return Promise.reject(new Conflict({
			message: 'Du kannst nicht ausschließlich für andere Abgeben. Füge dich selbst zur Abgabe hinzu!',
		}));
	}
};

const noSubmissionBefore = (context) => {
// check that no one has already submitted for the current User
	const submissionsForMe = context.data.submissions.filter((submission) => // is there an submission for the current user?
		(submission.teamMembers.includes(context.params.account.userId))
		|| ((submission.studentId || {})._id == context.params.account.userId));
	if (submissionsForMe.length > 0) {
		return Promise.reject(new Conflict({
			message: `${submissionsForMe[0].studentId.firstName} ${submissionsForMe[0].studentId.lastName} hat bereits für dich abgegeben!`,
		}));
	}
	return Promise.resolve(context);
};

const noDuplicateSubmissionForTeamMembers = (context) => {
	if (!context.data.isTeacher && context.data.teamMembers) {
		// check if a teamMember submitted a solution on his own => display names
		let newTeamMembers = context.data.teamMembers;
		if (context.data.submission) {
			newTeamMembers = newTeamMembers.filter((teamMember) => !context.data.submission.teamMembers.includes(teamMember.toString()));
		}

		let toRemove = '';
		const submissionsForTeamMembers = context.data.submissions.filter((submission) => {
			for (let i = 0; i < newTeamMembers.length; i += 1) {
				const teamMember = newTeamMembers[i].toString();
				if (submission.teamMembers.includes(teamMember)
						|| (((submission.studentId || {})._id || {}).toString() == teamMember)
				) {
					toRemove += (toRemove == '') ? '' : ', ';
					toRemove += `${submission.studentId.firstName} ${submission.studentId.lastName}`;
					return true;
				}
			}
			return false;
		});
		if (submissionsForTeamMembers.length > 0) {
			return Promise.reject(new Conflict({
				message: `${toRemove + ((submissionsForTeamMembers.length == 1) ? ' hat' : ' haben')} bereits eine Lösung abgegeben! Entferne diese${(submissionsForTeamMembers.length == 1) ? 's Mitglied!' : ' Mitglieder!'}`,
			}));
		}
		return Promise.resolve(context);
	}
};

const populateCourseGroup = (context) => {
	if (!context.data.courseGroupId) {
		return Promise.resolve(context);
	}

	return context.app.service('/courseGroups/').get(context.data.courseGroupId).then((courseGroup) => {
		context.courseGroupTemp = courseGroup;
		return Promise.resolve(context);
	});
};

const maxTeamMembers = (context) => {
	if (!context.data.isTeacher && context.data.homework.teamSubmissions) {
		if (context.data.homework.maxTeamMembers) {
			// NOTE the following conditional is a bit hard to understand. To prevent side effects, I added a pre-conditional above.
			if ((context.data.homework.maxTeamMembers || 0) >= 1
				&& ((context.data.teamMembers || []).length > context.data.homework.maxTeamMembers)
				|| (context.courseGroupTemp && (context.courseGroupTemp.userIds || []).length > context.data.homework.maxTeamMembers)) {
				return Promise.reject(new Conflict({
					message: `Dein Team ist größer als erlaubt! ( maximal ${context.data.homework.maxTeamMembers} Teammitglieder erlaubt)`,
				}));
			}
		}
	} else if ((context.data.teamMembers || []).length > 1) {
		return Promise.reject(new Conflict({
			message: 'Teamabgaben sind nicht erlaubt!',
		}));
	}
	return Promise.resolve(context);
};

const canRemoveOwner = (context) => {
	if (context.data.teamMembers
&& !context.data.teamMembers.includes(context.data.submission.studentId)
&& !context.data.courseGroupId) {
		if (context.data.isOwner) {
			return Promise.reject(new Conflict({
				message: 'Du hast diese Abgabe erstellt. Du darfst dich nicht selbst von dieser löschen!',
			}));
		}
		return Promise.reject(new Conflict({
			message: 'Du darfst den Ersteller nicht von der Abgabe löschen!',
		}));
	}
	return Promise.resolve(context);
};

const canGrade = (context) => {
	if (!context.data.isTeacher
&& (Number.isInteger(context.data.grade) || typeof context.data.gradeComment === 'string')) { // students try to grade? BLOCK!
		return Promise.reject(new Forbidden());
	}
	return Promise.resolve(context);
};

const hasEditPermission = (context) => {
// may check that the teacher can't edit the submission itself (only grading allowed)
	if (context.data.isTeamMember || context.data.isTeacher) {
		return Promise.resolve(context);
	}
	return Promise.reject(new Forbidden());
};

const hasDeletePermission = (context) => {
	if (context.data.isTeamMember) {
		return Promise.resolve(context);
	}
	return Promise.reject(new Forbidden());
};

const hasViewPermission = async (context, submission, currentUserId) => {
	// user is submitter
	const submissionUserId = submission.studentId.toString();
	if (equalIds(submission.studentId, currentUserId) {
		return submission;
	}

	// user is team member (should only be available if team work for this homework is enabled)
	const teamMemberIds = submission.teamMembers ? submission.teamMembers.map((m) => m.toString()) : [];
	if (teamMemberIds.includes(currentUserId)) {
		return submission;
	}

	try {
		const homeworkService = context.app.service('/homework');
		const homework = await homeworkService.get(submission.homeworkId);
		// is private teacher homework
		if (equalIds(homework.teacherId, currentUserId)) {
			return submission;
		}

		const courseService = context.app.service('/courses');
		const course = await courseService.get(homework.courseId);
		const teacherIds = course.teacherIds.map((t) => t.toString());
		const substitutionIds = (course.substitutionIds || []).map((s) => s.toString())

		// user is course or substitution teacher
		if (teacherIds.includes(currentUserId) || substitutionIds.includes(currentUserId)) {
			return submission;
		}

		// user is course student and submissions are public
		const courseStudentIds = course.userIds.map((u) => u.toString());
		if (courseStudentIds.includes(currentUserId) && homework.publicSubmissions) {
			return submission;
		}

		// user is part of a courseGroup
		if (submission.courseGroupId) {
			const courseGroupService = context.app.service('/courseGroups');
			const courseGroup = await courseGroupService.get(submission.courseGroupId);
			const courseGroupStudents = courseGroup.userIds.map((u) => u.toString());
			if (courseGroupStudents.includes(currentUserId)) {
				return submission;
			}
		}
	} catch (err) {
		return Promise.reject(new GeneralError({ message: "can't reach homework service" }));
	}

	// user is someone else and not authorized
	return Promise.resolve();
};

const checkGetSubmissionViewPermission = async (context) => {
	const currentUserId = context.params.account.userId;

	const hasViewPermissionResult = await hasViewPermission(context, context.result, currentUserId);
	if (hasViewPermissionResult) {
		return context;
	}
	// user is someone else and not authorized
	throw new Forbidden();
};

const filterApplicableSubmissions = async (context) => {
	const currentUserId = context.params.account.userId;
	const submissions = context.result.data;

	const filterSubmission = await Promise.all(submissions.map((submission)=> hasViewPermission(context, submission, currentUserId)));
	const result = submissions.filter((_v, index) => filterSubmission[index]);

	context.result.total = result.length;
	context.result.data = result;
	return Promise.resolve(context);
};

exports.before = () => ({
	all: [authenticate('jwt'), stringifyUserId],
	find: [
		iff(isProvider('external'), globalHooks.restrictToCurrentSchool),
		globalHooks.hasPermission('SUBMISSIONS_VIEW'),
		iff(isProvider('external'), filterRequestedSubmissions),
		globalHooks.mapPaginationQuery.bind(this),
	],
	get: [globalHooks.hasPermission('SUBMISSIONS_VIEW')],
	create: [
		globalHooks.hasPermission('SUBMISSIONS_CREATE'),
		insertHomeworkData, insertSubmissionsData,
		setTeamMembers,
		noSubmissionBefore,
		noDuplicateSubmissionForTeamMembers,
		populateCourseGroup,
		maxTeamMembers,
		canGrade,
	],
	update: [
		globalHooks.hasPermission('SUBMISSIONS_EDIT'),
		insertSubmissionData,
		insertHomeworkData,
		insertSubmissionsData,
		hasEditPermission,
		preventNoTeamMember,
		canRemoveOwner,
		noDuplicateSubmissionForTeamMembers,
		populateCourseGroup,
		maxTeamMembers,
		canGrade],
	patch: [
		globalHooks.hasPermission('SUBMISSIONS_EDIT'),
		insertSubmissionData,
		insertHomeworkData,
		insertSubmissionsData,
		hasEditPermission,
		preventNoTeamMember,
		canRemoveOwner,
		noDuplicateSubmissionForTeamMembers,
		populateCourseGroup,
		maxTeamMembers,
		globalHooks.permitGroupOperation,
		canGrade,
	],
	remove: [
		globalHooks.hasPermission('SUBMISSIONS_CREATE'),
		insertSubmissionData,
		insertHomeworkData,
		insertSubmissionsData,
		globalHooks.permitGroupOperation,
		hasDeletePermission,
	],
});

exports.after = {
	all: [],
	find: [iff(isProvider('external'), filterApplicableSubmissions)],
	get: [checkGetSubmissionViewPermission],
	create: [],
	update: [],
	patch: [],
	remove: [],
};
<|endoftext|>


<commit_before>/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @author       Pavle Goloskokovic <pgoloskokovic@gmail.com> (http://prunegames.com)
 * @copyright    2019 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

var Base64ToArrayBuffer = require('../../utils/base64/Base64ToArrayBuffer');
var BaseSoundManager = require('../BaseSoundManager');
var Class = require('../../utils/Class');
var Events = require('../events');
var WebAudioSound = require('./WebAudioSound');

/**
 * @classdesc
 * Web Audio API implementation of the sound manager.
 *
 * @class WebAudioSoundManager
 * @extends Phaser.Sound.BaseSoundManager
 * @memberof Phaser.Sound
 * @constructor
 * @since 3.0.0
 *
 * @param {Phaser.Game} game - Reference to the current game instance.
 */
var WebAudioSoundManager = new Class({

    Extends: BaseSoundManager,

    initialize:

    function WebAudioSoundManager (game)
    {
        /**
         * The AudioContext being used for playback.
         *
         * @name Phaser.Sound.WebAudioSoundManager#context
         * @type {AudioContext}
         * @private
         * @since 3.0.0
         */
        this.context = this.createAudioContext(game);

        /**
         * Gain node responsible for controlling global muting.
         *
         * @name Phaser.Sound.WebAudioSoundManager#masterMuteNode
         * @type {GainNode}
         * @private
         * @since 3.0.0
         */
        this.masterMuteNode = this.context.createGain();

        /**
         * Gain node responsible for controlling global volume.
         *
         * @name Phaser.Sound.WebAudioSoundManager#masterVolumeNode
         * @type {GainNode}
         * @private
         * @since 3.0.0
         */
        this.masterVolumeNode = this.context.createGain();

        this.masterMuteNode.connect(this.masterVolumeNode);

        this.masterVolumeNode.connect(this.context.destination);

        /**
         * Destination node for connecting individual sounds to.
         *
         * @name Phaser.Sound.WebAudioSoundManager#destination
         * @type {AudioNode}
         * @private
         * @since 3.0.0
         */
        this.destination = this.masterMuteNode;

        this.locked = this.context.state === 'suspended' && ('ontouchstart' in window || 'onclick' in window);

        BaseSoundManager.call(this, game);

        if (this.locked)
        {
            this.unlock();
        }
    },

    /**
     * Method responsible for instantiating and returning AudioContext instance.
     * If an instance of an AudioContext class was provided through the game config,
     * that instance will be returned instead. This can come in handy if you are reloading
     * a Phaser game on a page that never properly refreshes (such as in an SPA project)
     * and you want to reuse already instantiated AudioContext.
     *
     * @method Phaser.Sound.WebAudioSoundManager#createAudioContext
     * @private
     * @since 3.0.0
     *
     * @param {Phaser.Game} game - Reference to the current game instance.
     *
     * @return {AudioContext} The AudioContext instance to be used for playback.
     */
    createAudioContext: function (game)
    {
        var audioConfig = game.config.audio;

        if (audioConfig && audioConfig.context)
        {
            audioConfig.context.resume();

            return audioConfig.context;
        }

        return new AudioContext();
    },

    /**
     * Adds a new sound into the sound manager.
     *
     * @method Phaser.Sound.WebAudioSoundManager#add
     * @since 3.0.0
     *
     * @param {string} key - Asset key for the sound.
     * @param {Phaser.Types.Sound.SoundConfig} [config] - An optional config object containing default sound settings.
     *
     * @return {Phaser.Sound.WebAudioSound} The new sound instance.
     */
    add: function (key, config)
    {
        var sound = new WebAudioSound(this, key, config);

        this.sounds.push(sound);

        return sound;
    },

    /**
     * Decode audio data into a format ready for playback via Web Audio.
     *
     * The audio data can be a base64 encoded string, an audio media-type data uri, or an ArrayBuffer instance.
     *
     * The `audioKey` is the key that will be used to save the decoded audio to the audio cache.
     *
     * Instead of passing a single entry you can instead pass an array of `Phaser.Types.Sound.DecodeAudioConfig`
     * objects as the first and only argument.
     *
     * Decoding is an async process, so be sure to listen for the events to know when decoding has completed.
     *
     * Once the audio has decoded it can be added to the Sound Manager or played via its key.
     *
     * @method Phaser.Sound.WebAudioSoundManager#decodeAudio
     * @fires Phaser.Sound.Events#DECODED
     * @fires Phaser.Sound.Events#DECODED_ALL
     * @since 3.18.0
     *
     * @param {(Phaser.Types.Sound.DecodeAudioConfig[]|string)} [audioKey] - The string-based key to be used to reference the decoded audio in the audio cache, or an array of audio config objects.
     * @param {(ArrayBuffer|string)} [audioData] - The audio data, either a base64 encoded string, an audio media-type data uri, or an ArrayBuffer instance.
     */
    decodeAudio: function (audioKey, audioData)
    {
        var audioFiles;

        if (!Array.isArray(audioKey))
        {
            audioFiles = [ { key: audioKey, data: audioData } ];
        }
        else
        {
            audioFiles = audioKey;
        }

        var cache = this.game.cache.audio;
        var remaining = audioFiles.length;

        for (var i = 0; i < audioFiles.length; i++)
        {
            var entry = audioFiles[i];

            var key = entry.key;
            var data = entry.data;

            if (typeof data === 'string')
            {
                data = Base64ToArrayBuffer(data);
            }

            var success = function (key, audioBuffer)
            {
                cache.add(key, audioBuffer);

                this.emit(Events.DECODED, key);

                remaining--;

                if (remaining === 0)
                {
                    this.emit(Events.DECODED_ALL);
                }
            }.bind(this, key);

            var failure = function (key, error)
            {
                //  eslint-disable-next-line no-console
                console.error('Error decoding audio: ' + key + ' - ', error ? error.message : '');

                remaining--;

                if (remaining === 0)
                {
                    this.emit(Events.DECODED_ALL);
                }
            }.bind(this, key);

            this.context.decodeAudioData(data, success, failure);
        }
    },

    /**
     * Unlocks Web Audio API on the initial input event.
     *
     * Read more about how this issue is handled here in [this article](https://medium.com/@pgoloskokovic/unlocking-web-audio-the-smarter-way-8858218c0e09).
     *
     * @method Phaser.Sound.WebAudioSoundManager#unlock
     * @since 3.0.0
     */
    unlock: function ()
    {
        var _this = this;

        var unlockHandler = function unlockHandler ()
        {
            if (_this.context)
            {
                _this.context.resume().then(function ()
                {
                    document.body.removeEventListener('touchstart', unlockHandler);
                    document.body.removeEventListener('touchend', unlockHandler);
                    document.body.removeEventListener('click', unlockHandler);

                    _this.unlocked = true;
                }, function ()
                {
                    document.body.removeEventListener('touchstart', unlockHandler);
                    document.body.removeEventListener('touchend', unlockHandler);
                    document.body.removeEventListener('click', unlockHandler);
                });
            }
        };

        if (document.body)
        {
            document.body.addEventListener('touchstart', unlockHandler, false);
            document.body.addEventListener('touchend', unlockHandler, false);
            document.body.addEventListener('click', unlockHandler, false);
        }
    },

    /**
     * Method used internally for pausing sound manager if
     * Phaser.Sound.WebAudioSoundManager#pauseOnBlur is set to true.
     *
     * @method Phaser.Sound.WebAudioSoundManager#onBlur
     * @protected
     * @since 3.0.0
     */
    onBlur: function ()
    {
        if (!this.locked)
        {
            this.context.suspend();
        }
    },

    /**
     * Method used internally for resuming sound manager if
     * Phaser.Sound.WebAudioSoundManager#pauseOnBlur is set to true.
     *
     * @method Phaser.Sound.WebAudioSoundManager#onFocus
     * @protected
     * @since 3.0.0
     */
    onFocus: function ()
    {
        if (!this.locked)
        {
            this.context.resume();
        }
    },

    /**
     * Calls Phaser.Sound.BaseSoundManager#destroy method
     * and cleans up all Web Audio API related stuff.
     *
     * @method Phaser.Sound.WebAudioSoundManager#destroy
     * @since 3.0.0
     */
    destroy: function ()
    {
        this.destination = null;
        this.masterVolumeNode.disconnect();
        this.masterVolumeNode = null;
        this.masterMuteNode.disconnect();
        this.masterMuteNode = null;

        if (this.game.config.audio && this.game.config.audio.context)
        {
            this.context.suspend();
        }
        else
        {
            var _this = this;

            this.context.close().then(function ()
            {

                _this.context = null;

            });
        }

        BaseSoundManager.prototype.destroy.call(this);
    },

    /**
     * Sets the muted state of all this Sound Manager.
     *
     * @method Phaser.Sound.WebAudioSoundManager#setMute
     * @fires Phaser.Sound.Events#GLOBAL_MUTE
     * @since 3.3.0
     *
     * @param {boolean} value - `true` to mute all sounds, `false` to unmute them.
     *
     * @return {Phaser.Sound.WebAudioSoundManager} This Sound Manager.
     */
    setMute: function (value)
    {
        this.mute = value;

        return this;
    },

    /**
     * @name Phaser.Sound.WebAudioSoundManager#mute
     * @type {boolean}
     * @fires Phaser.Sound.Events#GLOBAL_MUTE
     * @since 3.0.0
     */
    mute: {

        get: function ()
        {
            return (this.masterMuteNode.gain.value === 0);
        },

        set: function (value)
        {
            this.masterMuteNode.gain.setValueAtTime(value ? 0 : 1, 0);

            this.emit(Events.GLOBAL_MUTE, this, value);
        }

    },

    /**
     * Sets the volume of this Sound Manager.
     *
     * @method Phaser.Sound.WebAudioSoundManager#setVolume
     * @fires Phaser.Sound.Events#GLOBAL_VOLUME
     * @since 3.3.0
     *
     * @param {number} value - The global volume of this Sound Manager.
     *
     * @return {Phaser.Sound.WebAudioSoundManager} This Sound Manager.
     */
    setVolume: function (value)
    {
        this.volume = value;

        return this;
    },

    /**
     * @name Phaser.Sound.WebAudioSoundManager#volume
     * @type {number}
     * @fires Phaser.Sound.Events#GLOBAL_VOLUME
     * @since 3.0.0
     */
    volume: {

        get: function ()
        {
            return this.masterVolumeNode.gain.value;
        },

        set: function (value)
        {
            this.masterVolumeNode.gain.setValueAtTime(value, 0);

            this.emit(Events.GLOBAL_VOLUME, this, value);
        }

    }

});

module.exports = WebAudioSoundManager;
<commit_msg>Update WebAudioSoundManager.js<commit_after>/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @author       Pavle Goloskokovic <pgoloskokovic@gmail.com> (http://prunegames.com)
 * @copyright    2019 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

var Base64ToArrayBuffer = require('../../utils/base64/Base64ToArrayBuffer');
var BaseSoundManager = require('../BaseSoundManager');
var Class = require('../../utils/Class');
var Events = require('../events');
var WebAudioSound = require('./WebAudioSound');

/**
 * @classdesc
 * Web Audio API implementation of the sound manager.
 *
 * @class WebAudioSoundManager
 * @extends Phaser.Sound.BaseSoundManager
 * @memberof Phaser.Sound
 * @constructor
 * @since 3.0.0
 *
 * @param {Phaser.Game} game - Reference to the current game instance.
 */
var WebAudioSoundManager = new Class({

    Extends: BaseSoundManager,

    initialize:

    function WebAudioSoundManager (game)
    {
        /**
         * The AudioContext being used for playback.
         *
         * @name Phaser.Sound.WebAudioSoundManager#context
         * @type {AudioContext}
         * @private
         * @since 3.0.0
         */
        this.context = this.createAudioContext(game);

        /**
         * Gain node responsible for controlling global muting.
         *
         * @name Phaser.Sound.WebAudioSoundManager#masterMuteNode
         * @type {GainNode}
         * @private
         * @since 3.0.0
         */
        this.masterMuteNode = this.context.createGain();

        /**
         * Gain node responsible for controlling global volume.
         *
         * @name Phaser.Sound.WebAudioSoundManager#masterVolumeNode
         * @type {GainNode}
         * @private
         * @since 3.0.0
         */
        this.masterVolumeNode = this.context.createGain();

        this.masterMuteNode.connect(this.masterVolumeNode);

        this.masterVolumeNode.connect(this.context.destination);

        /**
         * Destination node for connecting individual sounds to.
         *
         * @name Phaser.Sound.WebAudioSoundManager#destination
         * @type {AudioNode}
         * @private
         * @since 3.0.0
         */
        this.destination = this.masterMuteNode;

        this.locked = this.context.state === 'suspended' && ('ontouchstart' in window || 'onclick' in window);

        BaseSoundManager.call(this, game);

        if (this.locked)
        {
            this.unlock();
        }
    },

    /**
     * Method responsible for instantiating and returning AudioContext instance.
     * If an instance of an AudioContext class was provided through the game config,
     * that instance will be returned instead. This can come in handy if you are reloading
     * a Phaser game on a page that never properly refreshes (such as in an SPA project)
     * and you want to reuse already instantiated AudioContext.
     *
     * @method Phaser.Sound.WebAudioSoundManager#createAudioContext
     * @private
     * @since 3.0.0
     *
     * @param {Phaser.Game} game - Reference to the current game instance.
     *
     * @return {AudioContext} The AudioContext instance to be used for playback.
     */
    createAudioContext: function (game)
    {
        var audioConfig = game.config.audio;

        if (audioConfig && audioConfig.context)
        {
            audioConfig.context.resume();

            return audioConfig.context;
        }

        return new AudioContext();
    },

    /**
     * Adds a new sound into the sound manager.
     *
     * @method Phaser.Sound.WebAudioSoundManager#add
     * @since 3.0.0
     *
     * @param {string} key - Asset key for the sound.
     * @param {Phaser.Types.Sound.SoundConfig} [config] - An optional config object containing default sound settings.
     *
     * @return {Phaser.Sound.WebAudioSound} The new sound instance.
     */
    add: function (key, config)
    {
        var sound = new WebAudioSound(this, key, config);

        this.sounds.push(sound);

        return sound;
    },

    /**
     * Decode audio data into a format ready for playback via Web Audio.
     *
     * The audio data can be a base64 encoded string, an audio media-type data uri, or an ArrayBuffer instance.
     *
     * The `audioKey` is the key that will be used to save the decoded audio to the audio cache.
     *
     * Instead of passing a single entry you can instead pass an array of `Phaser.Types.Sound.DecodeAudioConfig`
     * objects as the first and only argument.
     *
     * Decoding is an async process, so be sure to listen for the events to know when decoding has completed.
     *
     * Once the audio has decoded it can be added to the Sound Manager or played via its key.
     *
     * @method Phaser.Sound.WebAudioSoundManager#decodeAudio
     * @fires Phaser.Sound.Events#DECODED
     * @fires Phaser.Sound.Events#DECODED_ALL
     * @since 3.18.0
     *
     * @param {(Phaser.Types.Sound.DecodeAudioConfig[]|string)} [audioKey] - The string-based key to be used to reference the decoded audio in the audio cache, or an array of audio config objects.
     * @param {(ArrayBuffer|string)} [audioData] - The audio data, either a base64 encoded string, an audio media-type data uri, or an ArrayBuffer instance.
     */
    decodeAudio: function (audioKey, audioData)
    {
        var audioFiles;

        if (!Array.isArray(audioKey))
        {
            audioFiles = [ { key: audioKey, data: audioData } ];
        }
        else
        {
            audioFiles = audioKey;
        }

        var cache = this.game.cache.audio;
        var remaining = audioFiles.length;

        for (var i = 0; i < audioFiles.length; i++)
        {
            var entry = audioFiles[i];

            var key = entry.key;
            var data = entry.data;

            if (typeof data === 'string')
            {
                data = Base64ToArrayBuffer(data);
            }

            var success = function (key, audioBuffer)
            {
                cache.add(key, audioBuffer);

                this.emit(Events.DECODED, key);

                remaining--;

                if (remaining === 0)
                {
                    this.emit(Events.DECODED_ALL);
                }
            }.bind(this, key);

            var failure = function (key, error)
            {
                //  eslint-disable-next-line no-console
                console.error('Error decoding audio: ' + key + ' - ', error ? error.message : '');

                remaining--;

                if (remaining === 0)
                {
                    this.emit(Events.DECODED_ALL);
                }
            }.bind(this, key);

            this.context.decodeAudioData(data, success, failure);
        }
    },

    /**
     * Unlocks Web Audio API on the initial input event.
     *
     * Read more about how this issue is handled here in [this article](https://medium.com/@pgoloskokovic/unlocking-web-audio-the-smarter-way-8858218c0e09).
     *
     * @method Phaser.Sound.WebAudioSoundManager#unlock
     * @since 3.0.0
     */
    unlock: function ()
    {
        var _this = this;

        var unlockHandler = function unlockHandler ()
        {
            if (_this.context)
            {
                _this.context.resume().then(function ()
                {
                    document.body.removeEventListener('touchstart', unlockHandler);
                    document.body.removeEventListener('touchend', unlockHandler);
                    document.body.removeEventListener('click', unlockHandler);

                    _this.unlocked = true;
                }, function ()
                {
                    document.body.removeEventListener('touchstart', unlockHandler);
                    document.body.removeEventListener('touchend', unlockHandler);
                    document.body.removeEventListener('click', unlockHandler);
                });
            }
        };

        if (document.body)
        {
            document.body.addEventListener('touchstart', unlockHandler, false);
            document.body.addEventListener('touchend', unlockHandler, false);
            document.body.addEventListener('click', unlockHandler, false);
        }
    },

    /**
     * Method used internally for pausing sound manager if
     * Phaser.Sound.WebAudioSoundManager#pauseOnBlur is set to true.
     *
     * @method Phaser.Sound.WebAudioSoundManager#onBlur
     * @protected
     * @since 3.0.0
     */
    onBlur: function ()
    {
        if (!this.locked)
        {
            this.context.suspend();
        }
    },

    /**
     * Method used internally for resuming sound manager if
     * Phaser.Sound.WebAudioSoundManager#pauseOnBlur is set to true.
     *
     * @method Phaser.Sound.WebAudioSoundManager#onFocus
     * @protected
     * @since 3.0.0
     */
    onFocus: function ()
    {
        if (!this.locked)
        {
            this.context.resume();
        }
    },

    /**
     * Calls Phaser.Sound.BaseSoundManager#destroy method
     * and cleans up all Web Audio API related stuff.
     *
     * @method Phaser.Sound.WebAudioSoundManager#destroy
     * @since 3.0.0
     */
    destroy: function ()
    {
        this.destination = null;
        this.masterVolumeNode.disconnect();
        this.masterVolumeNode = null;
        this.masterMuteNode.disconnect();
        this.masterMuteNode = null;

        if (this.game.config.audio && this.game.config.audio.context)
        {
            this.context.suspend();
        }
        else
        {
            var _this = this;

            this.context.close().then(function ()
            {
                _this.context = null;
            });
        }

        BaseSoundManager.prototype.destroy.call(this);
    },

    /**
     * Sets the muted state of all this Sound Manager.
     *
     * @method Phaser.Sound.WebAudioSoundManager#setMute
     * @fires Phaser.Sound.Events#GLOBAL_MUTE
     * @since 3.3.0
     *
     * @param {boolean} value - `true` to mute all sounds, `false` to unmute them.
     *
     * @return {Phaser.Sound.WebAudioSoundManager} This Sound Manager.
     */
    setMute: function (value)
    {
        this.mute = value;

        return this;
    },

    /**
     * @name Phaser.Sound.WebAudioSoundManager#mute
     * @type {boolean}
     * @fires Phaser.Sound.Events#GLOBAL_MUTE
     * @since 3.0.0
     */
    mute: {

        get: function ()
        {
            return (this.masterMuteNode.gain.value === 0);
        },

        set: function (value)
        {
            this.masterMuteNode.gain.setValueAtTime(value ? 0 : 1, 0);

            this.emit(Events.GLOBAL_MUTE, this, value);
        }

    },

    /**
     * Sets the volume of this Sound Manager.
     *
     * @method Phaser.Sound.WebAudioSoundManager#setVolume
     * @fires Phaser.Sound.Events#GLOBAL_VOLUME
     * @since 3.3.0
     *
     * @param {number} value - The global volume of this Sound Manager.
     *
     * @return {Phaser.Sound.WebAudioSoundManager} This Sound Manager.
     */
    setVolume: function (value)
    {
        this.volume = value;

        return this;
    },

    /**
     * @name Phaser.Sound.WebAudioSoundManager#volume
     * @type {number}
     * @fires Phaser.Sound.Events#GLOBAL_VOLUME
     * @since 3.0.0
     */
    volume: {

        get: function ()
        {
            return this.masterVolumeNode.gain.value;
        },

        set: function (value)
        {
            this.masterVolumeNode.gain.setValueAtTime(value, 0);

            this.emit(Events.GLOBAL_VOLUME, this, value);
        }

    }

});

module.exports = WebAudioSoundManager;
<|endoftext|>


<commit_before>(function() {

    const
        SCALE_FACTOR = 1.40056,
        LAT_MIN = 0
        LAT_MAX = 164,
        LNG_MIN = 0,
        LNG_MAX = 252,
        BORDER = 5,
        CENTER = [LAT_MAX / 2, LNG_MAX / 2],
        RED = '#ff0000'
        DEFAULT_SPEED = 300
    ;

    // patch a leaflet bug, see https://github.com/bbecquet/Leaflet.PolylineDecorator/issues/17
    L.PolylineDecorator.include(L.Mixin.Events);

    function calculateDistance(a, b) {
		var dx = b.lng - a.lng;
        var dy = b.lat - a.lat;
        var raw = SCALE_FACTOR * Math.sqrt(dx * dx + dy * dy);
        return raw.toFixed(0);
	}

    function mathDegreesToGeographic(degrees) {
        if (degrees < 0) {
            degrees += 360;
        }
        return (450 - degrees) % 360;
    }

    function calculateHeading(start, end) {
        var radians = Math.atan2(end.lat - start.lat, end.lng - start.lng);
        var degrees = radians * 180 / Math.PI;
        degrees = mathDegreesToGeographic(degrees);
        return degrees.toFixed(1);
    }

    function newFlightDecorator(route) {
        return L.polylineDecorator(route, {
            patterns: [
                {
                    offset: 100,
                    repeat: 300,
                    symbol: L.Symbol.arrowHead({
                        pathOptions: {
                            opacity: 0,
                            fillOpacity: 1,
                            color: RED
                        }
                    })
                }
            ]
        });
    }

    function applyFlightPlan(route) {
        var id = route._leaflet_id;
        var coords = route.getLatLngs();
        var decorator = newFlightDecorator(route);
        decorator.parentId = id;
        decorator.addTo(map);
        for (var i = 0; i < coords.length-1; i++) {
            var distance = calculateDistance(coords[i], coords[i+1]).toString();
            var heading = calculateHeading(coords[i], coords[i+1]).toString();
            var markerContent = distance + 'km | ' + heading + '°';
            var marker =  L.marker(coords[i], {
                clickable: false,
                icon: L.divIcon({
                    className: 'flight-vertext',
                    html: markerContent,
                    iconAnchor: coords[i],
                    iconSize: [250, 0]
                })
            });
            marker.parentId = id;
            marker.addTo(map);
        }
    }

    function deleteAssociatedLayers(parentLayers) {
        var toDelete = []
        parentLayers.eachLayer(function(layer) {
            toDelete.push(layer._leaflet_id);
        });

        map.eachLayer(function(layer) {
            if (toDelete.indexOf(layer.parentId) != -1) {
                drawnItems.removeLayer(layer);
                map.removeLayer(layer);
            }
        });
    }

    var map = L.map('map', {
        crs: L.CRS.Simple,
        attributionControl: false
    }).setView(CENTER, 3);

    L.tileLayer('img/map/{z}/{x}/{y}.png', {
        minZoom: 2,
        maxZoom: 6,
        noWrap: true,
        tms: true,
        continuousWorld: true
    }).addTo(map);

    var drawnItems = new L.FeatureGroup();
    map.addLayer(drawnItems);
    var hiddenLayers = new L.FeatureGroup();

    var editOptions = {
        selectedPathOptions: {
            maintainColor: true,
            opacity: 0.4
        }
    }
    var drawControl = new L.Control.Draw({
        draw: {
            polygon: false,
            rectangle: false,
            circle: false,
            polyline: {
                showLength: false,
                shapeOptions: {
                    color: RED,
                    weight: 3,
                    opacity: 0.8
                }
            }
        },
        edit: {
            featureGroup: drawnItems,
            edit: L.Browser.touch ? false : editOptions
        }
    });
    map.addControl(drawControl);

    var titleControl = new L.Control.TitleControl({});
    map.addControl(titleControl);

    map.on('draw:created', function(e) {
        console.log(e);
        map.addLayer(e.layer);
        drawnItems.addLayer(e.layer);
        if (e.layerType === 'polyline') {
            applyFlightPlan(e.layer);
        }
    });

    map.on('draw:deleted', function(e) {
        console.log(e);
        deleteAssociatedLayers(e.layers);
    });

    map.on('draw:edited', function(e) {
        console.log(e);
        deleteAssociatedLayers(e.layers);
        e.layers.eachLayer(function(layer) {
            if (layer.getLatLngs) {
                applyFlightPlan(layer);
            }
        });
    });

    map.on('draw:editstart', function(e) {
        map.eachLayer(function(layer) {
            if (typeof layer.parentId !== 'undefined') {
                map.removeLayer(layer);
                hiddenLayers.addLayer(layer);
            }
        });
    });

    map.on('draw:editstop', function(e) {
        hiddenLayers.eachLayer(function(layer) {
            if (typeof layer.parentId !== 'undefined') {
                hiddenLayers.removeLayer(layer);
                map.addLayer(layer);
            }
        });
    })

    map.setMaxBounds(new L.LatLngBounds(
        [LAT_MIN - BORDER, LNG_MIN - BORDER],
        [LAT_MAX + BORDER, LNG_MAX + BORDER]
    ));

    // debug other events
    var otherEvents = ['draw:deletestart', 'draw:deleteend', 'draw:editstop', 'draw:drawvertex',
            'draw:editvertex', 'draw:editstart', 'draw:drawstop', 'draw:drawstart']
    for (var i = 0; i < otherEvents.length; i++) {
        map.on(otherEvents[i], function(e) {
            console.log(e);
        });
    }

})();
<commit_msg>hiding polyline children/decorations during edit and deletion to prevent orphan items, working yay<commit_after>(function() {

    const
        SCALE_FACTOR = 1.40056,
        LAT_MIN = 0
        LAT_MAX = 164,
        LNG_MIN = 0,
        LNG_MAX = 252,
        BORDER = 5,
        CENTER = [LAT_MAX / 2, LNG_MAX / 2],
        RED = '#ff0000'
        DEFAULT_SPEED = 300
    ;

    var map, drawnItems, hiddenLayers;

    // patch a leaflet bug, see https://github.com/bbecquet/Leaflet.PolylineDecorator/issues/17
    L.PolylineDecorator.include(L.Mixin.Events);

    function calculateDistance(a, b) {
		var dx = b.lng - a.lng;
        var dy = b.lat - a.lat;
        var raw = SCALE_FACTOR * Math.sqrt(dx * dx + dy * dy);
        return raw.toFixed(0);
	}

    function mathDegreesToGeographic(degrees) {
        if (degrees < 0) {
            degrees += 360;
        }
        return (450 - degrees) % 360;
    }

    function calculateHeading(start, end) {
        var radians = Math.atan2(end.lat - start.lat, end.lng - start.lng);
        var degrees = radians * 180 / Math.PI;
        degrees = mathDegreesToGeographic(degrees);
        return degrees.toFixed(1);
    }

    function newFlightDecorator(route) {
        return L.polylineDecorator(route, {
            patterns: [
                {
                    offset: 100,
                    repeat: 300,
                    symbol: L.Symbol.arrowHead({
                        pathOptions: {
                            opacity: 0,
                            fillOpacity: 1,
                            color: RED
                        }
                    })
                }
            ]
        });
    }

    function applyFlightPlan(route) {
        var id = route._leaflet_id;
        var coords = route.getLatLngs();
        var decorator = newFlightDecorator(route);
        decorator.parentId = id;
        decorator.addTo(map);
        for (var i = 0; i < coords.length-1; i++) {
            var distance = calculateDistance(coords[i], coords[i+1]).toString();
            var heading = calculateHeading(coords[i], coords[i+1]).toString();
            var markerContent = distance + 'km | ' + heading + '°';
            var marker =  L.marker(coords[i], {
                clickable: false,
                icon: L.divIcon({
                    className: 'flight-vertext',
                    html: markerContent,
                    iconAnchor: coords[i],
                    iconSize: [250, 0]
                })
            });
            marker.parentId = id;
            marker.addTo(map);
        }
    }

    function deleteAssociatedLayers(parentLayers) {
        var toDelete = []
        parentLayers.eachLayer(function(layer) {
            toDelete.push(layer._leaflet_id);
        });

        map.eachLayer(function(layer) {
            if (toDelete.indexOf(layer.parentId) != -1) {
                map.removeLayer(layer);
            }
        });
        hiddenLayers.eachLayer(function(layer) {
            if (toDelete.indexOf(layer.parentId) != -1) {
                hiddenLayers.removeLayer(layer);
            }
        });
    }

    function transferChildLayer(from, to) {
        from.eachLayer(function(layer) {
            if (typeof layer.parentId !== 'undefined') {
                from.removeLayer(layer);
                to.addLayer(layer);
            }
        });
    }

    function showChildLayers() {
        transferChildLayer(hiddenLayers, map);
    }

    function hideChildLayers() {
        transferChildLayer(map, hiddenLayers);
    }

    map = L.map('map', {
        crs: L.CRS.Simple,
        attributionControl: false
    }).setView(CENTER, 3);

    L.tileLayer('img/map/{z}/{x}/{y}.png', {
        minZoom: 2,
        maxZoom: 6,
        noWrap: true,
        tms: true,
        continuousWorld: true
    }).addTo(map);

    drawnItems = new L.FeatureGroup();
    map.addLayer(drawnItems);
    hiddenLayers = new L.FeatureGroup();

    var editOptions = {
        selectedPathOptions: {
            maintainColor: true,
            opacity: 0.4
        }
    }
    var drawControl = new L.Control.Draw({
        draw: {
            polygon: false,
            rectangle: false,
            circle: false,
            polyline: {
                showLength: false,
                shapeOptions: {
                    color: RED,
                    weight: 3,
                    opacity: 0.8
                }
            }
        },
        edit: {
            featureGroup: drawnItems,
            edit: L.Browser.touch ? false : editOptions
        }
    });
    map.addControl(drawControl);

    var titleControl = new L.Control.TitleControl({});
    map.addControl(titleControl);

    map.on('draw:created', function(e) {
        console.log(e);
        map.addLayer(e.layer);
        drawnItems.addLayer(e.layer);
        if (e.layerType === 'polyline') {
            applyFlightPlan(e.layer);
        }
    });

    map.on('draw:deleted', function(e) {
        console.log(e);
        deleteAssociatedLayers(e.layers);
    });

    map.on('draw:edited', function(e) {
        console.log(e);
        deleteAssociatedLayers(e.layers);
        e.layers.eachLayer(function(layer) {
            if (layer.getLatLngs) {
                applyFlightPlan(layer);
            }
        });
    });

    map.on('draw:editstart', function(e) {
        hideChildLayers();
    });

    map.on('draw:editstop', function(e) {
        showChildLayers();
    });

    map.on('draw:deletestart', function(e) {
        hideChildLayers();
    });

    map.on('draw:deletestop', function(e) {
        showChildLayers();
    });

    map.setMaxBounds(new L.LatLngBounds(
        [LAT_MIN - BORDER, LNG_MIN - BORDER],
        [LAT_MAX + BORDER, LNG_MAX + BORDER]
    ));

    // debug other events
    var otherEvents = ['draw:deletestart', 'draw:deleteend', 'draw:editstop', 'draw:drawvertex',
            'draw:editvertex', 'draw:editstart', 'draw:drawstop', 'draw:drawstart']
    for (var i = 0; i < otherEvents.length; i++) {
        map.on(otherEvents[i], function(e) {
            console.log(e);
        });
    }

})();
<|endoftext|>


<commit_before>'use strict';

import fs from 'fs';
import path from 'path';
import cjson from 'cjson';
import { sync as mkdirp } from 'mkdirp';
import extractFromJSX from './extractFromJSX';
import { sync as rimraf } from 'rimraf';
import { buildXML, createAppJson, createWorkspaceJson } from './artifacts';
import { execSync, spawn, fork } from 'child_process';
import { generate } from 'astring';
import { sync as resolve } from 'resolve';

let watching = false;

/**
 * Produces a minimal build of ExtReact by crawling your React source code and extracting the xtypes used
 * in JSX tags
 */
module.exports = class ReactExtJSWebpackPlugin {

    /**
     * @param {Object[]} builds
     * @param {Boolean} [debug=false] Set to true to prevent cleanup of build temporary build artifacts that might be helpful in troubleshooting issues.
     * @param {String} sdk The full path to the ExtReact SDK
     * @param {String} [toolkit='modern'] "modern" or "classic"
     * @param {String} theme The name of the ExtReact theme package to use, for example "theme-material"
     * @param {String[]} packages An array of ExtReact packages to include
     * @param {String[]} overrides An array with the paths of directories or files to search. Any classes
     * declared in these locations will be automatically required and included in the build.
     * If any file defines an ExtReact override (using Ext.define with an "override" property),
     * that override will in fact only be included in the build if the target class specified
     * in the "override" property is also included.
     * @param {String} output The path to directory where the ExtReact bundle should be written
     * @param {Boolean} asynchronous Set to true to run Sencha Cmd builds asynchronously. This makes the webpack build finish much faster, but the app may not load correctly in your browser until Sencha Cmd is finished building the ExtReact bundle
     * @param {Boolean} production Set to true for production builds.  This tell Sencha Cmd to compress the generated JS bundle.
     */
    constructor(options) {
        options = { ...this.getDefaultOptions(), ...options };
        const { builds } = options;

        if (Object.keys(builds).length === 0) {
            const { builds, ...buildOptions } = options;
            builds.ext = buildOptions;
        }

        for (let name in builds)
            this._validateBuildConfig(name, builds[name]);

        Object.assign(this, {
            ...options,
            currentFile: null,
            manifest: null,
            dependencies: []
        });
    }

    /**
     * Default config options
     * @protected
     * @return {Object}
     */
    getDefaultOptions() {
        return {
            builds: {},
            debug: false,
            watch: false,
            test: /\.jsx?$/,

            /* begin single build only */
            output: 'ext-react',
            toolkit: 'modern',
            theme: 'theme-triton',
            packages: null,
            packageDirs: [],
            overrides: [],
            asynchronous: false,
            production: false,
            manifestExtractor: extractFromJSX,
            /* end single build only */
        }
    }

    apply(compiler) {

        const me = this;

        /**
         * Adds the code for the specified function call to the manifest.js file
         * @param {Object} call A function call AST node.
         */
        const addToManifest = function(call) {
            try {
                const file = this.state.module.resource;
                me.dependencies[file] = [ ...(me.dependencies[file] || []), generate(call) ];
            } catch (e) {
                console.error(`Error processing ${file}`);
            }
        };

        compiler.plugin('watch-run', (watching, cb) => {
            this.watch = true;
            cb();
        });

        // extract xtypes from JSX tags
        compiler.plugin('compilation', (compilation, data) => {
            compilation.plugin('succeed-module', (module) => {
                this.currentFile = module.resource;

                if (module.resource && module.resource.match(this.test) && !module.resource.match(/node_modules/)) {
                    const doParse = () => {
                        this.dependencies[this.currentFile] = [
                            ...(this.dependencies[this.currentFile] || []),
                            ...this.manifestExtractor(module._source._value, compilation, module)
                        ];
                    };

                    if (this.debug) {
                        doParse();
                    } else {
                        try { doParse(); } catch (e) { console.error('error parsing ' + this.currentFile); }
                    }
                }
            });

            data.normalModuleFactory.plugin("parser", function(parser, options) {
                // extract xtypes and classes from Ext.create calls
                parser.plugin('call Ext.create', addToManifest);

                // copy Ext.require calls to the manifest.  This allows the users to explicitly require a class if the plugin fails to detect it.
                parser.plugin('call Ext.require', addToManifest);

                // copy Ext.define calls to the manifest.  This allows users to write standard ExtReact classes.
                parser.plugin('call Ext.define', addToManifest);
            })
        });

        // once all modules are processed, create the optimized ExtReact build.
        compiler.plugin('emit', (compilation, callback) => {
            const modules = compilation.chunks.reduce((a, b) => a.concat(b.modules), []);
            const build = this.builds[Object.keys(this.builds)[0]];

            let outputPath = path.join(compiler.outputPath, this.output);

            // webpack-dev-server overwrites the outputPath to "/", so we need to prepend contentBase
            if (compiler.outputPath === '/' && compiler.options.devServer) {
                outputPath = path.join(compiler.options.devServer.contentBase, outputPath);
            }

            // the following is needed for html-webpack-plugin to include <script> and <link> tags for ExtReact
            const jsChunk = compilation.addChunk(`${this.output}-js`);

            jsChunk.hasRuntime = jsChunk.isInitial = () => true;
            jsChunk.files.push(path.join(this.output, 'ext.js'));
            jsChunk.files.push(path.join(this.output, 'ext.css'));
            jsChunk.id = -2; // this forces html-webpack-plugin to include ext.js first

            if (this.asynchronous) callback();

            this._buildExtBundle('ext', modules, outputPath, build)
                .then(() => {
                    // const cssVarPath = path.join(this.output, 'css-vars.js');

                    // if (fs.existsSync(path.join(outputPath, 'css-vars.js'))) {
                    //     const cssVarChunk = compilation.addChunk(`${this.output}-css-vars`);
                    //     cssVarChunk.hasRuntime = cssVarChunk.isInitial = () => true;
                    //     cssVarChunk.files.push(cssVarPath);
                    //     cssVarChunk.id = -1;
                    // }

                    !this.asynchronous && callback();
                })
                .catch(e => {
                    console.error(e);
                    compilation.errors.push(new Error('[@extjs/reactor-webpack-plugin]: ' + e.toString()));
                });
        });
    }

    /**
     * Checks each build config for missing/invalid properties
     * @param {String} name The name of the build
     * @param {String} build The build config
     * @private
     */
    _validateBuildConfig(name, build) {
        let { sdk } = build;

        if (!sdk) {
            try {
                build.sdk = path.dirname(resolve('@extjs/ext-react', { basedir: process.cwd() }))
                build.packageDirs = [...(build.packageDirs || []), path.dirname(build.sdk)];
                build.packages = build.packages || this._findPackages(build);
            } catch (e) {
                throw new Error(`@extjs/ext-react not found.  You can install it with "npm install --save @extjs/ext-react" or, if you have a local copy of the SDK, specify the path to it using the "sdk" option in build "${name}."`);
            }
        }
    }

    /**
     * Return the names of all ExtReact packages in the same parent directory as ext-react (typically node_modules/@extjs)
     * @private
     * @param {String} sdk Path to ext-react
     * @return {String[]}
     */
    _findPackages({sdk, theme, toolkit}) {
        const modulesDir = path.join(sdk, '..');

        return fs.readdirSync(modulesDir)
            // Filter out theme packages -- Cmd will figure these out during build.
            .filter(dir => !dir.match(/ext-react-.*-theme/))
            // Filter out directories without 'package.json'
            .filter(dir => fs.existsSync(path.join(modulesDir, dir, 'package.json')))
            // Generate array of package names
            .map(dir => {
                const packageInfo = JSON.parse(fs.readFileSync(path.join(modulesDir, dir, 'package.json')));
                if(packageInfo.sencha) return packageInfo.sencha.name;
            })
            // Incase any undefineds make it in.
            .filter(name => name);
    }

    /**
     * Returns the path to the sencha cmd executable
     * @private
     * @return {String}
     */
    _getSenchCmdPath() {
        try {
            // use @extjs/sencha-cmd from node_modules
            return require('@extjs/sencha-cmd');
        } catch (e) {
            // attempt to use globally installed Sencha Cmd
            return 'sencha';
        }
    }

    /**
     /**
     * Builds a minimal version of the ExtReact framework based on the classes used
     * @param {String} name The name of the build
     * @param {Module[]} modules webpack modules
     * @param {String} output The path to where the framework build should be written
     * @param {String} [toolkit='modern'] "modern" or "classic"
     * @param {String} output The path to the directory to create which will contain the js and css bundles
     * @param {String} theme The name of the ExtReact theme package to use, for example "theme-material"
     * @param {String[]} packages An array of ExtReact packages to include
     * @param {String[]} packageDirs Directories containing packages
     * @param {String[]} overrides An array of locations for overrides
     * @param {String} sdk The full path to the ExtReact SDK
     * @private
     */
    _buildExtBundle(name, modules, output, { toolkit='modern', theme, packages=[], packageDirs=[], sdk, overrides }) {
        let sencha = this._getSenchCmdPath();

        return new Promise((resolve, reject) => {
            this.onBuildComplete = resolve;
            this.onBuildFail = reject;

            if (!watching) {
                rimraf(output);
                mkdirp(output);
            }

            let statements = ['Ext.require(["Ext.app.Application", "Ext.Component", "Ext.Widget"])']; // for some reason command doesn't load component when only panel is required

            for (let module of modules) {
                const deps = this.dependencies[module.resource];
                if (deps) statements = statements.concat(deps);
            }

            const js = statements.join(';\n');
            const manifest = path.join(output, 'manifest.js');

            // add ext-react/packages automatically if present
            const userPackages = path.join('.', 'ext-react', 'packages');

            if (fs.existsSync(userPackages)) {
                packageDirs.push(userPackages)
            }

            if (fs.existsSync(path.join(sdk, 'ext'))) {
                // local checkout of the SDK repo
                packageDirs.push(path.join('ext', 'packages'));
                sdk = path.join(sdk, 'ext');
            }

            if (!watching) {
                fs.writeFileSync(path.join(output, 'build.xml'), buildXML({ compress: this.production }), 'utf8');
                fs.writeFileSync(path.join(output, 'app.json'), createAppJson({ theme, packages, toolkit, overrides, packageDirs }), 'utf8');
                fs.writeFileSync(path.join(output, 'workspace.json'), createWorkspaceJson(sdk, packageDirs, output), 'utf8');
            }

            let cmdRebuildNeeded = false;

            if (this.manifest === null || js !== this.manifest) {
                // Only write manifest if it differs from the last run.  This prevents unnecessary cmd rebuilds.
                this.manifest = js;
                fs.writeFileSync(manifest, js, 'utf8');
                cmdRebuildNeeded = true;
                console.log(`\nbuilding ExtReact bundle: ${name} => ${output}`);
            }

            if (this.watch) {
                if (!watching) {
                    watching = fork(sencha, ['ant', 'watch'], { cwd: output, silent: true });
                    watching.stdout.pipe(process.stdout);
                    watching.stdout.on('data', data => {
                        if (data.toString().match(/Waiting for changes\.\.\./)) {
                            this.onBuildComplete(output);
                        }
                    });
                    watching.on('exit', code => this.onBuildFail())
                }

                if (!cmdRebuildNeeded) resolve(output);
            } else {
                execSync(`${sencha} ant build`, { cwd: output, stdio: 'inherit' });
                resolve(output);
            }
        });
    }
};

<commit_msg>Improve more, look at package.json for package type, and filter out 'theme' types.<commit_after>'use strict';

import fs from 'fs';
import path from 'path';
import cjson from 'cjson';
import { sync as mkdirp } from 'mkdirp';
import extractFromJSX from './extractFromJSX';
import { sync as rimraf } from 'rimraf';
import { buildXML, createAppJson, createWorkspaceJson } from './artifacts';
import { execSync, spawn, fork } from 'child_process';
import { generate } from 'astring';
import { sync as resolve } from 'resolve';

let watching = false;

/**
 * Produces a minimal build of ExtReact by crawling your React source code and extracting the xtypes used
 * in JSX tags
 */
module.exports = class ReactExtJSWebpackPlugin {

    /**
     * @param {Object[]} builds
     * @param {Boolean} [debug=false] Set to true to prevent cleanup of build temporary build artifacts that might be helpful in troubleshooting issues.
     * @param {String} sdk The full path to the ExtReact SDK
     * @param {String} [toolkit='modern'] "modern" or "classic"
     * @param {String} theme The name of the ExtReact theme package to use, for example "theme-material"
     * @param {String[]} packages An array of ExtReact packages to include
     * @param {String[]} overrides An array with the paths of directories or files to search. Any classes
     * declared in these locations will be automatically required and included in the build.
     * If any file defines an ExtReact override (using Ext.define with an "override" property),
     * that override will in fact only be included in the build if the target class specified
     * in the "override" property is also included.
     * @param {String} output The path to directory where the ExtReact bundle should be written
     * @param {Boolean} asynchronous Set to true to run Sencha Cmd builds asynchronously. This makes the webpack build finish much faster, but the app may not load correctly in your browser until Sencha Cmd is finished building the ExtReact bundle
     * @param {Boolean} production Set to true for production builds.  This tell Sencha Cmd to compress the generated JS bundle.
     */
    constructor(options) {
        options = { ...this.getDefaultOptions(), ...options };
        const { builds } = options;

        if (Object.keys(builds).length === 0) {
            const { builds, ...buildOptions } = options;
            builds.ext = buildOptions;
        }

        for (let name in builds)
            this._validateBuildConfig(name, builds[name]);

        Object.assign(this, {
            ...options,
            currentFile: null,
            manifest: null,
            dependencies: []
        });
    }

    /**
     * Default config options
     * @protected
     * @return {Object}
     */
    getDefaultOptions() {
        return {
            builds: {},
            debug: false,
            watch: false,
            test: /\.jsx?$/,

            /* begin single build only */
            output: 'ext-react',
            toolkit: 'modern',
            theme: 'theme-triton',
            packages: null,
            packageDirs: [],
            overrides: [],
            asynchronous: false,
            production: false,
            manifestExtractor: extractFromJSX,
            /* end single build only */
        }
    }

    apply(compiler) {

        const me = this;

        /**
         * Adds the code for the specified function call to the manifest.js file
         * @param {Object} call A function call AST node.
         */
        const addToManifest = function(call) {
            try {
                const file = this.state.module.resource;
                me.dependencies[file] = [ ...(me.dependencies[file] || []), generate(call) ];
            } catch (e) {
                console.error(`Error processing ${file}`);
            }
        };

        compiler.plugin('watch-run', (watching, cb) => {
            this.watch = true;
            cb();
        });

        // extract xtypes from JSX tags
        compiler.plugin('compilation', (compilation, data) => {
            compilation.plugin('succeed-module', (module) => {
                this.currentFile = module.resource;

                if (module.resource && module.resource.match(this.test) && !module.resource.match(/node_modules/)) {
                    const doParse = () => {
                        this.dependencies[this.currentFile] = [
                            ...(this.dependencies[this.currentFile] || []),
                            ...this.manifestExtractor(module._source._value, compilation, module)
                        ];
                    };

                    if (this.debug) {
                        doParse();
                    } else {
                        try { doParse(); } catch (e) { console.error('error parsing ' + this.currentFile); }
                    }
                }
            });

            data.normalModuleFactory.plugin("parser", function(parser, options) {
                // extract xtypes and classes from Ext.create calls
                parser.plugin('call Ext.create', addToManifest);

                // copy Ext.require calls to the manifest.  This allows the users to explicitly require a class if the plugin fails to detect it.
                parser.plugin('call Ext.require', addToManifest);

                // copy Ext.define calls to the manifest.  This allows users to write standard ExtReact classes.
                parser.plugin('call Ext.define', addToManifest);
            })
        });

        // once all modules are processed, create the optimized ExtReact build.
        compiler.plugin('emit', (compilation, callback) => {
            const modules = compilation.chunks.reduce((a, b) => a.concat(b.modules), []);
            const build = this.builds[Object.keys(this.builds)[0]];

            let outputPath = path.join(compiler.outputPath, this.output);

            // webpack-dev-server overwrites the outputPath to "/", so we need to prepend contentBase
            if (compiler.outputPath === '/' && compiler.options.devServer) {
                outputPath = path.join(compiler.options.devServer.contentBase, outputPath);
            }

            // the following is needed for html-webpack-plugin to include <script> and <link> tags for ExtReact
            const jsChunk = compilation.addChunk(`${this.output}-js`);

            jsChunk.hasRuntime = jsChunk.isInitial = () => true;
            jsChunk.files.push(path.join(this.output, 'ext.js'));
            jsChunk.files.push(path.join(this.output, 'ext.css'));
            jsChunk.id = -2; // this forces html-webpack-plugin to include ext.js first

            if (this.asynchronous) callback();

            this._buildExtBundle('ext', modules, outputPath, build)
                .then(() => {
                    // const cssVarPath = path.join(this.output, 'css-vars.js');

                    // if (fs.existsSync(path.join(outputPath, 'css-vars.js'))) {
                    //     const cssVarChunk = compilation.addChunk(`${this.output}-css-vars`);
                    //     cssVarChunk.hasRuntime = cssVarChunk.isInitial = () => true;
                    //     cssVarChunk.files.push(cssVarPath);
                    //     cssVarChunk.id = -1;
                    // }

                    !this.asynchronous && callback();
                })
                .catch(e => {
                    console.error(e);
                    compilation.errors.push(new Error('[@extjs/reactor-webpack-plugin]: ' + e.toString()));
                });
        });
    }

    /**
     * Checks each build config for missing/invalid properties
     * @param {String} name The name of the build
     * @param {String} build The build config
     * @private
     */
    _validateBuildConfig(name, build) {
        let { sdk } = build;

        if (!sdk) {
            try {
                build.sdk = path.dirname(resolve('@extjs/ext-react', { basedir: process.cwd() }))
                build.packageDirs = [...(build.packageDirs || []), path.dirname(build.sdk)];
                build.packages = build.packages || this._findPackages(build.sdk);
            } catch (e) {
                throw new Error(`@extjs/ext-react not found.  You can install it with "npm install --save @extjs/ext-react" or, if you have a local copy of the SDK, specify the path to it using the "sdk" option in build "${name}."`);
            }
        }
    }

    /**
     * Return the names of all ExtReact packages in the same parent directory as ext-react (typically node_modules/@extjs)
     * @private
     * @param {String} sdk Path to ext-react
     * @return {String[]}
     */
    _findPackages(sdk) {
        const modulesDir = path.join(sdk, '..');

        return fs.readdirSync(modulesDir)
            // Filter out directories without 'package.json'
            .filter(dir => fs.existsSync(path.join(modulesDir, dir, 'package.json')))
            // Generate array of package names
            .map(dir => {
                const packageInfo = JSON.parse(fs.readFileSync(path.join(modulesDir, dir, 'package.json')));
                // Don't include theme type packages.
                if(packageInfo.sencha && packageInfo.sencha.type !== 'theme') {
                    return packageInfo.sencha.name;
                }
            })
            // Remove any undefineds from map
            .filter(name => name);
    }

    /**
     * Returns the path to the sencha cmd executable
     * @private
     * @return {String}
     */
    _getSenchCmdPath() {
        try {
            // use @extjs/sencha-cmd from node_modules
            return require('@extjs/sencha-cmd');
        } catch (e) {
            // attempt to use globally installed Sencha Cmd
            return 'sencha';
        }
    }

    /**
     /**
     * Builds a minimal version of the ExtReact framework based on the classes used
     * @param {String} name The name of the build
     * @param {Module[]} modules webpack modules
     * @param {String} output The path to where the framework build should be written
     * @param {String} [toolkit='modern'] "modern" or "classic"
     * @param {String} output The path to the directory to create which will contain the js and css bundles
     * @param {String} theme The name of the ExtReact theme package to use, for example "theme-material"
     * @param {String[]} packages An array of ExtReact packages to include
     * @param {String[]} packageDirs Directories containing packages
     * @param {String[]} overrides An array of locations for overrides
     * @param {String} sdk The full path to the ExtReact SDK
     * @private
     */
    _buildExtBundle(name, modules, output, { toolkit='modern', theme, packages=[], packageDirs=[], sdk, overrides }) {
        let sencha = this._getSenchCmdPath();

        return new Promise((resolve, reject) => {
            this.onBuildComplete = resolve;
            this.onBuildFail = reject;

            if (!watching) {
                rimraf(output);
                mkdirp(output);
            }

            let statements = ['Ext.require(["Ext.app.Application", "Ext.Component", "Ext.Widget"])']; // for some reason command doesn't load component when only panel is required

            for (let module of modules) {
                const deps = this.dependencies[module.resource];
                if (deps) statements = statements.concat(deps);
            }

            const js = statements.join(';\n');
            const manifest = path.join(output, 'manifest.js');

            // add ext-react/packages automatically if present
            const userPackages = path.join('.', 'ext-react', 'packages');

            if (fs.existsSync(userPackages)) {
                packageDirs.push(userPackages)
            }

            if (fs.existsSync(path.join(sdk, 'ext'))) {
                // local checkout of the SDK repo
                packageDirs.push(path.join('ext', 'packages'));
                sdk = path.join(sdk, 'ext');
            }

            if (!watching) {
                fs.writeFileSync(path.join(output, 'build.xml'), buildXML({ compress: this.production }), 'utf8');
                fs.writeFileSync(path.join(output, 'app.json'), createAppJson({ theme, packages, toolkit, overrides, packageDirs }), 'utf8');
                fs.writeFileSync(path.join(output, 'workspace.json'), createWorkspaceJson(sdk, packageDirs, output), 'utf8');
            }

            let cmdRebuildNeeded = false;

            if (this.manifest === null || js !== this.manifest) {
                // Only write manifest if it differs from the last run.  This prevents unnecessary cmd rebuilds.
                this.manifest = js;
                fs.writeFileSync(manifest, js, 'utf8');
                cmdRebuildNeeded = true;
                console.log(`\nbuilding ExtReact bundle: ${name} => ${output}`);
            }

            if (this.watch) {
                if (!watching) {
                    watching = fork(sencha, ['ant', 'watch'], { cwd: output, silent: true });
                    watching.stdout.pipe(process.stdout);
                    watching.stdout.on('data', data => {
                        if (data.toString().match(/Waiting for changes\.\.\./)) {
                            this.onBuildComplete(output);
                        }
                    });
                    watching.on('exit', code => this.onBuildFail())
                }

                if (!cmdRebuildNeeded) resolve(output);
            } else {
                execSync(`${sencha} ant build`, { cwd: output, stdio: 'inherit' });
                resolve(output);
            }
        });
    }
};

<|endoftext|>


<commit_before>import defaultStorage from './storage/default'
import isPlainObject from 'lodash.isplainobject'
import { AUTHENTICATE, FETCH, INVALIDATE_SESSION } from './actionTypes'
import {
  authenticateFailed,
  authenticateSucceeded,
  invalidateSession,
  invalidateSessionFailed,
  restore,
  restoreFailed,
  updateSession
} from './actions'
import {
  getSessionData,
  getAuthenticator,
  getIsAuthenticated
} from './selectors'
import invariant from 'invariant'
import warning from 'warning'

const validateAuthenticatorsPresence = ({ authenticator, authenticators }) => {
  invariant(
    authenticator != null || authenticators != null,
    'No authenticator was given. Be sure to configure an authenticator ' +
      'by using the `authenticator` option for a single authenticator or ' +
      'using the `authenticators` option to allow multiple authenticators'
  )
}

const validateAuthenticatorsIsArray = authenticators => {
  invariant(
    Array.isArray(authenticators),
    'Expected `authenticators` to be an array. If you only need a single ' +
      'authenticator, consider using the `authenticator` option.'
  )
}

const validateAuthenticatorIsObject = authenticator => {
  invariant(
    isPlainObject(authenticator),
    'Expected `authenticator` to be an object. If you need multiple ' +
      'authenticators, consider using the `authenticators` option.'
  )
}

const validateConfig = config => {
  const { authenticator, authenticators } = config

  validateAuthenticatorsPresence(config)
  authenticator == null && validateAuthenticatorsIsArray(authenticators)
  authenticators == null && validateAuthenticatorIsObject(authenticator)
}

export default (config = {}) => {
  validateConfig(config)

  const {
    authenticator,
    authenticators,
    authorize,
    refresh,
    storage = defaultStorage
  } = config

  const findAuthenticator = authenticator
    ? () => authenticator
    : name => authenticators.find(authenticator => authenticator.name === name)

  return ({ dispatch, getState }) => {
    const { authenticated = {} } = storage.restore() || {}
    const { authenticator: authenticatorName, ...data } = authenticated
    const authenticator = findAuthenticator(authenticatorName)

    if (authenticator) {
      authenticator
        .restore(data)
        .then(
          () => dispatch(restore(authenticated)),
          () => dispatch(restoreFailed())
        )
    } else {
      dispatch(restoreFailed())
    }

    return next => action => {
      const sync = () => {
        const { session: prevSession } = getState()
        const result = next(action)
        const { session } = getState()

        if (session.data !== prevSession.data) {
          const { authenticator, data } = session

          storage.persist({ authenticated: { ...data, authenticator } })
        }

        return result
      }

      switch (action.type) {
        case AUTHENTICATE: {
          const authenticator = findAuthenticator(action.meta.authenticator)

          if (!authenticator) {
            throw new Error(
              `No authenticator with name \`${action.meta.authenticator}\` ` +
                'was found. Be sure you have defined it in the authenticators ' +
                'config.'
            )
          }

          return authenticator
            .authenticate(action.payload)
            .then(
              data => dispatch(authenticateSucceeded(authenticator.name, data)),
              error => Promise.reject(dispatch(authenticateFailed(error)))
            )
        }
        case FETCH: {
          const state = getState()
          const { url, options = {} } = action.payload
          const { headers = {} } = options

          if (authorize) {
            authorize(getSessionData(state), (name, value) => {
              headers[name] = value
            })
          }

          return fetch(url, { ...options, headers }).then(response => {
            if (response.status === 401 && getIsAuthenticated(state)) {
              dispatch(invalidateSession())
            } else if (refresh) {
              const result = refresh(response)

              result !== null && dispatch(updateSession(result))
            }

            return response
          })
        }
        case INVALIDATE_SESSION: {
          const state = getState()
          const authenticatorName = getAuthenticator(state)
          const authenticator = findAuthenticator(authenticatorName)

          if (!getIsAuthenticated(state)) {
            warning(
              false,
              'You are trying to invalidate a session that is not authenticated.'
            )
            return Promise.reject(dispatch(invalidateSessionFailed()))
          }

          if (!authenticator) {
            throw new Error(
              `No authenticator with name \`${authenticatorName}\` ` +
                'was found. Be sure you have defined it in the authenticators ' +
                'config.'
            )
          }

          return authenticator
            .invalidate(getSessionData(state))
            .then(sync, () =>
              Promise.reject(dispatch(invalidateSessionFailed()))
            )
        }
        default: {
          return sync()
        }
      }
    }
  }
}
<commit_msg>Use invariant for authenticate action<commit_after>import defaultStorage from './storage/default'
import isPlainObject from 'lodash.isplainobject'
import { AUTHENTICATE, FETCH, INVALIDATE_SESSION } from './actionTypes'
import {
  authenticateFailed,
  authenticateSucceeded,
  invalidateSession,
  invalidateSessionFailed,
  restore,
  restoreFailed,
  updateSession
} from './actions'
import {
  getSessionData,
  getAuthenticator,
  getIsAuthenticated
} from './selectors'
import invariant from 'invariant'
import warning from 'warning'

const validateAuthenticatorsPresence = ({ authenticator, authenticators }) => {
  invariant(
    authenticator != null || authenticators != null,
    'No authenticator was given. Be sure to configure an authenticator ' +
      'by using the `authenticator` option for a single authenticator or ' +
      'using the `authenticators` option to allow multiple authenticators'
  )
}

const validateAuthenticatorsIsArray = authenticators => {
  invariant(
    Array.isArray(authenticators),
    'Expected `authenticators` to be an array. If you only need a single ' +
      'authenticator, consider using the `authenticator` option.'
  )
}

const validateAuthenticatorIsObject = authenticator => {
  invariant(
    isPlainObject(authenticator),
    'Expected `authenticator` to be an object. If you need multiple ' +
      'authenticators, consider using the `authenticators` option.'
  )
}

const validateConfig = config => {
  const { authenticator, authenticators } = config

  validateAuthenticatorsPresence(config)
  authenticator == null && validateAuthenticatorsIsArray(authenticators)
  authenticators == null && validateAuthenticatorIsObject(authenticator)
}

export default (config = {}) => {
  validateConfig(config)

  const {
    authenticator,
    authenticators,
    authorize,
    refresh,
    storage = defaultStorage
  } = config

  const findAuthenticator = authenticator
    ? () => authenticator
    : name => authenticators.find(authenticator => authenticator.name === name)

  return ({ dispatch, getState }) => {
    const { authenticated = {} } = storage.restore() || {}
    const { authenticator: authenticatorName, ...data } = authenticated
    const authenticator = findAuthenticator(authenticatorName)

    if (authenticator) {
      authenticator
        .restore(data)
        .then(
          () => dispatch(restore(authenticated)),
          () => dispatch(restoreFailed())
        )
    } else {
      dispatch(restoreFailed())
    }

    return next => action => {
      const sync = () => {
        const { session: prevSession } = getState()
        const result = next(action)
        const { session } = getState()

        if (session.data !== prevSession.data) {
          const { authenticator, data } = session

          storage.persist({ authenticated: { ...data, authenticator } })
        }

        return result
      }

      switch (action.type) {
        case AUTHENTICATE: {
          const authenticator = findAuthenticator(action.meta.authenticator)

          invariant(
            authenticator,
            `No authenticator with name \`${action.meta.authenticator}\` ` +
              'was found. Be sure you have defined it in the authenticators ' +
              'config.'
          )

          return authenticator
            .authenticate(action.payload)
            .then(
              data => dispatch(authenticateSucceeded(authenticator.name, data)),
              error => Promise.reject(dispatch(authenticateFailed(error)))
            )
        }
        case FETCH: {
          const state = getState()
          const { url, options = {} } = action.payload
          const { headers = {} } = options

          if (authorize) {
            authorize(getSessionData(state), (name, value) => {
              headers[name] = value
            })
          }

          return fetch(url, { ...options, headers }).then(response => {
            if (response.status === 401 && getIsAuthenticated(state)) {
              dispatch(invalidateSession())
            } else if (refresh) {
              const result = refresh(response)

              result !== null && dispatch(updateSession(result))
            }

            return response
          })
        }
        case INVALIDATE_SESSION: {
          const state = getState()
          const authenticatorName = getAuthenticator(state)
          const authenticator = findAuthenticator(authenticatorName)

          if (!getIsAuthenticated(state)) {
            warning(
              false,
              'You are trying to invalidate a session that is not authenticated.'
            )
            return Promise.reject(dispatch(invalidateSessionFailed()))
          }

          if (!authenticator) {
            throw new Error(
              `No authenticator with name \`${authenticatorName}\` ` +
                'was found. Be sure you have defined it in the authenticators ' +
                'config.'
            )
          }

          return authenticator
            .invalidate(getSessionData(state))
            .then(sync, () =>
              Promise.reject(dispatch(invalidateSessionFailed()))
            )
        }
        default: {
          return sync()
        }
      }
    }
  }
}
<|endoftext|>