· 8 years ago · Aug 31, 2018, 12:44 AM
1// There are a lot of uses of `default` in switch statements here.
2// This makes that ok.
3
4// =================
5// L I B R A R I E S
6// =================
7const ABC = require("@akitabox/constants");
8const _ = require("underscore");
9const mongoose = require("mongoose");
10
11// =================
12// H E L P E R S
13// =================
14const displayDates = include("src/plugins/mongoose/displayDates");
15const toJSONAsync = include("src/plugins/mongoose/toJSONAsync");
16
17/**
18 * Model representing a user action.
19 *
20 * @constructs UserActionModel
21 *
22 * @property {String} route_name Route name - the name of the action the user took
23 * @property {Date} time Timestamp of the action
24 * @property {Number} status Status code returned for this request. Should be 200 unless
25 * there was an error or a redirect.
26 * @property {String} url_pathname URL pathname
27 * @property {String} method Method: GET, POST, PUT, DELETE, PATCH, OPTIONS, TRACE, CONNECT
28 * @property {Boolean} is_api_call True if action is API call
29 * @property {String} ip IP address
30 * @property {Application} calling_app App that made this request
31 * @property {String} email_domain Domain of the user's email address saved separately for easy
32 * querying
33 * @property {Account} user _id of the user taking the action's account
34 * @property {Object} body Request body
35 * @property {Object} query Query
36 * @property {String} browser Browser used
37 * @property {String} operating_system Operating System
38 * @property {String} user_agent User agent
39 * @property {String} action Action
40 * @property {Number} page Page
41 * @property {String} tab Tab
42 * @property {Account} account Building owner, NOT the user
43 * @property {Application} application Application
44 * @property {Attachment} attachment Attachment
45 * @property {Comment} comment Comment
46 * @property {Commit} commit Commit
47 * @property {Document} document Document
48 * @property {FutureTask} future_task Future task
49 * @property {Building} building Building _id
50 * @property {Level} level Level
51 * @property {ObjectId} portal Portal - references a building, but cannot be populated
52 * @property {QRCode} qrcode QR Code
53 * @property {Request} request Request
54 * @property {Commit} revision Commit ID containing the revision for the document
55 * @property {Role} role Role
56 * @property {Tag} tag Tag
57 * @property {TagCategory} tag_category Tag category
58 * @property {TagCategoryValue} tag_category_value Tag category value
59 * @property {Task} task Task
60 * @property {PinType} pin_type Pin type
61 * @property {PinTypeColor} pin_type_color Pin type color
62 * @property {PinField} pin_field Pin field
63 * @property {Asset} asset Asset
64 * @property {Room} room Room
65 * @property {PinValue} pin_value Pin value
66 * @property {FMRedirect} fm_redirect FM redirect
67 */
68let UserAction = new mongoose.Schema({
69 // Required Fields
70 route_name: { type: String, required: true, lowercase: true },
71 time: { type: Date, required: true, default: Date.now },
72 duration_ms: { type: Number, required: true, Min: 0 },
73 status: { type: Number, required: true, default: 200 },
74 url_pathname: { type: String, required: true },
75 method: {
76 type: String,
77 required: true,
78 uppercase: true,
79 enum: [
80 "GET",
81 "POST",
82 "PUT",
83 "DELETE",
84 "PATCH",
85 "OPTIONS",
86 "TRACE",
87 "CONNECT",
88 ],
89 },
90 is_api_call: { type: Boolean, required: true },
91 ip: { type: String, required: true },
92
93 // Optional Non-URL Fields
94 calling_app: { type: String, ref: "Application", default: null },
95 email_domain: { type: String, default: null },
96 user: { type: String, ref: "Account", default: null },
97 body: { type: Object, default: {} },
98 query: { type: Object, default: {} },
99 browser: { type: String, default: null },
100 operating_system: { type: String, default: null },
101 user_agent: { type: String, default: null },
102
103 // String URL Parameters
104 action: String,
105 page: Number,
106 tab: String,
107
108 // Mongo ID URL Parameters
109 account: { type: String, ref: "Account" },
110 attachment: { type: mongoose.Schema.Types.ObjectId, ref: "Attachment" },
111 application: { type: String, ref: "Application" },
112 comment: { type: mongoose.Schema.Types.ObjectId, ref: "Comment" },
113 commit: { type: mongoose.Schema.Types.ObjectId, ref: "Commit" },
114 document: { type: mongoose.Schema.Types.ObjectId, ref: "Document" },
115 future_task: { type: mongoose.Schema.Types.ObjectId, ref: "FutureTask" },
116 issue_type: { type: mongoose.Schema.Types.ObjectId, ref: "IssueType" },
117 building: { type: mongoose.Schema.Types.ObjectId, ref: "Building" },
118 level: { type: mongoose.Schema.Types.ObjectId, ref: "Level" },
119 maintenance_type: {
120 type: mongoose.Schema.Types.ObjectId,
121 ref: "MaintenanceType",
122 },
123 organization: { type: mongoose.Schema.Types.ObjectId, ref: "Organization" },
124 portal: { type: mongoose.Schema.Types.ObjectId },
125 qrcode: { type: mongoose.Schema.Types.ObjectId, ref: "QRCode" },
126 request: { type: mongoose.Schema.Types.ObjectId, ref: "Request" },
127 revision: { type: mongoose.Schema.Types.ObjectId, ref: "Commit" },
128 role: { type: mongoose.Schema.Types.ObjectId, ref: "Role" },
129 tag: { type: mongoose.Schema.Types.ObjectId, ref: "Tag" },
130 tag_category: { type: mongoose.Schema.Types.ObjectId, ref: "TagCategory" },
131 tag_category_value: {
132 type: mongoose.Schema.Types.ObjectId,
133 ref: "TagCategoryValue",
134 },
135 task: { type: mongoose.Schema.Types.ObjectId, ref: "Task" },
136 trade: { type: mongoose.Schema.Types.ObjectId, ref: "Trade" },
137 pin_type: { type: mongoose.Schema.Types.ObjectId, ref: "PinType" },
138 pin_type_color: { type: mongoose.Schema.Types.ObjectId, ref: "PinTypeColor" },
139 pin_field: { type: mongoose.Schema.Types.ObjectId, ref: "PinField" },
140 asset: { type: mongoose.Schema.Types.ObjectId, ref: "Asset" },
141 pin: { type: mongoose.Schema.Types.ObjectId, ref: "Pin" }, // FIXME: LEAVE THIS IN OR WE CAN'T MIGRATE ANYTHING!!!!!!!!!
142 room: { type: mongoose.Schema.Types.ObjectId, ref: "Room" },
143 pin_value: { type: mongoose.Schema.Types.ObjectId, ref: "PinValue" },
144 fm_redirect: { type: mongoose.Schema.Types.ObjectId, ref: "FMRedirect" },
145});
146
147// INDICES =========================================================================================================
148
149UserAction.index({ route_name: 1 });
150UserAction.index({ time: -1 });
151UserAction.index({ status: 1 });
152UserAction.index({ method: 1 });
153UserAction.index({ user: 1 });
154UserAction.index({ account: 1 });
155UserAction.index({ building: 1 });
156
157// PLUGINS =========================================================================================================
158
159UserAction.plugin(toJSONAsync);
160UserAction.plugin(displayDates, {
161 addDisplayAlt: true,
162});
163
164// VIRTUALS ========================================================================================================
165
166/**
167 * Creates a human-readable sentence for the action performed in this user_action to be used in entity-level
168 * activity feeds.
169 *
170 * @function getEntityActionDisplay
171 * @memberof UserActionModel.prototype
172 *
173 * @return {String} Action HTML
174 */
175UserAction.virtual("entity_action_display").get(
176 function getEntityActionDisplay() {
177 let importantMethods = ["PATCH", "POST", "PUT"];
178 let displayHelpers = this.model("UserAction").route_name()[
179 this.route_name.toUpperCase()
180 ];
181
182 if (
183 !_.contains(importantMethods, this.method) ||
184 this.status !== 200 ||
185 _.isUndefined(displayHelpers)
186 ) {
187 /*
188 * Do not care about GET, POST, DELETE, HEAD, OPTIONS, etc user_actions for entity activity feeds.
189 * Do not care about unsuccessful requests for entity activity feeds.
190 * If displayHelpers is undefined, missing route name.
191 */
192 return "";
193 }
194
195 let type = "entity";
196 let isHtml = false;
197 if ((this.method === "PATCH" || this.method === "PUT") && displayHelpers.updateActionDisplay) {
198 return displayHelpers.updateActionDisplay(type, isHtml, this);
199 } else if (this.method === "POST" && displayHelpers.createActionDisplay) {
200 return displayHelpers.createActionDisplay(type, isHtml, this);
201 }
202 // We had a display helper for the route_name but not the method.
203 return "";
204 }
205);
206
207/**
208 * Creates an html element for the action performed in this user_action to be used in entity-level activity feeds.
209 *
210 * @function getEntityActionHTML
211 * @memberof UserActionModel.prototype
212 *
213 * @return {String} Action HTML
214 */
215UserAction.virtual("entity_action_html").get(function getEntityActionHTML() {
216 let importantMethods = ["PATCH", "POST", "PUT"];
217 let displayHelpers = this.model("UserAction").route_name()[
218 this.route_name.toUpperCase()
219 ];
220
221 if (
222 !_.contains(importantMethods, this.method) ||
223 this.status !== 200 ||
224 _.isUndefined(displayHelpers)
225 ) {
226 /*
227 * Do not care about GET, POST, DELETE, HEAD, OPTIONS, etc user_actions for entity activity feeds.
228 * Do not care about unsuccessful requests for entity activity feeds.
229 * If displayHelpers is undefined, missing route name.
230 */
231 return "";
232 }
233
234 let type = "entity";
235 let isHtml = true;
236 if ((this.method === "PATCH" || this.method === "PUT") && displayHelpers.updateActionDisplay) {
237 return displayHelpers.updateActionDisplay(type, isHtml, this);
238 } else if (this.method === "POST" && displayHelpers.createActionDisplay) {
239 return displayHelpers.createActionDisplay(type, isHtml, this);
240 }
241 // We had a display helper for the route_name but not the method.
242 return "";
243});
244
245/**
246 * Creates a human-readable sentence for the action performed in this user_action to be used in building-level
247 * activity feeds.
248 *
249 * @function getBuildingActionDisplay
250 * @memberof UserActionModel.prototype
251 *
252 * @return {String} Action HTML
253 */
254UserAction.virtual("building_action_display").get(
255 function getBuildingActionDisplay() {
256 let importantMethods = ["POST", "PUT", "PATCH", "DELETE"];
257 let displayHelpers = this.model("UserAction").route_name()[
258 this.route_name.toUpperCase()
259 ];
260
261 if (
262 !_.contains(importantMethods, this.method) ||
263 this.status !== 200 ||
264 _.isUndefined(displayHelpers)
265 ) {
266 /*
267 * Do not care about GET, HEAD, OPTIONS, etc user_actions for building activity feeds.
268 * Do not care about unsuccessful requests for building activity feeds.
269 * If displayHelpers is undefined, missing route name.
270 */
271 return "";
272 }
273
274 let type = "building";
275 let isHtml = false;
276 if (this.method === "DELETE" && displayHelpers.deleteActionDisplay) {
277 return displayHelpers.deleteActionDisplay(type, isHtml, this);
278 } else if (this.method === "POST" && displayHelpers.createActionDisplay) {
279 return displayHelpers.createActionDisplay(type, isHtml, this);
280 } else if (
281 (this.method === "PUT" || this.method === "PATCH") &&
282 displayHelpers.updateActionDisplay
283 ) {
284 return displayHelpers.updateActionDisplay(type, isHtml, this);
285 }
286 // We had a display helper for the route_name but not the method.
287 return "";
288 }
289);
290
291/**
292 * Creates an html element for the action performed in this user_action to be used in building-level activity .
293 *
294 * @function getBuildingActionHTML
295 * @memberof UserActionModel.prototype
296 *
297 * @return {String} Action HTML
298 */
299UserAction.virtual("building_action_html").get(
300 function getBuildingActionHTML() {
301 let importantMethods = ["POST", "PUT", "PATCH", "DELETE"];
302 let displayHelpers = this.model("UserAction").route_name()[
303 this.route_name.toUpperCase()
304 ];
305
306 if (!_.contains(importantMethods, this.method) || this.status !== 200) {
307 /*
308 * Do not care about GET, HEAD, OPTIONS, etc user_actions for building activity feeds.
309 * Do not care about unsuccessful requests for building activity feeds.
310 */
311 return "";
312 } else if (_.isUndefined(displayHelpers)) {
313 /*
314 * Missing route name.
315 * This shouldn't happen, but if it does then it's fucked and we should just go away.
316 */
317 return "";
318 }
319
320 let type = "building";
321 let isHtml = true;
322
323 if (this.method === "DELETE" && displayHelpers.deleteActionDisplay) {
324 return displayHelpers.deleteActionDisplay(type, isHtml, this);
325 } else if (this.method === "POST" && displayHelpers.createActionDisplay) {
326 return displayHelpers.createActionDisplay(type, isHtml, this);
327 } else if (
328 (this.method === "PUT" || this.method === "PATCH") &&
329 displayHelpers.updateActionDisplay
330 ) {
331 return displayHelpers.updateActionDisplay(type, isHtml, this);
332 }
333 // We had a display helper for the route_name but not the method.
334 return "";
335 }
336);
337
338// TODO account_action_display for account activity feeds
339// TODO organization_action_display for organization activity feeds
340
341/**
342 * Returns JSON string of body.
343 *
344 * @function getStringifiedBody
345 * @memberof UserActionModel.prototype
346 *
347 * @return {String} JSON string of body
348 */
349UserAction.virtual("stringified_body").get(function getStringifiedBody() {
350 return JSON.stringify(this.body);
351});
352
353/**
354 * Returns JSON string of query.
355 *
356 * @function getStringifiedQuery
357 * @memberof UserActionModel.prototype
358 *
359 * @return {String} JSON string of query
360 */
361UserAction.virtual("stringified_query").get(function getStringifiedQuery() {
362 return JSON.stringify(this.query);
363});
364
365/**
366 * Returns true if status is 200, false otherwise.
367 *
368 * @function isStatusOK
369 * @memberof UserActionModel.prototype
370 *
371 * @return {Boolean} True when status is 200
372 */
373UserAction.virtual("is_200").get(function isStatusOK() {
374 return this.status === 200;
375});
376
377/**
378 * Returns true if status is not 200, false if it is.
379 *
380 * @function isStatusNotOK
381 * @memberof UserActionModel.prototype
382 *
383 * @return {Boolean} True when status is not 200
384 */
385UserAction.virtual("is_not_200").get(function isStatusNotOK() {
386 return this.status !== 200;
387});
388
389/**
390 * Returns true if status is 500, false otherwise.
391 *
392 * @function isInternalServerError
393 * @memberof UserActionModel.prototype
394 *
395 * @return {Boolean} True when status is 500
396 */
397UserAction.virtual("is_500").get(function isInternalServerError() {
398 return this.status === 500;
399});
400
401/**
402 * Returns true if user exists, false otherwise.
403 *
404 * @function hasUser
405 * @memberof UserActionModel.prototype
406 *
407 * @return {Boolean} True when has user
408 */
409UserAction.virtual("has_user").get(function hasUser() {
410 return Boolean(this.user);
411});
412
413// VALIDATORS ======================================================================================================
414require("./UserAction.validators").load(UserAction);
415
416// STATIC METHODS ==================================================================================================
417/**
418 * Gets constants associated with this model
419 *
420 * @returns {Object}
421 */
422UserAction.static("constants", function() {
423 return ABC.MODELS.USER_ACTION;
424});
425
426/**
427 * Returns fields required to be provided by the user on creation. Called at the router level on a CREATE action.
428 * If a field is not provided by the user, a 400 is returned. Other fields are required for creation but are set
429 * by the application. If these fields are not set, a 500 should be returned.
430 *
431 * @function requiredCreateFields
432 * @memberof UserActionModel
433 * @static
434 *
435 * @return {Array} List of required create fields.
436 */
437UserAction.static("requiredCreateFields", function() {
438 return ["route_name", "url_pathname", "method", "is_api_call", "ip"];
439});
440
441/**
442 * Returns fields that cannot be set (by the user or application) during creation. These fields are removed from
443 * the data provided to the Model just prior to creation at the controller level.
444 *
445 * @function badCreateFields
446 * @memberof UserActionModel
447 * @static
448 *
449 * @return {Array} List of bad create fields
450 */
451UserAction.static("badCreateFields", function() {
452 return ["_id", "__v", "time"];
453});
454
455// badUpdateFields N/A user does not make API calls to update User Actions
456
457/**
458 * Returns fields to populate during a query.
459 *
460 * @function populateFields
461 * @memberof UserActionModel
462 * @static
463 *
464 * @return {String} Fields to populate
465 */
466UserAction.static("populateFields", function() {
467 return (
468 "asset application comment commit document future_task level pin_field pin_type + " +
469 "qrcode request role room tag_category task user"
470 );
471});
472
473/**
474 * Returns object of routes and their helper functions.
475 *
476 * @function route_name
477 * @memberof UserActionModel
478 * @static
479 *
480 * @return {Object} Route helpers
481 */
482UserAction.static("route_name", function() {
483 const Asset = mongoose.model("Asset");
484 const Application = mongoose.model("Application");
485 const Attachment = mongoose.model("Attachment");
486 const Comment = mongoose.model("Comment");
487 const Document = mongoose.model("Document");
488 const FutureTask = mongoose.model("FutureTask");
489 const IssueType = mongoose.model("IssueType");
490 const Level = mongoose.model("Level");
491 const MaintenanceType = mongoose.model("MaintenanceType");
492 const Organization = mongoose.model("Organization");
493 const PinField = mongoose.model("PinField");
494 const PinType = mongoose.model("PinType");
495 const PinTypeColor = mongoose.model("PinTypeColor");
496 const Building = mongoose.model("Building");
497 const Request = mongoose.model("Request");
498 const Role = mongoose.model("Role");
499 const Room = mongoose.model("Room");
500 const Tag = mongoose.model("Tag");
501 const TagCategory = mongoose.model("TagCategory");
502 const TagCategoryValue = mongoose.model("TagCategoryValue");
503 const Task = mongoose.model("Task");
504 const Trade = mongoose.model("Trade");
505
506 // The list of accepted action names
507 let routeHelpers = {
508 ACCOUNT_DETAIL: {
509 updateActionDisplay: function() {
510 // Do not have an account-level activity feed... yet.
511 return "";
512 },
513
514 // Currently do not support deleting accounts.
515 },
516 ACCOUNT_LIST: {
517 // Accounts are created through the registration route.
518 },
519 ACCOUNT_SETTINGS: {
520 // Route only renders account settings (account updates are made to account_detail).
521 },
522 APPLICATION_DETAIL: {
523 updateActionDisplay: function(type, isHtml, userAction) {
524 // Don't really care about the special update cases for applications at the moment.
525 return standardUpdateActionDisplay(
526 type,
527 isHtml,
528 userAction,
529 userAction.application,
530 Application
531 );
532 },
533 deleteActionDisplay: function(type, isHtml, userAction) {
534 return standardDeleteActionDisplay(
535 type,
536 isHtml,
537 userAction,
538 Application
539 );
540 },
541 },
542 APPLICATION_LIST: {
543 createActionDisplay: function(type, isHtml, userAction) {
544 return standardCreateActionDisplay(
545 type,
546 isHtml,
547 userAction,
548 Application
549 );
550 },
551 },
552 ASSET_DETAIL: {
553 updateActionDisplay: function(type, isHtml, userAction) {
554 return routeHelpers.PIN_DETAIL.updateActionDisplay(
555 type,
556 isHtml,
557 userAction,
558 Asset
559 );
560 },
561 deleteActionDisplay: function(type, isHtml, userAction) {
562 return standardDeleteActionDisplay(type, isHtml, userAction, Asset);
563 },
564 },
565 ASSET_LIST: {
566 createActionDisplay: function(type, isHtml, userAction) {
567 return standardCreateActionDisplay(type, isHtml, userAction, Asset);
568 },
569 },
570 ASSET_VALUE_DETAIL: {
571 updateActionDisplay: function(type, isHtml, userAction) {
572 return routeHelpers.PIN_VALUE_DETAIL.updateActionDisplay(
573 type,
574 isHtml,
575 userAction,
576 Asset
577 );
578 },
579 /*
580 * Values are not deleted through this route (deleted when parent pin field is removed from the pin
581 * type).
582 */
583 },
584 ASSET_VALUE_LIST: {
585 /*
586 * Values are not created through this route (created when the parent pin field is created in the pin
587 * type).
588 */
589 },
590 ATTACHMENT_DETAIL: {},
591 ATTACHMENT_LIST: {
592 createActionDisplay: function(type, isHtml, userAction) {
593 let user = userAction ? userAction.user : null;
594 let username = user ? user.email : "Unknown User";
595 let document = userAction.document;
596 let filename = document ? document.path : `Unknown file`;
597 let url = document ? `${ABC.getDesktop().URL_BASE}${document.media_uri}` : ``;
598 let options = {
599 target: "_blank",
600 };
601
602 if (isHtml) {
603 return `${addBoldHtmlTag(isHtml, username)} added 1 ${addBoldHtmlTag(isHtml, "photo")}<br>${toHTMLAnchor(filename, url, options)}`;
604 } else {
605 return `${username} added 1 photo \n ${filename}`;
606 }
607 },
608 },
609 BUILDING_DETAIL: {
610 updateActionDisplay: function(type, isHtml, userAction) {
611 let Model = Building;
612 const modelConstants = Model.constants();
613 let entityAlias = modelConstants.SINGULAR;
614 let entity = userAction.building;
615 let actionHtml =
616 addBoldHtmlTag(isHtml, userAction.user.email) + " updated ";
617 let patchString = null;
618
619 if (userAction.method === "PATCH") {
620 // PATCH, so we can guess what was changed.
621 let getAlias = ABC.MODELS.BUILDING.getAlias;
622 let schema = _.keys(Model.schema.paths);
623 let body = _.keys(userAction.body);
624 let keys = _.intersection(body, schema);
625
626 if (_.intersection(keys, Model.addressFields()).length > 0) {
627 patchString = addBoldHtmlTag(isHtml, "Address");
628 } else {
629 patchString = getUpdateFieldString(isHtml, keys, getAlias);
630 }
631 }
632
633 switch (type) {
634 // The building is the entity.
635 case "entity":
636 case "building":
637 if (patchString) {
638 actionHtml +=
639 "this " +
640 addBoldHtmlTag(isHtml, entityAlias) +
641 "'s " +
642 patchString;
643 } else {
644 actionHtml +=
645 "this " + addBoldHtmlTag(isHtml, entityAlias) + "'s settings";
646 }
647
648 break;
649
650 // case 'account':
651 // case 'organization':
652 default:
653 // Just send off the standard update message.
654 return standardUpdateActionDisplay(
655 type,
656 isHtml,
657 userAction,
658 entity,
659 Model
660 );
661 }
662
663 return actionHtml;
664 },
665 deleteActionDisplay: function(type, isHtml, userAction) {
666 let Model = Building;
667 const modelConstants = Model.constants();
668 let entityAlias = modelConstants.SINGULAR;
669 let entity = userAction.building;
670 let actionHtml;
671
672 // Archived a building (unarchive is below in BUILDING_SETTINGS).
673 switch (type) {
674 // The building is the entity.
675 case "entity":
676 case "building":
677 actionHtml =
678 addBoldHtmlTag(isHtml, userAction.user.email) +
679 " archived this " +
680 addBoldHtmlTag(isHtml, entityAlias);
681
682 break;
683
684 // case 'account':
685 // case 'organization':
686 default:
687 // Just send off the standard update message.
688 return standardUpdateActionDisplay(
689 type,
690 isHtml,
691 userAction,
692 entity,
693 Model
694 );
695 }
696
697 return actionHtml;
698 },
699 },
700 BUILDING_LIST: {
701 createActionDisplay: function() {
702 // Do not have an account- or organization-level activity feed... yet.
703 return "";
704 },
705 },
706 BUILDING_SETTINGS: {
707 /*
708 * Route only renders building settings (building updates are made to building_detail and other routes).
709 * ... except for restoring an archived building.
710 */
711 createActionDisplay: function(type, isHtml, userAction) {
712 // Building unarchive may be a post and land here in 'create' instead of 'update' :(
713 return routeHelpers.BUILDING_SETTINGS.updateActionDisplay(
714 type,
715 isHtml,
716 userAction
717 );
718 },
719 updateActionDisplay: function(type, isHtml, userAction) {
720 let Model = Building;
721 const modelConstants = Model.constants();
722 let entityAlias = modelConstants.SINGULAR;
723 let entity = userAction.building;
724 let actionHtml;
725
726 switch (type) {
727 // The building is the entity.
728 case "entity":
729 case "building":
730 actionHtml =
731 addBoldHtmlTag(isHtml, userAction.user.email) +
732 " restored this " +
733 addBoldHtmlTag(isHtml, entityAlias);
734 break;
735
736 // case 'account':
737 // case 'organization':
738 default:
739 // Just send off the standard update message.
740 return standardUpdateActionDisplay(
741 type,
742 isHtml,
743 userAction,
744 entity,
745 Model
746 );
747 }
748
749 return actionHtml;
750 },
751 },
752 // Comments are queried for separately in the activity feeds.
753 COMMENT_DETAIL: {
754 updateActionDisplay: function(type, isHtml, userAction) {
755 /*
756 * entity: user updated a Note on this Item.
757 * building: user updated a Note on a Item.
758 */
759 let Model = Comment;
760 const modelConstants = Model.constants();
761 let entityAlias = modelConstants.SINGULAR;
762 let entity = userAction.comment;
763
764 let actionHtml =
765 addBoldHtmlTag(isHtml, userAction.user.email) +
766 " updated a " +
767 addBoldHtmlTag(isHtml, entityAlias) +
768 " on";
769
770 if (!entity) {
771 /*
772 * Looks like population failed because the entity was deleted.
773 * So just use the standard helper.
774 */
775 return standardUpdateActionDisplay(
776 type,
777 isHtml,
778 userAction,
779 entity,
780 Model
781 );
782 }
783
784 switch (type) {
785 case "entity":
786 actionHtml += " this ";
787 break;
788 case "building":
789 if (entity.entity_alias.toLowerCase() === "building") {
790 actionHtml += " this ";
791 } else {
792 actionHtml += " a ";
793 }
794 break;
795 // case 'account':
796 // case 'organization':
797 default:
798 // Just send off the standard update message.
799 return standardUpdateActionDisplay(
800 type,
801 isHtml,
802 userAction,
803 entity,
804 Model
805 );
806 }
807
808 // Fake some things so we can (try to) make a link.
809 if (
810 entity.entity &&
811 entity[entity.entity] &&
812 entity.entity_alias.toLowerCase() !== "building"
813 ) {
814 let _commentEntity = {
815 _id: entity[entity.entity],
816 };
817
818 actionHtml += addAnchorTag(
819 isHtml,
820 entity.entity_alias,
821 _commentEntity
822 );
823 } else {
824 // For whatever reason, we cant make the link, so just make it bold.
825 actionHtml += addBoldHtmlTag(isHtml, entity.entity_alias);
826 }
827
828 return actionHtml;
829 },
830 deleteActionDisplay: function(type, isHtml, userAction) {
831 /*
832 * Entity was deleted.
833 * So just use the standard helper.
834 */
835 return standardDeleteActionDisplay(type, isHtml, userAction, Comment);
836 },
837 },
838 COMMENT_LIST: {
839 createActionDisplay: function(type, isHtml, userAction) {
840 /*
841 * entity: user added a Note to this Item.
842 * building: user added a Note to a Item.
843 */
844 let Model = Comment;
845 const modelConstants = Model.constants();
846 let entityAlias = modelConstants.SINGULAR;
847 let actionHtml =
848 addBoldHtmlTag(isHtml, userAction.user.email) +
849 " added a " +
850 addBoldHtmlTag(isHtml, entityAlias) +
851 " to";
852
853 // Fake create the item (without saving of course).
854 let _entity = new Model(userAction.body);
855
856 if (!_entity || !_entity.entity_alias) {
857 /*
858 * Looks like population failed because the entity was deleted.
859 * So just use the standard helper.
860 */
861 return standardCreateActionDisplay(type, isHtml, userAction, Model);
862 }
863
864 switch (type) {
865 case "entity":
866 actionHtml += " this ";
867 break;
868 case "building":
869 if (_entity.entity_alias.toLowerCase() === "building") {
870 actionHtml += " this ";
871 } else {
872 actionHtml += " a ";
873 }
874 break;
875 // case 'account':
876 // case 'organization':
877 default:
878 // Just send off the standard update message.
879 return standardCreateActionDisplay(type, isHtml, userAction, Model);
880 }
881
882 // Fake some things so we can (try to) make a link.
883 if (
884 _entity.entity &&
885 _entity[_entity.entity] &&
886 _entity.entity_alias.toLowerCase() !== "building"
887 ) {
888 let _commentEntity = {
889 _id: _entity[_entity.entity],
890 };
891
892 actionHtml += addAnchorTag(
893 isHtml,
894 _entity.entity_alias,
895 _commentEntity
896 );
897 } else {
898 // For whatever reason, we cant make the link, so just make it bold.
899 actionHtml += addBoldHtmlTag(isHtml, _entity.entity_alias);
900 }
901
902 return actionHtml;
903 },
904 },
905 CONTACT_US: {},
906 DEVELOPER_DOCUMENTATION: {},
907 DOCUMENT_DETAIL: {
908 updateActionDisplay: function(type, isHtml, userAction) {
909 let Model = Document;
910 const modelConstants = Model.constants();
911 let entityAlias = modelConstants.SINGULAR;
912 let nameField = modelConstants.NAME_FIELD;
913 let entity = userAction.document;
914 let actionHtml = addBoldHtmlTag(isHtml, userAction.user.email);
915 let patchString = null;
916 let name = null;
917
918 if (entity.job || entity.step) {
919 return null;
920 }
921
922 if (entity && entity[nameField]) {
923 name = entity[nameField];
924 } else if (!entity) {
925 /*
926 * Looks like population failed because the entity was deleted.
927 * Just use the standard helper.
928 */
929 return standardUpdateActionDisplay(
930 type,
931 isHtml,
932 userAction,
933 entity,
934 Model
935 );
936 }
937
938 switch (userAction.body.action) {
939 case "rename":
940 actionHtml += " renamed ";
941 break;
942 case "archive":
943 actionHtml += " archived ";
944 break;
945 case "unarchive":
946 actionHtml += " restored ";
947 break;
948 case "revert":
949 actionHtml += " reverted ";
950 break;
951 case "rotate":
952 actionHtml += " rotated ";
953 break;
954 default:
955 // Just send off the standard update message.
956 return standardUpdateActionDisplay(
957 type,
958 isHtml,
959 userAction,
960 entity,
961 Model
962 );
963 }
964
965 switch (type) {
966 case "entity":
967 actionHtml += "this " + addBoldHtmlTag(isHtml, entityAlias);
968
969 if (patchString) {
970 actionHtml += "'s " + patchString;
971 }
972
973 break;
974
975 case "building":
976 if (patchString) {
977 actionHtml +=
978 "the " +
979 patchString +
980 " of " +
981 addBoldHtmlTag(isHtml, entityAlias) +
982 " - " +
983 addAnchorTag(isHtml, name, entity);
984 } else {
985 actionHtml +=
986 addBoldHtmlTag(isHtml, entityAlias) +
987 " - " +
988 addAnchorTag(isHtml, name, entity);
989 }
990 break;
991 // case 'account':
992 // case 'organization':
993 default:
994 // Just send off the standard update message.
995 return standardUpdateActionDisplay(
996 type,
997 isHtml,
998 userAction,
999 entity,
1000 Model
1001 );
1002 }
1003
1004 return actionHtml;
1005 },
1006 deleteActionDisplay: function(type, isHtml, userAction) {
1007 return standardDeleteActionDisplay(type, isHtml, userAction, Document);
1008 },
1009 },
1010 DOCUMENT_DOWNLOAD: {},
1011 DOCUMENT_LIST: {
1012 createActionDisplay: function(type, isHtml, userAction) {
1013 return routeHelpers.UPLOAD.createActionDisplay(
1014 type,
1015 isHtml,
1016 userAction
1017 );
1018 },
1019 },
1020
1021 // Tracks document views in the fac.
1022 DOCUMENT_VIEW: {},
1023 FM_REDIRECT_DETAIL: {},
1024 FM_REDIRECT_LIST: {},
1025 FORGOT_PASSWORD: {
1026 createActionDisplay: function() {
1027 /*
1028 * POST to forget password.
1029 * Do not have an account-level activity feed... yet.
1030 */
1031 return "";
1032 },
1033 },
1034 FUTURE_TASK_DETAIL: {
1035 updateActionDisplay: function(type, isHtml, userAction) {
1036 let Model = FutureTask;
1037 const modelConstants = Model.constants();
1038 let entityAlias = modelConstants.SINGULAR;
1039 let nameField = modelConstants.NAME_FIELD;
1040 let entity = userAction.future_task;
1041 let actionHtml = addBoldHtmlTag(isHtml, userAction.user.email);
1042 let patchString = null;
1043 let name = null;
1044
1045 if (entity && entity[nameField]) {
1046 name = entity[nameField];
1047 } else if (!entity) {
1048 /*
1049 * Looks like population failed because the entity was deleted.
1050 * Just use the standard helper.
1051 */
1052 return standardUpdateActionDisplay(
1053 type,
1054 isHtml,
1055 userAction,
1056 entity,
1057 Model
1058 );
1059 }
1060
1061 switch (userAction.body.action) {
1062 case "update":
1063 actionHtml += " updated ";
1064
1065 if (userAction.method === "PATCH") {
1066 // PATCH, so we can guess what was changed.
1067 let getAlias = ABC.MODELS.MAINTENANCE_SCHEDULE.getAlias;
1068 let schema = _.keys(Model.schema.paths);
1069 let body = _.keys(userAction.body);
1070 let keys = _.intersection(body, schema);
1071
1072 if (
1073 _.intersection(keys, Model.repeatingConditionFields()).length >
1074 0
1075 ) {
1076 patchString = "Frequency";
1077 } else {
1078 patchString = getUpdateFieldString(isHtml, keys, getAlias);
1079 }
1080 }
1081 break;
1082
1083 case "cancel":
1084 actionHtml += " canceled ";
1085 break;
1086 case "reopen":
1087 actionHtml += " reopened ";
1088 break;
1089 default:
1090 // Just send off the standard update message.
1091 return standardUpdateActionDisplay(
1092 type,
1093 isHtml,
1094 userAction,
1095 entity,
1096 Model
1097 );
1098 }
1099
1100 switch (type) {
1101 case "entity":
1102 actionHtml += "this " + addBoldHtmlTag(isHtml, entityAlias);
1103
1104 if (patchString) {
1105 actionHtml += "'s " + patchString;
1106 }
1107
1108 break;
1109
1110 case "building":
1111 if (patchString) {
1112 actionHtml +=
1113 patchString +
1114 " in " +
1115 addBoldHtmlTag(isHtml, entityAlias) +
1116 " - " +
1117 addAnchorTag(isHtml, name, entity);
1118 } else {
1119 actionHtml +=
1120 addBoldHtmlTag(isHtml, entityAlias) +
1121 " - " +
1122 addAnchorTag(isHtml, name, entity);
1123 }
1124
1125 break;
1126
1127 // case 'account':
1128 // case 'organization':
1129 default:
1130 // Just send off the standard update message.
1131 return standardUpdateActionDisplay(
1132 type,
1133 isHtml,
1134 userAction,
1135 entity,
1136 Model
1137 );
1138 }
1139
1140 return actionHtml;
1141 },
1142 deleteActionDisplay: function(type, isHtml, userAction) {
1143 return standardDeleteActionDisplay(
1144 type,
1145 isHtml,
1146 userAction,
1147 FutureTask
1148 );
1149 },
1150 },
1151 FUTURE_TASK_LIST: {
1152 createActionDisplay: function(type, isHtml, userAction) {
1153 return standardCreateActionDisplay(
1154 type,
1155 isHtml,
1156 userAction,
1157 FutureTask
1158 );
1159 },
1160 },
1161 HELP: {},
1162 HOME: {},
1163 IS_AD_DOMAIN: {},
1164 ISSUE_TYPE_DETAIL: {
1165 updateActionDisplay: function(type, isHtml, userAction) {
1166 let Model = IssueType;
1167 const modelConstants = Model.constants();
1168 let entityAlias = modelConstants.SINGULAR;
1169 let nameField = modelConstants.NAME_FIELD;
1170 let entity = userAction.issue_type;
1171 let actionHtml = addBoldHtmlTag(isHtml, userAction.user.email);
1172 let name = null;
1173
1174 if (entity && entity[nameField]) {
1175 name = entity[nameField];
1176 } else if (!entity) {
1177 /*
1178 * Looks like population failed because the entity was deleted.
1179 * Just use the standard helper.
1180 */
1181 return standardUpdateActionDisplay(
1182 type,
1183 isHtml,
1184 userAction,
1185 entity,
1186 Model
1187 );
1188 }
1189
1190 switch (userAction.body.action) {
1191 case "archive":
1192 actionHtml += " archived ";
1193 break;
1194 case "restore":
1195 actionHtml += " restored ";
1196 break;
1197 case "update":
1198 actionHtml += " updated ";
1199 break;
1200 default:
1201 // Just send off the standard update message.
1202 return standardUpdateActionDisplay(
1203 type,
1204 isHtml,
1205 userAction,
1206 entity,
1207 Model
1208 );
1209 }
1210
1211 switch (type) {
1212 case "entity":
1213 actionHtml += "this " + addBoldHtmlTag(isHtml, entityAlias);
1214 break;
1215 case "building":
1216 actionHtml +=
1217 addBoldHtmlTag(isHtml, entityAlias) +
1218 " - " +
1219 addAnchorTag(isHtml, name, entity);
1220 break;
1221 // case 'account':
1222 // case 'organization':
1223 default:
1224 // Just send off the standard update message.
1225 return standardUpdateActionDisplay(
1226 type,
1227 isHtml,
1228 userAction,
1229 entity,
1230 Model
1231 );
1232 }
1233
1234 return actionHtml;
1235 },
1236 deleteActionDisplay: function(type, isHtml, userAction) {
1237 return standardDeleteActionDisplay(type, isHtml, userAction, IssueType);
1238 },
1239 },
1240 ISSUE_TYPE_LIST: {
1241 createActionDisplay: function(type, isHtml, userAction) {
1242 return standardCreateActionDisplay(type, isHtml, userAction, IssueType);
1243 },
1244 },
1245 LANDING: {},
1246 LEVEL_DETAIL: {
1247 updateActionDisplay: function(type, isHtml, userAction) {
1248 let Model = Level;
1249 const modelConstants = Model.constants();
1250 let entityAlias = modelConstants.SINGULAR;
1251 let nameField = modelConstants.NAME_FIELD;
1252 let entity = userAction.level;
1253 let actionHtml = addBoldHtmlTag(isHtml, userAction.user.email);
1254 let patchString = null;
1255 let name = null;
1256
1257 if (entity && entity[nameField]) {
1258 name = entity[nameField];
1259 } else if (!entity) {
1260 /*
1261 * Looks like population failed because the entity was deleted.
1262 * Just use the standard helper.
1263 */
1264 return standardUpdateActionDisplay(
1265 type,
1266 isHtml,
1267 userAction,
1268 entity,
1269 Model
1270 );
1271 }
1272
1273 actionHtml += " updated ";
1274
1275 switch (userAction.body.action) {
1276 case "updateDocument":
1277 patchString = "Floor Plan";
1278 break;
1279
1280 default: {
1281 if (userAction.method !== "PATCH") {
1282 // Just send off the standard update message.
1283 return standardUpdateActionDisplay(
1284 type,
1285 isHtml,
1286 userAction,
1287 entity,
1288 Model
1289 );
1290 }
1291
1292 // PATCH, so we can guess what was changed.
1293 let getAlias = ABC.MODELS.FLOOR.getAlias;
1294 let schema = _.keys(Model.schema.paths);
1295 let body = _.keys(userAction.body);
1296 let keys = _.intersection(body, schema);
1297
1298 patchString = getUpdateFieldString(isHtml, keys, getAlias);
1299
1300 break;
1301 }
1302 }
1303
1304 switch (type) {
1305 case "entity":
1306 actionHtml += "this " + addBoldHtmlTag(isHtml, entityAlias);
1307
1308 if (patchString) {
1309 actionHtml += "'s " + patchString;
1310 }
1311
1312 break;
1313 case "building":
1314 if (patchString) {
1315 actionHtml +=
1316 "the " +
1317 patchString +
1318 " of " +
1319 addBoldHtmlTag(isHtml, entityAlias) +
1320 " - " +
1321 addAnchorTag(isHtml, name, entity);
1322 } else {
1323 actionHtml +=
1324 addBoldHtmlTag(isHtml, entityAlias) +
1325 " - " +
1326 addAnchorTag(isHtml, name, entity);
1327 }
1328 break;
1329 // case 'account':
1330 // case 'organization':
1331 default:
1332 // Just send off the standard update message.
1333 return standardUpdateActionDisplay(
1334 type,
1335 isHtml,
1336 userAction,
1337 entity,
1338 Model
1339 );
1340 }
1341
1342 return actionHtml;
1343 },
1344 deleteActionDisplay: function(type, isHtml, userAction) {
1345 return standardDeleteActionDisplay(type, isHtml, userAction, Level);
1346 },
1347 },
1348 LEVEL_LIST: {
1349 createActionDisplay: function(type, isHtml, userAction) {
1350 return standardCreateActionDisplay(type, isHtml, userAction, Level);
1351 },
1352 },
1353 LOGIN: {
1354 createActionDisplay: function() {
1355 /*
1356 * POST to login.
1357 * Do not have an account-level activity feed... yet.
1358 */
1359 return "";
1360 },
1361 },
1362
1363 // GET to logout.
1364 LOGOUT: {},
1365 LOGOUT_APP: {},
1366 MAINTENANCE_TYPE_DETAIL: {
1367 updateActionDisplay: function(type, isHtml, userAction) {
1368 let Model = MaintenanceType;
1369 const modelConstants = Model.constants();
1370 let entityAlias = modelConstants.SINGULAR;
1371 let nameField = modelConstants.NAME_FIELD;
1372 let entity = userAction.maintenance_type;
1373 let actionHtml = addBoldHtmlTag(isHtml, userAction.user.email);
1374 let name = null;
1375
1376 if (entity && entity[nameField]) {
1377 name = entity[nameField];
1378 } else if (!entity) {
1379 /*
1380 * Looks like population failed because the entity was deleted.
1381 * Just use the standard helper.
1382 */
1383 return standardUpdateActionDisplay(
1384 type,
1385 isHtml,
1386 userAction,
1387 entity,
1388 Model
1389 );
1390 }
1391
1392 switch (userAction.body.action) {
1393 case "archive":
1394 actionHtml += " archived ";
1395 break;
1396 case "restore":
1397 actionHtml += " restored ";
1398 break;
1399 case "update":
1400 actionHtml += " updated ";
1401 break;
1402 default:
1403 // Just send off the standard update message.
1404 return standardUpdateActionDisplay(
1405 type,
1406 isHtml,
1407 userAction,
1408 entity,
1409 Model
1410 );
1411 }
1412
1413 switch (type) {
1414 case "entity":
1415 actionHtml += "this " + addBoldHtmlTag(isHtml, entityAlias);
1416 break;
1417 case "building":
1418 actionHtml +=
1419 addBoldHtmlTag(isHtml, entityAlias) +
1420 " - " +
1421 addAnchorTag(isHtml, name, entity);
1422 break;
1423 // case 'account':
1424 // case 'organization':
1425 default:
1426 // Just send off the standard update message.
1427 return standardUpdateActionDisplay(
1428 type,
1429 isHtml,
1430 userAction,
1431 entity,
1432 Model
1433 );
1434 }
1435
1436 return actionHtml;
1437 },
1438 deleteActionDisplay: function(type, isHtml, userAction) {
1439 return standardDeleteActionDisplay(
1440 type,
1441 isHtml,
1442 userAction,
1443 MaintenanceType
1444 );
1445 },
1446 },
1447 MAINTENANCE_TYPE_LIST: {
1448 createActionDisplay: function(type, isHtml, userAction) {
1449 return standardCreateActionDisplay(
1450 type,
1451 isHtml,
1452 userAction,
1453 MaintenanceType
1454 );
1455 },
1456 },
1457 ORGANIZATION_LIST: {
1458 // no reason to have a createActionDisplay
1459 },
1460 ORGANIZATION_DETAIL: {
1461 updateActionDisplay: function(type, isHtml, userAction) {
1462 let Model = Organization;
1463 const modelConstants = Model.constants();
1464 let entityAlias = modelConstants.SINGULAR;
1465 let entity = userAction.organization;
1466 let actionHtml =
1467 addBoldHtmlTag(isHtml, userAction.user.email) + " updated ";
1468 let patchString = null;
1469
1470 if (userAction.method === "PATCH") {
1471 // PATCH, so we can guess what was changed.
1472 let getAlias = ABC.MODELS.ORGANIZATION.getAlias;
1473 let schema = _.keys(Model.schema.paths);
1474 let body = _.keys(userAction.body);
1475 let keys = _.intersection(body, schema);
1476 patchString = getUpdateFieldString(isHtml, keys, getAlias);
1477 }
1478
1479 switch (type) {
1480 // The organization is the entity.
1481 case "entity":
1482 case "organization":
1483 if (patchString) {
1484 actionHtml +=
1485 "this " +
1486 addBoldHtmlTag(isHtml, entityAlias) +
1487 "'s " +
1488 patchString;
1489 } else {
1490 actionHtml +=
1491 "this " + addBoldHtmlTag(isHtml, entityAlias) + "'s settings";
1492 }
1493
1494 break;
1495
1496 // case 'account':
1497 // case 'organization':
1498 default:
1499 // Just send off the standard update message.
1500 return standardUpdateActionDisplay(
1501 type,
1502 isHtml,
1503 userAction,
1504 entity,
1505 Model
1506 );
1507 }
1508
1509 return actionHtml;
1510 },
1511 },
1512 OAUTH2_SETUP: {},
1513 PIN_DETAIL: {
1514 updateActionDisplay: function(type, isHtml, userAction, Model) {
1515 if (_.isEmpty(Model)) {
1516 if (userAction.asset) Model = Asset;
1517 else if (userAction.room) Model = Room;
1518 }
1519 const modelConstants = Model.constants();
1520 let entityAlias = modelConstants.SINGULAR;
1521 let nameField = modelConstants.NAME_FIELD;
1522 let entity = userAction[modelConstants.MODEL];
1523 let actionHtml = addBoldHtmlTag(isHtml, userAction.user.email);
1524 let patchString = null;
1525 let name = null;
1526
1527 if (entity && entity[nameField]) {
1528 name = entity[nameField];
1529 } else if (!entity) {
1530 /*
1531 * Looks like population failed because the entity was deleted.
1532 * Just use the standard helper.
1533 */
1534 return standardUpdateActionDisplay(
1535 type,
1536 isHtml,
1537 userAction,
1538 entity,
1539 Model
1540 );
1541 }
1542
1543 switch (userAction.body.action) {
1544 case "update":
1545 actionHtml += " updated ";
1546 break;
1547
1548 case "setLocation":
1549 actionHtml += " updated ";
1550 patchString = addBoldHtmlTag(isHtml, "Location");
1551 break;
1552
1553 case "updateRoom":
1554 // Can only be an Asset for this action.
1555 entityAlias = ABC.MODELS.ASSET.SINGULAR;
1556 actionHtml += " updated ";
1557 patchString = addBoldHtmlTag(isHtml, ABC.MODELS.ROOM.SINGULAR);
1558 break;
1559
1560 case "updateLevel":
1561 actionHtml += " updated ";
1562 patchString = addBoldHtmlTag(isHtml, ABC.MODELS.FLOOR.SINGULAR);
1563 break;
1564
1565 default:
1566 // Just send off the standard update message.
1567 return standardUpdateActionDisplay(
1568 type,
1569 isHtml,
1570 userAction,
1571 entity,
1572 Model
1573 );
1574 }
1575
1576 switch (type) {
1577 case "entity":
1578 actionHtml += "this " + addBoldHtmlTag(isHtml, entityAlias);
1579
1580 if (patchString) {
1581 actionHtml += "'s " + patchString;
1582 }
1583
1584 break;
1585
1586 case "building":
1587 if (patchString) {
1588 actionHtml +=
1589 "the " +
1590 patchString +
1591 " in " +
1592 addBoldHtmlTag(isHtml, entityAlias) +
1593 " - " +
1594 addAnchorTag(isHtml, name, entity);
1595 } else {
1596 actionHtml +=
1597 addBoldHtmlTag(isHtml, entityAlias) +
1598 " - " +
1599 addAnchorTag(isHtml, name, entity);
1600 }
1601 break;
1602
1603 // case 'account':
1604 // case 'organization':
1605 default:
1606 // Just send off the standard update message.
1607 return standardUpdateActionDisplay(
1608 type,
1609 isHtml,
1610 userAction,
1611 entity,
1612 Model
1613 );
1614 }
1615
1616 return actionHtml;
1617 },
1618 },
1619 PIN_FIELD_DETAIL: {
1620 updateActionDisplay: function(type, isHtml, userAction) {
1621 // TODO get all the special update cases
1622 return childUpdateActionDisplay(
1623 type,
1624 isHtml,
1625 userAction,
1626 userAction.pin_field,
1627 PinField,
1628 userAction.pin_type,
1629 PinType
1630 );
1631 },
1632 deleteActionDisplay: function(type, isHtml, userAction) {
1633 return childDeleteActionDisplay(
1634 type,
1635 isHtml,
1636 userAction,
1637 PinField,
1638 userAction.pin_type,
1639 PinType
1640 );
1641 },
1642 },
1643 PIN_FIELD_LIST: {
1644 createActionDisplay: function(type, isHtml, userAction) {
1645 return childCreateActionDisplay(
1646 type,
1647 isHtml,
1648 userAction,
1649 PinField,
1650 userAction.pin_type,
1651 PinType
1652 );
1653 },
1654 },
1655
1656 // Only GET calls for hard-coded trees not stored in the pin field itself.
1657 PIN_FIELD_TREE: {},
1658 PIN_TYPE_COLOR_DETAIL: {
1659 updateActionDisplay: function(type, isHtml, userAction) {
1660 // TODO get all the special update cases
1661 return childUpdateActionDisplay(
1662 type,
1663 isHtml,
1664 userAction,
1665 userAction.pin_type_color,
1666 PinTypeColor,
1667 userAction.pin_type,
1668 PinType
1669 );
1670 },
1671 deleteActionDisplay: function(type, isHtml, userAction) {
1672 return childDeleteActionDisplay(
1673 type,
1674 isHtml,
1675 userAction,
1676 PinTypeColor,
1677 userAction.pin_type,
1678 PinType
1679 );
1680 },
1681 },
1682 PIN_TYPE_COLOR_LIST: {
1683 createActionDisplay: function(type, isHtml, userAction) {
1684 return childCreateActionDisplay(
1685 type,
1686 isHtml,
1687 userAction,
1688 PinTypeColor,
1689 userAction.pin_type,
1690 PinType
1691 );
1692 },
1693 },
1694 PIN_TYPE_DETAIL: {
1695 updateActionDisplay: function(type, isHtml, userAction) {
1696 // TODO get all the special update cases
1697 return standardUpdateActionDisplay(
1698 type,
1699 isHtml,
1700 userAction,
1701 userAction.pin_type,
1702 PinType
1703 );
1704 },
1705 deleteActionDisplay: function(type, isHtml, userAction) {
1706 return standardDeleteActionDisplay(type, isHtml, userAction, PinType);
1707 },
1708 },
1709 PIN_TYPE_LIST: {
1710 createActionDisplay: function(type, isHtml, userAction) {
1711 return standardCreateActionDisplay(type, isHtml, userAction, PinType);
1712 },
1713 },
1714 PIN_VALUE_DETAIL: {
1715 updateActionDisplay: function(type, isHtml, userAction, Model) {
1716 if (_.isEmpty(Model)) {
1717 if (userAction.asset) Model = Asset;
1718 else if (userAction.room) Model = Room;
1719 }
1720 const modelConstants = Model.constants();
1721 let entityAlias = modelConstants.SINGULAR;
1722 let nameField = modelConstants.NAME_FIELD;
1723 let entity = userAction[modelConstants.MODEL];
1724 let parentEntity = userAction.pin_type;
1725 let actionHtml =
1726 addBoldHtmlTag(isHtml, userAction.user.email) + " updated ";
1727 let patchString = null;
1728 let name = null;
1729
1730 if (entity && entity[nameField]) {
1731 name = entity[nameField];
1732 } else if (!entity) {
1733 /*
1734 * Looks like population failed because the entity was deleted.
1735 * Just use the standard helper.
1736 */
1737 return standardUpdateActionDisplay(
1738 type,
1739 isHtml,
1740 userAction,
1741 entity,
1742 Model
1743 );
1744 }
1745
1746 // Try to set patchString to the pin field corresponding to the value that was updated.
1747 if (
1748 entity &&
1749 _.isArray(entity.values) &&
1750 parentEntity &&
1751 _.isArray(parentEntity.fields) &&
1752 userAction.pin_value
1753 ) {
1754 // Find the value so we can get its pin field id.
1755 let pinValue = entity.values.id(userAction.pin_value);
1756
1757 if (pinValue) {
1758 // Find the value's pin field in the pin type.
1759 let pinField = parentEntity.fields.id(pinValue.pinField);
1760
1761 if (pinField && pinField.name) {
1762 // Finally have the pin field name.
1763 patchString = addBoldHtmlTag(isHtml, pinField.name);
1764 }
1765 }
1766 }
1767
1768 switch (type) {
1769 case "entity":
1770 actionHtml += "this " + addBoldHtmlTag(isHtml, entityAlias);
1771
1772 if (patchString) {
1773 actionHtml += "'s " + patchString;
1774 }
1775
1776 break;
1777
1778 case "building":
1779 if (patchString) {
1780 actionHtml +=
1781 "the " +
1782 patchString +
1783 " in " +
1784 addBoldHtmlTag(isHtml, entityAlias) +
1785 " - " +
1786 addAnchorTag(isHtml, name, entity);
1787 } else {
1788 actionHtml +=
1789 addBoldHtmlTag(isHtml, entityAlias) +
1790 " - " +
1791 addAnchorTag(isHtml, name, entity);
1792 }
1793 break;
1794
1795 // case 'account':
1796 // case 'organization':
1797 default:
1798 // Just send off the standard update message.
1799 return standardUpdateActionDisplay(
1800 type,
1801 isHtml,
1802 userAction,
1803 entity,
1804 Model
1805 );
1806 }
1807
1808 return actionHtml;
1809 },
1810 /*
1811 * Values are not deleted through this route (deleted when parent pin field is removed from the pin
1812 * type).
1813 */
1814 },
1815
1816 // Values are not created through this route (created when the parent pin field is created in the pin type).
1817 PIN_VALUE_LIST: {},
1818 PORTAL_TOKEN: {},
1819 PRIVACY_POLICY: {},
1820 // Only GET calls.
1821 QRCODE_LIST: {},
1822 // Only GET calls.
1823 QRCODE_REDIRECT: {},
1824 REGISTER: {
1825 createActionDisplay: function() {
1826 /*
1827 * POST to register new accounts.
1828 * Do not have an account-level activity feed... yet.
1829 */
1830 return "";
1831 },
1832 },
1833 REQUEST_DETAIL: {
1834 updateActionDisplay: function(type, isHtml, userAction) {
1835 let Model = Request;
1836 const modelConstants = Model.constants();
1837 let entityAlias = modelConstants.SINGULAR;
1838 let entity = userAction.request;
1839 let actionHtml = addBoldHtmlTag(isHtml, userAction.user.email);
1840
1841 if (!entity) {
1842 /*
1843 * Looks like population failed because the entity was deleted.
1844 * Use the standard helper.
1845 */
1846 return standardUpdateActionDisplay(
1847 type,
1848 isHtml,
1849 userAction,
1850 entity,
1851 Model
1852 );
1853 }
1854
1855 switch (userAction.body.action) {
1856 case "deny":
1857 actionHtml += " denied ";
1858 break;
1859 case "reopen":
1860 actionHtml += " reopened ";
1861 break;
1862 case "update":
1863 default:
1864 // Send off the standard update message.
1865 return standardUpdateActionDisplay(
1866 type,
1867 isHtml,
1868 userAction,
1869 entity,
1870 Model
1871 );
1872 }
1873
1874 switch (type) {
1875 case "entity":
1876 actionHtml += "this " + addBoldHtmlTag(isHtml, entityAlias);
1877 break;
1878
1879 case "building":
1880 actionHtml +=
1881 "the " +
1882 addAnchorTag(isHtml, entityAlias, entity) +
1883 " submitted by " +
1884 addBoldHtmlTag(isHtml, entity.requester_email) +
1885 " on " +
1886 entity.cre_date;
1887
1888 break;
1889
1890 // case 'account':
1891 // case 'organization':
1892 default:
1893 // Send off the standard update message.
1894 return standardUpdateActionDisplay(
1895 type,
1896 isHtml,
1897 userAction,
1898 entity,
1899 Model
1900 );
1901 }
1902
1903 return actionHtml;
1904 },
1905 deleteActionDisplay: function(type, isHtml, userAction) {
1906 return standardDeleteActionDisplay(type, isHtml, userAction, Request);
1907 },
1908 },
1909 REQUEST_LIST: {
1910 createActionDisplay: function(type, isHtml, userAction) {
1911 /*
1912 * Cannot use standardCreateActionDisplay (Request.nameField() is not
1913 * applicable and want a different verb 'submitted'). And user may not
1914 * be set for requests made from Portal.
1915 */
1916 let Model = Request;
1917 const modelConstants = Model.constants();
1918 let entityAlias = modelConstants.SINGULAR;
1919 let user;
1920 if (userAction.user && _.isString(userAction.user.email)) {
1921 user = addBoldHtmlTag(isHtml, userAction.user.email);
1922 } else {
1923 // from the portal
1924 user = "A building occupant";
1925 }
1926 return user + " submitted a new " + addBoldHtmlTag(isHtml, entityAlias);
1927 },
1928 },
1929
1930 // Only GET calls for request portals.
1931 REQUEST_PORTAL: {},
1932 RESET_PASSWORD: {
1933 createActionDisplay: function() {
1934 /*
1935 * POST to reset passwords.
1936 * Do not have an account-level activity feed... yet.
1937 */
1938 return "";
1939 },
1940 },
1941
1942 // Only GET calls.
1943 REVISION_DETAIL: {},
1944
1945 // Only GET calls.
1946 REVISION_LIST: {},
1947 ROLE_DETAIL: {
1948 updateActionDisplay: function(type, isHtml, userAction) {
1949 let Model = Role;
1950 let entity = userAction.role;
1951
1952 if (!entity) {
1953 /*
1954 * Looks like population failed because the entity was deleted.
1955 * Use the standard helper.
1956 */
1957 return standardUpdateActionDisplay(
1958 type,
1959 isHtml,
1960 userAction,
1961 entity,
1962 Model
1963 );
1964 }
1965
1966 switch (userAction.body.action) {
1967 case "update":
1968 /*
1969 * Updating role notification preferences.
1970 * Do not care about these updates for the building or entity activity feeds.
1971 * We will later if we have an account-level feed.
1972 */
1973 return "";
1974
1975 default:
1976 // Updating role permissions.
1977 switch (type) {
1978 case "entity":
1979 // Do not have feeds for individual roles.
1980 return "";
1981
1982 case "building": {
1983 // Need to get the display role based on the perms in the body.
1984 let _body = entity.toJSON();
1985
1986 if (userAction.body.type) {
1987 _body.type = userAction.body.type;
1988 }
1989 let _role = new Model(_body);
1990
1991 return (
1992 addBoldHtmlTag(isHtml, userAction.user.email) +
1993 " updated the building permissions for " +
1994 addBoldHtmlTag(isHtml, entity.email) +
1995 " to " +
1996 addBoldHtmlTag(isHtml, _role.display_role)
1997 );
1998 }
1999
2000 // case 'account':
2001 // case 'organization':
2002 default:
2003 // Just send off the standard update message.
2004 return standardUpdateActionDisplay(
2005 type,
2006 isHtml,
2007 userAction,
2008 entity,
2009 Model
2010 );
2011 }
2012 }
2013 },
2014 deleteActionDisplay: function(type, isHtml, userAction) {
2015 // Archive / expire a role.
2016 let Model = Role;
2017 let actionHtml =
2018 addBoldHtmlTag(isHtml, userAction.user.email) + " removed ";
2019
2020 // Fake create the item (without saving of course) to get the display_role.
2021 let _role = new Model(userAction.body);
2022
2023 // Need to get the created role's email... but our data is really fucked up...
2024 let email = userAction.body.email;
2025
2026 if (!email && _role.email) {
2027 email = _role.email;
2028 } else if (!email && userAction.role) {
2029 email = userAction.role.email;
2030 }
2031
2032 actionHtml += addBoldHtmlTag(isHtml, email) + " from the Building";
2033
2034 return actionHtml;
2035 },
2036 },
2037 ROLE_LIST: {
2038 createActionDisplay: function(type, isHtml, userAction) {
2039 let Model = Role;
2040 let actionHtml =
2041 addBoldHtmlTag(isHtml, userAction.user.email) + " invited ";
2042
2043 // Fake create the item (without saving of course) to get the display_role.
2044 let _role = new Model(userAction.body);
2045
2046 // Need to get the created role's email... but our data is really fucked up...
2047 let email = userAction.body.email;
2048
2049 if (!email && _role.email) {
2050 email = _role.email;
2051 } else if (!email && userAction.role) {
2052 email = userAction.role.email;
2053 }
2054
2055 actionHtml +=
2056 addBoldHtmlTag(isHtml, email) +
2057 " to the Building with " +
2058 addBoldHtmlTag(isHtml, _role.display_role) +
2059 " permissions";
2060
2061 return actionHtml;
2062 },
2063 },
2064 ROOM_DETAIL: {
2065 updateActionDisplay: function(type, isHtml, userAction) {
2066 return routeHelpers.PIN_DETAIL.updateActionDisplay(
2067 type,
2068 isHtml,
2069 userAction,
2070 Room
2071 );
2072 },
2073 deleteActionDisplay: function(type, isHtml, userAction) {
2074 return standardDeleteActionDisplay(type, isHtml, userAction, Room);
2075 },
2076 },
2077 ROOM_LIST: {
2078 createActionDisplay: function(type, isHtml, userAction) {
2079 return standardCreateActionDisplay(type, isHtml, userAction, Room);
2080 },
2081 },
2082 // Values are not deleted through this route (deleted when parent pin field is removed from the pin type).
2083 ROOM_VALUE_DETAIL: {
2084 updateActionDisplay: function(type, isHtml, userAction) {
2085 return routeHelpers.PIN_VALUE_DETAIL.updateActionDisplay(
2086 type,
2087 isHtml,
2088 userAction,
2089 Room
2090 );
2091 },
2092 },
2093
2094 // Values are not created through this route (created when the parent pin field is created in the pin type).
2095 ROOM_VALUE_LIST: {},
2096
2097 // GET to send validation.
2098 SEND_VALIDATION: {},
2099
2100 // Tags cannot be updated.
2101 TAG_DETAIL: {
2102 deleteActionDisplay: function(type, isHtml, userAction) {
2103 return childDeleteActionDisplay(
2104 type,
2105 isHtml,
2106 userAction,
2107 Tag,
2108 userAction.document,
2109 Document
2110 );
2111 },
2112 },
2113 TAG_LIST: {
2114 createActionDisplay: function(type, isHtml, userAction) {
2115 return childCreateActionDisplay(
2116 type,
2117 isHtml,
2118 userAction,
2119 Tag,
2120 userAction.document,
2121 Document
2122 );
2123 },
2124 },
2125
2126 // Tag categories cannot be updated.
2127 TAG_CATEGORY_DETAIL: {
2128 deleteActionDisplay: function(type, isHtml, userAction) {
2129 return standardDeleteActionDisplay(
2130 type,
2131 isHtml,
2132 userAction,
2133 TagCategory
2134 );
2135 },
2136 },
2137 TAG_CATEGORY_LIST: {
2138 createActionDisplay: function(type, isHtml, userAction) {
2139 return standardCreateActionDisplay(
2140 type,
2141 isHtml,
2142 userAction,
2143 TagCategory
2144 );
2145 },
2146 },
2147
2148 // Tag category values cannot be updated.
2149 TAG_VALUE_DETAIL: {
2150 deleteActionDisplay: function(type, isHtml, userAction) {
2151 return childDeleteActionDisplay(
2152 type,
2153 isHtml,
2154 userAction,
2155 TagCategoryValue,
2156 userAction.tag_category,
2157 TagCategory
2158 );
2159 },
2160 },
2161 TAG_VALUE_LIST: {
2162 createActionDisplay: function(type, isHtml, userAction) {
2163 return childCreateActionDisplay(
2164 type,
2165 isHtml,
2166 userAction,
2167 TagCategoryValue,
2168 userAction.tag_category,
2169 TagCategory
2170 );
2171 },
2172 },
2173 TASK_DETAIL: {
2174 updateActionDisplay: function(type, isHtml, userAction) {
2175 let Model = Task;
2176 const modelConstants = Model.constants();
2177 let entityAlias = modelConstants.SINGULAR;
2178 let nameField = modelConstants.NAME_FIELD;
2179 let entity = userAction.task;
2180 let actionHtml = addBoldHtmlTag(isHtml, userAction.user.email);
2181 let patchString = null;
2182 let name = null;
2183
2184 if (entity && entity[nameField]) {
2185 name = entity[nameField];
2186 } else if (!entity) {
2187 /*
2188 * Looks like population failed because the entity was deleted.
2189 * Use the standard helper.
2190 */
2191 return standardUpdateActionDisplay(
2192 type,
2193 isHtml,
2194 userAction,
2195 entity,
2196 Model
2197 );
2198 }
2199
2200 switch (userAction.body.action) {
2201 case "update":
2202 actionHtml += " updated ";
2203
2204 if (userAction.method === "PATCH") {
2205 // PATCH, so we can guess what was changed,
2206 let getAlias = ABC.MODELS.WORK_ORDER.getAlias;
2207 let schema = _.keys(Model.schema.paths);
2208 let body = _.keys(userAction.body);
2209 let keys = _.intersection(body, schema);
2210
2211 patchString = getUpdateFieldString(isHtml, keys, getAlias);
2212 }
2213 break;
2214
2215 case "cancel":
2216 actionHtml += " canceled ";
2217 break;
2218
2219 case "complete":
2220 actionHtml += " completed ";
2221 break;
2222
2223 case "reopen":
2224 actionHtml += " reopened ";
2225 break;
2226
2227 case "assign":
2228 if (userAction.body.assignee) {
2229 actionHtml += " reassigned ";
2230 } else {
2231 actionHtml += " unassigned ";
2232 }
2233 break;
2234
2235 default:
2236 // Send off the standard update message.
2237 return standardUpdateActionDisplay(
2238 type,
2239 isHtml,
2240 userAction,
2241 entity,
2242 Model
2243 );
2244 }
2245
2246 switch (type) {
2247 case "entity":
2248 actionHtml += "this " + addBoldHtmlTag(isHtml, entityAlias);
2249
2250 if (patchString) {
2251 actionHtml += "'s " + patchString;
2252 }
2253 break;
2254
2255 case "building":
2256 if (patchString) {
2257 actionHtml +=
2258 patchString +
2259 " in " +
2260 addBoldHtmlTag(isHtml, entityAlias) +
2261 " - " +
2262 addAnchorTag(isHtml, name, entity);
2263 } else {
2264 actionHtml +=
2265 addBoldHtmlTag(isHtml, entityAlias) +
2266 " - " +
2267 addAnchorTag(isHtml, name, entity);
2268 }
2269 break;
2270
2271 // case 'account':
2272 // case 'organization':
2273 default:
2274 // Send off the standard update message.
2275 return standardUpdateActionDisplay(
2276 type,
2277 isHtml,
2278 userAction,
2279 entity,
2280 Model
2281 );
2282 }
2283
2284 // Message was added with Task/Work Order was completed
2285 const messageToRequestor = userAction.body.message_to_requestor;
2286 if (messageToRequestor) {
2287 actionHtml +=
2288 " with message: " +
2289 addItalicHtmlTag(isHtml, `"${messageToRequestor}"`);
2290 }
2291
2292 return actionHtml;
2293 },
2294 deleteActionDisplay: function(type, isHtml, userAction) {
2295 return standardDeleteActionDisplay(type, isHtml, userAction, Task);
2296 },
2297 },
2298 TASK_LIST: {
2299 createActionDisplay: function(type, isHtml, userAction) {
2300 /*
2301 * No way to get the correct task.number (that is part of the task name) so we can't use
2302 * standardCreateActionDisplay.
2303 */
2304 let Model = Task;
2305 const modelConstants = Model.constants();
2306 let entityAlias = modelConstants.SINGULAR;
2307 let nameField = modelConstants.NAME_FIELD;
2308 let actionHtml =
2309 addBoldHtmlTag(isHtml, userAction.user.email) +
2310 " created a new " +
2311 addBoldHtmlTag(isHtml, entityAlias);
2312
2313 // Fake create the item (without saving of course).
2314 let _entity = new Model(userAction.body);
2315
2316 // Add the name of the item if can be pulled out of the fake entity.
2317 let name = _entity[nameField];
2318
2319 if (name) {
2320 /*
2321 * Can't add an anchor tag (do not have the id of the created thing...) so just add the name
2322 * in bold.
2323 */
2324 actionHtml += " - " + addBoldHtmlTag(isHtml, name);
2325 }
2326
2327 if (_entity.is_from_request) {
2328 // Add an extra bit if the task was created in response to a service request.
2329 let _serviceRequest = {
2330 _id: _entity.request,
2331 };
2332
2333 actionHtml +=
2334 " in response to a " +
2335 addAnchorTag(isHtml, "Service Request", _serviceRequest);
2336 }
2337
2338 /*
2339 * Tasks created as part of a task schedule are not created through the API so their creation will
2340 * not have associated user actions.
2341 */
2342 return actionHtml;
2343 },
2344 },
2345 TERMS_OF_USE: {},
2346 TRADE_DETAIL: {
2347 updateActionDisplay: function(type, isHtml, userAction) {
2348 let Model = Trade;
2349 const modelConstants = Model.constants();
2350 let entityAlias = modelConstants.SINGULAR;
2351 let nameField = modelConstants.NAME_FIELD;
2352 let entity = userAction.trade;
2353 let actionHtml = addBoldHtmlTag(isHtml, userAction.user.email);
2354 let name = null;
2355
2356 if (entity && entity[nameField]) {
2357 name = entity[nameField];
2358 } else if (!entity) {
2359 /*
2360 * Looks like population failed because the entity was deleted.
2361 * Just use the standard helper.
2362 */
2363 return standardUpdateActionDisplay(
2364 type,
2365 isHtml,
2366 userAction,
2367 entity,
2368 Model
2369 );
2370 }
2371
2372 switch (userAction.body.action) {
2373 case "archive":
2374 actionHtml += " archived ";
2375 break;
2376 case "restore":
2377 actionHtml += " restored ";
2378 break;
2379 case "update":
2380 actionHtml += " updated ";
2381 break;
2382 default:
2383 // Just send off the standard update message.
2384 return standardUpdateActionDisplay(
2385 type,
2386 isHtml,
2387 userAction,
2388 entity,
2389 Model
2390 );
2391 }
2392
2393 switch (type) {
2394 case "entity":
2395 actionHtml += "this " + addBoldHtmlTag(isHtml, entityAlias);
2396 break;
2397 case "building":
2398 actionHtml +=
2399 addBoldHtmlTag(isHtml, entityAlias) +
2400 " - " +
2401 addAnchorTag(isHtml, name, entity);
2402 break;
2403 // case 'account':
2404 // case 'organization':
2405 default:
2406 // Just send off the standard update message.
2407 return standardUpdateActionDisplay(
2408 type,
2409 isHtml,
2410 userAction,
2411 entity,
2412 Model
2413 );
2414 }
2415
2416 return actionHtml;
2417 },
2418 deleteActionDisplay: function(type, isHtml, userAction) {
2419 return standardDeleteActionDisplay(type, isHtml, userAction, Trade);
2420 },
2421 },
2422 TRADE_LIST: {
2423 createActionDisplay: function(type, isHtml, userAction) {
2424 return standardCreateActionDisplay(type, isHtml, userAction, Trade);
2425 },
2426 },
2427 // Only GET calls.
2428 TRANSACTION_DETAIL: {},
2429
2430 // Only GET calls.
2431 TRANSACTION_LIST: {},
2432 UPGRADE_BROWSER: {},
2433 UPLOAD: {
2434 // GET POST and DELETE calls for multipart uploads.
2435 createActionDisplay: function(type, isHtml, userAction) {
2436 // TODO put more info in the userAction about the files that were uploaded
2437 const modelConstants = ABC.MODELS.DOCUMENT;
2438 let entityAlias = modelConstants.SINGULAR;
2439 return (
2440 addBoldHtmlTag(isHtml, userAction.user.email) +
2441 " uploaded a " +
2442 addBoldHtmlTag(isHtml, entityAlias)
2443 );
2444 },
2445 },
2446 };
2447 return routeHelpers;
2448});
2449
2450/**
2451 * Adds bold HTML tag to string.
2452 *
2453 * @function addBoldHtmlTag
2454 * @memberof UserActionModel.prototype
2455 * @private
2456 *
2457 * @param {Boolean} isHtml True when string is HTML
2458 * @param {String} str String
2459 *
2460 * @return {String} HTML string
2461 */
2462function addBoldHtmlTag(isHtml, str) {
2463 return isHtml ? addHtmlTag(isHtml, str, "b") : str;
2464}
2465
2466/**
2467 * Adds italic HTML tag to string.
2468 *
2469 * @function addItalicHtmlTag
2470 * @memberof UserActionModel.prototype
2471 * @private
2472 *
2473 * @param {Boolean} isHtml True when string is HTML
2474 * @param {String} str String
2475 *
2476 * @return {String} HTML string
2477 */
2478function addItalicHtmlTag(isHtml, str) {
2479 return isHtml ? addHtmlTag(isHtml, str, "i") : str;
2480}
2481
2482/**
2483 * Adds HTML tag to string.
2484 *
2485 * @function addHtmlTag
2486 * @memberof UserActionModel.prototype
2487 * @private
2488 *
2489 * @param {Boolean} isHtml True when string is HTML
2490 * @param {string} str String
2491 * @param {string} tag HTML tag to add
2492 *
2493 * @return {String} HTML string
2494 */
2495function addHtmlTag(isHtml, str, tag) {
2496 return isHtml ? "<" + tag + ">" + str + "</" + tag + ">" : str;
2497}
2498
2499/**
2500 * Adds anchor tag.
2501 *
2502 * @param {Boolean} isHtml True if content is HTML
2503 * @param {String} content Content
2504 * @param {Object} model Model
2505 *
2506 * @return {String} HTML
2507 */
2508function addAnchorTag(isHtml, content, model) {
2509 if (!model || model.deleted || model.is_deleted || model.expired) {
2510 // Can't send a link for these (will give 404), just return bold content.
2511 return isHtml
2512 ? addBoldHtmlTag(isHtml, content) + " (deleted)"
2513 : content + " (deleted)";
2514 } else if (!isHtml) {
2515 return content;
2516 }
2517
2518 let uri = model ? model.uri : null;
2519
2520 if (uri && (/\/pin_types$/.test(uri) || /\/pin_types\//.test(uri))) {
2521 // Looks like we need to open a new tab (for pin types)
2522 let parts = uri.split("/pin_types");
2523 uri = parts.join("/settings/pin_types");
2524 return '<a target="_blank" href="' + uri + '">' + content + "</a>";
2525 } else if (uri) {
2526 // Looks like we can render on the dashboard.
2527 return '<a href="' + uri + '">' + content + "</a>";
2528 }
2529 // Return bold content.
2530 return addBoldHtmlTag(isHtml, content);
2531}
2532
2533/**
2534 *
2535 * @param {string} [text] The actual text to display
2536 * @param {string} [url] The value of the href attribute
2537 * @param {Object} [options] Contains all the allowed options
2538 * @param {string} [options.target] Value for the target HTML attribute
2539 * @param {string} [options.title] Value for the title HTML attribute
2540 *
2541 * @return {string}
2542 */
2543function toHTMLAnchor(text = "", url = "", options = {}) {
2544 let href= `href="${url}"`;
2545 let target = ``;
2546 let title = `title="${text}"`;
2547
2548 if (options.target) {
2549 target = `target="${options.target}"`;
2550 }
2551
2552 if (options.title) {
2553 title = `title="${options.title}"`;
2554 }
2555
2556 return `<a ${target} ${href} ${title}>${text}</a>`;
2557}
2558
2559/**
2560 * Generates a standard human-readable create action display that occurred in the provided user action.
2561 *
2562 * @function standardCreateActionDisplay
2563 * @memberof UserActionModel.prototype
2564 * @private
2565 *
2566 * @param {String} type Building (entity N/A for creates)
2567 * @param {Boolean} isHtml If true, return an HTML string
2568 * @param {UserAction} userAction User action
2569 * @param {Object} Model Model
2570 *
2571 * @return {String} HTML
2572 */
2573function standardCreateActionDisplay(type, isHtml, userAction, Model) {
2574 const modelConstants = Model.constants();
2575
2576 let entityAlias = modelConstants.SINGULAR;
2577 let actionHtml =
2578 addBoldHtmlTag(isHtml, userAction.user.email) +
2579 " created a new " +
2580 addBoldHtmlTag(isHtml, entityAlias);
2581
2582 // Fake create the item (without saving of course).
2583 let _entity = new Model(userAction.body);
2584
2585 // Add the name of the item if can be pulled out of the fake entity.
2586 let nameField = modelConstants.NAME_FIELD;
2587 let name = _entity[nameField];
2588
2589 if (name) {
2590 /*
2591 * Can't add an anchor tag (do not have the id of the created thing...), so add the name in bold.
2592 */
2593 actionHtml += " - " + addBoldHtmlTag(isHtml, name);
2594 }
2595
2596 return actionHtml;
2597}
2598
2599/**
2600 * Generates a human-readable create action display for the child element in the provided user action.
2601 *
2602 * @function childCreateActionDisplay
2603 * @memberof UserActionModel.prototype
2604 * @private
2605 *
2606 * @param {String} type Building (entity N/A for creates)
2607 * @param {Boolean} isHtml If true, return an Html string
2608 * @param {UserAction} userAction User Action
2609 * @param {Object} Model Model
2610 * @param {Object} parentEntity Parent entity
2611 * @param {Object} ParentModel Parent Model
2612 *
2613 * @return {String} Create action display
2614 */
2615function childCreateActionDisplay(
2616 type,
2617 isHtml,
2618 userAction,
2619 Model,
2620 parentEntity,
2621 ParentModel
2622) {
2623 let actionHtml = standardCreateActionDisplay(type, isHtml, userAction, Model);
2624
2625 // Add some info about the parent if we can.
2626 const parentModelConstants = ParentModel.constants();
2627 let parentNameField = parentModelConstants.NAME_FIELD;
2628 let parentName = parentEntity ? parentEntity[parentNameField] : null;
2629
2630 if (parentName) {
2631 // Add the stuff about the parent and the anchor tag for the parent.
2632 actionHtml +=
2633 " in " +
2634 addBoldHtmlTag(isHtml, parentModelConstants.SINGULAR) +
2635 " - " +
2636 addAnchorTag(isHtml, parentName, parentEntity);
2637 }
2638
2639 return actionHtml;
2640}
2641
2642/**
2643 * Generates a standard human-readable update action display that occurred in the provided user action.
2644 *
2645 * @function standardUpdateActionDisplay
2646 * @memberof UserActionModel.prototype
2647 * @private
2648 *
2649 * @param {String} type Building or entity
2650 * @param {boolean} isHtml If true, return an Html string
2651 * @param {UserAction} userAction User action
2652 * @param {Object} entity Entity
2653 * @param {Object} Model Model
2654 *
2655 * @return {String} Update action display
2656 */
2657function standardUpdateActionDisplay(type, isHtml, userAction, entity, Model) {
2658 const modelConstants = Model.constants();
2659 let nameField = modelConstants.NAME_FIELD;
2660 let entityAlias = modelConstants.SINGULAR;
2661 let actionHtml = addBoldHtmlTag(isHtml, userAction.user.email) + " updated ";
2662
2663 switch (type) {
2664 case "entity":
2665 actionHtml += "this " + addBoldHtmlTag(isHtml, entityAlias);
2666 break;
2667
2668 case "building":
2669 if (entity && entity[nameField]) {
2670 actionHtml +=
2671 addBoldHtmlTag(isHtml, entityAlias) +
2672 " - " +
2673 addAnchorTag(isHtml, entity[nameField], entity);
2674 } else {
2675 actionHtml += "a " + addBoldHtmlTag(isHtml, entityAlias);
2676 }
2677
2678 break;
2679 // case 'account':
2680 // case 'organization':
2681 default:
2682 return "";
2683 }
2684
2685 return actionHtml;
2686}
2687
2688/**
2689 * Generates a human-readable update action display for the child element in the provided user action.
2690 *
2691 * @function childUpdateActionDisplay
2692 * @memberof UserActionModel.prototype
2693 * @private
2694 *
2695 * @param {String} type Building or entity
2696 * @param {Boolean} isHtml If true, return an Html string
2697 * @param {UserAction} userAction User action
2698 * @param {Object} entity Entity
2699 * @param {Object} Model Model
2700 * @param {Object} parentEntity Parent entity
2701 * @param {Object} ParentModel Parent Model
2702 *
2703 * @return {String} Update action display
2704 */
2705function childUpdateActionDisplay(
2706 type,
2707 isHtml,
2708 userAction,
2709 entity,
2710 Model,
2711 parentEntity,
2712 ParentModel
2713) {
2714 let actionHtml = standardUpdateActionDisplay(
2715 type,
2716 isHtml,
2717 userAction,
2718 entity,
2719 Model
2720 );
2721
2722 switch (type) {
2723 case "entity":
2724 // Nothing to add.
2725 break;
2726
2727 case "building": {
2728 // Add some info about the parent if we can.
2729 const parentModelConstants = ParentModel.constants();
2730 let parentNameField = parentModelConstants.NAME_FIELD;
2731 let parentName = parentEntity ? parentEntity[parentNameField] : null;
2732 if (parentName) {
2733 // Add the stuff about the parent and the anchor tag for the parent.
2734 actionHtml +=
2735 " in " +
2736 addBoldHtmlTag(isHtml, parentModelConstants.SINGULAR) +
2737 " - " +
2738 addAnchorTag(isHtml, parentName, parentEntity);
2739 }
2740 break;
2741 }
2742 // case 'account':
2743 // case 'organization':
2744 default:
2745 return "";
2746 }
2747
2748 return actionHtml;
2749}
2750
2751/**
2752 * Generates a standard human-readable delete action display that occurred in the provided user action.
2753 *
2754 * @function standardDeleteActionDisplay
2755 * @memberof UserActionModel.prototype
2756 * @private
2757 *
2758 * @param {String} type Building or entity
2759 * @param {boolean} isHtml If true, return an Html string
2760 * @param {UserAction} userAction User action
2761 * @param {Object} Model Model
2762 *
2763 * @return {String} Delete action display
2764 */
2765function standardDeleteActionDisplay(type, isHtml, userAction, Model) {
2766 /*
2767 * Entity is null in the userAction (because it was deleted).
2768 * There is no way that we can get the name of the entity out of the userAction.
2769 */
2770 const modelConstants = Model.constants();
2771 let actionHtml = addBoldHtmlTag(isHtml, userAction.user.email) + " deleted a";
2772
2773 let entityAlias = modelConstants.SINGULAR;
2774
2775 if (/a|e|i|o|u/i.test(entityAlias.charAt(0))) {
2776 actionHtml += "n ";
2777 } else {
2778 actionHtml += " ";
2779 }
2780
2781 actionHtml += addBoldHtmlTag(isHtml, entityAlias);
2782
2783 return actionHtml;
2784}
2785
2786/**
2787 * Generates a human-readable delete action display for the child element in the provided user action.
2788 *
2789 * @function childDeleteActionDisplay
2790 * @memberof UserActionModel.prototype
2791 * @private
2792 *
2793 * @param {String} type Building or entity
2794 * @param {Boolean} isHtml If true, return an Html string
2795 * @param {UserAction} userAction User action
2796 * @param {Object} Model Model
2797 * @param {Object} parentEntity Parent entity
2798 * @param {Object} ParentModel ParentModel
2799 *
2800 * @return {String} Delete action display
2801 */
2802function childDeleteActionDisplay(
2803 type,
2804 isHtml,
2805 userAction,
2806 Model,
2807 parentEntity,
2808 ParentModel
2809) {
2810 let actionHtml = standardDeleteActionDisplay(type, isHtml, userAction, Model);
2811
2812 // Add some info about the parent if we can.
2813 const parentModelConstants = ParentModel.constants();
2814 let parentNameField = parentModelConstants.NAME_FIELD;
2815 let parentName = parentEntity ? parentEntity[parentNameField] : null;
2816
2817 if (parentName) {
2818 // Add the stuff about the parent and the anchor tag for the parent.
2819 actionHtml +=
2820 " from " +
2821 addBoldHtmlTag(isHtml, parentModelConstants.SINGULAR) +
2822 " - " +
2823 addAnchorTag(isHtml, parentName, parentEntity);
2824 }
2825
2826 return actionHtml;
2827}
2828
2829/**
2830 * Generates human-readable action_display sentences provided a list of fields that were updated.
2831 *
2832 * @function getUpdateFieldString
2833 * @memberof UserActionModel.prototype
2834 * @private
2835 *
2836 * @param {Boolean} isHtml If true, return an Html string
2837 * @param {Array} keys Array of fields that were updated
2838 * @param {Function} getAlias Function that when given a schema
2839 * key, returns a human-readable string.
2840 *
2841 * @return {String} Update field string
2842 */
2843function getUpdateFieldString(isHtml, keys, getAlias) {
2844 let fieldString = "";
2845
2846 // Cycle through the provided keys (for fields that were updated).
2847 for (let i = 0; i < keys.length; i++) {
2848 /*
2849 * Append the phrase with appropriate punctuation depending on the number of fields that were updated and
2850 * the position of this field.
2851 */
2852 let alias = getAlias(keys[i]);
2853 if (keys.length === 1 || i === 0) {
2854 // Do not include starting ' '.
2855 fieldString += addBoldHtmlTag(isHtml, alias);
2856 } else if (i < keys.length - 1) {
2857 fieldString += ", " + addBoldHtmlTag(isHtml, alias);
2858 } else {
2859 fieldString += " and " + addBoldHtmlTag(isHtml, alias);
2860 }
2861 }
2862
2863 return fieldString;
2864}
2865
2866// METHODS =========================================================================================================
2867
2868// OTHER ===========================================================================================================
2869
2870UserAction.pre("save", function preSave(next) {
2871 // Make sure we don't store things we aren't supposed to know.
2872 let blacklistedFields = [
2873 "password",
2874 "secret",
2875 "new_password",
2876 "existing_password",
2877 "encrypted_credentials",
2878 "credentials",
2879 ];
2880
2881 if (this.body) {
2882 this.body = _.omit(this.body, blacklistedFields);
2883 }
2884
2885 if (this.query) {
2886 this.query = _.omit(this.query, blacklistedFields);
2887 }
2888
2889 return next();
2890});
2891
2892if (!UserAction.options.toJSON) {
2893 UserAction.options.toJSON = {};
2894}
2895
2896UserAction.options.toJSON.transform = function(doc, ret, options) {
2897 delete ret.__v;
2898 delete ret.ip;
2899
2900 if (options.includeDisplayDates) {
2901 let utcOffset = options.utcOffset || null;
2902 let format = options.dateFormat || null;
2903
2904 ret = _.extend(ret, doc.getDisplayDates(utcOffset, format));
2905 }
2906
2907 ret.entity_action_display = doc.entity_action_display;
2908 ret.building_action_display = doc.building_action_display;
2909 ret.entity_action_html = doc.entity_action_html;
2910 ret.building_action_html = doc.building_action_html;
2911 ret.is_200 = doc.is_200;
2912 ret.is_not_200 = doc.is_not_200;
2913 ret.is_500 = doc.is_500;
2914 ret.has_user = doc.has_user;
2915 ret.stringified_body = doc.stringified_body;
2916 ret.stringified_query = doc.stringified_query;
2917
2918 if (options && options.stringify) {
2919 // For the popovers in admin dashboard.
2920 ret.stringified = JSON.stringify(_.clone(ret));
2921 }
2922};
2923
2924// read user actions from the secondary databases instead of the primary
2925UserAction.set("read", "secondaryPreferred");
2926
2927module.exports = mongoose.model("UserAction", UserAction);
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939function a() {
2940 var courses = [];
2941 var studentRecords = openStudentEnrollmentRecords();
2942
2943 while (studentRecords.readNextRecord()) {
2944 var courseNumber = studentRecords.getCourseNumber();
2945 var studentName = studentRecords.getStudentName();
2946
2947 // lets say, courseNumber is 3
2948 if (!courses[courseNumber]) {
2949 // courseNumber = 3
2950 // courses[3] do you e
2951 courses[courseNumber]= [];
2952 // okay, courses = [null, null, null, ["bob", "jack"]]
2953 }
2954
2955 maleStudents[courseNumber].push(studentName);
2956 femaleStudents[courseNumber].push(studentName);
2957 }
2958
2959 // we should export all the names and the croues tob e storted out
2960 // now we just need to print
2961 // <table>
2962 // <th>.courtsers students total m/f .....
2963 for (var i = 18000; i < courses.length; i++) {
2964 var courseNumber = i;
2965 var students = courses[courseNumber];
2966
2967 if (!students) {
2968 continue;
2969 }
2970
2971 innerHtml += "<tr><td>" + courseNumber + "</td>";
2972 innerHtml += "<td>";
2973
2974 for (var a = 0; a < students.length; a++) {
2975 var student = students[a];
2976
2977 innerHtml += student + ", ";
2978 }
2979
2980 innerHtml += "</td><td>" + students.length + "</td></tr>";
2981
2982
2983 }
2984 // </table>
2985}