add: support for account deletion
This commit is contained in:
parent
1defbc8240
commit
922f2ed732
19 changed files with 1205 additions and 830 deletions
|
|
@ -253,7 +253,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<% gettext '{0} accounts sorted by {1} in {2} order.'
|
||||
<% admin.dropdown name="display" <% markgettext all %> <% markgettext blocked %> <% markgettext trusted %> <% markgettext privileged %> %>
|
||||
<% admin.dropdown name="display" <% markgettext all %> <% markgettext deleted %> <% markgettext blocked %> <% markgettext trusted %> <% markgettext privileged %> %>
|
||||
<% admin.dropdown name="sorting" <% markgettext Registration %> <% markgettext 'last login' %> <% markgettext Name %> %>
|
||||
<% admin.dropdown name="order" <% markgettext descending %> <% markgettext ascending %> %>
|
||||
%>
|
||||
|
|
@ -296,6 +296,7 @@
|
|||
<table class='uk-table uk-table-striped uk-table-hover uk-table-condensed'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><% gettext Date %></th>
|
||||
<th><% gettext Method %></th>
|
||||
<th><% gettext Reference %></th>
|
||||
<th><% gettext Name %></th>
|
||||
|
|
@ -370,7 +371,7 @@
|
|||
<td>
|
||||
<% item.notes prefix="<span title='" suffix="' data-uk-tooltip><i class='uk-icon-info-circle uk-text-muted'></i></span>" %>
|
||||
</td>
|
||||
<td class='uk-text-right uk-text-nowrap;'>
|
||||
<td class='uk-text-right uk-text-nowrap'>
|
||||
<% item.link delete "<i class='uk-icon-trash-o'></i>" %>
|
||||
<% admin.link block "<i class='uk-icon-ban'></i>" <% item.self %> %>
|
||||
<% item.link edit "<i class='uk-icon-pencil'></i>" %>
|
||||
|
|
@ -391,6 +392,7 @@
|
|||
|
||||
<% #job %>
|
||||
<tr>
|
||||
<td><% item.date | format short %></td>
|
||||
<td><% gettext <% item.method %> | titleize %></td>
|
||||
<td><a href='<% item.target.href %>'><% item.target.name %></a></td>
|
||||
<td><% item.name %></td>
|
||||
|
|
@ -420,6 +422,9 @@
|
|||
<% #openSite %>
|
||||
<i class='uk-icon-globe'></i>
|
||||
|
||||
<% #deletedUser %>
|
||||
<div class='uk-badge'><% gettext Deleted %></div>
|
||||
|
||||
<% #blockedUser %>
|
||||
<div class='uk-badge uk-badge-danger'><% gettext Blocked %></div>
|
||||
|
||||
|
|
|
|||
|
|
@ -34,8 +34,9 @@ Admin.MAXBATCHSIZE = 50;
|
|||
* @constructor
|
||||
*/
|
||||
Admin.Job = function(target, method, user) {
|
||||
var file;
|
||||
user || (user = session.user);
|
||||
if (!user) user = session.user;
|
||||
|
||||
var file, date;
|
||||
|
||||
this.__defineGetter__('target', function() {
|
||||
return target;
|
||||
|
|
@ -53,6 +54,10 @@ Admin.Job = function(target, method, user) {
|
|||
return file.getName();
|
||||
});
|
||||
|
||||
this.__defineGetter__('date', function() {
|
||||
return date;
|
||||
});
|
||||
|
||||
this.remove = function(isCareless) {
|
||||
// isCareless is `true` after a site is completely removed, to prevent NullPointer exception
|
||||
if (!isCareless) target.job = null;
|
||||
|
|
@ -69,6 +74,7 @@ Admin.Job = function(target, method, user) {
|
|||
target = global[data.type].getById(data.id);
|
||||
method = data.method;
|
||||
user = User.getById(data.user);
|
||||
date = new Date(file.lastModified());
|
||||
}
|
||||
} else {
|
||||
throw Error('Insufficient arguments');
|
||||
|
|
@ -141,7 +147,10 @@ Admin.dequeue = function() {
|
|||
app.log('Processing queued job ' + (i + 1) + ' of ' + max);
|
||||
switch (job.method) {
|
||||
case 'remove':
|
||||
Site.remove.call(job.target);
|
||||
if (job.target.deleted) {
|
||||
if (job.target.constructor === Site) Site.remove.call(job.target);
|
||||
if (job.target.constructor === User) User.remove.call(job.target);
|
||||
}
|
||||
break;
|
||||
case 'import':
|
||||
Importer.run(job.target, job.user);
|
||||
|
|
@ -167,12 +176,23 @@ Admin.getDeletionDate = function(site) {
|
|||
return new Date(site.deleted.getTime() + Date.ONEDAY * Admin.SITEREMOVALGRACEPERIOD);
|
||||
};
|
||||
|
||||
Admin.purgeAccounts = function() {
|
||||
var now = Date.now();
|
||||
|
||||
root.admin.deletedUsers.forEach(function() {
|
||||
if (!this.deleted) return; // already gone
|
||||
if (now - this.deleted > 0 && !this.job) {
|
||||
this.job = Admin.queue(this, 'remove', this);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Admin.purgeSites = function() {
|
||||
var now = new Date;
|
||||
|
||||
root.admin.deletedSites.forEach(function() {
|
||||
if (now > Admin.getDeletionDate(this)) {
|
||||
if (this.job) return; // Site is already scheduled for deletion
|
||||
if (this.job) return; // There is already a job scheduled for this site
|
||||
this.job = Admin.queue(this, 'remove', this.modifier);
|
||||
}
|
||||
});
|
||||
|
|
@ -662,9 +682,10 @@ Admin.prototype.filterUsers = function(data) {
|
|||
data || (data = {});
|
||||
|
||||
var displays = {
|
||||
1: "status = 'blocked'",
|
||||
2: "status = 'trusted'",
|
||||
3: "status = 'privileged'"
|
||||
1: "status = 'deleted'",
|
||||
2: "status = 'blocked'",
|
||||
3: "status = 'trusted'",
|
||||
4: "status = 'privileged'"
|
||||
};
|
||||
|
||||
var sortings = {
|
||||
|
|
@ -885,7 +906,7 @@ Admin.prototype.link_macro = function (param, action, text, target) {
|
|||
switch (action) {
|
||||
case 'block':
|
||||
var user = target.creator || target;
|
||||
if (user.status !== User.PRIVILEGED && user.status !== User.BLOCKED) {
|
||||
if (user.status !== User.PRIVILEGED && user.status !== User.BLOCKED && (user.status !== User.DELETED || user.deleted)) {
|
||||
var url = user.href('block');
|
||||
return renderLink.call(global, param, url, text || String.EMPTY, this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,3 +34,8 @@ deletedSites.order = modified desc
|
|||
users = collection(User)
|
||||
users.accessName = name
|
||||
users.order = created desc
|
||||
|
||||
deletedUsers = collection(User)
|
||||
deletedUsers.filter = status = 'deleted'
|
||||
deletedUsers.order = modified desc
|
||||
|
||||
|
|
|
|||
|
|
@ -345,6 +345,7 @@ function scheduler() {
|
|||
Admin.invokeCallbacks();
|
||||
Admin.updateDomains();
|
||||
Admin.updateHealth();
|
||||
Admin.purgeAccounts();
|
||||
Admin.purgeSites();
|
||||
return app.properties.schedulerInterval;
|
||||
}
|
||||
|
|
@ -1095,15 +1096,35 @@ function formatNumber(number, pattern) {
|
|||
* @returns {String} The formatted date string.
|
||||
*/
|
||||
function formatDate(date, format) {
|
||||
if (!date) {
|
||||
return null;
|
||||
}
|
||||
if (!date) return null;
|
||||
|
||||
var pattern;
|
||||
var site = res.handlers.site;
|
||||
var locale = site ? site.getLocale() : null;
|
||||
var timezone = site ? site.getTimeZone() : null;
|
||||
|
||||
const getExpiry = diff => {
|
||||
let text;
|
||||
if (diff < Date.ONEMINUTE) {
|
||||
text = gettext('soon');
|
||||
} else if (diff < Date.ONEHOUR) {
|
||||
text = ngettext('in {0} minute', 'in {0} minutes', Math.round(diff / Date.ONEMINUTE));
|
||||
} else if (diff < Date.ONEDAY) {
|
||||
text = ngettext('in {0} hour', 'in {0} hours', Math.round(diff / Date.ONEHOUR));
|
||||
} else if (diff < 2 * Date.ONEDAY) {
|
||||
text = gettext('tomorrow');
|
||||
} else if (diff < 7 * Date.ONEDAY) {
|
||||
text = ngettext('in {0} day', 'in {0} days', Math.round(diff / Date.ONEDAY));
|
||||
} else if (diff < 30 * Date.ONEDAY) {
|
||||
text = ngettext('in {0} week', 'in {0} weeks', Math.round(diff / 7 / Date.ONEDAY));
|
||||
} else if (diff < 365 * Date.ONEDAY) {
|
||||
text = ngettext('in {0} month', 'in {0} months', Math.round(diff / 30 / Date.ONEDAY));
|
||||
} else {
|
||||
text = ngettext('in {0} year', 'in {0} years', Math.round(diff / 365 / Date.ONEDAY));
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
switch (format) {
|
||||
case null:
|
||||
case undefined:
|
||||
|
|
@ -1131,28 +1152,12 @@ function formatDate(date, format) {
|
|||
break;
|
||||
|
||||
case 'text':
|
||||
var text,
|
||||
now = new Date,
|
||||
diff = now - date;
|
||||
var text;
|
||||
var now = new Date;
|
||||
var diff = now - date;
|
||||
|
||||
if (diff < 0) {
|
||||
diff = -diff;
|
||||
if (diff < Date.ONEMINUTE) {
|
||||
text = gettext('soon');
|
||||
} else if (diff < Date.ONEHOUR) {
|
||||
text = ngettext('in {0} minute', 'in {0} minutes', Math.round(diff / Date.ONEMINUTE));
|
||||
} else if (diff < Date.ONEDAY) {
|
||||
text = ngettext('in {0} hour', 'in {0} hours', Math.round(diff / Date.ONEHOUR));
|
||||
} else if (diff < 2 * Date.ONEDAY) {
|
||||
text = gettext('tomorrow');
|
||||
} else if (diff < 7 * Date.ONEDAY) {
|
||||
text = ngettext('in {0} day', 'in {0} days', Math.round(diff / Date.ONEDAY));
|
||||
} else if (diff < 30 * Date.ONEDAY) {
|
||||
text = ngettext('in {0} week', 'in {0} weeks', Math.round(diff / 7 / Date.ONEDAY));
|
||||
} else if (diff < 365 * Date.ONEDAY) {
|
||||
text = ngettext('in {0} month', 'in {0} months', Math.round(diff / 30 / Date.ONEDAY));
|
||||
} else {
|
||||
text = ngettext('in {0} year', 'in {0} years', Math.round(diff / 365 / Date.ONEDAY));
|
||||
}
|
||||
text = getExpiry(-diff);
|
||||
} else if (diff < Date.ONEMINUTE) {
|
||||
text = gettext('right now');
|
||||
} else if (diff < Date.ONEHOUR) {
|
||||
|
|
@ -1172,6 +1177,9 @@ function formatDate(date, format) {
|
|||
}
|
||||
return text.replace(/(\d+)\s+/, '$1\xa0'); // Add a no-break space after first digits
|
||||
|
||||
case 'expiry':
|
||||
return getExpiry(date - new Date());
|
||||
|
||||
default:
|
||||
pattern = format;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ Sql.REFERRERS = 'select referrer, count(*) as requests from ' +
|
|||
'by referrer order by requests desc, referrer asc';
|
||||
|
||||
/**
|
||||
* SQL command for deleting all log entries older than 2 days.
|
||||
* SQL command for deleting all log entries older than a specific period.
|
||||
* @constant
|
||||
*/
|
||||
Sql.PURGEREFERRERS = "delete from log where action = 'main' and " +
|
||||
|
|
@ -184,7 +184,7 @@ Sql.COMMENT_SEARCH = "select comment.id from content as comment, content as stor
|
|||
* SQL query for searching accounts which are not already members of the desired site.
|
||||
* @constant
|
||||
*/
|
||||
Sql.MEMBERSEARCH = "select id, name, created from account where name $0 '$1' order by name asc limit $2";
|
||||
Sql.MEMBERSEARCH = "select id, name, created, status from account where status not in ('blocked', 'deleted') and name $0 '$1' order by name asc limit $2";
|
||||
|
||||
/**
|
||||
* SQL query for retrieving all story IDs in a site’s archive.
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ HopObject.remove = function(options) {
|
|||
var item;
|
||||
while (this.size() > 0) {
|
||||
item = this.get(0);
|
||||
if (!item) return;
|
||||
if (item.constructor.remove) {
|
||||
item.constructor.remove.call(item, options);
|
||||
} else if (!options) {
|
||||
|
|
@ -107,6 +108,10 @@ HopObject.prototype.onRequest = function() {
|
|||
User.autoLogin();
|
||||
res.handlers.membership = User.getMembership();
|
||||
|
||||
if (session.user && !session.user.deleted && session.user.status === User.DELETED) {
|
||||
User.logout();
|
||||
}
|
||||
|
||||
if (User.getCurrentStatus() === User.BLOCKED) {
|
||||
User.logout();
|
||||
res.status = 403;
|
||||
|
|
@ -117,8 +122,7 @@ HopObject.prototype.onRequest = function() {
|
|||
}
|
||||
|
||||
// Simulate 404 for sites which are due for deletion by cronjob
|
||||
if (res.handlers.site.job && !User.require(User.PRIVILEGED) ||
|
||||
res.handlers.site.mode === Site.DELETED && !Membership.require(Membership.OWNER)) {
|
||||
if (res.handlers.site.mode === Site.DELETED && !User.require(User.PRIVILEGED) && !Membership.require(Membership.OWNER)) {
|
||||
res.handlers.site = root;
|
||||
root.notfound_action();
|
||||
res.stop();
|
||||
|
|
@ -183,7 +187,8 @@ HopObject.prototype.delete_action = function() {
|
|||
}
|
||||
}
|
||||
|
||||
res.data.action = this.href(req.action);
|
||||
if (!res.data.action) res.data.action = this.href(req.action);
|
||||
|
||||
res.data.title = gettext('Confirm Deletion');
|
||||
res.data.body = this.renderSkinAsString('$HopObject#confirm', {
|
||||
text: this.getConfirmText(req.action),
|
||||
|
|
|
|||
|
|
@ -28,18 +28,15 @@ HopObject.prototype.handleMetadata = function(name) {
|
|||
this.__defineGetter__(name, function() {
|
||||
return this.getMetadata(name);
|
||||
});
|
||||
|
||||
this.__defineSetter__(name, function(value) {
|
||||
return this.setMetadata(name, value);
|
||||
});
|
||||
|
||||
this[name + '_macro'] = function(param) {
|
||||
var value;
|
||||
if (value = this[name]) {
|
||||
res.write(value);
|
||||
}
|
||||
return;
|
||||
return this[name] || null;
|
||||
};
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ Members.prototype.getPermission = function(action) {
|
|||
var sitePermission = this._parent.getPermission('main');
|
||||
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
return !session.user.deleted && session.user.status !== User.DELETED;
|
||||
|
||||
case 'edit':
|
||||
case 'export':
|
||||
case 'subscriptions':
|
||||
|
|
@ -227,6 +230,11 @@ Members.prototype.timeline_action = function() {
|
|||
return void User.prototype.timeline_action.call(session.user);
|
||||
};
|
||||
|
||||
Members.prototype.delete_action = function() {
|
||||
res.handlers.context = this;
|
||||
return void User.prototype.delete_action.call(session.user);
|
||||
};
|
||||
|
||||
Members.prototype.salt_txt_action = function() {
|
||||
res.contentType = 'text/plain';
|
||||
var user;
|
||||
|
|
|
|||
|
|
@ -45,10 +45,7 @@
|
|||
<% #edit %>
|
||||
<h1><% response.title %></h1>
|
||||
<div class='uk-article-meta'>
|
||||
<% gettext 'Created by {0} on {1}' <% user.name %> <% user.created short %> %>
|
||||
<% if <% user.created %> is <% user.modified %> then '' else
|
||||
<% gettext 'Last modified by {0} on {1}' <% user.name %> <% this.modified short %> prefix=<br> %>
|
||||
%>
|
||||
<% if <% user.status %> is 'deleted' then <% user.skin $User#deleted %> else <% user.skin $User#meta %> %>
|
||||
</div>
|
||||
<div class='uk-margin-top uk-margin-bottom'>
|
||||
<% context.link timeline <% gettext Timeline %> %> |
|
||||
|
|
@ -103,7 +100,7 @@
|
|||
<button class='uk-button uk-button-primary' type="submit" id="submit" name="save" value="1">
|
||||
<% gettext Save %>
|
||||
</button>
|
||||
<% user.link delete <% gettext 'Delete' %> class='uk-button' %>
|
||||
<% context.link delete <% gettext 'Delete' %> class='uk-button' %>
|
||||
<a href='<% site.href %>' class="uk-button uk-button-link"><% gettext Cancel %></a>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -135,7 +132,7 @@
|
|||
<label class='uk-form-label' for='status'>
|
||||
<% gettext Information %>
|
||||
</label>
|
||||
<div><% ngettext "{0} Site" "{0} Sites" <% count <% user.self sites %> %> %></div>
|
||||
<div><% ngettext "{0} Site" "{0} Sites" <% count <% user.self ownerships %> %> %></div>
|
||||
<div><% ngettext "{0} Story" "{0} Stories" <% count <% user.self stories %> %> %></div>
|
||||
<div><% ngettext "{0} Comment" "{0} Comments" <% count <% user.self comments %> %> %></div>
|
||||
<div><% ngettext "{0} Image" "{0} Images" <% count <% user.self images %> %> %></div>
|
||||
|
|
@ -181,6 +178,34 @@
|
|||
</table>
|
||||
<% response.pager %>
|
||||
|
||||
<% #delete %>
|
||||
<div class='uk-alert uk-alert-danger'>
|
||||
<% gettext 'You are about to delete the whole account which currently contains {0}, {1}, {2}, {3}, {4} and {5}.'
|
||||
<% ngettext '{0} site' '{0} sites' <% count <% user.self ownerships %> %> %>
|
||||
<% ngettext '{0} story' '{0} stories' <% count <% user.self stories %> %> %>
|
||||
<% ngettext '{0} comment' '{0} comments' <% count <% user.self comments %> %> %>
|
||||
<% ngettext '{0} image' '{0} images' <% count <% user.self images %> %> %>
|
||||
<% ngettext '{0} file' '{0} files' <% count <% user.self files %> %> %>
|
||||
<% ngettext '{0} poll' '{0} polls' <% count <% user.self polls %> %> %> %>
|
||||
<strong><% gettext 'All of this will be deleted irreversibly.' %></strong>
|
||||
<% gettext 'Are you sure you want to proceed?' %>
|
||||
</div>
|
||||
|
||||
<% #meta %>
|
||||
<% gettext 'Created on {0}' <% user.created short %> %>
|
||||
<% if <% user.created %> is <% user.modified %> then '' else
|
||||
<% gettext 'Last modified on {0}' <% this.modified short %> prefix=<br> %>
|
||||
%>
|
||||
|
||||
<% #deleted %>
|
||||
<% if <% user.deleted %> is null then
|
||||
<% gettext 'Deleted on {0}' <% user.created short %> %>
|
||||
else
|
||||
<% gettext 'Scheduled for deletion {0}' <% user.deleted | format expiry %>
|
||||
suffix=<% context.link 'edit?undelete=1' <% gettext "Click to cancel deletion" prefix="<i class='uk-icon-times-circle uk-margin-small-left' data-uk-tooltip=\"{pos: 'right'}\" title='" suffix="'></i>" %> %>
|
||||
%>
|
||||
%>
|
||||
|
||||
<% #notify_reset %>
|
||||
<% gettext 'Hello {0}.' <% user.name %> %>
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ markgettext('account');
|
|||
markgettext('a account // accusative');
|
||||
|
||||
this.handleMetadata('accepted');
|
||||
this.handleMetadata('deleted');
|
||||
this.handleMetadata('export');
|
||||
this.handleMetadata('hash');
|
||||
this.handleMetadata('job');
|
||||
|
|
@ -62,14 +63,29 @@ User.add = function(data) {
|
|||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME: Still needs a solution whether and how to remove a user’s sites
|
||||
*/
|
||||
User.remove = function() {
|
||||
// FIXME: Removing an account is non-trivial as even one single modifier_id could break things if the corresponding account relation simply was removed. Thus, we might need a `deleted` property or similar to flag a removal and then take appropriate measures for related objects.
|
||||
throw Error(gettext('Currently, it is not possible to delete an account. Please accept our humble apologies.'));
|
||||
return;
|
||||
}
|
||||
if (this.constructor === User) {
|
||||
this.ownerships.forEach(function() {
|
||||
const site = this.site;
|
||||
// Don’t delete sites with multiple owners
|
||||
if (site.members.owners.count() > 1) return;
|
||||
Site.remove.call(site);
|
||||
});
|
||||
HopObject.remove.call(this.comments);
|
||||
HopObject.remove.call(this.stories);
|
||||
HopObject.remove.call(this.images);
|
||||
HopObject.remove.call(this.files);
|
||||
HopObject.remove.call(this.polls);
|
||||
HopObject.remove.call(this.votes);
|
||||
HopObject.remove.call(this.subscriptions);
|
||||
// We only delete metadata but don’t remove the account to prevent identity takeover
|
||||
this.deleteMetadata();
|
||||
this.email = String.EMPTY;
|
||||
// We gonna use the creation date as the deletion date from now on (until restoration of course)
|
||||
this.created = this.modified = new Date();
|
||||
return User.require(User.PRIVILEGED) ? this.href('edit') : root.href();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -85,7 +101,7 @@ User.getByName = function(name) {
|
|||
* @returns {String[]}
|
||||
* @see defineConstants
|
||||
*/
|
||||
User.getStatus = defineConstants(User, markgettext('Blocked'), markgettext('Regular'), markgettext('Trusted'), markgettext('Privileged'));
|
||||
User.getStatus = defineConstants(User, markgettext('Deleted'), markgettext('Blocked'), markgettext('Regular'), markgettext('Trusted'), markgettext('Privileged'));
|
||||
|
||||
/**
|
||||
* @returns {String}
|
||||
|
|
@ -270,7 +286,7 @@ User.logout = function() {
|
|||
*/
|
||||
User.require = function(requiredStatus, user) {
|
||||
if (!user) user = session.user;
|
||||
var status = [User.BLOCKED, User.REGULAR, User.TRUSTED, User.PRIVILEGED];
|
||||
var status = [User.BLOCKED, User.REGULAR, User.DELETED, User.TRUSTED, User.PRIVILEGED];
|
||||
if (requiredStatus && user) {
|
||||
return status.indexOf(user.status) >= status.indexOf(requiredStatus);
|
||||
}
|
||||
|
|
@ -348,6 +364,10 @@ User.rename = function(currentName, newName) {
|
|||
return currentName;
|
||||
}
|
||||
|
||||
User.getDeletionDate = function() {
|
||||
return new Date(Date.now() + Date.ONEDAY * 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* A User object represents a login to Antville.
|
||||
* @name User
|
||||
|
|
@ -388,13 +408,27 @@ User.prototype.onLogout = function() { /* ... */ }
|
|||
* @returns {Boolean}
|
||||
*/
|
||||
User.prototype.getPermission = function(action) {
|
||||
if (action === 'delete') return false;
|
||||
return User.require(User.PRIVILEGED);
|
||||
if (!User.require(User.PRIVILEGED)) return false;
|
||||
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
return !this.deleted && this.status !== User.DELETED;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
User.prototype.edit_action = function () {
|
||||
console.log(this.countContributions());
|
||||
if (!res.handlers.context) res.handlers.context = this;
|
||||
|
||||
if (req.data.undelete) {
|
||||
this.deleted = null;
|
||||
this.status = User.REGULAR;
|
||||
res.redirect(res.handlers.context.href('edit'));
|
||||
}
|
||||
|
||||
if (req.postParams.save) {
|
||||
try {
|
||||
this.update(req.postParams);
|
||||
|
|
@ -451,6 +485,8 @@ User.prototype.export_action = function() {
|
|||
};
|
||||
|
||||
User.prototype.timeline_action = function() {
|
||||
if (!res.handlers.context) res.handlers.context = this;
|
||||
|
||||
const collection = [];
|
||||
const sql = new Sql();
|
||||
const page = req.queryParams.page;
|
||||
|
|
@ -472,22 +508,51 @@ User.prototype.timeline_action = function() {
|
|||
collection.push(object);
|
||||
});
|
||||
|
||||
res.data.list = renderList(collection, this.renderTimelineItem, pageSize, page);
|
||||
res.data.list = renderList(collection, this.renderTimelineItem, null, page);
|
||||
res.data.pager = renderPager(count, this.href(req.action), pageSize, page);
|
||||
res.data.title = gettext('Timeline');
|
||||
res.data.action = this.href(req.action);
|
||||
res.data.body = this.renderSkinAsString('$User#timeline');
|
||||
root.renderSkin('Site#page');
|
||||
};
|
||||
|
||||
User.prototype.delete_action = function() {
|
||||
if (!res.handlers.context) res.handlers.context = this;
|
||||
res.data.action = res.handlers.context.href(req.action);
|
||||
if (req.postParams.proceed) {
|
||||
this.status = User.DELETED;
|
||||
const total = this.countContributions();
|
||||
if (total < 1) {
|
||||
// If a site contains no content, delete it immediately
|
||||
return void HopObject.prototype.delete_action.call(this);
|
||||
}
|
||||
// Otherwise, queue for deletion
|
||||
this.deleted = User.getDeletionDate();
|
||||
this.log(root, 'Deleted account ' + this.name);
|
||||
res.message = gettext('The account {0} is queued for deletion.', this.name);
|
||||
res.redirect(res.handlers.context.href('edit'));
|
||||
} else {
|
||||
HopObject.prototype.delete_action.call(this);
|
||||
}
|
||||
};
|
||||
|
||||
User.prototype.getConfirmText = function () {
|
||||
return gettext('You are about to delete the account {0}.', this.getTitle());
|
||||
};
|
||||
|
||||
User.prototype.getConfirmExtra = function () {
|
||||
return this.renderSkinAsString('$User#delete');
|
||||
};
|
||||
|
||||
User.prototype.renderTimelineItem = function(item) {
|
||||
Admin.prototype.renderActivity(item, '$Admin#timelineItem');
|
||||
};
|
||||
|
||||
User.prototype.countContributions = function() {
|
||||
return [this.stories, this.images, this.files, this.polls, this.comments, this.votes].reduce((total, collection) => {
|
||||
return total + collection.size();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} data
|
||||
|
|
@ -510,14 +575,15 @@ User.prototype.update = function(data) {
|
|||
if (this.status === User.PRIVILEGED && data.status !== User.PRIVILEGED && root.admins.count() < 2) {
|
||||
throw Error(gettext('You cannot revoke permissions from the only privileged user.'));
|
||||
}
|
||||
if (data.status !== this.status) {
|
||||
this.deleted = data.status === User.DELETED ? User.getDeletionDate() : null;
|
||||
}
|
||||
this.status = data.status;
|
||||
this.notes = data.notes;
|
||||
}
|
||||
this.email = data.email;
|
||||
this.url = data.url;
|
||||
if (this === session.user) {
|
||||
this.touch();
|
||||
}
|
||||
if (this === session.user) this.touch();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
@ -526,6 +592,7 @@ User.prototype.update = function(data) {
|
|||
*/
|
||||
User.prototype.touch = function() {
|
||||
this.modified = new Date;
|
||||
if (session.user) this.modifier = session.
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,13 @@ _children.local = id
|
|||
_children.foreign = creator_id
|
||||
_children.order = role asc, created desc
|
||||
|
||||
ownerships = collection(Membership)
|
||||
ownerships.local = id
|
||||
ownerships.foreign = creator_id
|
||||
ownerships.filter.additionalTables = site
|
||||
ownerships.filter = site.id = membership.site_id and role = 'owner'
|
||||
ownerships.order = site.name asc
|
||||
|
||||
memberships = collection(Membership)
|
||||
memberships.local = id
|
||||
memberships.foreign = creator_id
|
||||
|
|
|
|||
|
|
@ -41,19 +41,6 @@ User.prototype.__defineSetter__("sysadmin", function(privileged) {
|
|||
this.status = privileged ? User.PRIVILEGED : User.DEFAULT;
|
||||
});
|
||||
|
||||
User.prototype.status_macro = function(param) {
|
||||
// This macro is allowed for privileged users only
|
||||
if (!User.require(User.PRIVILEGED)) {
|
||||
return;
|
||||
}
|
||||
if (param.as === "editor") {
|
||||
this.select_macro(param, "status");
|
||||
} else {
|
||||
res.write(this.status);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
User.prototype.name_macro = function(param) {
|
||||
if (param.as === "link" && this.url) {
|
||||
link_filter(this.name, param, this.url);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
428
i18n/de.po
428
i18n/de.po
File diff suppressed because it is too large
Load diff
430
i18n/en.po
430
i18n/en.po
File diff suppressed because it is too large
Load diff
|
|
@ -60,6 +60,7 @@ global.messages['de-x-male'] = {
|
|||
"Choice": "Antwortmöglichkeit",
|
||||
"Choices": "Antwortmöglichkeiten",
|
||||
"Claustra": "Claustra",
|
||||
"Click to cancel deletion": "",
|
||||
"Click “Cancel” now if you are not really sure you want to proceed.": "Klicken Sie jetzt auf »Abbrechen«, falls Sie nicht sicher sind, ob Sie fortfahren möchten.",
|
||||
"Close": "Schließen",
|
||||
"Closed": "Geschlossen",
|
||||
|
|
@ -97,13 +98,15 @@ global.messages['de-x-male'] = {
|
|||
"Created": "Erstellt",
|
||||
"Created by {0} on {1}": "Erstellt von {0} am {1}",
|
||||
"Created by {0} on {1}.": "Erstellt von {0} am {1}.",
|
||||
"Created on {0}": "Erstellt am {0}",
|
||||
"Created {0}": "Erstellt {0}",
|
||||
"Currently, it is not possible to delete an account. Please accept our humble apologies.": "Derzeit ist das Löschen von Konten nicht möglich. Wir bitten um Verständnis.",
|
||||
"Data Privacy Statement": "Datenschutzerklärung",
|
||||
"Date": "Datum",
|
||||
"Date string in Unix timestamp format": "Datum im Unix-Format",
|
||||
"Delete": "Löschen",
|
||||
"Deleted": "Gelöscht",
|
||||
"Deleted Site": "Gelöschte Site",
|
||||
"Deleted on {0}": "Gelöscht am {0}",
|
||||
"Description": "Beschreibung",
|
||||
"Details": "Einzelheiten",
|
||||
"Development": "Entwicklung",
|
||||
|
|
@ -179,6 +182,7 @@ global.messages['de-x-male'] = {
|
|||
"Last Login": "Letzte Anmeldung",
|
||||
"Last Update": "Letzte Änderung",
|
||||
"Last modified by {0} on {1}": "Zuletzt geändert von {0} am {1}",
|
||||
"Last modified on {0}": "Zuletzt geändert am {0}",
|
||||
"Last modified {0}": "Zuletzt geändert {0}",
|
||||
"Layout": "Layout",
|
||||
"Layout Images": "Layout-Bilder",
|
||||
|
|
@ -242,7 +246,7 @@ global.messages['de-x-male'] = {
|
|||
"Please enter a query in the search form.": "Bitte geben Sie eine Suchanfrage in das Suchformular ein.",
|
||||
"Please enter a user name and e-mail address.": "Bitte geben Sie einen Namen und eine E-Mail-Adresse ein.",
|
||||
"Please enter a username.": "Bitte geben Sie einen Namen ein.",
|
||||
"Please enter a valid URL": "Bitte geben Sie eine gültige Internet-Adresse ein.",
|
||||
"Please enter a valid URL": "Bitte geben Sie eine gültige Internet-Adresse ein",
|
||||
"Please enter a valid e-mail address": "Bitte geben Sie eine gültige E-Mail-Adresse an",
|
||||
"Please enter at least something into the “title” or “text” field.": "Bitte geben Sie zumindest etwas in eines der beiden Felder »Titel« oder »Text« ein.",
|
||||
"Please enter something into the comment field.": "Bitte geben Sie etwas in das Kommentarfeld ein.",
|
||||
|
|
@ -296,6 +300,7 @@ global.messages['de-x-male'] = {
|
|||
"Running Polls": "Laufende Umfragen",
|
||||
"Save": "Speichern",
|
||||
"Save and Run": "Speichern und starten",
|
||||
"Scheduled for deletion {0}": "",
|
||||
"Search": "Suche",
|
||||
"Send": "Senden",
|
||||
"Send Request": "Anfrage senden",
|
||||
|
|
@ -360,6 +365,7 @@ global.messages['de-x-male'] = {
|
|||
"The URL endpoint for each of these APIs is located at": "Die Internet-Adresse für jede dieser Schnittstellen lautet",
|
||||
"The account data will be available for download from here within the next days.": "Die Kontodaten stehen demnächst hier zum Download bereit.",
|
||||
"The account is queued for export.": "Der Export der Kontodaten wird vorbereitet.",
|
||||
"The account {0} is queued for deletion.": "Das Konto {0} ist zur Löschung vorgesehen.",
|
||||
"The callback URL will be invoked as an HTTP POST request with the following parameters:": "Die Rückruf-Adresse wird mit folgenden Parametern durch die »HTTP Post«-Methode aufgerufen:",
|
||||
"The changes were saved successfully.": "Die Änderungen wurden erfolgreich gespeichert.",
|
||||
"The chosen name is too long. Please enter a shorter one.": "Der gewählte Name ist zu lang. Bitte geben Sie einen kürzeren ein.",
|
||||
|
|
@ -461,6 +467,7 @@ global.messages['de-x-male'] = {
|
|||
"You are about to delete the image {0}.": "Sie sind im Begriff, das Bild {0} zu löschen.",
|
||||
"You are about to delete the membership of user {0}.": "Sie sind im Begriff, die Mitgliedschaft von {0} zu löschen.",
|
||||
"You are about to delete the site {0}.": "Sie sind im Begriff, die Website {0} zu löschen.",
|
||||
"You are about to delete the whole account which currently contains {0}, {1}, {2}, {3}, {4} and {5}.": "Sie sind im Begriff, das komplette Konto zu löschen, das zur Zeit {0}, {1}, {2}, {3}, {4} und {5} umfasst.",
|
||||
"You are about to delete the whole site which currently contains {0}, {1}, {2}, {3} and {4}.": "Sie sind im Begriff, die komplette Website zu löschen, die zur Zeit {0}, {1}, {2}, {3} und {4} umfasst.",
|
||||
"You are about to reset the layout of site {0}.": "Sie sind im Begriff, das Layout der Website {0} zurückzusetzen.",
|
||||
"You are about to reset the skin {0}.{1}.": "Sie sind im Begriff, den Skin {0}.{1} zurückzusetzen.",
|
||||
|
|
@ -562,7 +569,7 @@ global.messages['de-x-male'] = {
|
|||
"privileged": "privilegiert",
|
||||
"public": "öffentlich",
|
||||
"readonly": "schreibgeschützt",
|
||||
"remove": "Derzeit ist das Löschen von Konten nicht möglich. Wir bitten um Verständnis.",
|
||||
"remove": "löschen",
|
||||
"restricted": "eingeschränkt",
|
||||
"right now": "vor kurzem",
|
||||
"shared": "geteilt",
|
||||
|
|
@ -628,6 +635,8 @@ global.messages['de-x-male'] = {
|
|||
"{0} polls": "{0} Umfragen",
|
||||
"{0} request": "{0} Aufruf",
|
||||
"{0} requests": "{0} Aufrufe",
|
||||
"{0} site": "{0} Website",
|
||||
"{0} sites": "{0} Websites",
|
||||
"{0} sites sorted by {1} in {2} order.": "Typ: {0} Sortierung: {1} {2}",
|
||||
"{0} story": "{0} Beitrag",
|
||||
"{0} stories": "{0} Beiträge",
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ global.messages['de'] = {
|
|||
"Choice": "Antwortmöglichkeit",
|
||||
"Choices": "Antwortmöglichkeiten",
|
||||
"Claustra": "Claustra",
|
||||
"Click to cancel deletion": "",
|
||||
"Click “Cancel” now if you are not really sure you want to proceed.": "Klicken Sie jetzt auf »Abbrechen«, falls Sie nicht sicher sind, ob Sie fortfahren möchten.",
|
||||
"Close": "Schließen",
|
||||
"Closed": "Geschlossen",
|
||||
|
|
@ -97,13 +98,15 @@ global.messages['de'] = {
|
|||
"Created": "Erstellt",
|
||||
"Created by {0} on {1}": "Erstellt von {0} am {1}",
|
||||
"Created by {0} on {1}.": "Erstellt von {0} am {1}.",
|
||||
"Created on {0}": "Erstellt am {0}",
|
||||
"Created {0}": "Erstellt {0}",
|
||||
"Currently, it is not possible to delete an account. Please accept our humble apologies.": "Derzeit ist das Löschen von Konten nicht möglich. Wir bitten um Verständnis.",
|
||||
"Data Privacy Statement": "Datenschutzerklärung",
|
||||
"Date": "Datum",
|
||||
"Date string in Unix timestamp format": "Datum im Unix-Format",
|
||||
"Delete": "Löschen",
|
||||
"Deleted": "Gelöscht",
|
||||
"Deleted Site": "Gelöschte Site",
|
||||
"Deleted on {0}": "Gelöscht am {0}",
|
||||
"Description": "Beschreibung",
|
||||
"Details": "Einzelheiten",
|
||||
"Development": "Entwicklung",
|
||||
|
|
@ -179,6 +182,7 @@ global.messages['de'] = {
|
|||
"Last Login": "Letzte Anmeldung",
|
||||
"Last Update": "Letzte Änderung",
|
||||
"Last modified by {0} on {1}": "Zuletzt geändert von {0} am {1}",
|
||||
"Last modified on {0}": "Zuletzt geändert am {0}",
|
||||
"Last modified {0}": "Zuletzt geändert {0}",
|
||||
"Layout": "Layout",
|
||||
"Layout Images": "Layout-Bilder",
|
||||
|
|
@ -296,6 +300,7 @@ global.messages['de'] = {
|
|||
"Running Polls": "Laufende Umfragen",
|
||||
"Save": "Speichern",
|
||||
"Save and Run": "Speichern und starten",
|
||||
"Scheduled for deletion {0}": "",
|
||||
"Search": "Suche",
|
||||
"Send": "Senden",
|
||||
"Send Request": "Anfrage senden",
|
||||
|
|
@ -360,6 +365,7 @@ global.messages['de'] = {
|
|||
"The URL endpoint for each of these APIs is located at": "Die Internet-Adresse für jede dieser Schnittstellen lautet",
|
||||
"The account data will be available for download from here within the next days.": "Die Kontodaten stehen demnächst hier zum Download bereit.",
|
||||
"The account is queued for export.": "Der Export der Kontodaten wird vorbereitet.",
|
||||
"The account {0} is queued for deletion.": "Das Konto {0} ist zur Löschung vorgesehen.",
|
||||
"The callback URL will be invoked as an HTTP POST request with the following parameters:": "Die Rückruf-Adresse wird mit folgenden Parametern durch die »HTTP Post«-Methode aufgerufen:",
|
||||
"The changes were saved successfully.": "Die Änderungen wurden erfolgreich gespeichert.",
|
||||
"The chosen name is too long. Please enter a shorter one.": "Der gewählte Name ist zu lang. Bitte geben Sie einen kürzeren ein.",
|
||||
|
|
@ -461,6 +467,7 @@ global.messages['de'] = {
|
|||
"You are about to delete the image {0}.": "Sie sind im Begriff, das Bild {0} zu löschen.",
|
||||
"You are about to delete the membership of user {0}.": "Sie sind im Begriff, die Mitgliedschaft von {0} zu löschen.",
|
||||
"You are about to delete the site {0}.": "Sie sind im Begriff, die Website {0} zu löschen.",
|
||||
"You are about to delete the whole account which currently contains {0}, {1}, {2}, {3}, {4} and {5}.": "Sie sind im Begriff, das komplette Konto zu löschen, das zur Zeit {0}, {1}, {2}, {3}, {4} und {5} umfasst.",
|
||||
"You are about to delete the whole site which currently contains {0}, {1}, {2}, {3} and {4}.": "Sie sind im Begriff, die komplette Website zu löschen, die zur Zeit {0}, {1}, {2}, {3} und {4} umfasst.",
|
||||
"You are about to reset the layout of site {0}.": "Sie sind im Begriff, das Layout der Website {0} zurückzusetzen.",
|
||||
"You are about to reset the skin {0}.{1}.": "Sie sind im Begriff, den Skin {0}.{1} zurückzusetzen.",
|
||||
|
|
@ -562,7 +569,7 @@ global.messages['de'] = {
|
|||
"privileged": "privilegiert",
|
||||
"public": "öffentlich",
|
||||
"readonly": "schreibgeschützt",
|
||||
"remove": "Derzeit ist das Löschen von Konten nicht möglich. Wir bitten um Verständnis.",
|
||||
"remove": "löschen",
|
||||
"restricted": "eingeschränkt",
|
||||
"right now": "vor kurzem",
|
||||
"shared": "geteilt",
|
||||
|
|
@ -628,6 +635,8 @@ global.messages['de'] = {
|
|||
"{0} polls": "{0} Umfragen",
|
||||
"{0} request": "{0} Aufruf",
|
||||
"{0} requests": "{0} Aufrufe",
|
||||
"{0} site": "{0} Website",
|
||||
"{0} sites": "{0} Websites",
|
||||
"{0} sites sorted by {1} in {2} order.": "Typ: {0} Sortierung: {1} {2}",
|
||||
"{0} story": "{0} Beitrag",
|
||||
"{0} stories": "{0} Beiträge",
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ global.messages['en'] = {
|
|||
"Choice": "Choice",
|
||||
"Choices": "Choices",
|
||||
"Claustra": "Claustra",
|
||||
"Click to cancel deletion": "Click to cancel deletion",
|
||||
"Click “Cancel” now if you are not really sure you want to proceed.": "Click “Cancel” now if you are not really sure you want to proceed.",
|
||||
"Close": "Close",
|
||||
"Closed": "Closed",
|
||||
|
|
@ -97,13 +98,15 @@ global.messages['en'] = {
|
|||
"Created": "Created",
|
||||
"Created by {0} on {1}": "Created by {0} on {1}",
|
||||
"Created by {0} on {1}.": "Created by {0} on {1}.",
|
||||
"Created on {0}": "Created on {0}",
|
||||
"Created {0}": "Created {0}",
|
||||
"Currently, it is not possible to delete an account. Please accept our humble apologies.": "Currently, it is not possible to delete an account. Please accept our humble apologies.",
|
||||
"Data Privacy Statement": "Data Privacy Statement",
|
||||
"Date": "Date",
|
||||
"Date string in Unix timestamp format": "Date string in Unix timestamp format",
|
||||
"Delete": "Delete",
|
||||
"Deleted": "Deleted",
|
||||
"Deleted Site": "Deleted Site",
|
||||
"Deleted on {0}": "Deleted on {0}",
|
||||
"Description": "Description",
|
||||
"Details": "Details",
|
||||
"Development": "Development",
|
||||
|
|
@ -179,6 +182,7 @@ global.messages['en'] = {
|
|||
"Last Login": "Last Login",
|
||||
"Last Update": "Last Update",
|
||||
"Last modified by {0} on {1}": "Last modified by {0} on {1}",
|
||||
"Last modified on {0}": "Last modified on {0}",
|
||||
"Last modified {0}": "Last modified {0}",
|
||||
"Layout": "Layout",
|
||||
"Layout Images": "Layout Images",
|
||||
|
|
@ -296,6 +300,7 @@ global.messages['en'] = {
|
|||
"Running Polls": "Running Polls",
|
||||
"Save": "Save",
|
||||
"Save and Run": "Save and Run",
|
||||
"Scheduled for deletion {0}": "Scheduled for deletion {0}",
|
||||
"Search": "Search",
|
||||
"Send": "Send",
|
||||
"Send Request": "Send Request",
|
||||
|
|
@ -360,6 +365,7 @@ global.messages['en'] = {
|
|||
"The URL endpoint for each of these APIs is located at": "The URL endpoint for each of these APIs is located at",
|
||||
"The account data will be available for download from here within the next days.": "The account data will be available for download from here within the next days.",
|
||||
"The account is queued for export.": "The account is queued for export.",
|
||||
"The account {0} is queued for deletion.": "The account {0} is queued for deletion.",
|
||||
"The callback URL will be invoked as an HTTP POST request with the following parameters:": "The callback URL will be invoked as an HTTP POST request with the following parameters:",
|
||||
"The changes were saved successfully.": "The changes were saved successfully.",
|
||||
"The chosen name is too long. Please enter a shorter one.": "The chosen name is too long. Please enter a shorter one.",
|
||||
|
|
@ -461,6 +467,7 @@ global.messages['en'] = {
|
|||
"You are about to delete the image {0}.": "You are about to delete the image {0}.",
|
||||
"You are about to delete the membership of user {0}.": "You are about to delete the membership of user {0}.",
|
||||
"You are about to delete the site {0}.": "You are about to delete the site {0}.",
|
||||
"You are about to delete the whole account which currently contains {0}, {1}, {2}, {3}, {4} and {5}.": "You are about to delete the whole account which currently contains {0}, {1}, {2}, {3}, {4} and {5}.",
|
||||
"You are about to delete the whole site which currently contains {0}, {1}, {2}, {3} and {4}.": "You are about to delete the whole site which currently contains {0}, {1}, {2}, {3} and {4}.",
|
||||
"You are about to reset the layout of site {0}.": "You are about to reset the layout of site {0}.",
|
||||
"You are about to reset the skin {0}.{1}.": "You are about to reset the skin {0}.{1}.",
|
||||
|
|
@ -628,6 +635,8 @@ global.messages['en'] = {
|
|||
"{0} polls": "{0} polls",
|
||||
"{0} request": "{0} request",
|
||||
"{0} requests": "{0} requests",
|
||||
"{0} site": "{0} site",
|
||||
"{0} sites": "{0} sites",
|
||||
"{0} sites sorted by {1} in {2} order.": "{0} sites sorted by {1} in {2} order.",
|
||||
"{0} story": "{0} story",
|
||||
"{0} stories": "{0} stories",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue