Source

api/user/StudentProfileCollection.methods.ts

  1. import { ValidatedMethod } from 'meteor/mdg:validated-method';
  2. import { CallPromiseMixin } from 'meteor/didericis:callpromise-mixin';
  3. import { Meteor } from 'meteor/meteor';
  4. import moment from 'moment';
  5. import { StudentProfile } from '../../typings/radgrad';
  6. import { AcademicTerms } from '../academic-term/AcademicTermCollection';
  7. import { CourseInstances } from '../course/CourseInstanceCollection';
  8. import { OpportunityInstances } from '../opportunity/OpportunityInstanceCollection';
  9. import { RadGradProperties } from '../radgrad/RadGradProperties';
  10. import { Reviews } from '../review/ReviewCollection';
  11. import { VerificationRequests } from '../verification/VerificationRequestCollection';
  12. import { Users } from './UserCollection';
  13. import { CareerGoals } from '../career/CareerGoalCollection';
  14. import { StudentProfiles } from './StudentProfileCollection';
  15. import { FacultyProfiles } from './FacultyProfileCollection';
  16. import { AdvisorProfiles } from './AdvisorProfileCollection';
  17. import { AdminProfiles } from './AdminProfileCollection';
  18. import { ProfileCareerGoals } from './profile-entries/ProfileCareerGoalCollection';
  19. import { ProfileInterests } from './profile-entries/ProfileInterestCollection';
  20. import { Interests } from '../interest/InterestCollection';
  21. import { ProfileCourses } from './profile-entries/ProfileCourseCollection';
  22. import { Courses } from '../course/CourseCollection';
  23. import { ProfileOpportunities } from './profile-entries/ProfileOpportunityCollection';
  24. import { Opportunities } from '../opportunity/OpportunityCollection';
  25. import { ROLE } from '../role/Role';
  26. export interface PublicProfileData {
  27. website?: string;
  28. picture?: string;
  29. level?: number;
  30. ice?: { i; c; e };
  31. careerGoals?: string[];
  32. interests?: string[];
  33. courses?: string[];
  34. opportunities?: string[];
  35. }
  36. /**
  37. * Helper function to create and return an object with fields for all the publicly shared profile data for username.
  38. * @param username The user who's public data is to be shared.
  39. * @returns {PublicProfileData}
  40. */
  41. const generatePublicProfileDataObject = (username) => {
  42. const publicData: PublicProfileData = {};
  43. if (Meteor.isServer) {
  44. const profile = Users.getProfile(username);
  45. const userID = Users.getID(username);
  46. // Advisors, Faculty, Admins should share everything by default
  47. // when their accounts are created, but can manually opt-out if they want.
  48. if (profile.sharePicture) {
  49. publicData.picture = profile.picture;
  50. }
  51. if (profile.shareWebsite) {
  52. publicData.website = profile.website;
  53. }
  54. if (profile.shareLevel) {
  55. publicData.level = profile.level;
  56. }
  57. if (profile.shareICE) {
  58. // if shareICE exists, then the user must be a student.
  59. publicData.ice = StudentProfiles.getEarnedICE(username);
  60. }
  61. if (profile.shareCareerGoals) {
  62. const profileDocs = ProfileCareerGoals.findNonRetired({ userID });
  63. const careerGoalSlugs = profileDocs.map((doc) => CareerGoals.findSlugByID(doc.careerGoalID));
  64. publicData.careerGoals = CareerGoals.sort(careerGoalSlugs);
  65. }
  66. if (profile.shareInterests) {
  67. const profileDocs = ProfileInterests.findNonRetired({ userID });
  68. const interestSlugs = profileDocs.map((doc) => Interests.findSlugByID(doc.interestID));
  69. publicData.interests = Interests.sort(interestSlugs);
  70. }
  71. if (profile.shareCourses) {
  72. const profileDocs = ProfileCourses.findNonRetired({ userID });
  73. const courseSlugs = profileDocs.map((doc) => Courses.findSlugByID(doc.courseID));
  74. publicData.courses = Courses.sort(courseSlugs);
  75. }
  76. if (profile.shareOpportunities) {
  77. const profileDocs = ProfileOpportunities.findNonRetired({ userID });
  78. const opportunitySlugs = profileDocs.map((doc) => Opportunities.findSlugByID(doc.opportunityID));
  79. publicData.opportunities = Opportunities.sort(opportunitySlugs);
  80. }
  81. }
  82. return publicData;
  83. };
  84. /**
  85. * Meteor method used to retrieve public data for a student profile card.
  86. * Returns an object with fields containing the visible profile data.
  87. */
  88. export const getPublicProfileData = new ValidatedMethod({
  89. name: 'ProfileCollection.getPublicProfileData',
  90. mixins: [CallPromiseMixin],
  91. validate: null,
  92. run({ username }) {
  93. if (Meteor.isServer) {
  94. return generatePublicProfileDataObject(username);
  95. }
  96. return null;
  97. },
  98. });
  99. /**
  100. * Meteor method to set a Profile field (usually a "share" field).
  101. * After setting the share value, generates and returns an object with fields containing the visible profile data.
  102. */
  103. export const setPublicProfileData = new ValidatedMethod({
  104. name: 'ProfileCollection.setPublicProfileData',
  105. mixins: [CallPromiseMixin],
  106. validate: null,
  107. run({ username, fieldName, fieldValue }) {
  108. // console.log(username, fieldName, fieldValue);
  109. if (Meteor.isServer) {
  110. const profile = Users.getProfile(username);
  111. let profileCollection;
  112. if (profile.role === ROLE.STUDENT) {
  113. profileCollection = StudentProfiles;
  114. } else if (profile.role === ROLE.FACULTY) {
  115. profileCollection = FacultyProfiles;
  116. } else if (profile.role === ROLE.ADVISOR) {
  117. profileCollection = AdvisorProfiles;
  118. } else {
  119. profileCollection = AdminProfiles;
  120. }
  121. const updateObject = {};
  122. updateObject[fieldName] = fieldValue;
  123. profileCollection.update(profile._id, updateObject);
  124. }
  125. // Now that we've updated the profile collection's share field, generate the new set of public data and return.
  126. return generatePublicProfileDataObject(username);
  127. },
  128. });
  129. export const getLastAcademicTermMethod = new ValidatedMethod({
  130. name: 'StudentProfile.getLastAcademicTerm',
  131. mixins: [CallPromiseMixin],
  132. validate: null,
  133. run(user: string) {
  134. if (Meteor.isServer) {
  135. return StudentProfiles.getLastAcademicTerm(user);
  136. }
  137. return null;
  138. },
  139. });
  140. const updateStudentToAlumni = (student: StudentProfile): boolean => {
  141. const oneYearInTerms = RadGradProperties.getQuarterSystem() ? 4 : 3;
  142. const currentTermNumber = AcademicTerms.getCurrentAcademicTermNumber();
  143. const studentLastTermNumber = StudentProfiles.getLastAcademicTerm(student.username).termNumber;
  144. if (currentTermNumber > studentLastTermNumber + oneYearInTerms) {
  145. StudentProfiles.update(student._id, { isAlumni: true });
  146. return true;
  147. }
  148. return false;
  149. };
  150. export const updateStudentToAlumniMethod = new ValidatedMethod({
  151. name: 'StudentProfile.updateStudentToAlumni',
  152. mixins: [CallPromiseMixin],
  153. validate: null,
  154. run(user: string) {
  155. if (Meteor.isServer) {
  156. const profile = Users.getProfile(user);
  157. if (updateStudentToAlumni(profile)) {
  158. return `${user}'s last academic term was over a year ago.`;
  159. }
  160. return `${user} is still current`;
  161. }
  162. return '';
  163. },
  164. });
  165. export const updateAllStudentsToAlumniMethod = new ValidatedMethod({
  166. name: 'StudentProfile.updateAllStudentsToAlumni',
  167. mixins: [CallPromiseMixin],
  168. validate: null,
  169. run(user: string) {
  170. if (Meteor.isServer) {
  171. const students = StudentProfiles.find({ isAlumni: false }).fetch();
  172. let count = 0;
  173. students.forEach((student) => {
  174. if (updateStudentToAlumni(student)) {
  175. count++;
  176. }
  177. });
  178. return `${count} students moved to alumni`;
  179. }
  180. return '';
  181. },
  182. });
  183. const retireOldStudent = (student: StudentProfile): boolean => {
  184. const twoYearInTerms = RadGradProperties.getQuarterSystem() ? 8 : 6;
  185. const currentTermNumber = AcademicTerms.getCurrentAcademicTermNumber();
  186. const studentLastTermNumber = StudentProfiles.getLastAcademicTerm(student.username).termNumber;
  187. if (currentTermNumber > studentLastTermNumber + twoYearInTerms) {
  188. StudentProfiles.update(student._id, { retired: true });
  189. return true;
  190. }
  191. return false;
  192. };
  193. export const retireOldStudentMethod = new ValidatedMethod({
  194. name: 'StudentProfile.retireOldStudent',
  195. mixins: [CallPromiseMixin],
  196. validate: null,
  197. run(student: string) {
  198. if (Meteor.isServer) {
  199. const profile = Users.getProfile(student);
  200. if (retireOldStudent(profile)) {
  201. return `Retired ${student} because their last academic term was over two years ago.`;
  202. }
  203. return `${student} is still current.`;
  204. }
  205. return '';
  206. },
  207. });
  208. export const retireAllOldStudentsMethod = new ValidatedMethod({
  209. name: 'StudentProfile.retireAllOldStudents',
  210. mixins: [CallPromiseMixin],
  211. validate: null,
  212. run() {
  213. if (Meteor.isServer) {
  214. const students = StudentProfiles.find({}).fetch();
  215. let count = 0;
  216. students.forEach((student) => {
  217. if (retireOldStudent(student)) {
  218. count++;
  219. }
  220. });
  221. return `Retired ${count} students.`;
  222. }
  223. return '';
  224. },
  225. });
  226. export const matriculateStudentMethod = new ValidatedMethod({
  227. name: 'StudentProfile.matriculateStudent',
  228. mixins: [CallPromiseMixin],
  229. validate: null,
  230. run(user: string) {
  231. if (Meteor.isServer) {
  232. StudentProfiles.assertValidRoleForMethod(this.userId);
  233. const profile = Users.getProfile(user);
  234. if (profile.role !== ROLE.STUDENT && profile.role !== ROLE.ALUMNI) {
  235. throw new Meteor.Error(`${profile.username} isn't a student`, 'You can only matriculate students.');
  236. }
  237. if (!profile.retired) {
  238. throw new Meteor.Error(`${profile.username} isn't retired`, 'Retire the student first.');
  239. }
  240. StudentProfiles.removeIt(profile.username);
  241. }
  242. },
  243. });
  244. const buildStudentDumpObject = (studentUsernameOrID: string) => {
  245. const timestamp = new Date();
  246. const collections = [];
  247. const studentProfileCollection = {
  248. name: StudentProfiles.getCollectionName(),
  249. contents: [],
  250. };
  251. const profile = Users.getProfile(studentUsernameOrID);
  252. studentProfileCollection.contents.push(StudentProfiles.dumpOne(profile._id));
  253. collections.push(studentProfileCollection);
  254. const courseInstanceCollection = {
  255. name: CourseInstances.getCollectionName(),
  256. contents: CourseInstances.dumpUser(studentUsernameOrID),
  257. };
  258. collections.push(courseInstanceCollection);
  259. const opportunityInstanceCollection = {
  260. name: OpportunityInstances.getCollectionName(),
  261. contents: OpportunityInstances.dumpUser(studentUsernameOrID),
  262. };
  263. collections.push(opportunityInstanceCollection);
  264. const profileCareerGoalCollection = {
  265. name: ProfileCareerGoals.getCollectionName(),
  266. contents: ProfileCareerGoals.dumpUser(studentUsernameOrID),
  267. };
  268. collections.push(profileCareerGoalCollection);
  269. const profileCourseCollection = {
  270. name: ProfileCourses.getCollectionName(),
  271. contents: ProfileCourses.dumpUser(studentUsernameOrID),
  272. };
  273. collections.push(profileCourseCollection);
  274. const profileInterestCollection = {
  275. name: ProfileInterests.getCollectionName(),
  276. contents: ProfileInterests.dumpUser(studentUsernameOrID),
  277. };
  278. collections.push(profileInterestCollection);
  279. const profileOpportunityCollection = {
  280. name: ProfileOpportunities.getCollectionName(),
  281. contents: ProfileOpportunities.dumpUser(studentUsernameOrID),
  282. };
  283. collections.push(profileOpportunityCollection);
  284. const reviewCollection = {
  285. name: Reviews.getCollectionName(),
  286. contents: Reviews.dumpUser(studentUsernameOrID),
  287. };
  288. collections.push(reviewCollection);
  289. const verificationCollection = {
  290. name: VerificationRequests.getCollectionName(),
  291. contents: VerificationRequests.dumpUser(studentUsernameOrID),
  292. };
  293. collections.push(verificationCollection);
  294. return { timestamp, collections };
  295. };
  296. export const dumpStudentMethod = new ValidatedMethod({
  297. name: 'StudentProfile.dumpStudent',
  298. mixins: [CallPromiseMixin],
  299. validate: null,
  300. run(studentUsernameOrID: string) {
  301. if (Meteor.isServer) {
  302. StudentProfiles.assertValidRoleForMethod(this.userId);
  303. const profile = Users.getProfile(studentUsernameOrID);
  304. if (profile.role !== ROLE.STUDENT && profile.role !== ROLE.ALUMNI) {
  305. throw new Meteor.Error(`${profile.username} isn't a student`, 'You can only dump students.');
  306. }
  307. return buildStudentDumpObject(studentUsernameOrID);
  308. }
  309. return {};
  310. },
  311. });
  312. const shouldMatriculate = (studentProfile: StudentProfile): boolean => {
  313. const threeYearInTerms = RadGradProperties.getQuarterSystem() ? 12 : 9;
  314. const currentTermNumber = AcademicTerms.getCurrentAcademicTermNumber();
  315. const studentLastTermNumber = StudentProfiles.getLastAcademicTerm(studentProfile.username).termNumber;
  316. return currentTermNumber > studentLastTermNumber + threeYearInTerms;
  317. };
  318. const getStudentsToMatriculate = (): string[] => {
  319. const students = StudentProfiles.find({}).fetch();
  320. const threeYearInTerms = RadGradProperties.getQuarterSystem() ? 12 : 9;
  321. const currentTermNumber = AcademicTerms.getCurrentAcademicTermNumber();
  322. const retVal = [];
  323. students.forEach((student) => {
  324. const studentLastTermNumber = StudentProfiles.getLastAcademicTerm(student.username).termNumber;
  325. if (currentTermNumber > studentLastTermNumber + threeYearInTerms) {
  326. retVal.push(student.username);
  327. }
  328. });
  329. return retVal;
  330. };
  331. export const matriculateAllOldStudentsMethod = new ValidatedMethod({
  332. name: 'StudentProfiles.matriculateAllOldStudents',
  333. mixins: [CallPromiseMixin],
  334. validate: null,
  335. run() {
  336. if (Meteor.isServer) {
  337. StudentProfiles.assertValidRoleForMethod(this.userId);
  338. const studentsToMatriculate = getStudentsToMatriculate();
  339. const retVal = [];
  340. const mDate = moment().format('YYYY-MM-DD');
  341. studentsToMatriculate.forEach((student) => {
  342. const fileName = `${student}-${mDate}`;
  343. const record = {
  344. fileName,
  345. contents: buildStudentDumpObject(student),
  346. };
  347. retVal.push(record);
  348. StudentProfiles.removeIt(student);
  349. });
  350. }
  351. },
  352. });
  353. export const updateAllStudentsStatusMethod = new ValidatedMethod({
  354. name: 'StudentProfiles.updateAllStudentsStatus',
  355. mixins: [CallPromiseMixin],
  356. validate: null,
  357. run() {
  358. if (Meteor.isServer) {
  359. StudentProfiles.assertValidRoleForMethod(this.userId);
  360. const students = StudentProfiles.find({}).fetch();
  361. let alumniCount = 0;
  362. let retiredCount = 0;
  363. let matriculatedCount = 0;
  364. const studentRecords = [];
  365. const mDate = moment().format('YYYY-MM-DD');
  366. students.forEach((student) => {
  367. if (updateStudentToAlumni(student)) {
  368. alumniCount++;
  369. }
  370. if (retireOldStudent(student)) {
  371. retiredCount++;
  372. }
  373. if (shouldMatriculate(student)) {
  374. matriculatedCount++;
  375. const fileName = `${student.username}-${mDate}`;
  376. const record = {
  377. fileName,
  378. contents: buildStudentDumpObject(student.username),
  379. };
  380. studentRecords.push(record);
  381. StudentProfiles.removeIt(student);
  382. }
  383. });
  384. return {
  385. alumniCount,
  386. retiredCount,
  387. matriculatedCount,
  388. studentRecords,
  389. };
  390. }
  391. return {};
  392. },
  393. });
  394. export const findOddStudentsMethod = new ValidatedMethod({
  395. name: 'StudentProfiles.findOddStudents',
  396. mixins: [CallPromiseMixin],
  397. validate: null,
  398. run() {
  399. const retVal = [];
  400. if (Meteor.isServer) {
  401. StudentProfiles.assertValidRoleForMethod(this.userId);
  402. const students = StudentProfiles.find({}).fetch();
  403. students.forEach((student) => {
  404. const studentID = student.userID;
  405. const cis = CourseInstances.find({ studentID }).fetch();
  406. const ois = OpportunityInstances.find({ studentID }).fetch();
  407. if (cis.length === 0 && ois.length === 0) {
  408. retVal.push(student);
  409. }
  410. });
  411. return retVal;
  412. }
  413. return retVal;
  414. },
  415. });