Source

api/user/BaseProfileCollection.ts

  1. import { Meteor } from 'meteor/meteor';
  2. import _ from 'lodash';
  3. import moment from 'moment';
  4. import SimpleSchema from 'simpl-schema';
  5. import BaseSlugCollection from '../base/BaseSlugCollection';
  6. import { AcademicYearInstances } from '../degree-plan/AcademicYearInstanceCollection';
  7. import { CourseInstances } from '../course/CourseInstanceCollection';
  8. import { OpportunityInstances } from '../opportunity/OpportunityInstanceCollection';
  9. import { Reviews } from '../review/ReviewCollection';
  10. import { Slugs } from '../slug/SlugCollection';
  11. import { Users } from './UserCollection';
  12. import { ROLE } from '../role/Role';
  13. import { VerificationRequests } from '../verification/VerificationRequestCollection';
  14. import { BaseProfile } from '../../typings/radgrad';
  15. import { ProfileCareerGoals } from './profile-entries/ProfileCareerGoalCollection';
  16. import { ProfileCourses } from './profile-entries/ProfileCourseCollection';
  17. import { ProfileInterests } from './profile-entries/ProfileInterestCollection';
  18. import { ProfileOpportunities } from './profile-entries/ProfileOpportunityCollection';
  19. export const defaultProfilePicture = '/images/default-profile-picture.png';
  20. // Technical debt add shareInternships? and other to this collection since advisors and faculty can now have Profile*
  21. /**
  22. * Set up the object to be used to map role names to their corresponding collections.
  23. * @memberOf api/user
  24. */
  25. const rolesToCollectionNames = {};
  26. rolesToCollectionNames[ROLE.ADVISOR] = 'AdvisorProfileCollection';
  27. rolesToCollectionNames[ROLE.FACULTY] = 'FacultyProfileCollection';
  28. rolesToCollectionNames[ROLE.STUDENT] = 'StudentProfileCollection';
  29. rolesToCollectionNames[ROLE.ADMIN] = 'AdminProfileCollection';
  30. /**
  31. * BaseProfileCollection is an abstract superclass of all profile collections.
  32. * @extends api/base.BaseSlugCollection
  33. * @memberOf api/user
  34. */
  35. class BaseProfileCollection extends BaseSlugCollection {
  36. constructor(type, schema) {
  37. super(
  38. type,
  39. schema.extend(
  40. new SimpleSchema({
  41. username: String,
  42. firstName: String,
  43. lastName: String,
  44. role: String,
  45. picture: { type: String, optional: true },
  46. website: { type: String, optional: true },
  47. userID: SimpleSchema.RegEx.Id,
  48. retired: { type: Boolean, optional: true },
  49. sharePicture: { type: Boolean, optional: true },
  50. shareWebsite: { type: Boolean, optional: true },
  51. shareCareerGoals: { type: Boolean, optional: true },
  52. shareCourses: { type: Boolean, optional: true },
  53. shareInterests: { type: Boolean, optional: true },
  54. shareOpportunities: { type: Boolean, optional: true },
  55. courseExplorerFilter: { type: String, optional: true }, // TODO is this still used?
  56. opportunityExplorerSortOrder: { type: String, optional: true }, // TODO is this still used?
  57. lastVisited: { type: Object, optional: true, blackbox: true },
  58. acceptedTermsAndConditions: { type: String, optional: true },
  59. refusedTermsAndConditions: { type: String, optional: true },
  60. }),
  61. ),
  62. );
  63. }
  64. /**
  65. * The subclass methods need a way to create a profile with a valid, though fake, userId.
  66. * @returns {string}
  67. */
  68. public getFakeUserId() {
  69. return 'ABCDEFGHJKLMNPQRS';
  70. }
  71. /**
  72. * Returns the name of the collection associated with the given profile.
  73. * @param profile A Profile object.
  74. * @returns { String } The name of a profile collection.
  75. */
  76. public getCollectionNameForProfile(profile: BaseProfile) {
  77. return rolesToCollectionNames[profile.role];
  78. }
  79. /**
  80. * Returns the Profile's docID associated with instance, or throws an error if it cannot be found.
  81. * If instance is a docID, then it is returned unchanged. If instance is a slug, its corresponding docID is returned.
  82. * If instance is the value for the username field in this collection, then return that document's ID.
  83. * If instance is the userID for the profile, then return the Profile's ID.
  84. * If instance is an object with an _id field, then that value is checked to see if it's in the collection.
  85. * @param { String } instance Either a valid docID, valid userID or a valid slug string.
  86. * @returns { String } The docID associated with instance.
  87. * @throws { Meteor.Error } If instance is not a docID or a slug.
  88. */
  89. public getID(instance) {
  90. let id;
  91. // If we've been passed a document, check to see if it has an _id field and use that if available.
  92. if (_.isObject(instance) && _.has(instance, '_id')) {
  93. // @ts-ignore
  94. instance = instance._id; // eslint-disable-line no-param-reassign, dot-notation
  95. }
  96. // If instance is the value of the username field for some document in the collection, then return its ID.
  97. const usernameBasedDoc = this.collection.findOne({ username: instance });
  98. if (usernameBasedDoc) {
  99. return usernameBasedDoc._id;
  100. }
  101. // If instance is the value of the userID field for some document in the collection, then return its ID.
  102. const userIDBasedDoc = this.collection.findOne({ userID: instance });
  103. if (userIDBasedDoc) {
  104. return userIDBasedDoc._id;
  105. }
  106. // Otherwise see if we can find instance as a docID or as a slug.
  107. try {
  108. id = this.collection.findOne({ _id: instance }) ? instance : this.findIdBySlug(instance);
  109. } catch (err) {
  110. throw new Meteor.Error(`Error in ${this.collectionName} getID(): Failed to convert ${instance} to an ID.`);
  111. }
  112. return id;
  113. }
  114. /**
  115. * Returns the profile associated with the specified user.
  116. * @param user The user (either their username (email) or their userID).
  117. * @return The profile document.
  118. * @throws { Meteor.Error } If user is not a valid user, or profile is not found.
  119. */
  120. public getProfile(user) {
  121. const userID = Users.getID(user);
  122. const doc = this.collection.findOne({ userID });
  123. if (!doc) {
  124. throw new Meteor.Error(`No profile found for user ${user}`);
  125. }
  126. return doc;
  127. }
  128. /**
  129. * Returns the profile document associated with username, or null if none was found.
  130. * @param username A username, such as 'johnson@hawaii.edu'.
  131. * @returns The profile document, or null.
  132. */
  133. public findByUsername(username) {
  134. return this.collection.findOne({ username });
  135. }
  136. /**
  137. * Returns non-null if the user has a profile in this collection.
  138. * @param user The user (either their username (email) or their userID).
  139. * @return The profile document if the profile exists, or null if not found.
  140. * @throws { Meteor.Error } If user is not a valid user.
  141. */
  142. public hasProfile(user) {
  143. const userID = Users.getID(user);
  144. return this.collection.findOne({ userID });
  145. }
  146. /**
  147. * Returns true if the user has set their picture.
  148. * @param user The user (either their username (email) or their userID).
  149. * @return {boolean}
  150. */
  151. public hasSetPicture(user) {
  152. const userID = Users.getID(user);
  153. const doc = this.collection.findOne({ userID });
  154. // console.log(doc);
  155. if (!doc) {
  156. return false;
  157. }
  158. return !(_.isNil(doc.picture) || doc.picture === defaultProfilePicture);
  159. }
  160. /**
  161. * Updates an entry in lastVisited for this profile with a new (or updated) timestamp for the given page.
  162. * @param user user The user (either their username (email) or their userID).
  163. * @param page The page. Should be one of PAGEIDS.
  164. */
  165. public updateLastVisitedEntry(user, pageID) {
  166. const userID = Users.getID(user);
  167. const doc = this.collection.findOne({ userID });
  168. if (!doc) {
  169. throw new Meteor.Error(`Error in updateLastVisited: Unknown ${user} for page ${pageID}`);
  170. }
  171. // ensure we have an object to update.
  172. const lastVisitedObject = doc.lastVisited || {};
  173. const oldTimestamp = lastVisitedObject[pageID];
  174. const newTimestamp = moment().format('YYYY-MM-DD');
  175. // Guarantee that we only call update when the timestamp has actually changed to avoid reactive re-rendering.
  176. if (oldTimestamp !== newTimestamp) {
  177. lastVisitedObject[pageID] = newTimestamp;
  178. const keyField = `lastVisited.${pageID}`;
  179. const setObject = {};
  180. setObject[keyField] = newTimestamp;
  181. this.collection.update({ userID }, { $set: setObject });
  182. }
  183. }
  184. /**
  185. * Returns the userID associated with the given profile.
  186. * @param profileID The ID of the profile.
  187. * @returns The associated userID.
  188. */
  189. public getUserID(profileID) {
  190. return this.collection.findOne(profileID).userID;
  191. }
  192. /**
  193. * Internal method for use by subclasses.
  194. * @param doc The profile document.
  195. * @returns {Array} An array of problems
  196. */
  197. protected checkIntegrityCommonFields(doc) {
  198. const problems = [];
  199. if (!Users.isDefined(doc.userID)) {
  200. problems.push(`Bad userID: ${doc.userID}`);
  201. }
  202. return problems;
  203. }
  204. /**
  205. * Removes this profile, given its profile ID.
  206. * Also removes this user from Meteor Accounts.
  207. * @param profileID The ID for this profile object.
  208. */
  209. public removeIt(profileID) {
  210. // console.log('BaseProfileCollection.removeIt', profileID);
  211. const profile = this.collection.findOne({ _id: profileID });
  212. const userID = profile.userID;
  213. if (!Users.isReferenced(userID)) {
  214. // Automatically remove references to user from other collections that are "private" to this user.
  215. [CourseInstances, OpportunityInstances, AcademicYearInstances, VerificationRequests, ProfileCareerGoals, ProfileCourses, ProfileInterests, ProfileOpportunities, Reviews].forEach((collection) => collection.removeUser(userID));
  216. Meteor.users.remove({ _id: userID });
  217. Slugs.getCollection().remove({ name: profile.username });
  218. return super.removeIt(profileID);
  219. }
  220. throw new Meteor.Error(`User ${profile.username} is a sponsor of un-retired Opportunities.`);
  221. }
  222. /**
  223. * Override the BaseCollection.publish. We only publish profiles if the user is logged in.
  224. */
  225. public publish(): void {
  226. if (Meteor.isServer) {
  227. Meteor.publish(this.collectionName, () => {
  228. if (!Meteor.user()) {
  229. return [];
  230. }
  231. return this.collection.find();
  232. });
  233. }
  234. }
  235. /**
  236. * Internal method for use by subclasses.
  237. * Destructively modifies updateData with the values of the passed fields.
  238. * Call this function for side-effect only.
  239. */
  240. protected updateCommonFields(
  241. updateData,
  242. {
  243. firstName,
  244. lastName,
  245. picture,
  246. website,
  247. retired,
  248. courseExplorerFilter,
  249. opportunityExplorerSortOrder,
  250. shareWebsite,
  251. sharePicture,
  252. shareInterests,
  253. shareCareerGoals,
  254. shareCourses,
  255. shareOpportunities,
  256. acceptedTermsAndConditions,
  257. refusedTermsAndConditions,
  258. },
  259. ) {
  260. // console.log('updateCommonFields', firstName, lastName, picture, website, retired, courseExplorerFilter, opportunityExplorerSortOrder, shareWebsite, sharePicture, shareInterests, shareCareerGoals, shareCourses, shareOpportunities, acceptedTermsAndConditions, refusedTermsAndConditions);
  261. if (firstName) {
  262. updateData.firstName = firstName; // eslint-disable-line no-param-reassign
  263. }
  264. if (lastName) {
  265. updateData.lastName = lastName; // eslint-disable-line no-param-reassign
  266. }
  267. if (_.isString(picture)) {
  268. updateData.picture = picture; // eslint-disable-line no-param-reassign
  269. }
  270. if (_.isString(website)) {
  271. updateData.website = website; // eslint-disable-line no-param-reassign
  272. }
  273. if (_.isBoolean(retired)) {
  274. updateData.retired = retired; // eslint-disable-line no-param-reassign
  275. }
  276. if (_.isString(courseExplorerFilter)) {
  277. updateData.courseExplorerFilter = courseExplorerFilter; // eslint-disable-line no-param-reassign
  278. }
  279. if (_.isString(opportunityExplorerSortOrder)) {
  280. updateData.opportunityExplorerSortOrder = opportunityExplorerSortOrder; // eslint-disable-line no-param-reassign
  281. }
  282. if (_.isBoolean(shareCareerGoals)) {
  283. updateData.shareCareerGoals = shareCareerGoals; // eslint-disable-line no-param-reassign
  284. }
  285. if (_.isBoolean(shareCourses)) {
  286. updateData.shareCourses = shareCourses; // eslint-disable-line no-param-reassign
  287. }
  288. if (_.isBoolean(shareInterests)) {
  289. updateData.shareInterests = shareInterests; // eslint-disable-line no-param-reassign
  290. }
  291. if (_.isBoolean(shareOpportunities)) {
  292. updateData.shareOpportunities = shareOpportunities; // eslint-disable-line no-param-reassign
  293. }
  294. if (_.isBoolean(sharePicture)) {
  295. updateData.sharePicture = sharePicture; // eslint-disable-line no-param-reassign
  296. }
  297. if (_.isBoolean(shareWebsite)) {
  298. updateData.shareWebsite = shareWebsite; // eslint-disable-line no-param-reassign
  299. }
  300. if (_.isString(acceptedTermsAndConditions)) {
  301. updateData.acceptedTermsAndConditions = acceptedTermsAndConditions; // eslint-disable-line no-param-reassign
  302. }
  303. if (_.isString(refusedTermsAndConditions)) {
  304. updateData.refusedTermsAndConditions = refusedTermsAndConditions; // eslint-disable-line no-param-reassign
  305. }
  306. // console.log('_updateCommonFields', updateData);
  307. }
  308. }
  309. /**
  310. * The BaseProfileCollection used by all Profile classes.
  311. * @memberOf api/user
  312. */
  313. export default BaseProfileCollection;