Source

api/opportunity/OpportunityCollection.ts

  1. import SimpleSchema from 'simpl-schema';
  2. import { Meteor } from 'meteor/meteor';
  3. import _ from 'lodash';
  4. import { CareerGoals } from '../career/CareerGoalCollection';
  5. import { Courses } from '../course/CourseCollection';
  6. import { Internships } from '../internship/InternshipCollection';
  7. import { Reviews } from '../review/ReviewCollection';
  8. import { Slugs } from '../slug/SlugCollection';
  9. import { Teasers } from '../teaser/TeaserCollection';
  10. import { Interests } from '../interest/InterestCollection';
  11. import { ROLE } from '../role/Role';
  12. import { ProfileOpportunities } from '../user/profile-entries/ProfileOpportunityCollection';
  13. import { Users } from '../user/UserCollection';
  14. import { OpportunityTypes } from './OpportunityTypeCollection';
  15. import { OpportunityInstances } from './OpportunityInstanceCollection';
  16. import BaseSlugCollection from '../base/BaseSlugCollection';
  17. import { assertICE, iceSchema } from '../ice/IceProcessor';
  18. import { CareerGoal, Course, Internship, OpportunityDefine, OpportunityUpdate, OpportunityUpdateData } from '../../typings/radgrad';
  19. export const defaultProfilePicture = '/images/radgrad_logo.png';
  20. /**
  21. * Represents an Opportunity, such as "LiveWire Internship".
  22. * To represent an Opportunity taken by a specific student in a specific academicTerm, use OpportunityInstance.
  23. * @extends api/base.BaseSlugCollection
  24. * @memberOf api/opportunity
  25. */
  26. class OpportunityCollection extends BaseSlugCollection {
  27. /**
  28. * Creates the Opportunity collection.
  29. */
  30. constructor() {
  31. super('Opportunity', new SimpleSchema({
  32. name: { type: String },
  33. slugID: { type: String },
  34. description: { type: String },
  35. opportunityTypeID: { type: SimpleSchema.RegEx.Id },
  36. sponsorID: { type: SimpleSchema.RegEx.Id },
  37. interestIDs: [SimpleSchema.RegEx.Id],
  38. // Optional data
  39. eventDate1: { type: Date, optional: true },
  40. eventDateLabel1: { type: String, optional: true },
  41. eventDate2: { type: Date, optional: true },
  42. eventDateLabel2: { type: String, optional: true },
  43. eventDate3: { type: Date, optional: true },
  44. eventDateLabel3: { type: String, optional: true },
  45. eventDate4: { type: Date, optional: true },
  46. eventDateLabel4: { type: String, optional: true },
  47. ice: { type: iceSchema, optional: true },
  48. picture: { type: String, optional: true, defaultValue: 'images/header-panel/header-opportunities.png' },
  49. retired: { type: Boolean, optional: true },
  50. }));
  51. }
  52. /**
  53. * Defines a new Opportunity.
  54. * @example
  55. * Opportunities.define({ name: 'ATT Hackathon',
  56. * slug: 'att-hackathon',
  57. * description: 'Programming challenge at Sacred Hearts Academy, $10,000 prize',
  58. * opportunityType: 'event',
  59. * sponsor: 'philipjohnson',
  60. * ice: { i: 10, c: 0, e: 10},
  61. * interests: ['software-engineering'],
  62. * });
  63. * @param { Object } description Object with keys name, slug, description, opportunityType, sponsor, interests,
  64. * @param name the name of the opportunity.
  65. * @param slug must not be previously defined.
  66. * @param description the description of the opportunity. Can be markdown.
  67. * @param opportunityType must be defined slug.
  68. * @param interests must be a (possibly empty) array of interest slugs or IDs.
  69. * @param sponsor must be a User with role 'FACULTY', 'ADVISOR', or 'ADMIN'.
  70. * @param ice must be a valid ICE object.
  71. * @param eventDate1
  72. * @param eventDateLabel1
  73. * @param eventDate2
  74. * @param eventDateLabel2
  75. * @param eventDate3
  76. * @param eventDateLabel3
  77. * @param eventDate4
  78. * @param eventDateLabel4
  79. * @param picture The URL to the opportunity picture. (optional, defaults to a default picture.)
  80. * @param retired optional, true if the opportunity is retired.
  81. * @throws {Meteor.Error} If the definition includes a defined slug or undefined interest, sponsor, opportunityType,
  82. * or startActive or endActive are not valid.
  83. * @returns The newly created docID.
  84. */
  85. public define({
  86. name,
  87. slug,
  88. description,
  89. opportunityType,
  90. sponsor,
  91. interests,
  92. ice,
  93. eventDate1,
  94. eventDateLabel1,
  95. eventDate2,
  96. eventDateLabel2,
  97. eventDate3,
  98. eventDateLabel3,
  99. eventDate4,
  100. eventDateLabel4,
  101. picture,
  102. retired = false,
  103. }: OpportunityDefine) {
  104. // Get instances, or throw error
  105. const opportunityTypeID = OpportunityTypes.getID(opportunityType);
  106. const sponsorID = Users.getID(sponsor);
  107. Users.assertInRole(sponsorID, [ROLE.FACULTY, ROLE.ADVISOR, ROLE.ADMIN]);
  108. assertICE(ice);
  109. const interestIDs = Interests.getIDs(interests);
  110. // check if slug is defined
  111. if (Slugs.isSlugForEntity(slug, this.getType())) {
  112. // console.log(`${slug} is already defined for ${this.getType()}`);
  113. return Slugs.getEntityID(slug, this.getType());
  114. }
  115. const slugID = Slugs.define({ name: slug, entityName: this.getType() });
  116. const opportunityID = this.collection.insert({
  117. name, slugID, description, opportunityTypeID, sponsorID,
  118. interestIDs, ice, eventDate1, eventDateLabel1, eventDate2, eventDateLabel2,
  119. eventDate3, eventDateLabel3, eventDate4, eventDateLabel4, retired, picture,
  120. });
  121. Slugs.updateEntityID(slugID, opportunityID);
  122. // console.log(opportunityID);
  123. // Return the id to the newly created Opportunity.
  124. return opportunityID;
  125. }
  126. /**
  127. * Update an Opportunity.
  128. * @param instance The docID or slug associated with this opportunity.
  129. * @param name a string (optional).
  130. * @param description a string (optional).
  131. * @param opportunityType docID or slug (optional).
  132. * @param sponsor user in role admin, advisor, or faculty. (optional).
  133. * @param interests array of slugs or IDs, (optional).
  134. * @param academicTerms array of slugs or IDs, (optional).
  135. * @param eventDate a Date (optional). // Deprecated
  136. * @param ice An ICE object (optional).
  137. * @param retired boolean (optional).
  138. * @param picture a string (optional).
  139. */
  140. public update(instance: string, {
  141. name,
  142. description,
  143. opportunityType,
  144. sponsor,
  145. interests,
  146. eventDate1,
  147. eventDateLabel1,
  148. eventDate2,
  149. eventDateLabel2,
  150. eventDate3,
  151. eventDateLabel3,
  152. eventDate4,
  153. eventDateLabel4,
  154. clearEventDate1,
  155. clearEventDate2,
  156. clearEventDate3,
  157. clearEventDate4,
  158. ice,
  159. retired,
  160. picture,
  161. }: OpportunityUpdate) {
  162. // console.log('OpportunityCollection.update');
  163. const docID = this.getID(instance);
  164. // const unsetData: any = {};
  165. const updateData: OpportunityUpdateData = {};
  166. if (name) {
  167. updateData.name = name;
  168. }
  169. if (description) {
  170. updateData.description = description;
  171. }
  172. if (opportunityType) {
  173. updateData.opportunityTypeID = OpportunityTypes.getID(opportunityType);
  174. }
  175. if (sponsor) {
  176. const sponsorID = Users.getID(sponsor);
  177. Users.assertInRole(sponsorID, [ROLE.FACULTY, ROLE.ADVISOR, ROLE.ADMIN]);
  178. updateData.sponsorID = sponsorID;
  179. }
  180. if (interests) {
  181. updateData.interestIDs = Interests.getIDs(interests);
  182. }
  183. if (eventDate1) {
  184. updateData.eventDate1 = eventDate1;
  185. }
  186. if (_.isString(eventDateLabel1)) {
  187. updateData.eventDateLabel1 = eventDateLabel1;
  188. }
  189. if (eventDate2) {
  190. updateData.eventDate2 = eventDate2;
  191. }
  192. if (_.isString(eventDateLabel2)) {
  193. updateData.eventDateLabel2 = eventDateLabel2;
  194. }
  195. if (eventDate3) {
  196. updateData.eventDate3 = eventDate3;
  197. }
  198. if (_.isString(eventDateLabel3)) {
  199. updateData.eventDateLabel3 = eventDateLabel3;
  200. }
  201. if (eventDate4) {
  202. updateData.eventDate4 = eventDate4;
  203. }
  204. if (_.isString(eventDateLabel4)) {
  205. updateData.eventDateLabel4 = eventDateLabel4;
  206. }
  207. if (ice) {
  208. assertICE(ice);
  209. updateData.ice = ice;
  210. }
  211. if (_.isBoolean(retired)) {
  212. updateData.retired = retired;
  213. const profileOpportunities = ProfileOpportunities.find({ opportunityID: docID }).fetch();
  214. profileOpportunities.forEach((po) => ProfileOpportunities.update(po._id, { retired }));
  215. const reviews = Reviews.find({ revieweeID: docID }).fetch();
  216. reviews.forEach((review) => Reviews.update(review._id, { retired }));
  217. const opportunity = this.findDoc(docID);
  218. const teasers = Teasers.find({ targetSlugID: opportunity.slugID }).fetch();
  219. teasers.forEach((teaser) => Teasers.update(teaser._id, { retired }));
  220. }
  221. if (picture) {
  222. updateData.picture = picture;
  223. }
  224. if (clearEventDate1) {
  225. updateData.eventDate1 = null;
  226. updateData.eventDateLabel1 = '';
  227. }
  228. if (clearEventDate2) {
  229. updateData.eventDate2 = null;
  230. updateData.eventDateLabel2 = '';
  231. }
  232. if (clearEventDate3) {
  233. updateData.eventDate3 = null;
  234. updateData.eventDateLabel3 = '';
  235. }
  236. if (clearEventDate4) {
  237. updateData.eventDate4 = null;
  238. updateData.eventDateLabel4 = '';
  239. }
  240. // console.log(updateData);
  241. this.collection.update(docID, { $set: updateData });
  242. }
  243. /**
  244. * Remove the Opportunity.
  245. * @param instance The docID or slug of the entity to be removed.
  246. * @throws { Meteor.Error } If docID is not a Opportunity, or if this Opportunity has any associated opportunity instances.
  247. */
  248. public removeIt(instance: string) {
  249. // console.log('OpportunityCollection.removeIt', instance);
  250. const docID = this.getID(instance);
  251. // Check that this opportunity is not referenced by any Opportunity Instance.
  252. const instances = OpportunityInstances.find({ opportunityID: docID }).fetch();
  253. if (instances.length > 0) {
  254. throw new Meteor.Error(`Opportunity ${instance} is referenced by an OpportunityInstances`);
  255. }
  256. // Check that this opportunity is not referenced by any Teaser.
  257. const oppDoc = this.findDoc(docID);
  258. const teasers = Teasers.find({ targetSlugID: oppDoc.slugID }).fetch();
  259. if (teasers.length > 0) {
  260. throw new Meteor.Error(`Opportunity ${instance} referenced by a teaser.`);
  261. }
  262. return super.removeIt(docID);
  263. }
  264. /**
  265. * Asserts that userId is logged in as an Admin, Faculty, or Advisor.
  266. * This is used in the define, update, and removeIt Meteor methods associated with each class.
  267. * @param userId The userId of the logged in user. Can be null or undefined
  268. * @throws { Meteor.Error } If there is no logged in user, or the user is not in the allowed roles.
  269. */
  270. public assertValidRoleForMethod(userId: string) {
  271. this.assertRole(userId, [ROLE.ADMIN, ROLE.ADVISOR, ROLE.FACULTY]);
  272. }
  273. /**
  274. * Returns the OpportunityType associated with the Opportunity with the given instanceID.
  275. * @param instanceID The id of the Opportunity.
  276. * @returns {Object} The associated Opportunity.
  277. * @throws {Meteor.Error} If instanceID is not a valid ID.
  278. */
  279. public getOpportunityTypeDoc(instanceID: string) {
  280. this.assertDefined(instanceID);
  281. const instance = this.collection.findOne({ _id: instanceID });
  282. return OpportunityTypes.findDoc(instance.opportunityTypeID);
  283. }
  284. /**
  285. * Returns an array of strings, each one representing an integrity problem with this collection.
  286. * Returns an empty array if no problems were found.
  287. * Checks slugID, opportunityTypeID, sponsorID, interestIDs, termIDs
  288. * @returns {Array} A (possibly empty) array of strings indicating integrity issues.
  289. */
  290. public checkIntegrity() {
  291. const problems = [];
  292. this.find().forEach((doc) => {
  293. if (!Slugs.isDefined(doc.slugID)) {
  294. problems.push(`Bad slugID: ${doc.slugID}`);
  295. }
  296. if (!OpportunityTypes.isDefined(doc.opportunityTypeID)) {
  297. problems.push(`Bad opportunityTypeID: ${doc.opportunityTypeID}`);
  298. }
  299. if (!Users.isDefined(doc.sponsorID)) {
  300. problems.push(`Bad sponsorID: ${doc.sponsorID}`);
  301. }
  302. doc.interestIDs.forEach((interestID) => {
  303. if (!Interests.isDefined(interestID)) {
  304. problems.push(`Bad interestID: ${interestID}`);
  305. }
  306. });
  307. });
  308. return problems;
  309. }
  310. /**
  311. * Returns true if Opportunity has the specified interest.
  312. * @param opportunity The opportunity(docID or slug)
  313. * @param interest The Interest (docID or slug).
  314. * @returns {boolean} True if the opportunity has the associated Interest.
  315. * @throws { Meteor.Error } If opportunity is not a opportunity or interest is not a Interest.
  316. */
  317. public hasInterest(opportunity: string, interest: string) {
  318. const interestID = Interests.getID(interest);
  319. const doc = this.findDoc(opportunity);
  320. return ((doc.interestIDs).includes(interestID));
  321. }
  322. /**
  323. * Returns a list of CareerGoals that have common interests.
  324. * @param {string} docIdOrSlug an opportunity ID or slug.
  325. * @return {CareerGoals[]} Courses that have common interests.
  326. */
  327. public findRelatedCareerGoals(docIdOrSlug: string): CareerGoal[] {
  328. const docID = this.getID(docIdOrSlug);
  329. const interestIDs = this.findDoc(docID).interestIDs;
  330. const goals = CareerGoals.findNonRetired();
  331. return goals.filter((goal) => goal.interestIDs.filter((x) => interestIDs.includes(x)).length > 0);
  332. }
  333. /**
  334. * Returns a list of Courses that have common interests.
  335. * @param {string} docIdOrSlug an opportunity ID or slug.
  336. * @return {Course[]} Courses that have common interests.
  337. */
  338. public findRelatedCourses(docIdOrSlug: string): Course[] {
  339. const docID = this.getID(docIdOrSlug);
  340. const interestIDs = this.findDoc(docID).interestIDs;
  341. const courses = Courses.findNonRetired();
  342. return courses.filter((course) => course.interestIDs.filter((x) => interestIDs.includes(x)).length > 0);
  343. }
  344. /**
  345. * Returns a list of Internships that have common interests.
  346. * @param {string} docIdOrSlug an opportunity ID or slug.
  347. * @return {Internship[]} Internships that have common interests.
  348. */
  349. public findRelatedInternships(docIdOrSlug: string): Internship[] {
  350. const docID = this.getID(docIdOrSlug);
  351. const interestIDs = this.findDoc(docID).interestIDs;
  352. const internships = Internships.findNonRetired();
  353. return internships.filter((internship) => internship.interestIDs.filter(x => interestIDs.includes(x)).length > 0);
  354. }
  355. /**
  356. * Returns a list of Opportunity names corresponding to the passed list of Opportunity docIDs.
  357. * @param instanceIDs A list of Opportunity docIDs.
  358. * @returns { Array }
  359. * @throws { Meteor.Error} If any of the instanceIDs cannot be found.
  360. */
  361. public findNames(instanceIDs: string[]) {
  362. // console.log('Opportunity.findNames(%o)', instanceIDs);
  363. return instanceIDs.map((instanceID) => this.findDoc(instanceID).name);
  364. }
  365. /**
  366. * Returns an object representing the Opportunity docID in a format acceptable to define().
  367. * @param docID The docID of an Opportunity.
  368. * @returns { Object } An object representing the definition of docID.
  369. */
  370. public dumpOne(docID: string): OpportunityDefine {
  371. const doc = this.findDoc(docID);
  372. const name = doc.name;
  373. const slug = Slugs.getNameFromID(doc.slugID);
  374. const opportunityType = OpportunityTypes.findSlugByID(doc.opportunityTypeID);
  375. const sponsor = Users.getProfile(doc.sponsorID).username;
  376. const description = doc.description;
  377. const ice = doc.ice;
  378. const interests = doc.interestIDs.map((interestID) => Interests.findSlugByID(interestID));
  379. const eventDate1 = doc.eventDate1;
  380. const eventDate2 = doc.eventDate2;
  381. const eventDate3 = doc.eventDate3;
  382. const eventDate4 = doc.eventDate4;
  383. const eventDateLabel1 = doc.eventDateLabel1;
  384. const eventDateLabel2 = doc.eventDateLabel2;
  385. const eventDateLabel3 = doc.eventDateLabel3;
  386. const eventDateLabel4 = doc.eventDateLabel4;
  387. const picture = doc.picture;
  388. const retired = doc.retired;
  389. return {
  390. name,
  391. slug,
  392. description,
  393. opportunityType,
  394. sponsor,
  395. ice,
  396. interests,
  397. eventDate1,
  398. eventDate2,
  399. eventDate3,
  400. eventDate4,
  401. eventDateLabel1,
  402. eventDateLabel2,
  403. eventDateLabel3,
  404. eventDateLabel4,
  405. picture,
  406. retired,
  407. };
  408. }
  409. }
  410. /**
  411. * Provides the singleton instance of this class to all other entities.
  412. * @type {api/opportunity.OpportunityCollection}
  413. * @memberOf api/opportunity
  414. */
  415. export const Opportunities = new OpportunityCollection();