antville/code/Story/Story.js

652 lines
16 KiB
JavaScript
Raw Normal View History

// The Antville Project
// http://code.google.com/p/antville
//
// Copyright 20012014 by the Workers of Antville.
//
// Licensed under the Apache License, Version 2.0 (the ``License'');
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
2014-07-04 15:32:18 +02:00
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an ``AS IS'' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileOverview Defines the Story prototype.
*/
markgettext('Story');
markgettext('story');
this.handleMetadata('title');
this.handleMetadata('text');
Story.ALLOWED_MACROS = [
'file',
'image',
'link',
'poll',
'story.link'
];
/**
* @function
* @param {Object} data
* @param {Site} site
* @param {User} user
* @returns {Story}
*/
Story.add = function(data, site, user) {
2014-07-04 15:32:18 +02:00
HopObject.confirmConstructor(Story);
site || (site = res.handlers.site);
user || (user = session.user);
var story = new Story;
story.name = String.EMPTY;
story.requests = 0;
story.created = story.modified = new Date;
story.site = site;
story.creator = story.modifier = user;
story.update(data);
site.stories.add(story);
return story;
}
/**
* @function
*/
Story.remove = function() {
2014-07-04 15:32:18 +02:00
if (this.constructor === Story) {
HopObject.remove.call(this.comments);
this.setTags(null);
this.deleteMetadata();
this.remove();
}
return;
}
/**
* @function
* @returns {String[]}
* @see defineConstants
*/
Story.getStatus = defineConstants(Story, markgettext('closed'),
markgettext('public'), markgettext('shared'), markgettext('open'));
/**
* @function
* @returns {String[]}
* @see defineConstants
*/
Story.getModes = defineConstants(Story, markgettext('hidden'),
markgettext('featured'));
/**
* @function
* @returns {String[]}
* @see defineConstants
*/
Story.getCommentModes = defineConstants(Story, markgettext('closed'),
/* markgettext('readonly'), markgettext('moderated'), */
markgettext('open'));
* Fixed reference to parent site in Archive * Fixed _children.filter in Archive * Added missing permission checks * Modified global defineConstants() method to return the getter function instead of automatically defining it with given argument * Added HopObject.macro_macro() method to display userland macro code * Removed colorpicker (will be replaced by third-party library) * Removed obsolete global constants and functions * Overhauled and tested global userland macros like story_macro(), image_macro() etc. * Implemented global list_macro() to replace any special listFoobar_macro() methods * Moved global autoLogin() method into User prototype * Overhauled global randomize_macro() * Renamed global evalURL() method to validateUrl() as well as evalEmail() to validateEmail() * Re-added accidentally removed subskins to Members.skin * Fixed some skin names which were changed recently * Remove delete_action() from Membership * Fixed foreign key of images collection in Membership * Removed global username_macro() and replaced it with appropriate membership macros * Moved contents of systemscripts.skin into javascript.skin in Root prototype * Removed main_css_action(), main_js_action() and sitecounter_macro() methods from Root * Added accessname to sites collection in Root * Upgraded jQuery to version 1.2.1 * Replaced call for global history_macro() with corresponding list_macro() call * Renamed "public" collection of Stories prototype to "featured" * Moved a lot of styles from Root's style.skin to the one in Site * Added comments collection to Site * Moved embed.skin as subskin #embed into Site.skin * Fixed some minor issues in Story.js (removed check for creator before setting the story's mode) * Defined cookie names as constants of User which can be overriden via app.properties userCookie and hashCookie * Moved a lot of code into compatibility module
2007-10-11 23:03:17 +00:00
/**
* @name Story
* @constructor
* @property {Comment[]} _children
* @property {String} commentMode
* @property {Comment[]} comments
* @property {Date} created
* @property {User} creator
* @property {Metadata} metadata
* @property {String} mode
* @property {Date} modified
* @property {User} modifier
* @property {String} name
* @property {Number} parent_id
* @property {String} parent_type
* @property {String} prototype
* @property {Number} requests
* @property {Site} site
* @property {String} status
* @property {TagHub[]} tags
* @property {String} text
* @property {String} title
* @extends HopObject
*/
Story.prototype.constructor = function() {
2014-07-04 15:32:18 +02:00
HopObject.confirmConstructor(this);
return this;
}
/**
*
* @param {String} action
* @returns {Boolean}
*/
Story.prototype.getPermission = function(action) {
if (!this.site.getPermission('main')) {
2014-07-04 15:32:18 +02:00
return false;
}
switch (action) {
case '.':
case 'main':
2014-07-04 15:32:18 +02:00
return this.status !== Story.CLOSED ||
this.creator === session.user ||
Membership.require(Membership.MANAGER) ||
User.require(User.PRIVILEGED);
case 'comment':
2014-07-04 15:32:18 +02:00
return this.site.commentMode === Site.ENABLED &&
(this.commentMode === Story.OPEN ||
this.commentMode === Story.MODERATED);
case 'delete':
2014-07-04 15:32:18 +02:00
return this.creator === session.user ||
Membership.require(Membership.MANAGER) ||
User.require(User.PRIVILEGED);
case 'edit':
2014-12-16 23:07:59 +01:00
case 'mode':
case 'rotate': // FIXME: Action moved to compat layer
case 'status':
2014-07-04 15:32:18 +02:00
return this.creator === session.user ||
Membership.require(Membership.MANAGER) ||
(this.status === Story.SHARED &&
Membership.require(Membership.CONTRIBUTOR)) ||
(this.status === Story.OPEN &&
Membership.require(Membership.SUBSCRIBER)) ||
User.require(User.PRIVILEGED);
}
return false;
}
Story.prototype.main_action = function() {
2014-07-04 15:32:18 +02:00
res.data.title = this.getTitle(10);
res.data.body = this.renderSkinAsString('Story#main');
this.site.renderSkin('Site#page');
2014-07-04 15:32:18 +02:00
this.site.log();
this.count();
this.log();
return;
}
/**
*
* @param {Number} limit
* @returns {String}
*/
Story.prototype.getTitle = function(limit) {
var key = this + ':title:' + limit;
2014-07-04 15:32:18 +02:00
if (!res.meta[key]) {
if (this.title) {
res.meta[key] = stripTags(this.title).clip(limit, '...', '\\s');
2014-07-04 15:32:18 +02:00
} else if (this.text) {
var parts = stripTags(this.text).embody(limit, '...', '\\s');
2014-07-04 15:32:18 +02:00
res.meta[key] = parts.head;
res.meta[this + ':text:' + limit] = parts.tail;
2014-07-04 15:32:18 +02:00
}
}
return String(res.meta[key]) || '...';
}
Story.prototype.edit_action = function() {
2014-07-04 15:32:18 +02:00
if (req.postParams.save) {
try {
this.update(req.postParams);
delete session.data.backup;
res.message = gettext('The story was successfully updated.');
res.redirect(this.href('edit'));
2014-07-04 15:32:18 +02:00
} catch (ex) {
res.message = ex;
app.log(ex);
}
}
res.data.action = this.href(req.action);
res.data.title = gettext('Edit Story');
res.data.body = this.renderSkinAsString('Story#edit');
this.site.renderSkin('Site#page');
2014-07-04 15:32:18 +02:00
return;
}
/**
*
* @param {Object} data
*/
Story.prototype.update = function(data) {
2014-07-04 15:32:18 +02:00
var site = this.site || res.handlers.site;
if (!data.title && !data.text) {
throw Error(gettext('Please enter at least something into the “title” or “text” field.'));
2014-07-04 15:32:18 +02:00
}
if (data.created) {
try {
this.created = data.created.toDate('yyyy-MM-dd HH:mm', site.getTimeZone());
2014-07-04 15:32:18 +02:00
} catch (ex) {
throw Error(gettext('Cannot parse timestamp {0} as a date.', data.created));
2014-07-04 15:32:18 +02:00
app.log(ex);
}
}
// Get difference to current content before applying changes
var delta = this.getDelta(data);
2014-12-17 23:59:32 +01:00
this.title = data.title ? stripTags(data.title.trim()) : String.EMPTY;
2014-07-04 15:32:18 +02:00
this.text = data.text ? data.text.trim() : String.EMPTY;
this.status = data.status || Story.PUBLIC;
this.mode = data.mode || Story.FEATURED;
this.commentMode = data.commentMode || Story.OPEN;
this.setCustomContent(data);
// FIXME: To be removed resp. moved to Stories.create_action and
// Story.edit_action if work-around for Helma bug #607 fails
// We need persistence for setting the tags
this.isTransient() && this.persist();
this.setTags(data.tags || data.tag_array);
if (delta > 50) {
this.modified = new Date;
if (this.status !== Story.CLOSED) {
site.modified = this.modified;
}
site.callback(this);
// Notification is sent in Stories.create_action()
}
this.clearCache();
this.modifier = session.user;
return;
}
2014-12-16 23:07:59 +01:00
Story.prototype.status_action = function () {
this.status = (this.status === Story.CLOSED ? Story.PUBLIC : Story.CLOSED);
2014-07-04 15:32:18 +02:00
return res.redirect(req.data.http_referer || this._parent.href());
2014-12-16 23:07:59 +01:00
};
Story.prototype.mode_action = function () {
this.mode = (this.mode === Story.HIDDEN ? Story.FEATURED : Story.HIDDEN);
return res.redirect(req.data.http_referer || this._parent.href());
};
Story.prototype.comment_action = function() {
2014-07-04 15:32:18 +02:00
// Check if user is logged in since we allow linking here for any user
if (!User.require(User.REGULAR)) {
User.setLocation(this.href(req.action) + '#form');
res.message = gettext('Please login first.');
res.redirect(this.site.members.href('login'));
2014-07-04 15:32:18 +02:00
}
if (req.postParams.save) {
try {
var comment = Comment.add(req.postParams, this);
comment.notify(req.action);
delete session.data.backup;
res.message = gettext('The comment was successfully created.');
2014-07-04 15:32:18 +02:00
res.redirect(comment.href());
} catch (ex) {
res.message = ex;
app.log(ex);
}
}
res.handlers.parent = this;
res.data.action = this.href(req.action);
res.data.title = gettext('Add Comment');
2014-07-04 15:32:18 +02:00
HopObject.confirmConstructor(Comment);
res.data.body = (new Comment).renderSkinAsString('Comment#edit');
this.site.renderSkin('Site#page');
2014-07-04 15:32:18 +02:00
return;
}
/**
*
* @param {String} name
* @returns {Object}
*/
Story.prototype.getFormValue = function(name) {
2014-07-04 15:32:18 +02:00
if (req.isPost()) {
return req.postParams[name];
}
switch (name) {
case 'commentMode':
2014-07-04 15:32:18 +02:00
return this.commentMode || Story.OPEN;
case 'mode':
2014-07-04 15:32:18 +02:00
return this.mode || Story.FEATURED;
case 'status':
2014-07-04 15:32:18 +02:00
return this.status || Story.PUBLIC;
case 'tags':
2014-07-04 15:32:18 +02:00
return this.getTags().join(Tag.DELIMITER);
}
return this[name] || this.getMetadata(name);
}
/**
*
* @param {String} name
* @returns {String[]}
*/
Story.prototype.getFormOptions = function(name) {
2014-07-04 15:32:18 +02:00
switch (name) {
case 'commentMode':
2014-07-04 15:32:18 +02:00
return Story.getCommentModes();
case 'mode':
2014-07-04 15:32:18 +02:00
return Story.getModes();
case 'status':
2014-07-04 15:32:18 +02:00
return Story.getStatus();
case 'tags':
2014-07-04 15:32:18 +02:00
// FIXME: This could become a huge select element...
return [];
}
return;
}
/**
*
* @param {Object} data
*/
Story.prototype.setCustomContent = function(data) {
2014-07-04 15:32:18 +02:00
var metadata = {};
for (var key in data) {
if (this.isCustomContent(key)) {
metadata[key] = data[key];
}
}
return HopObject.prototype.setMetadata.call(this, metadata);
}
/**
*
* @param {String} name
*/
Story.prototype.isCustomContent = function(key) {
2014-07-04 15:32:18 +02:00
return this[key] === undefined && key !== 'save';
}
/**
* Increment the request counter
*/
Story.prototype.count = function() {
2014-07-04 15:32:18 +02:00
if (session.user === this.creator) {
return;
}
var story;
var key = 'Story#' + this._id;
2014-07-04 15:32:18 +02:00
if (story = app.data.requests[key]) {
story.requests += 1;
} else {
app.data.requests[key] = {
type: this.constructor,
id: this._id,
requests: this.requests + 1
};
}
return;
}
/**
* Calculate the difference of a storys current and its updated content
* @param {Object} data
* @returns {Number}
*/
Story.prototype.getDelta = function(data) {
2014-07-04 15:32:18 +02:00
if (this.isTransient()) {
return Infinity;
}
var deltify = function(s1, s2) {
var len1 = s1 ? String(s1).length : 0;
var len2 = s2 ? String(s2).length : 0;
return Math.abs(len1 - len2);
};
var delta = 0;
delta += deltify(data.title, this.title);
delta += deltify(data.text, this.text);
for (var key in data) {
if (this.isCustomContent(key)) {
delta += deltify(data[key], this.getMetadata(key))
}
}
// In-between updates (1 hour) get zero delta
var timex = (new Date - this.modified) > Date.ONEHOUR ? 1 : 0;
return delta * timex;
}
/**
*
* @param {String} name
* @returns {HopObject}
*/
Story.prototype.getMacroHandler = function(name) {
if (name === 'metadata') {
2014-07-04 15:32:18 +02:00
return this.getMetadata();
}
return null;
}
/**
*
* @param {Object} param
*/
Story.prototype.summary_macro = function(param) {
2014-07-04 15:32:18 +02:00
param.limit || (param.limit = 15);
var keys, summary;
if (arguments.length > 1) {
res.push();
var content;
for (var i=1; i<arguments.length; i+=1) {
if (content = this.getMetadata(arguments[i])) {
res.write(content);
res.write(String.SPACE);
}
2014-07-04 15:32:18 +02:00
}
summary = res.pop();
}
if (!summary) {
summary = (this.title || String.EMPTY) + String.SPACE +
(this.text || String.EMPTY);
}
var clipped = stripTags(summary).clip(param.limit, param.clipping, '\\s');
2014-07-04 15:32:18 +02:00
var head = clipped.split(/(\s)/, param.limit * 0.6).join(String.EMPTY);
var tail = clipped.substring(head.length).trim();
head = head.trim();
if (!head && !tail) {
head = '…';
2014-07-04 15:32:18 +02:00
}
param.link ? html.link({href: this.href()}, head) : res.write(head);
res.writeln('\n');
2014-07-04 15:32:18 +02:00
res.write(tail);
return;
}
/**
*
* @param {Object} param
* @param {String} mode
*/
Story.prototype.comments_macro = function(param, mode) {
2014-07-04 15:32:18 +02:00
var story = this.story || this;
2014-12-16 23:07:59 +01:00
var comments = this.story ? this : this.comments;
2014-07-04 15:32:18 +02:00
if (story.site.commentMode === Site.DISABLED ||
story.commentMode === Site.CLOSED) {
return;
} else if (mode) {
2014-12-16 23:07:59 +01:00
var n = comments.size() || 0;
if (mode === 'count' || mode === 'size') {
2014-12-16 23:07:59 +01:00
res.write(n);
} else if (mode === 'link' || mode === 'summary') {
2014-12-16 23:07:59 +01:00
var text = ngettext('{0} comment', '{0} comments', n);
if (n < 1 || mode === 'summary') {
res.write(text);
} else {
html.link({href: this.href() + '#comments'}, text);
}
2014-07-04 15:32:18 +02:00
}
} else {
this.prefetchChildren();
this.forEach(function() {
html.openTag('a', {name: this._id});
html.closeTag('a');
2014-07-04 15:32:18 +02:00
this.renderSkin(this.parent.constructor === Story ?
'Comment#main' : 'Comment#reply');
2014-07-04 15:32:18 +02:00
});
}
return;
}
/**
*
* @param {Object} param
* @param {String} mode
*/
Story.prototype.tags_macro = function(param, mode) {
if (mode === 'link') {
2014-07-04 15:32:18 +02:00
var tags = [];
this.tags.list().forEach(function(item) {
res.push();
if (item.tag) {
renderLink(param, item.tag.href(), item.tag.name);
tags.push(res.pop());
}
});
return res.write(tags.join(Tag.DELIMITER));
2014-12-16 23:07:59 +01:00
} else if (mode === 'count') {
return this.tags.count();
2014-07-04 15:32:18 +02:00
}
return res.write(this.getFormValue('tags'));
}
/**
*
* @param {Object} param
* @param {Number} limit
*/
Story.prototype.referrers_macro = function(param, limit) {
2014-07-04 15:32:18 +02:00
if (!User.require(User.PRIVILEGED) &&
!Membership.require(Membership.OWNER)) {
return;
}
limit = Math.min(limit || param.limit || 100, 100);
if (limit < 1) {
return;
}
var self = this;
var sql = new Sql;
sql.retrieve(Sql.REFERRERS, 'Story', this._id);
2014-07-04 15:32:18 +02:00
res.push();
var n = 0;
sql.traverse(function() {
if (n < limit && this.requests && this.referrer) {
this.text = encode(this.referrer.head(50));
this.referrer = encode(this.referrer);
self.renderSkin('$Story#referrer', this);
2014-07-04 15:32:18 +02:00
}
n += 1;
});
param.referrers = res.pop();
if (param.referrers) {
this.renderSkin('$Story#referrers', param);
2014-07-04 15:32:18 +02:00
}
return;
}
/**
*
* @param {Object} value
* @param {Object} param
* @param {String} mode
* @returns {String}
*/
Story.prototype.format_filter = function(value, param, mode) {
2014-07-04 15:32:18 +02:00
if (value) {
switch (mode) {
case 'plain':
2014-07-04 15:32:18 +02:00
return this.url_filter(stripTags(value), param, mode);
case 'quotes':
2014-07-04 15:32:18 +02:00
return stripTags(value).replace(/(?:\x22|\x27)/g, function(quote) {
return '&#' + quote.charCodeAt(0) + ';';
2014-07-04 15:32:18 +02:00
});
case 'image':
var image = HopObject.getFromPath(value, 'images');
2014-07-04 15:32:18 +02:00
if (image) {
res.push();
image.render_macro(param);
return res.pop();
}
2014-07-04 15:32:18 +02:00
break;
default:
value = this.macro_filter(format(value), param);
return this.url_filter(value, param);
}
}
return String.EMTPY;
}
/**
* Enables certain macros for being used in a story or comment thus, any content object.
* @param {String|Skin} value A skin object or a string that is going to be turned into a skin object.
* @returns {String}
*/
Story.prototype.macro_filter = function(value) {
2014-07-04 15:32:18 +02:00
var skin = value.constructor === String ? createSkin(value) : value;
Story.ALLOWED_MACROS.forEach(function(value, index) {
skin.allowMacro(value);
});
var site;
if (this.site !== res.handlers.site) {
site = res.handlers.site;
res.handlers.site = this.site;
}
value = this.renderSkinAsString(skin);
site && (res.handlers.site = site);
return value;
}
/**
*
* @param {String} value
* @param {Object} param
* @param {String} mode
* @returns {String}
*/
Story.prototype.url_filter = function(value, param, mode) {
2014-07-04 15:32:18 +02:00
param.limit || (param.limit = 50);
// FIXME: The first RegExp has troubles with <a href=http://... (no quotes)
//var re = /(^|\/>|\s+)([\w+-_]+:\/\/[^\s]+?)([\.,;:\)\]\"]?)(?=[\s<]|$)/gim;
var re = /(^|\/>|\s+)([!fhtpsr]+:\/\/[^\s]+?)([\.,;:\)\]\"]?)(?=[\s<]|$)/gim
return value.replace(re, function(str, head, url, tail) {
if (url.startsWith('!')) {
2014-07-04 15:32:18 +02:00
return head + url.substring(1) + tail;
}
res.push();
res.write(head);
if (mode === 'plain') {
2014-07-04 15:32:18 +02:00
res.write(url.clip(param.limit));
} else {
var text, location = /:\/\/([^\/]*)/.exec(url)[1];
text = location;
if (mode === 'extended') {
text = url.replace(/^.+\/([^\/]*)$/, '$1');
}
2014-07-04 15:32:18 +02:00
html.link({href: url, title: url}, text.clip(param.limit));
if (mode === 'extended' && text !== location) {
res.write(' <small>(' + location + ')</small>');
}
2014-07-04 15:32:18 +02:00
}
res.write(tail);
return res.pop();
});
}
/**
* @returns {String}
*/
Story.prototype.getConfirmText = function() {
return gettext("You are about to delete a story by user {0}.",
this.creator ? this.creator.name : 'null');
}