Commit 1172dbbe authored by Konrad Mohrfeldt's avatar Konrad Mohrfeldt

Merge remote-tracking branch 'origin/release-2-3' into 487-internal-content

parents 02403e3b 27435397
[bumpversion]
current_version = 2.2.3
current_version = 2.2.5
[bumpversion:file:VERSION]
......@@ -3,4 +3,4 @@ max-line-length = 99
exclude = */migrations, node_modules, stadt/settings/local.py, build,
.pybuild, debian/stadtgestalten, debian/tmp, debian/root,
gitlab-ci-build-venv
ignore = E121,E123,E126,E226,E24,E704,W503
ignore = E121,E123,E126,E226,E24,E704,W503,N802
stadtgestalten (2.2.5-1) unstable; urgency=medium
* New upstream release
-- Robert <robert@humbug> Thu, 19 Apr 2018 12:10:17 +0200
stadtgestalten (2.2.4-1) unstable; urgency=medium
* New upstream release
-- Robert <robert@humbug> Tue, 20 Mar 2018 10:49:09 +0100
stadtgestalten (2.2.3-1) unstable; urgency=medium
* New upstream release
......
......@@ -15,4 +15,4 @@
9,29,49 * * * * root cd /tmp && chronic sh -c "stadtctl retry_deferred 2>&1 | tee -a /var/log/stadtgestalten/mailer-stadtgestalten.log"
# update search index
*/5 * * * * root chronic stadtctl update_index
*/5 * * * * root chronic stadtctl update_index --remove
......@@ -15,13 +15,13 @@ class Association(models.QuerySet):
query = models.Q(public=True)
# authenticated users can view associations for entities they are members in
if user.is_authenticated:
GESTALT_TYPE = contenttypes.ContentType.objects.get_for_model(gestalten.Gestalt)
GROUP_TYPE = contenttypes.ContentType.objects.get_for_model(groups.Group)
gestalt_type = contenttypes.ContentType.objects.get_for_model(gestalten.Gestalt)
group_type = contenttypes.ContentType.objects.get_for_model(groups.Group)
gestalt_groups = groups.Group.objects.filter(memberships__member=user.gestalt)
query |= (
(models.Q(entity_type=GROUP_TYPE)
(models.Q(entity_type=group_type)
& models.Q(entity_id__in=gestalt_groups))
| (models.Q(entity_type=GESTALT_TYPE)
| (models.Q(entity_type=gestalt_type)
& models.Q(entity_id=user.gestalt.id))
)
# if given a container we can allow access to associations for which the user
......
Beitrag mit Kommentaren online lesen und kommentieren:
{% if association %}{% url 'content-permalink' association.pk as path %}{% else %}{% url 'content-permalink' object.container.associations.first.pk as path %}{% endif %}{{ path|full_url }}#{% ref object %}
{{ text }}
{{ text|striptags }}
--
{% if contribution %}Du erhältst diese Benachrichtigung, weil Du den Beitrag {{ object.container.subject }} kommentiert hast.
......
......@@ -36,7 +36,8 @@ class Contribution(core.models.Model):
author = models.ForeignKey(
'gestalten.Gestalt', related_name='contributions', on_delete=models.PROTECT)
attached_to = models.ForeignKey(
'Contribution', null=True, related_name='attachments', on_delete=models.SET_NULL)
'Contribution', null=True, related_name='attachments', on_delete=models.SET_NULL,
blank=True)
deleted = models.DateTimeField(null=True, blank=True)
in_reply_to = models.ForeignKey(
'Contribution', null=True, blank=True, related_name='replies',
......
......@@ -133,6 +133,7 @@ def process_incoming_message(sender, message, **args):
else:
process_initial(address)
# FIXME: use X-Stadtgestalten-to header (mailbox without domain)
delivered_to = message.get_email_object()['Delivered-To']
if not delivered_to:
logger.error('Could not process message {}: no Delivered-To header'.format(message.id))
......@@ -150,7 +151,7 @@ def process_incoming_message(sender, message, **args):
logger.warning('Could not process receiver {} in message {}. {}'.format(
address, message.id, e))
django.core.mail.send_mail(
'Re: {}'.format(message.subject),
'Re: {}'.format(message.subject).replace('\n', ' ').replace('\r', ''),
'Konnte die Nachricht nicht verarbeiten. {}'.format(e),
from_email=django.conf.settings.DEFAULT_FROM_EMAIL,
recipient_list=[message.from_header],
......
......@@ -2,7 +2,7 @@ import django.views.generic.edit
from django.utils.timezone import now
import core
import features
from features.associations.views import AssociationMixin
from . import models
......@@ -47,9 +47,7 @@ class ContributionFormMixin(django.views.generic.edit.FormMixin):
return self.object.get_absolute_url()
class Delete(
features.associations.views.AssociationMixin, core.views.PermissionMixin,
django.views.generic.UpdateView):
class Delete(AssociationMixin, core.views.PermissionMixin, django.views.generic.UpdateView):
permission_required = 'contributions.delete'
model = models.Contribution
fields = []
......
{{ text }}
{{ text|striptags }}
--
Nachricht online lesen und beantworten:
......
{% extends 'content/created.txt' %}{% block content %}Galerie online ansehen und kommentieren:
{% url 'content-permalink' association.pk as path %}{{ path|full_url }}
Galerie mit {{ content.gallery_images.count }} Bild{{ content.gallery_images.count|pluralize:'ern' }}
Galerie mit {{ object.gallery_images.count }} Bild{{ object.gallery_images.count|pluralize:'ern' }}
{{ content.versions.last.text }}{% endblock %}
{{ object.versions.last.text }}{% endblock %}
......@@ -64,6 +64,17 @@ class Gestalt(core.models.Model):
def can_login(self):
return self.user.has_usable_password()
def delete(self, *args, **kwargs):
data = self.get_data()
unknown_gestalt = Gestalt.objects.get(id=settings.GROUPRISE_UNKNOWN_GESTALT_ID)
data['associations'].update(entity_id=unknown_gestalt.id)
data['contributions'].update(author=unknown_gestalt)
data['images'].update(creator=unknown_gestalt)
data['memberships_created'].update(created_by=unknown_gestalt)
data['versions'].update(author=unknown_gestalt)
data['votes'].update(voter=unknown_gestalt)
self.user.delete()
def get_absolute_url(self):
if self.public:
return self.get_profile_url()
......@@ -73,6 +84,29 @@ class Gestalt(core.models.Model):
def get_contact_url(self):
return urls.reverse('create-gestalt-conversation', args=(self.pk,))
def get_data(self):
'''
Return all data directly related to this gestalt. May be used e.g. in conjunction with
deleting users.
'''
data = {}
data['gestalt'] = self
data['user'] = self.user
# data['groups_created'] = ?
data['memberships'] = self.memberships
data['subscriptions'] = self.subscriptions
data['tokens'] = self.permissiontoken_set
data['settings'] = self.gestaltsetting_set
data['associations'] = self.associations
data['contributions'] = self.contributions
data['images'] = self.images
data['memberships_created'] = self.memberships_created
data['versions'] = self.versions
data['votes'] = self.votes
return data
def get_profile_url(self):
return urls.reverse(
'entity', args=[type(self).objects.get(pk=self.pk).user.username])
......
......@@ -18,6 +18,8 @@ add_perm('gestalten.change', is_authenticated & is_self)
add_perm('gestalten.change_email', is_authenticated)
add_perm('gestalten.change_password', is_authenticated)
add_perm('gestalten.delete', is_authenticated)
add_perm('account.confirm', always_allow)
add_perm('account.set_password', is_authenticated)
add_perm('account.signup', ~is_authenticated)
{% extends 'stadt/stadt.html' %}
{% block title %}Benutzerkonto löschen - {{ block.super }}{% endblock %}
{% block menu %}{% menu 'gestalt' %}{% endblock %}
{% block heading_title_text %}Einstellungen{% endblock %}
{% block content %}
<form method="post">
{% csrf_token %}
<p><strong>Möchtest Du Dein Benutzerkonto wirklich löschen?</strong></p>
<p><strong>Folgende Daten werden unwiderruflich gelöscht:</strong></p>
<ul>
<li>Benutzerkonto und Profil mit allen Einstellungen und E-Mail-Adressen</li>
<li>{{ view.data.subscriptions.count }} Abonnements von Gruppen</li>
<li>{{ view.data.memberships.count }} Mitgliedschaften in Gruppen</li>
</ul>
<p><strong>Folgende Daten werden unwiderruflich als <em>Unbekannte Gestalt</em> markiert:</strong></p>
<ul>
<li>{{ view.data.versions.count }} Beitragsversionen</li>
<li>{{ view.data.contributions.count }} Kommentare und Nachrichten</li>
<li>{{ view.data.images.count }} Bilder</li>
<li>{{ view.data.votes.count }} Stimmen in Umfragen</li>
</ul>
{% if about_group %}
<p>Solltest Du damit nicht einverstanden sein, führe diesen Schritt bitte nicht aus sondern
<a href="{% url 'create-group-conversation' about_group.pk %}">schreib uns stattdessen eine Nachricht</a>.</p>
{% endif %}
<button class="btn btn-danger">
Benutzerkonto löschen
</button>
</form>
{% endblock %}
......@@ -22,4 +22,15 @@
<button class="btn btn-primary">Einstellungen speichern</button>
</form>
<div class="section section-publish section-article" data-component="publish">
<h2>Gefahrenbereich</h2>
<div class="row">
<div class="col-md-5">
<a href="{% url 'delete-gestalt' %}" class="btn btn-danger btn-sm btn-block">
Benutzerkonto löschen
</a>
</div>
</div>
</div>
{% endblock %}
......@@ -2,6 +2,8 @@ from django.contrib import auth
from django.urls import reverse
from django.test import TestCase
from . import models
class GestaltMixin:
@classmethod
......@@ -62,6 +64,10 @@ class Settings(TestCase):
class AuthenticatedSettings(AuthenticatedMixin, TestCase):
def setUp(self):
auth.get_user_model().objects.create(email='unknown@example.org', username='unknown')
super().setUp()
def test_authenticated_settings(self):
# general settings accessible
r = self.client.get('/')
......@@ -90,3 +96,14 @@ class AuthenticatedSettings(AuthenticatedMixin, TestCase):
self.assertEqual(r.status_code, 200)
r = self.client.get(password_settings_url)
self.assertEqual(r.status_code, 302)
def test_delete(self):
delete_url = reverse('delete-gestalt')
r = self.client.get(reverse('settings'))
self.assertContains(r, 'href="{}'.format(delete_url))
r = self.client.get(delete_url)
self.assertEqual(r.status_code, 200)
r = self.client.post(delete_url)
self.assertRedirects(r, '/')
self.assertFalse(models.Gestalt.objects.filter(pk=self.gestalt.pk).exists())
......@@ -24,6 +24,11 @@ urlpatterns = [
views.Update.as_view(),
name='settings'),
url(
r'^stadt/settings/gestalt/delete/$',
views.Delete.as_view(),
name='delete-gestalt'),
url(
r'^stadt/settings/images/$',
views.UpdateImages.as_view(),
......
......@@ -5,7 +5,7 @@ from allauth.account import views as allauth_views
from crispy_forms import layout
from django.urls import reverse
from django.views import generic
from django.views.generic import edit as edit_views, UpdateView
from django.views.generic import edit as edit_views, DeleteView, UpdateView
import core
from core import views as utils_views
......@@ -32,6 +32,17 @@ class Create(utils_views.ActionMixin, views.SignupView):
return views.LoginView.get_success_url(self)
class Delete(PermissionMixin, DeleteView):
permission_required = 'gestalten.delete'
template_name = 'gestalten/delete.html'
success_url = '/'
def get_object(self):
gestalt = self.request.user.gestalt
self.data = gestalt.get_data()
return gestalt
class Detail(
core.views.PermissionMixin, django.views.generic.list.MultipleObjectMixin,
django.views.generic.DetailView):
......
from django.contrib import admin
from . import models
class SubscriptionAdmin(admin.ModelAdmin):
search_fields = ['subscriber__user__username']
admin.site.register(models.Subscription, SubscriptionAdmin)
......@@ -16,3 +16,6 @@ class Subscription(models.Model):
# subscriber
subscriber = models.ForeignKey(
'gestalten.Gestalt', on_delete=models.CASCADE, related_name='subscriptions')
def __str__(self):
return '{} - {}'.format(self.subscribed_to, self.subscriber)
......@@ -21,6 +21,6 @@ raven # debian (buster): python3-raven
requests # debian: python3-requests
rules # debian (buster): python3-django-rules
translitcodec # NOT IN DEBIAN
xapian-haystack # NOT IN DEBIAN
xapian-haystack # debian (buster): python3-xapian-haystack
# xapian-haystack also requires the xapian python bindings that you’ll find
# as "python3-xapian" in Debian/Ubuntu and "python-xapian" in ArchLinux
......@@ -17,7 +17,8 @@
font-family: @font-family-default;
user-select: none;
touch-action: manipulation;
min-height: 2.25em;
// FIXME: breaks text only buttons, e.g. file attachment links below contributions
// min-height: 2.25em;
font-size: 1rem;
+ & {
......
......@@ -226,6 +226,8 @@ HAS_PIWIK = True
STADTGESTALTEN_INTRO_TEXT = ''
GROUPRISE_UNKNOWN_GESTALT_ID = 1
# Authentication
# http://django-allauth.readthedocs.org/
......
......@@ -5,3 +5,4 @@ DATABASES = {
'ENGINE': 'django.db.backends.sqlite3',
}
}
GROUPRISE_UNKNOWN_GESTALT_ID = 1
......@@ -6,7 +6,6 @@ urlpatterns = [
urls.url(r'^stadt/admin/', admin.site.urls),
urls.url(r'^stadt/api/', urls.include('core.api_urls')),
urls.url(r'^stadt/', urls.include('features.associations.urls')),
urls.url(r'^stadt/', urls.include('features.conversations.urls')),
urls.url(r'^stadt/', urls.include('features.memberships.urls')),
urls.url(r'^stadt/', urls.include('features.sharing.urls')),
......@@ -26,5 +25,6 @@ urlpatterns = [
urls.url(r'^', urls.include('features.tags.urls')),
# matches */*/
urls.url(r'^', urls.include('features.associations.urls')),
urls.url(r'^', urls.include('features.content.urls')),
] + static.static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment