From vtk88com, 1 Week ago, written in Plain Text.
This paste is a reply to Untitled from vtk88com
- view diff
Embed
  1. define('core/models/Post',[
  2.     'jquery',
  3.     'underscore',
  4.     'backbone',
  5.     'moment',
  6.  
  7.     'core/config/urls',
  8.  
  9.     'core/api',
  10.     'core/strings',
  11.     'core/time',
  12.     'core/utils',
  13.     'core/utils/html',
  14.     'core/advice',
  15.     'remote/config',
  16.  
  17.     'core/models/mixins',
  18.  
  19.     'core/collections/VotersUserCollection',
  20.     'core/collections/VoteCollection',
  21. ], function (
  22.     $,
  23.     _,
  24.     Backbone,
  25.     moment,
  26.  
  27.     urls,
  28.  
  29.     api,
  30.     strings,
  31.     time,
  32.     utils,
  33.     htmlUtils,
  34.     advice,
  35.     config,
  36.  
  37.     modelMixins,
  38.  
  39.     VotersUserCollection,
  40.     VoteCollection
  41. ) {
  42.     'use strict';
  43.  
  44.     var VOTE_RATE_LIMIT = 1000; // 1s rate limit on voting
  45.     var LAST_VOTE = 0;
  46.     var MAX_POST_LENGTH = 25000;
  47.  
  48.     var acquireVoteLock = function () {
  49.         var now = $.now();
  50.         if (now - LAST_VOTE < VOTE_RATE_LIMIT) {
  51.             return false;
  52.         }
  53.         LAST_VOTE = now;
  54.         return true;
  55.     };
  56.  
  57.     var gettext = strings.get;
  58.  
  59.     var Post = Backbone.Model.extend({
  60.         votersCollectionClass: VotersUserCollection,
  61.  
  62.         defaults: function () {
  63.             return {
  64.                 createdAt: moment().format(time.ISO_8601),
  65.                 editableUntil: moment().add(config.max_post_edit_days, 'days').format(time.ISO_8601),
  66.                 dislikes: 0,
  67.                 isApproved: true, // Assume approved
  68.                 isDeleted: false,
  69.                 isEdited: false,
  70.                 isFlagged: false,
  71.                 isFlaggedByUser: false,
  72.                 isHighlighted: false,
  73.                 isRealtime: false,
  74.                 isImmediateReply: false,
  75.                 isMinimized: null,
  76.                 hasMedia: false,
  77.                 message: null,
  78.                 raw_message: null,
  79.                 likes: 0,
  80.                 media: [],
  81.                 parent: null,
  82.                 points: 0,
  83.                 depth: 0,
  84.                 userScore: 0,
  85.                 rating: null,
  86.             };
  87.         },
  88.  
  89.         initialize: function () {
  90.             this.votes = new VoteCollection();
  91.         },
  92.  
  93.         messageText: function () {
  94.             var msg = this.get('message');
  95.             return msg && htmlUtils.stripTags(msg);
  96.         },
  97.  
  98.         permalink: function (thread, useCurrentPage) {
  99.             var id = this.id;
  100.             if (!(id && thread)) {
  101.                 return '';
  102.             }
  103.             // Favor currentUrl if useCurrentPage is not false and always fallback to permalink
  104.             var baseUrl = useCurrentPage !== false &&  // default to true
  105.                           thread.currentUrl || thread.permalink();
  106.             var urlTool = window.document.createElement('a');
  107.             urlTool.href = baseUrl;
  108.             urlTool.hash = '#comment-' + id;
  109.  
  110.             return urlTool.href;
  111.         },
  112.  
  113.         shortLink: function () {
  114.             return urls.shortener + '/p/' + Number(this.id).toString(36);
  115.         },
  116.  
  117.         twitterText: function (permalink) {
  118.             var charsRemaining = 140;
  119.  
  120.             var author = this.author.get('name') || this.author.get('username');
  121.             charsRemaining -= author.length + 3;  // +3 for space dash space before author
  122.             charsRemaining -= permalink.length + 1;  // +1 for space before url
  123.             charsRemaining -= 2;  // For the quotation marks
  124.  
  125.             var text = utils.niceTruncate(this.messageText(), charsRemaining);
  126.  
  127.             return '"' + text + '" \u2014 ' + author;
  128.         },
  129.  
  130.         toJSON: function (options) {
  131.             var json = Backbone.Model.prototype.toJSON.call(this);
  132.             if (options) {
  133.                 var session = options.session;
  134.                 var thread = options.thread;
  135.  
  136.                 json.canBeEdited = this.canBeEdited(session, thread);
  137.                 json.canBeRepliedTo = this.canBeRepliedTo(session, thread);
  138.                 json.canBeShared = this.canBeShared();
  139.                 json.permalink = this.permalink(thread);
  140.             }
  141.  
  142.             json.shortLink = this.shortLink();
  143.             json.isMinimized = this.isMinimized();
  144.             json.plaintext = this.messageText();
  145.  
  146.             // Add relativeCreatedAt for display in the UI (e.g., "Comment created 1 hour ago")
  147.             json.relativeCreatedAt = this.getRelativeCreatedAt();
  148.             // formattedCreatedAt is nice absolute time (e.g. "Jan 1, 2008, 12:00 pm")
  149.             json.formattedCreatedAt = this.getFormattedCreatedAt();
  150.             // expose cid to make it easier to find the model in collections later
  151.             json.cid = this.cid;
  152.  
  153.             return json;
  154.         },
  155.  
  156.         /**
  157.          * Is this post visible to all users?
  158.          * @returns {boolean}
  159.          */
  160.         isPublic: function () {
  161.  
  162.             // Posts that are highlighted or sponsored are always public.
  163.             if (this.get('isHighlighted') || this.get('isSponsored')) {
  164.                 return true;
  165.             }
  166.             // Otherwise, if they are deleted they are definitely *not* public.
  167.             if (this.get('isDeleted')) {
  168.                 return false;
  169.             }
  170.             // Finally, if they are approved then they are public.
  171.             return this.get('isApproved');
  172.         },
  173.  
  174.         isMinimized: function () {
  175.             // If comment has been highlighted by a moderator, it is NOT minimized
  176.             // (highlighting is implicit approval by the moderator)
  177.             if (this.get('isHighlighted')) {
  178.                 return false;
  179.             }
  180.             // If the comment has been explicitly expanded (unminimized) by
  181.             // the user, it is NOT minimized
  182.             if (this.get('isMinimized') === false) {
  183.                 return false;
  184.             }
  185.             return !this.get('isApproved');
  186.         },
  187.  
  188.         // These methods are stubbed out because they rely on the Session object,
  189.         // which hasn't been ported to next-core yet. They still exist, and return
  190.         // sensible default values, because they're called from Post.prototype.toJSON.
  191.         //
  192.         // NOTE: Until Session is ported to next-core, any applications using Post w/
  193.         //       sessions will need to override these prototype methods
  194.  
  195.         isAuthorSessionUser: function () { return false; },
  196.  
  197.         canBeEdited: function () { return false; },
  198.  
  199.         canBeRepliedTo: function () { return false; },
  200.  
  201.         canBeShared: function () { return false; },
  202.  
  203.         validateMessage: function (attrs) {
  204.             if (_.isString(attrs.raw_message)) {
  205.                 if (attrs.raw_message === '') {
  206.                     return gettext('Comments can\'t be blank.');
  207.                 }
  208.                 // eslint-disable-next-line no-magic-numbers
  209.                 if (attrs.raw_message.length < 2) {
  210.                     return gettext('Comments must have at least 2 characters.');
  211.                 }
  212.                 if (attrs.raw_message.length > MAX_POST_LENGTH) {
  213.                     return strings.interpolate(gettext('Comments can\'t be longer than %(maxLength)s characters (currently %(currentLength)s characters).'), {
  214.                         maxLength: MAX_POST_LENGTH,
  215.                         currentLength: attrs.raw_message.length,
  216.                     });
  217.                 }
  218.             }
  219.         },
  220.  
  221.         validate: function (attrs) {
  222.             if (this.id || attrs.id) {
  223.                 return;
  224.             }
  225.             var messageError = this.validateMessage(attrs);
  226.             if (messageError) {
  227.                 return messageError;
  228.             }
  229.             if (attrs.author_email) {
  230.                 attrs.author_email = $.trim(attrs.author_email);
  231.             }
  232.             if (attrs.author_name) {
  233.                 attrs.author_name = $.trim(attrs.author_name);
  234.             }
  235.             if (attrs.author_email === '' && attrs.author_name === '') {
  236.                 return gettext('Please sign in or enter a name and email address.');
  237.             }
  238.             else if (attrs.author_email === '' || attrs.author_name === '') {
  239.                 return gettext('Please enter both a name and email address.');
  240.             }
  241.             else if (attrs.author_name && attrs.author_name.length && !attrs.age_declaration) {
  242.                 return gettext('Please confirm that you are 18 years of age or older.');
  243.             }
  244.  
  245.             // Simple client-side email validation. This is obviously not a complete
  246.             // regex, but it should catch 90+% of errors. We can catch the rest on
  247.             // the server.
  248.             if (_.isString(attrs.author_email) && !this.validateEmail(attrs.author_email)) {
  249.                 return gettext('Invalid email address format.');
  250.             }
  251.         },
  252.  
  253.         validateEmail: function (email) {
  254.             return utils.validateEmail(email);
  255.         },
  256.  
  257.         report: function (reason) {
  258.             this.set('isFlagged', true);
  259.  
  260.             var data = {
  261.                 post: this.id,
  262.             };
  263.  
  264.             if (reason) {
  265.                 data.reason = reason;
  266.             }
  267.             api.call('posts/report.json', {
  268.                 data: data,
  269.                 method: 'POST',
  270.             });
  271.         },
  272.  
  273.         _highlight: function (shouldBeHighlighted) {
  274.             this.set('isHighlighted', shouldBeHighlighted);
  275.             api.call('posts/' + (shouldBeHighlighted ? 'highlight' : 'unhighlight') + '.json', {
  276.                 data: {
  277.                     post: this.id,
  278.                 },
  279.                 method: 'POST',
  280.             });
  281.         },
  282.  
  283.         highlight: function () {
  284.             this._highlight(true);
  285.         },
  286.  
  287.         unhighlight: function () {
  288.             this._highlight(false);
  289.         },
  290.  
  291.         /**
  292.          * Override for post models that don't store the thread id
  293.          * in the thread attr
  294.          *
  295.          * @returns {string} id of thread.
  296.          */
  297.         getThreadId: function () {
  298.             return this.get('thread');
  299.         },
  300.  
  301.         getUpvotersUserCollection: _.memoize(function () {
  302.             var CollectionClass = this.votersCollectionClass;
  303.  
  304.             return new CollectionClass(undefined, {
  305.                 postId: this.id,
  306.                 threadId: this.getThreadId(),
  307.             });
  308.         }, function () {
  309.             return [this.id, '1'].join('');
  310.         }),
  311.  
  312.         getDownvotersUserCollection: _.memoize(function () {
  313.             var CollectionClass = this.votersCollectionClass;
  314.  
  315.             return new CollectionClass(undefined, {
  316.                 postId: this.id,
  317.                 threadId: this.getThreadId(),
  318.             });
  319.         }, function () {
  320.             return [this.id, '-1'].join('');
  321.         }),
  322.  
  323.         // This function does score calculations and increments/decrements
  324.         // counters. We can't just bind it to the 'add' event because
  325.         // we want to call _vote before making a network request and adding
  326.         // a freshly created vote to a collection. (sad face)
  327.  
  328.         _vote: function (vote, score, voter) {
  329.             var delta = vote - score;
  330.             var updates = {
  331.                 likes: this.get('likes'),
  332.                 dislikes: this.get('dislikes'),
  333.                 points: this.get('points'),
  334.             };
  335.  
  336.             // If the vote is unchanged from the user's previous vote,
  337.             // so ignore - there's nothing to do
  338.  
  339.             if (delta === 0) {
  340.                 return delta;
  341.             }
  342.             // Update likes and dislikes. This is tricky because a
  343.             // switched vote requires updating *both* attributes, whereas
  344.             // a new vote only touches one.
  345.  
  346.             if (vote > 0) {
  347.                 updates.likes += vote;
  348.                 updates.dislikes += score;
  349.             } else if (vote < 0) {
  350.                 updates.dislikes -= vote;
  351.                 updates.likes -= score;
  352.             } else if (score > 0) {
  353.                 updates.likes -= score;
  354.             } else {
  355.                 updates.dislikes += score;
  356.             }
  357.  
  358.             updates.points += delta;
  359.  
  360.             // add user into correct collection and remove from the other one
  361.             if (voter) {
  362.                 if (vote === 1) {
  363.                     this.getUpvotersUserCollection().add(voter);
  364.                     this.getDownvotersUserCollection().remove(voter);
  365.                 } else {
  366.                     this.getDownvotersUserCollection().add(voter);
  367.                     this.getUpvotersUserCollection().remove(voter);
  368.                 }
  369.             }
  370.  
  371.  
  372.             // All these values should be updated at once to prevent any "half-baked"
  373.             // data to trigger the view or any other thing that listens on change
  374.             // events of these properties.
  375.             this.set(updates);
  376.  
  377.             return delta;
  378.         },
  379.  
  380.         // Vote on a post, taking into account any earlier votes
  381.         // made by the current user (via the userScore attribute).
  382.         //
  383.         // @params vote {Integer} Can be one of -1, 0, or 1
  384.  
  385.         vote: function (vote) {
  386.             // Rate limit voting on the Post's public vote interface.
  387.             // We don't rate limit on the private interface because that
  388.             // is used for realtime votes which could come in faster than
  389.             // the rate limit.
  390.             if (!acquireVoteLock()) {
  391.                 return 0;
  392.             }
  393.             var self = this;
  394.             var delta = self._vote(vote, self.get('userScore'));
  395.  
  396.             if (delta === 0) {
  397.                 return;
  398.             }
  399.  
  400.             var newNumLikesReceived = self.author ? self.author.get('numLikesReceived') : 0;
  401.             if (self.get('userScore') === 1) {
  402.                 newNumLikesReceived -= 1;
  403.             } else if (vote === 1) {
  404.                 newNumLikesReceived += 1;
  405.             }
  406.  
  407.             self.set('userScore', vote);
  408.             api.call('posts/vote.json', {
  409.                 data: {
  410.                     post: self.id,
  411.                     vote: vote,
  412.                 },
  413.                 method: 'POST',
  414.                 success: function (data) {
  415.                     // We have merge:true in order to update existing vote instances,
  416.                     // so when the same vote will come from realtime it won't be recognized
  417.                     // as a change.
  418.                     self.votes.add({ id: data.response.id, score: vote }, { merge: true });
  419.  
  420.                     if (self.author) {
  421.                         self.author.set('numLikesReceived', newNumLikesReceived);
  422.                     }
  423.                 },
  424.             });
  425.         },
  426.  
  427.         _delete: function () {
  428.             this.set({
  429.                 isApproved: false,
  430.                 isDeleted: true,
  431.             });
  432.  
  433.             return api.call('posts/remove.json', {
  434.                 data: { post: this.id },
  435.                 method: 'POST',
  436.             });
  437.         },
  438.  
  439.         spam: function () {
  440.             this.set({
  441.                 isApproved: false,
  442.                 isDeleted: true,
  443.                 isSpam: true,
  444.             });
  445.  
  446.             this.trigger('spam');
  447.  
  448.             api.call('posts/spam.json', {
  449.                 data: { post: this.id },
  450.                 method: 'POST',
  451.             });
  452.         },
  453.  
  454.         _create: function (model, options) {
  455.             var self = this;
  456.             var attrs = model.attributes;
  457.             var params = {
  458.                 thread: attrs.thread,
  459.                 message: attrs.raw_message,
  460.                 rating: attrs.rating,
  461.             };
  462.  
  463.             if (attrs.parent) {
  464.                 params.parent = attrs.parent;
  465.             }
  466.             // Anon posting
  467.             if (attrs.author_name) {
  468.                 params.author_name = attrs.author_name;
  469.                 params.author_email = attrs.author_email;
  470.                 params.age_declaration = attrs.age_declaration;
  471.             }
  472.  
  473.             return api.call('posts/create.json', {
  474.                 data: params,
  475.                 method: 'POST',
  476.                 success: function (data) {
  477.                     self.set(data.response);
  478.                     if (options.success) {
  479.                         options.success();
  480.                     }
  481.                 },
  482.                 error: options.error,
  483.             });
  484.         },
  485.  
  486.         _update: function (model, options) {
  487.             var self = this;
  488.             var attrs = model.attributes;
  489.  
  490.             var params = {
  491.                 post: attrs.id,
  492.                 message: attrs.raw_message,
  493.                 rating: attrs.rating,
  494.             };
  495.  
  496.  
  497.             return api.call('posts/update.json', {
  498.                 data: params,
  499.                 method: 'POST',
  500.                 success: function (data) {
  501.                     self.set(data.response);
  502.                     if (options.success) {
  503.                         options.success();
  504.                     }
  505.                 },
  506.                 error: options.error,
  507.             });
  508.         },
  509.  
  510.         _read: function (_model, options) {
  511.             var self = this;
  512.             options = options || {};
  513.  
  514.             return api.call('posts/details.json', {
  515.                 data: { post: self.id },
  516.                 method: 'GET',
  517.                 success: function (data) {
  518.                     // When highlighting and unhighlighting posts in quick succession,
  519.                     // sometimes we send a cached version of the model that is unhighlighted.
  520.                     // If that happens, we fix that here in the response
  521.                     if (options.isHighlighted && !data.response.isHighlighted) {
  522.                         data.response.isHighlighted = true;
  523.                     }
  524.                     self.set(data.response);
  525.                     if (options.success) {
  526.                         options.success();
  527.                     }
  528.                 },
  529.                 error: options.error,
  530.             });
  531.         },
  532.  
  533.         sync: function (method, model, options) {
  534.             options = options || {};
  535.  
  536.             var error = options.error;
  537.             if (error) {
  538.                 options.error = function (xhr) {
  539.                     var response = {};
  540.                     try {
  541.                         response = JSON.parse(xhr.responseText);
  542.                     } catch (err) {
  543.                         // ignore
  544.                     }
  545.  
  546.                     error(response);
  547.                 };
  548.             }
  549.  
  550.             switch (method) {
  551.             case 'create':
  552.                 return this._create(model, options);
  553.             case 'update':
  554.                 return this._update(model, options);
  555.             case 'delete':
  556.                 return this._delete();
  557.             case 'read':
  558.                 return this._read(model, options);
  559.             default:
  560.                 return null;
  561.             }
  562.         },
  563.  
  564.         /**
  565.          * Generate key for storing draft posts in local storage.
  566.          * A draft post is one that has not yet been saved on the back
  567.          * end.
  568.          *
  569.          * Example: drafts:thread:1:parent:0
  570.          *
  571.          * @returns {string} key name
  572.          */
  573.         storageKey: function () {
  574.             // Only allow draft posts to be stored in local storage
  575.             if (!this.isNew()) {
  576.                 return;
  577.             }
  578.             if (!this.getThreadId()) {
  579.                 return;
  580.             }
  581.             return ['drafts', 'thread', this.getThreadId(), 'parent', this.get('parent') || 0].join(':');
  582.         },
  583.     }, {
  584.         /**
  585.          * Get an escaped approximation of how a raw message
  586.          * will be formatted by our API.
  587.          *
  588.          * This method currently _only_ formats newlines into
  589.          * <p> and <br> tags.
  590.          *     TODO: make this method handle mentions and various
  591.          *           supported tags.
  592.          *
  593.          * @params {string} text Text to format.
  594.          * @returns {string} Escaped and <p> <br> message.
  595.          */
  596.         formatMessage: (function () {
  597.  
  598.             var paragraphReg = /(?:\r\n|\r|\n){2,}/;
  599.             var lineReg = /\r\n|\r|\n/;
  600.  
  601.             return function (text) {
  602.  
  603.                 // Any series of two or more newlines is a paragraph.
  604.                 // Use `compact()` to filter empty values.
  605.                 var paragraphs = _.chain(text.split(paragraphReg))
  606.                     .compact()
  607.                     .value();
  608.  
  609.                 var html = _.map(paragraphs, function (paragraph) {
  610.  
  611.                     // Single newlines are spaced with <br> tags.
  612.                     // Use `compact()` to filter empty values.
  613.                     return _.chain(paragraph.split(lineReg))
  614.                         .compact()
  615.  
  616.                         // Escape text to prevent users from
  617.                         // accidentally XXSing themselves.
  618.                         .map(_.escape)
  619.                         .join('<br>')
  620.                         .value();
  621.  
  622.                 }).join('</p><p>');
  623.  
  624.                 return '<p>' + html + '</p>';
  625.             };
  626.  
  627.         })(),
  628.     });
  629.  
  630.     //
  631.     // Applied mixins
  632.     //
  633.  
  634.     modelMixins.withCreatedAt.call(Post.prototype);
  635.     advice.withAdvice.call(Post.prototype);
  636.  
  637.     //
  638.     // Optional mixins
  639.     //
  640.  
  641.     /**
  642.      * Adds `author` as a sub-model of Post
  643.      */
  644.  
  645.     Post.withAuthor = function (UserModel) {
  646.         this.around('set', function (set, key, val, options) {
  647.             var attrs;
  648.  
  649.             // eslint-disable-next-line eqeqeq, no-eq-null
  650.             if (key == null) {
  651.                 return this;
  652.             }
  653.             // Handle both `"key", value` and `{key: value}` -style arguments.
  654.             if (typeof key === 'object') {
  655.                 attrs = key;
  656.                 options = val;
  657.             } else {
  658.                 attrs = {};
  659.                 attrs[key] = val;
  660.             }
  661.  
  662.             var author = attrs.author;
  663.  
  664.             if (author) {
  665.                 // Convert 'author' attributes to a model instance. Attributes
  666.                 // should never contain nested properties, but that's what the API
  667.                 // returns, so we convert it here.
  668.                 //
  669.                 // NOTE: This kind of stuff is made easier with a Backbone plugin
  670.                 //       called Backbone-Relational, but I'm not sure we need it
  671.                 //       just yet. (https://github.com/PaulUithol/Backbone-relational)
  672.  
  673.                 if (_.isString(author) || _.isNumber(author)) {
  674.                     var id = author;
  675.                     author = {};
  676.                     author[UserModel.prototype.idAttribute || 'id'] = id;
  677.                 }
  678.  
  679.                 // Usually the Post will have access to the PostCollection,
  680.                 // but when the sort-order changes we need to grab the collection from the author
  681.                 var collection = this.collection || (this.author && this.author.collection);
  682.  
  683.                 // Convert the forum badge IDs on the post author to forum badge objects
  684.                 var forum = collection && collection.thread && collection.thread.forum;
  685.                 if (this.author && this.author.get('badges').length && this.author.get('badges')[0].id) {
  686.                     // If the author already has the forum badge objects
  687.                     author.badges = this.author.get('badges');
  688.                 } else if (forum && forum.get('badges') && author.badges) {
  689.                     var badges = [];
  690.                     var badgeIds = author.badges || [];
  691.                     var forumBadges = forum.get('badges');
  692.                     badgeIds.forEach(function (badgeId) {
  693.                         if (forumBadges[badgeId]) {
  694.                             badges.push(forumBadges[badgeId]);
  695.                         }
  696.                     });
  697.                     author.badges = badges;
  698.                 }
  699.  
  700.                 this.author = new UserModel(author);
  701.  
  702.                 // This is the standard event structure for when a related model is
  703.                 // set (used in home).
  704.                 this.trigger('changeRelated:author');
  705.  
  706.                 delete attrs.author;
  707.             }
  708.  
  709.             return set.call(this, attrs, options);
  710.         });
  711.  
  712.         this.around('toJSON', function (toJSON) {
  713.             var json = toJSON.apply(this, _.rest(arguments));
  714.  
  715.             if (this.author) {
  716.                 json.author = this.author.toJSON();
  717.             }
  718.             return json;
  719.         });
  720.     };
  721.  
  722.     /**
  723.      * Adds `media` as a sub-collection of Post
  724.      *
  725.      * @param {Object} MediaCollection - a MediaCollection class that encapsulates media attributes
  726.      * @returns {undefined}
  727.      */
  728.     Post.withMediaCollection = function (MediaCollection) {
  729.         this.after('set', function (attrs) {
  730.             if (attrs && typeof attrs !== 'string') {
  731.  
  732.                 // Convert 'media' attribute into collection
  733.                 if (!_.isUndefined(attrs.media)) {
  734.                     // Populate media collection
  735.                     if (this.media) {
  736.                         this.media.reset(attrs.media);
  737.                     } else {
  738.                         this.media = new MediaCollection(attrs.media);
  739.                     }
  740.                     delete attrs.media;
  741.                 }
  742.             }
  743.         });
  744.  
  745.         this.around('toJSON', function (toJSON) {
  746.             var json = toJSON.apply(this, _.rest(arguments));
  747.  
  748.             if (this.media) {
  749.                 json.media = this.media.toJSON();
  750.             }
  751.             return json;
  752.         });
  753.     };
  754.  
  755.     return Post;
  756. });
  757. // https://c.disquscdn.com/next/next-core/core/models/Post.js
  758.