app/imports/api/review/ReviewCollection.ts
import { Meteor } from 'meteor/meteor';
import _ from 'lodash';
import SimpleSchema from 'simpl-schema';
import { ROLE } from '../role/Role';
import { AcademicTerms } from '../academic-term/AcademicTermCollection';
import { Opportunities } from '../opportunity/OpportunityCollection';
import { Users } from '../user/UserCollection';
import { Courses } from '../course/CourseCollection';
import { ReviewDefine, ReviewUpdate, ReviewUpdateData } from '../../typings/radgrad';
import BaseCollection from '../base/BaseCollection';
/**
* Represents a course or opportunity review by a student.
* @extends api/base.BaseSlugCollection
* @memberOf api/review
*/
class ReviewCollection extends BaseCollection {
public COURSE: string;
public OPPORTUNITY: string;
/**
* Creates the Review collection.
*/
constructor() {
super('Review', new SimpleSchema({
studentID: { type: SimpleSchema.RegEx.Id },
reviewType: { type: String },
revieweeID: { type: SimpleSchema.RegEx.Id },
termID: { type: SimpleSchema.RegEx.Id },
rating: { type: SimpleSchema.Integer },
comments: { type: String },
moderated: { type: Boolean },
visible: { type: Boolean },
moderatorComments: { type: String, optional: true },
retired: { type: Boolean, optional: true },
}));
this.COURSE = 'course';
this.OPPORTUNITY = 'opportunity';
this.defineSchema = new SimpleSchema({
student: String,
reviewType: String,
reviewee: String,
academicTerm: String,
rating: { type: SimpleSchema.Integer, optional: true },
comments: String,
moderated: { type: Boolean, optional: true },
visible: { type: Boolean, optional: true },
moderatorComments: { type: String, optional: true },
retired: { type: Boolean, optional: true },
});
this.updateSchema = new SimpleSchema({
academicTerm: { type: String, optional: true },
rating: { type: SimpleSchema.Integer, optional: true },
comments: { type: String, optional: true },
moderated: { type: Boolean, optional: true },
visible: { type: Boolean, optional: true },
moderatorComments: { type: String, optional: true },
retired: { type: Boolean, optional: true },
});
}
/**
* Defines a new Review.
* @example
* Review.define({ student: 'abi@hawaii.edu',
* reviewType: 'course',
* reviewee: 'ics_111',
* academicTerm: 'Fall-2016',
* rating: 3,
* comments: 'This class is great!',
* moderated: false,
* visible: true,
* moderatedComments: 'sample comments here',
* retired: false});
* @param { Object } description Object with keys slug, student, reviewee,
* reviewType,academicTerm, rating, comments, moderated, public, and moderatorComments.
* Student must be a user with role 'STUDENT.'
* ReviewType must be either 'course' or 'opportunity'.
* Reviewee must be a defined course or opportunity slug, depending upon reviewType.
* academicTerm must be a defined slug.
* Moderated is optional and defaults to false.
* Visible is optional and defaults to true.
* ModeratorComments is optional.
* @throws {Meteor.Error} If the definition includes a defined slug, undefined student,
* undefined reviewee, undefined academicTerm, or invalid rating.
* @returns The newly created docID.
*/
public define({ student, reviewType, reviewee, academicTerm, rating = 3, comments, moderated = false, visible = true, moderatorComments, retired = false }: ReviewDefine) {
// Validate student, get studentID.
const studentID = Users.getID(student);
Users.assertInRole(studentID, [ROLE.STUDENT, ROLE.ALUMNI]);
// Validate reviewType, get revieweeID and assign slug if not provided.
this.assertValidReviewType(reviewType);
let revieweeID;
if (reviewType === this.COURSE) {
revieweeID = Courses.getID(reviewee);
} else if (reviewType === this.OPPORTUNITY) {
revieweeID = Opportunities.getID(reviewee);
}
// Validate academicTerm, get termID.
const termID = AcademicTerms.getID(academicTerm);
// Validate rating.
this.assertValidRating(rating);
// Guarantee that moderated and public are booleans.
moderated = !!moderated; // eslint-disable-line no-param-reassign
visible = !!visible; // eslint-disable-line no-param-reassign
// Check to see if the review exists.
const doc = this.collection.findOne({ studentID, reviewType, revieweeID, termID });
if (doc) {
throw new Meteor.Error(`Cannot create two reviews for ${reviewee} in the same term.`);
}
// Define the new Review.
const reviewID = this.collection.insert({
studentID,
reviewType,
revieweeID,
termID,
rating,
comments,
moderated,
visible,
moderatorComments,
retired,
});
// Return the id to the newly created Review.
return reviewID;
}
/**
* Throws an error if rating is not an integer between 1 and 5.
* @param rating the rating.
*/
public assertValidRating(rating: number) {
if (!_.isInteger(rating) || !_.inRange(rating, 1, 6)) {
throw new Meteor.Error(`Invalid rating: ${rating}`);
}
}
/**
* Throws an error if reviewType is not 'opportunity' or 'collection'.
* @param reviewType The review type.
*/
public assertValidReviewType(reviewType: string) {
if (!([this.OPPORTUNITY, this.COURSE].includes(reviewType))) {
throw new Meteor.Error(`Invalid reviewType: ${reviewType}`);
}
}
/**
* Update the review. Only academicTerm, rating, comments, moderated, visible, and moderatorComments can be updated.
* @param docID The review docID (required).
*/
public update(docID, { academicTerm, rating, comments, moderated, visible, moderatorComments, retired }: ReviewUpdate) {
this.assertDefined(docID);
const updateData: ReviewUpdateData = {};
if (academicTerm) {
updateData.termID = AcademicTerms.getID(academicTerm);
}
if (_.isNumber(rating)) {
this.assertValidRating(rating);
updateData.rating = rating;
}
if (comments) {
updateData.comments = comments;
}
if (_.isBoolean(moderated)) {
updateData.moderated = moderated;
}
if (_.isBoolean(visible)) {
updateData.visible = !!visible;
}
if (moderatorComments) {
updateData.moderatorComments = moderatorComments;
}
if (_.isBoolean(retired)) {
updateData.retired = retired;
}
this.collection.update(docID, { $set: updateData });
}
/**
* Remove the review.
* @param docID The docID of the review.
*/
public removeIt(docID: string) {
this.assertDefined(docID);
return super.removeIt(docID);
}
/**
* Removes all CourseInstance documents referring to user.
* @param user The user, either the ID or the username.
* @throws { Meteor.Error } If user is not an ID or username.
*/
public removeUser(user: string) {
const studentID = Users.getID(user);
this.collection.remove({ studentID });
}
/**
* Implementation of assertValidRoleForMethod. Asserts that userId is logged in as an Admin, Advisor or
* Student.
* This is used in the define, update, and removeIt Meteor methods associated with each class.
* @param userId The userId of the logged in user. Can be null or undefined
* @throws { Meteor.Error } If there is no logged in user, or the user is not an Admin or Advisor.
*/
public assertValidRoleForMethod(userId: string) {
this.assertRole(userId, [ROLE.ADMIN, ROLE.ADVISOR, ROLE.STUDENT]);
}
/**
* Returns an array of strings, each one representing an integrity problem with this collection.
* Returns an empty array if no problems were found.
* Checks slugID, opportunityTypeID, sponsorID, interestIDs, termIDs
* @returns {Array} A (possibly empty) array of strings indicating integrity issues.
*/
public checkIntegrity() {
const problems = [];
this.find().forEach((doc) => {
if (!Users.isDefined(doc.studentID)) {
problems.push(`Bad studentID: ${doc.studentID}`);
}
if (!Opportunities.isDefined(doc.revieweeID) && !Courses.isDefined(doc.revieweeID)) {
problems.push(`Bad reviewee: ${doc.revieweeID}`);
}
if (!AcademicTerms.isDefined(doc.termID)) {
problems.push(`Bad studentID: ${doc.termID}`);
}
});
return problems;
}
/**
* Updates the Review's modified, visible, and moderatorComments variables.
* @param reviewID The review ID.
* @param moderated The new moderated value.
* @param visible The new visible value.
* @param moderatorComments The new moderatorComments value.
*/
public updateModerated(reviewID: string, moderated: boolean, visible: boolean, moderatorComments: string) {
this.assertDefined(reviewID);
this.collection.update({ _id: reviewID },
{ $set: { moderated, visible, moderatorComments } });
}
/**
* Returns an object representing the Review docID in a format acceptable to define().
* @param docID The docID of an Review.
* @returns { Object } An object representing the definition of docID.
*/
public dumpOne(docID: string): ReviewDefine {
const doc = this.findDoc(docID);
const student = Users.getProfile(doc.studentID).username;
const reviewType = doc.reviewType;
let reviewee;
if (reviewType === this.COURSE) {
reviewee = Courses.findSlugByID(doc.revieweeID);
} else if (reviewType === this.OPPORTUNITY) {
reviewee = Opportunities.findSlugByID(doc.revieweeID);
}
const academicTerm = AcademicTerms.findSlugByID(doc.termID);
const rating = doc.rating;
const comments = doc.comments;
const moderated = doc.moderated;
const visible = doc.visible;
const moderatorComments = doc.moderatorComments;
const retired = doc.retired;
return {
student,
reviewType,
reviewee,
academicTerm,
rating,
comments,
moderated,
visible,
moderatorComments,
retired,
};
}
/**
* Dumps all the Reviews for the given usernameOrID.
* @param {string} usernameOrID
* @return {ReviewDefine[]}
*/
public dumpUser(usernameOrID: string): ReviewDefine[] {
const profile = Users.getProfile(usernameOrID);
const studentID = profile.userID;
const retVal = [];
const instances = this.find({ studentID }).fetch();
instances.forEach((instance) => {
retVal.push(this.dumpOne(instance._id));
});
return retVal;
}
}
/**
* Provides the singleton instance of this class to all other entities.
* @type {api/review.ReviewCollection}
* @memberOf api/review
*/
export const Reviews = new ReviewCollection();