add: support for account deletion

This commit is contained in:
Tobi Schäfer 2018-05-12 17:39:34 +02:00
parent 1defbc8240
commit 922f2ed732
19 changed files with 1205 additions and 830 deletions

View file

@ -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>

View file

@ -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);
}

View file

@ -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

View file

@ -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;
}

View file

@ -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 sites archive.

View file

@ -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),

View file

@ -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;
}
};
/**
*

View file

@ -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;

View file

@ -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 %> %>

View file

@ -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 users 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;
// Dont 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 dont 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;
}

View file

@ -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

View file

@ -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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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",

View file

@ -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",