Commit 113dff87 authored by Konrad Mohrfeldt's avatar Konrad Mohrfeldt

Merge branch 'release-2-2' into 216-search

parents 99ad4749 996df244
Pipeline #1200 failed with stage
in 51 seconds
[bumpversion]
current_version = 2.1.0
current_version = 2.1.7
[bumpversion:file:VERSION]
......
stadtgestalten (2.1.7-1) unstable; urgency=medium
* New upstream release
-- Robert <robert@humbug> Mon, 19 Feb 2018 12:09:40 +0100
stadtgestalten (2.1.6-1) unstable; urgency=medium
* New upstream release
-- Robert <robert@humbug> Mon, 19 Feb 2018 10:51:04 +0100
stadtgestalten (2.1.5-1) unstable; urgency=medium
* New upstream release
-- Robert <robert@humbug> Fri, 09 Feb 2018 12:53:21 +0100
stadtgestalten (2.1.4-1) unstable; urgency=medium
* New upstream release
-- Robert <robert@humbug> Thu, 08 Feb 2018 16:14:58 +0100
stadtgestalten (2.1.3-1) unstable; urgency=medium
* New upstream release
-- Robert <robert@humbug> Thu, 08 Feb 2018 14:58:40 +0100
stadtgestalten (2.1.2-1) unstable; urgency=medium
* New upstream release
-- Robert <robert@humbug> Thu, 08 Feb 2018 10:40:49 +0100
stadtgestalten (2.1.1-1) unstable; urgency=medium
* New upstream release
-- Konrad Mohrfeldt <konrad@rumpelstilzchen.lan> Thu, 08 Feb 2018 02:20:29 +0100
stadtgestalten (2.1.0-1) unstable; urgency=medium
* New upstream release
......
......@@ -38,7 +38,7 @@ class Association(models.QuerySet):
qs = self
qs = qs.filter(content__time__isnull=True) # events
qs = qs.filter(content__gallery_images__image__isnull=True) # galleries
qs = qs.filter(content__options__isnull=True) # polls
qs = qs.filter(content__poll__isnull=True) # polls
qs = qs.filter(content__versions__file__isnull=True) # files
return qs
......
......@@ -17,6 +17,8 @@ class Comment(contributions.Text):
class Create(forms.ModelForm):
container_class = models.Content
class Meta:
model = associations.Association
fields = ('pinned', 'public')
......@@ -67,13 +69,17 @@ class Create(forms.ModelForm):
'entity_type': self.instance.entity_type,
'slug': core.text.slugify(self.cleaned_data['title']),
})
self.instance.container = models.Content.objects.create(
container = self.container_class.objects.create(
title=self.cleaned_data['title'],
image=self.cleaned_data.get('image'),
place=self.cleaned_data.get('place', ''),
time=self.cleaned_data.get('time'),
until_time=self.cleaned_data.get('until_time'),
all_day=self.cleaned_data.get('all_day', False))
if not hasattr(container, 'content_ptr'):
self.instance.container = container
else:
self.instance.container = container.content_ptr
self.instance.container.versions.create(
author=self.author, text=self.cleaned_data['text'])
self.save_content_relations(commit)
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.10 on 2018-02-22 09:33
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('polls', '0014_auto_20180222_1033'),
('content2', '0009_auto_20180109_1302'),
]
operations = [
migrations.AddField(
model_name='content',
name='poll_new',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='content_new', to='polls.WorkaroundPoll'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.10 on 2018-02-22 10:15
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('content2', '0010_content_poll_new'),
('polls', '0015_auto_20180222_1034'),
]
operations = [
migrations.RenameField(
model_name='content',
old_name='poll_new',
new_name='poll',
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.10 on 2018-02-22 10:16
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('content2', '0011_auto_20180222_1115'),
]
operations = [
migrations.AlterField(
model_name='content',
name='poll',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='content', to='polls.WorkaroundPoll'),
),
]
......@@ -12,6 +12,9 @@ from features.groups import models as groups
class Content(core.models.Model):
is_conversation = False
# FIXME: remove when django bug #28988 is fixed
poll = models.OneToOneField('polls.WorkaroundPoll', null=True, related_name='content')
title = models.CharField(max_length=255)
image = models.ForeignKey('images.Image', blank=True, null=True, on_delete=models.SET_NULL)
......@@ -76,7 +79,9 @@ class Content(core.models.Model):
@property
def is_poll(self):
return self.options.count() > 0
# FIXME: change when django bug #28988 has been fixed
# return hasattr(self, 'poll')
return self.poll is not None
@property
def subject(self):
......
......@@ -65,7 +65,7 @@ class Detail(DetailBase):
class Permalink(django.views.generic.RedirectView):
def get_redirect_url(self, *args, **kwargs):
association = get_association_or_404(pk=kwargs.get('association_pk'))
return django.core.urlresolvers.reverse(
return django.urls.reverse(
'content', args=(association.entity.slug, association.slug))
......
......@@ -3,6 +3,8 @@ import logging
import django.conf
import django.db.models.signals
import django_mailbox.signals
from django.conf import settings
from django.db.models.functions import Lower
from django.dispatch import receiver
import core.models
......@@ -25,12 +27,15 @@ def contribution_created(sender, instance, **kwargs):
def get_sender(message):
try:
gestalt = gestalten.Gestalt.objects.get(user__email=message.from_address[0])
gestalt = gestalten.Gestalt.objects.annotate(email=Lower('user__email')).get(
email=message.from_address[0].lower())
except gestalten.Gestalt.DoesNotExist:
try:
gestalt = gestalten.Gestalt.objects.get(
user__emailaddress__email=message.from_address[0])
gestalt = gestalten.Gestalt.objects.annotate(
email=Lower('user__emailaddress__email')).get(
email=message.from_address[0].lower())
except gestalten.Gestalt.DoesNotExist:
gestalt = None
return gestalt
......@@ -112,6 +117,16 @@ def process_incoming_message(sender, message, **args):
'Du darfst mit dieser Gruppe kein Gespräch per E-Mail beginnen. Bitte '
'verwende die Schaltfläche auf der Webseite.')
def process_message(address):
try:
process_reply(address)
except core.models.PermissionToken.DoesNotExist:
if address == settings.STADTGESTALTEN_BOT_EMAIL:
for to_address in message.to_addresses:
process_initial(to_address)
else:
process_initial(address)
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))
......@@ -121,20 +136,17 @@ def process_incoming_message(sender, message, **args):
address = address.rstrip('>')
if not is_autoresponse(message):
try:
process_reply(address)
except core.models.PermissionToken.DoesNotExist:
try:
process_initial(address)
except (
groups.Group.DoesNotExist, ValueError,
django.core.exceptions.PermissionDenied) as e:
logger.error('Could not process receiver {} in message {}'.format(
address, message.id))
django.core.mail.send_mail(
'Re: {}'.format(message.subject),
'Konnte die Nachricht nicht verarbeiten. {}'.format(e),
from_email=django.conf.settings.DEFAULT_FROM_EMAIL,
recipient_list=[message.from_header],
fail_silently=True)
process_message(address)
except (
groups.Group.DoesNotExist, ValueError,
django.core.exceptions.PermissionDenied) as e:
logger.error('Could not process receiver {} in message {}'.format(
address, message.id))
django.core.mail.send_mail(
'Re: {}'.format(message.subject),
'Konnte die Nachricht nicht verarbeiten. {}'.format(e),
from_email=django.conf.settings.DEFAULT_FROM_EMAIL,
recipient_list=[message.from_header],
fail_silently=True)
else:
logger.warning('Ignored message {} as autoresponse'.format(message.id))
......@@ -2,10 +2,10 @@
--
Nachricht online lesen und beantworten:
{% 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 %}
{% if association %}{% url 'conversation' association.pk as path %}{% else %}{% url 'conversation' object.container.associations.first.pk as path %}{% endif %}{{ path|full_url }}#{% ref object %}
{% if contribution %}
Du erhältst diese Benachrichtigung, weil Du Dich an dem Gespräch {{ object.container.subject }} beteiligt hast.
{% url 'content-permalink' object.container.associations.first.pk as path %}{{ path|full_url }}#{% ref contribution %}
{% url 'conversation' object.container.associations.first.pk as path %}{{ path|full_url }}#{% ref contribution %}
{% elif subscription %}
Du erhältst diese Benachrichtigung weil Du {% if membership %}Mitglied der Gruppe {{ membership.group }} bist und die Gruppe{% else %}die Gruppe {{ subscription.subscribed_to }}{% endif %} abonniert hast.
{{ subscription.subscribed_to.get_absolute_url|full_url }}{% endif %}
......@@ -6,6 +6,8 @@ def get_requested_time(request):
month, year = query.get('month', None), query.get('year', None)
if month and year:
return timezone.datetime(year=int(year), month=int(month), day=1)
else:
return None
try:
return timezone.datetime(year=int(year), month=int(month), day=1)
except ValueError:
pass
return None
......@@ -41,7 +41,7 @@ class PasswordReset(utils_views.ActionMixin, views.PasswordResetView):
def get_context_data(self, **kwargs):
kwargs['login_url'] = allauth.account.utils.passthrough_next_redirect_url(
self.request, django.core.urlresolvers.reverse('login'),
self.request, django.urls.reverse('login'),
self.redirect_field_name)
return django.views.generic.FormView.get_context_data(self, **kwargs)
......
......@@ -17,10 +17,26 @@ def permission(path):
class GestaltSerializer(serializers.ModelSerializer):
name = serializers.CharField(source='__str__', read_only=True)
initials = serializers.CharField(source='get_initials', read_only=True)
url = serializers.CharField(source='get_absolute_url', read_only=True)
class Meta:
model = models.Gestalt
fields = ('id', 'name', 'initials', 'about', 'avatar', 'avatar_color')
fields = ('id', 'name', 'initials', 'about', 'avatar', 'avatar_color', 'url')
class GestaltOrAnonSerializer(serializers.Serializer):
id = serializers.IntegerField(required=False, allow_null=True)
name = serializers.CharField(required=False, allow_null=True)
def to_internal_value(self, data):
data = super().to_internal_value(data)
if 'id' in data and data['id'] is not None:
return models.Gestalt.objects.get(pk=data['id'])
# todo validate name if no valid id was provided
return data
class Meta:
fields = ('id', 'name')
class GestaltSettingSerializer(serializers.ModelSerializer):
......
......@@ -5,7 +5,7 @@
{% endblock %}
{% block content %}
<form method="post">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="section section-group">
......
......@@ -103,7 +103,8 @@ class UpdateEmail(PermissionMixin, views.EmailView):
@property
def success_url(self):
group = Group.objects.filter(slug=self.request.GET.get('group')).first()
return '{}?group={}'.format(reverse('email-settings'), group.slug)
slug = group.slug if group else ''
return '{}?group={}'.format(reverse('email-settings'), slug)
class UpdateEmailConfirm(utils_views.ActionMixin, edit_views.FormMixin, views.ConfirmEmailView):
......
......@@ -6,7 +6,7 @@ from django.contrib.contenttypes.fields import GenericRelation
from django import urls
from django.db import models
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFit, SmartResize, Transpose
from imagekit.processors import ResizeToFit, Transpose
import core.models
from core import colors
......@@ -37,9 +37,9 @@ class Group(core.models.Model):
blank=True, help_text='Der Avatar ist ein kleines quadratisches Vorschaubild, '
'an welchem sich die Gruppe leicht erkennen lässt.')
avatar_64 = ImageSpecField(
source='avatar', processors=[Transpose(), SmartResize(64, 64)], format='PNG')
source='avatar', processors=[Transpose(), ResizeToFit(64, 64)], format='PNG')
avatar_256 = ImageSpecField(
source='avatar', processors=[Transpose(), SmartResize(256, 256)], format='PNG')
source='avatar', processors=[Transpose(), ResizeToFit(256, 256)], format='PNG')
avatar_color = models.CharField(
max_length=7,
default=colors.get_random_color)
......@@ -95,8 +95,8 @@ class Group(core.models.Model):
def get_cover_url(self):
url = None
intro_gallery = self.associations.filter_galleries().filter(pinned=True, public=True) \
.order_content_by_time_created().first()
intro_gallery = self.associations.exclude_deleted().filter_galleries() \
.filter(pinned=True, public=True).order_content_by_time_created().first()
if intro_gallery:
url = intro_gallery.container.gallery_images.first().image.preview_group.url
return url
......
......@@ -133,23 +133,23 @@
{% endif %}
{# SETTINGS #}
<li>
{% load rules %}
{% has_perm 'groups.change' user group as can_change_group %}
{% has_perm 'groups.change_subscriptions_memberships' user group as can_edit_subscription_settings %}
{% if can_change_group %}
{% url 'group-settings' as settings_url %}
{% elif can_edit_subscription_settings %}
{% url 'subscriptions-memberships-settings' as settings_url %}
{% endif %}
{% if settings_url %}
{% load rules %}
{% has_perm 'groups.change' user group as can_change_group %}
{% has_perm 'groups.change_subscriptions_memberships' user group as can_edit_subscription_settings %}
{% if can_change_group %}
{% url 'group-settings' as settings_url %}
{% elif can_edit_subscription_settings %}
{% url 'subscriptions-memberships-settings' as settings_url %}
{% endif %}
{% if settings_url %}
<li>
<a href="{{ settings_url }}?group={{ group.slug }}"
class="nav-menu-item">
<i class="sg sg-fw sg-settings"></i>
<span class="nav-menu-item-label">Einstellungen</span>
</a>
{% endif %}
</li>
</li>
{% endif %}
</ol>
</div>
</div>
......
......@@ -109,9 +109,9 @@
{% has_perm 'subscriptions.create' user group as can_subscribe %}
{% if can_subscribe %}
{% if user.is_authenticated %}
<form action="{% url 'group-subscribe' group.pk %}" method="post">
<form action="{% url 'group-subscribe' group.pk %}" method="post" class="form-inline-text">
{% csrf_token %}
<button class="btn btn-link"
<button class="btn btn-text"
title="Bei neuen Beiträgen benachrichtigt werden">
Gruppe abonnieren
</button>
......
......@@ -5,7 +5,7 @@
{% endblock %}
{% block content %}
<form method="post">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="section section-group">
......
......@@ -3,6 +3,7 @@ from django import forms
from django.core.exceptions import ObjectDoesNotExist
from features.content import forms as content
from features.content.models import Content
from . import models
......@@ -22,17 +23,29 @@ class OptionMixin:
return super().is_valid() and self.options.is_valid()
def save_content_relations(self, commit):
# FIXME: remove when django bug #28988 is fixed
self.instance.container.poll = models.WorkaroundPoll.objects.create()
self.instance.container.save()
for form in self.options.forms:
form.instance.poll = self.instance.container
form.instance.poll = self.instance.container.poll
self.options.save(commit)
class Create(OptionMixin, content.Create):
# FIXME: replace by models.Poll when django bug #28988 is fixed
container_class = Content
text = forms.CharField(label='Beschreibung / Frage', widget=forms.Textarea({'rows': 2}))
poll_type = forms.ChoiceField(
label='Art der Umfrage',
choices=[('simple', 'einfach'), ('event', 'Datum / Zeit')],
label='Art der Antwortmöglichkeiten',
choices=[('simple', 'einfacher Text'), ('event', 'Datum / Zeit')],
initial='simple', widget=forms.Select({'data-poll-type': ''}))
vote_type = forms.ChoiceField(
label='Art der Abstimmmöglichkeiten',
choices=[('simple', 'Ja/Nein/Vielleicht'),
('condorcet', 'Stimmen ordnen (rangbasiert)')],
initial='simple', widget=forms.Select({'data-poll-vote-type': ''}))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
......@@ -58,28 +71,33 @@ class Create(OptionMixin, content.Create):
return False if self.is_type_change else super().is_valid()
def save(self, commit=True):
super().save(commit)
association = super().save(commit)
association.container.poll.condorcet = self.cleaned_data['vote_type'] == 'condorcet'
association.container.poll.save()
if commit:
self.send_post_create()
return self.instance
return association
class Update(OptionMixin, content.Update):
text = forms.CharField(label='Beschreibung / Frage', widget=forms.Textarea({'rows': 2}))
poll_type = forms.CharField(widget=forms.HiddenInput({'data-poll-type': ''}))
vote_type = forms.CharField(widget=forms.HiddenInput({'data-poll-vote-type': ''}))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
poll = self.instance.container.poll
self.fields['vote_type'].initial = str(poll.vote_type)
try:
models.Option.objects.filter(poll=self.instance.container).first().eventoption
models.Option.objects.filter(poll=poll).first().eventoption
self.options = EventOptionFormSet(
data=kwargs.get('data'),
queryset=models.EventOption.objects.filter(poll=self.instance.container))
queryset=models.EventOption.objects.filter(poll=poll))
self.fields['poll_type'].initial = 'event'
except ObjectDoesNotExist:
self.options = SimpleOptionFormSet(
data=kwargs.get('data'),
queryset=models.SimpleOption.objects.filter(poll=self.instance.container))
queryset=models.SimpleOption.objects.filter(poll=poll))
self.fields['poll_type'].initial = 'simple'
self.options.extra = 0
......@@ -93,15 +111,19 @@ class Update(OptionMixin, content.Update):
return super().get_initial_for_field(field, field_name)
VoteFormSet = forms.modelformset_factory(
models.Vote, fields=('endorse',), labels={'endorse': 'Zustimmung'},
SimpleVoteFormSet = forms.modelformset_factory(
models.SimpleVote, fields=('endorse',), labels={'endorse': 'Zustimmung'},
widgets={'endorse': forms.RadioSelect(
choices=[(True, 'Ja'), (False, 'Nein'), (None, 'Vielleicht')])})
CondorcetVoteFormSet = forms.modelformset_factory(
models.CondorcetVote, fields=('rank',), labels={'rank': 'Rang / Platz'})
class Vote(forms.ModelForm):
class Meta:
model = models.Vote
model = models.SimpleVote
fields = ('anonymous',)
labels = {'anonymous': 'Name/Alias'}
......@@ -115,14 +137,19 @@ class Vote(forms.ModelForm):
self.poll = poll
options = poll.options.all()
self.votes = VoteFormSet(data=kwargs.get('data'), queryset=models.Vote.objects.none())
if self.poll.condorcet:
self.votes = CondorcetVoteFormSet(
data=kwargs.get('data'), queryset=models.SimpleVote.objects.none())
else:
self.votes = SimpleVoteFormSet(
data=kwargs.get('data'), queryset=models.SimpleVote.objects.none())
self.votes.extra = len(options)
for i, form in enumerate(self.votes.forms):
form.instance.option = options[i]
def clean_anonymous(self):
anon = self.cleaned_data['anonymous']
if models.Vote.objects.filter(option__poll=self.poll, anonymous=anon).exists():
if models.SimpleVote.objects.filter(option__poll=self.poll, anonymous=anon).exists():
raise django.forms.ValidationError('%s hat bereits abgestimmt.' % anon)
return anon
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.7 on 2017-12-12 15:37
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('content2', '0008_auto_20170620_1022'),
('polls', '0004_auto_20170921_1502'),
]
operations = [
migrations.CreateModel(
name='CondorcetVote',
fields=[
('vote_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='polls.Vote')),
('rank', models.SmallIntegerField()),
],
options={
'abstract': False,
},
bases=('polls.vote',),
),
migrations.CreateModel(
name='Poll',
fields=[
('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='content2.Content')),
('condorcet', models.BooleanField(default=False)),
],
options={
'abstract': False,
},
bases=('content2.content',),
),
migrations.CreateModel(
name='SimpleVote',
fields=[
('vote_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='polls.Vote')),
('endorse_new', models.NullBooleanField(default=False)),
],
options={
'abstract': False,
},
bases=('polls.vote',),
),
migrations.AddField(
model_name='option',
name='poll_new',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='options_new', to='polls.Poll'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.7 on 2018-01-04 08:36
from __future__ import unicode_literals
from django.db import connection, migrations
def convert_polls(apps, schema_editor):
Content = apps.get_model('content2', 'Content')
with connection.cursor() as cursor:
for c in Content.objects.all():
if c.options.count() > 0:
cursor.execute('INSERT INTO polls_poll VALUES (%s, \'0\')', [c.id])
def adapt_options(apps, schema_editor):
Option = apps.get_model('polls', 'Option')
for o in Option.objects.all():
o.poll_new = o.poll.poll
o.save()
class Migration(migrations.Migration):
dependencies = [
('content2', '0008_auto_20170620_1022'),
('polls', '0005_auto_20171212_1637'),
]
operations = [
migrations.RunPython(convert_polls),
migrations.RunPython(adapt_options),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.7 on 2018-01-04 09:44
from __future__ import unicode_literals
from django.db import migrations