Commit f2118759 authored by Konrad Mohrfeldt's avatar Konrad Mohrfeldt

Merge branch 'release-2-2' into 489-in-reply-to

parents 8fa7f596 039e9e69
Pipeline #1204 failed with stage
in 2 seconds
[bumpversion]
current_version = 2.0.16
current_version = 2.1.7
[bumpversion:file:VERSION]
......
......@@ -6,7 +6,7 @@ db.sqlite3*
media/
node_modules
static/
whoosh_index
xapian_index
res/img/backdrops
stadt/settings/local.py
*.swp
......
......@@ -4,7 +4,10 @@ module.exports = ({ file, options }) => {
return {
plugins: {
'autoprefixer': isDebug ? false : options.autoprefixer || {},
'cssnano': isDebug ? false : options.cssnano || {}
'cssnano': isDebug ? false : options.cssnano || {},
'postcss-custom-properties': isDebug ? false : Object.assign({}, {
preserve: true
})
}
}
}
......@@ -78,6 +78,9 @@ class LayoutMixin:
if hasattr(self, 'inline') and self.inline:
h.field_template = 'bootstrap3/layout/inline_field.html'
h.form_class = 'form-inline'
if hasattr(self, 'search') and self.search:
h.field_template = 'bootstrap3/layout/inline_field.html'
h.form_class = 'form-search'
return h
def get_layout(self):
......
......@@ -12,17 +12,14 @@
{% block meta %}
{% include "core/_meta.html" %}
{% endblock %}
{% block meta_feed %}
<link rel="alternate" type="application/rss+xml" title="{{ site.name }}"
href="{% url 'feed' %}">
{% endblock %}
{% block meta_feed %}{% endblock %}
<meta name="robots" content="index,follow,noodp">
<meta name="rating" content="General">
{% include_assets 'early' %}
<script type="application/json" id="app-configuration">{% app_config %}</script>
{% comment %}core/_assets.html is generated by make assets{% endcomment %}
{% include "core/_assets.html" %}
{% include_assets 'early' %}
</head>
<body data-component="browser-warning" class="month-{% now "m" %}{% block body_class %}{% endblock %}">
......
......@@ -49,6 +49,7 @@
</header>
{% endblock %}
{% block content_before %}{% endblock %}
{% block content %}{% endblock %}
</div>
</div>
......
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
-- Robert <robert@humbug> Tue, 06 Feb 2018 09:24:33 +0100
stadtgestalten (2.0.18-1) unstable; urgency=medium
* New upstream release
-- Robert <robert@humbug> Wed, 31 Jan 2018 13:12:18 +0100
stadtgestalten (2.0.17-1) unstable; urgency=medium
* New upstream release
-- Robert <robert@humbug> Tue, 30 Jan 2018 16:31:59 +0100
stadtgestalten (2.0.16-1) unstable; urgency=medium
* New upstream release
......
......@@ -11,7 +11,7 @@ Architecture: all
# python3-requests: for django-allauth
# python3-six: for django-mailbox
Depends: python3-pil, python3-requests, python3-six, ${misc:Depends},
${python3:Depends}, moreutils
${python3:Depends}, moreutils, python3-xapian
Recommends: ghostscript, python3-psycopg2, uwsgi, uwsgi-plugin-python3,
uwsgi-plugin-router-access, ${misc:Recommends}
Suggests: sqlite3, postgresql-client-common
......
......@@ -13,3 +13,6 @@
# send mails with django-mailer
* * * * * root cd /tmp && chronic sh -c "stadtctl send_mail 2>&1 | tee -a /var/log/stadtgestalten/mailer-stadtgestalten.log"
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
# Asset Management
stadtgestalten assets are compiled during build time but in order to support dynamic setups we provide you with some functions to add new assets or add metadata to the markup.
## Function Reference
All following subsections refer to functions in the `core.assets` module. Optional arguments usually refer to element specific attributes with the exception of `stage` that refers to assets and references that are in included in the `head` element (`early` stage) or at the end of the body element (`late` stage).
### add_javascript_reference
Add a `script` element referencing the provided url via the `src` attribute.
Optional arguments: `defer`, `async`, `stage`
### add_javascript_inline
Add a literal JavaScript string that is embedded as a `script` element.
Optional arguments: `stage`
### add_style_reference
Add a `link` element referencing the provided url via the `href` attribute.
Optional arguments: `media`, `**kwargs` as link attributes
### add_style_inline
Add a literal CSS string that is embedded as a `style` element.
NOTE: This breaks support for inline styles due to Content-Security-Policy restrictions. You will render some JavaScript components inoperable. These problems are likely to be subtle in nature and you’ll most likely won’t notice them immediately. You have been warned :).
Optional arguments: `media`, `scoped`
### add_link
Add a `link` element to the `head` referencing the provided url via the `href` attribute.
Optional arguments: `rel`, `**kwargs` as link attributes
### add_meta
Add a `meta` element to the `head` with the provided `name` and `content` attribute.
### add_csp_directive
Adds a Content-Security-Policy directive with the provided `directive` (like `style-src`) and `value`. Note that stadtgestalten serves CSP via HTTP response headers so you won’t find any of the provided directives in the HTML output.
## Overriding Styles
If you want to override styles or colors you can do that by adding
a custom stylesheet. The easiest way to do this is to add the following line to your local configuration (or `/etc/stadtgestalten/settings.py`):
```python
from core.assets import add_style_reference
add_style_reference('stadt/custom.css')
```
This will add a stylesheet reference to `/stadt/static/stadt/custom.css`. On your local machine this matches the `$PROJECT_DIR/build/static/custom.css` path. On a production server you would add a configuraton to the webserver that creates an alias for the mentioned path to the local filesystem. A simple nginx example:
```nginx
location /stadt/static/stadt/custom.css {
alias /var/www/custom.css;
}
```
Take a look at the next section about style variables to learn about easy style overrides with CSS variables.
Note: Please refrain vom using the `add_style_inline` function from the `core.assets` module as it will break JavaScript-generated inline-styles in HTML.
## Style Variables
To ease the process of overriding styles, stadtgestalten supports CSS variables where applicable. CSS variables are a nice thing because with a single rule you can override a setting on the entire platform. Please mind that support for these type of variables is a browser feature and rather new, so you may still want to override individual styles depending on your target audience.
### Supported Variables
The following variables are supported:
`--color-primary`
: used throughout the platform as brand color. determines the appearance of links, buttons, focus rings, etc.
`-color-primary-dark`
: used for active states where `--color-primary` is used. if you override `--color-primary` you want to override this variable.
### CSS Example
```css
:root {
--color-primary: red;
--color-primary-dark: crimson;
}
```
* Das Konzept der **Abonnements** haben wir ausführlich überarbeitet. Ihr könnt nun eine Gruppe abonnieren und/ oder ihr beitreten. *Abonnieren* heißt E-Mails bekommen, *Beitreten* heißt in der Gruppe Dinge tun dürfen. Ihr könnt nun beispielsweise auch einer Gruppe beitreten und anschließend das Abo in den Einstellungen abbestellen (also Mitglied bleiben aber keine Mails mehr bekommen).
* Außerdem haben wir auch die **Einstellungen** für Profil und Gruppe komplett überarbeitet.
* **Kommentare** werden nun mit Strg-Enter abgeschickt, dies sollte auch für den ersten Beitrag eines Gespräches funktionieren.
* Häufig erstellte Arten von **Beiträgen** können nun auch über das Hauptmenü erstellt werden.
* Die Software arbeitet an verschiedenen Stellen mit **E-Mails**. Versendete E-Mails setzen nun auch den `Archived-At` und `List-Id` Header. Letzterer zur besseren Sortierung von Mails. Außerdem wird die Absenderadresse mit einem Zusatz versehen, so dass die Absender unterscheidbar werden.
* Der **Kalender** enthält nun eine sichtbare Markierung für den aktuellen Tag.
* Die interne Behandlung von **Bildern** haben wir vollständig überarbeitet (und dabei hoffentlich verbessert).
* Beim **Bearbeiten** von Beiträgen bekommt Ihr nun eine Warnung, wenn Ihr das Browserfenster vor dem Speichern schließen möchtet.
* Alle internen technischen Abhängigkeiten der Software sind auf einem aktuellem Stand, darunter auch Django 2.
* Und ganz nebenbei beheben wir kleinere bis mittlere Fehler, die vermutlich nur wenige von Euch mitbekommen. Diesmal war es eine zweistellige Anzahl.
default_app_config = 'features.associations.apps.AssociationsConfig'
from django.apps import AppConfig
class AssociationsConfig(AppConfig):
name = "features.associations"
def ready(self):
pass
......@@ -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
......
import re
from haystack import indexes
from haystack.indexes import Indexable, SearchIndex
from .models import Association
MAX_TERM_LENGTH_REGEX = re.compile(r'[^\s]{240,}')
class AssociationIndex(Indexable, SearchIndex):
text = indexes.CharField(document=True, use_template=True)
def prepare_text(self, obj):
# xapian doesn’t support terms longer than 245 characters, but
# our texts sometimes contain links and other stuff that may be
# longer than that. replace everything that is longer than 240 chars
return re.sub(MAX_TERM_LENGTH_REGEX, '', self.prepared_data['text'])
def get_model(self):
return Association
def index_queryset(self, using=None):
return self.get_model().objects.exclude_deleted().filter(public=True)
{% if association.container.is_poll %}
{% include 'polls/_preview.html' %}
{% elif association.container.is_gallery %}
{% include 'galleries/_preview.html' %}
{% elif association.container.is_file %}
{% include 'files/_preview.html' %}
{% elif association.container.is_event %}
{% include 'events/_preview.html' %}
{% else %}
{% include 'articles/_preview.html' %}
{% endif %}
{{ object.slug }}
{{ object.container.title }}
{{ object.container.versions.last.text }}
{{ object.container.place }}
{% for tagged in object.container.taggeds.all %}
{{ tagged.tag.name }}
{% endfor %}
{% for contribution in object.container.contributions.all %}
{{ contribution.contribution.text }}
{% endfor %}
......@@ -10,6 +10,8 @@ from features.images.models import Image
class Create(forms.ModelForm):
container_class = models.Content
class Meta:
model = associations.Association
fields = ('pinned', 'public')
......@@ -60,13 +62,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):
......
<ol class="content-preview-list">
{% for association in associations %}
<li>
{% if association.container.is_poll %}
{% include 'polls/_preview.html' %}
{% elif association.container.is_gallery %}
{% include 'galleries/_preview.html' %}
{% elif association.container.is_file %}
{% include 'files/_preview.html' %}
{% elif association.container.is_event %}
{% include 'events/_preview.html' %}
{% else %}
{% include 'articles/_preview.html' %}
{% endif %}
{% include 'associations/_preview.html' %}
</li>
{% endfor %}
</ol>
......@@ -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" class="form form-modern section">
{% csrf_token %}
<ul>
{% for address in user.emailaddress_set.all %}
......@@ -35,23 +35,29 @@
</ul>
<div>
<button class="btn btn-default" type="submit" name="action_send" >
<button class="btn btn-sm btn-default" type="submit" name="action_send" >
Adresse bestätigen
</button>
{% if user.emailaddress_set.count > 1 %}
<button class="btn btn-default" type="submit" name="action_primary" >
<button class="btn btn-sm btn-default" type="submit" name="action_primary" >
Hauptadresse wählen
</button>
<button class="btn btn-danger" type="submit" name="action_remove" >
<button class="btn btn-sm btn-danger" type="submit" name="action_remove" >
Adresse entfernen
</button>
{% endif %}
</div>
</form>
<form method="post">
<form method="post" class="form form-modern">
{% csrf_token %}
{% field form.email %}
<button name="action_add" class="btn btn-primary">E-Mail-Adresse hinzufügen</button>
<div class="row">
<div class="col-md-8">
{% field form.email %}
</div>
<div class="col-md-4">
<button name="action_add" class="btn btn-primary">E-Mail-Adresse hinzufügen</button>
</div>
</div>
</form>