Initial commit

This commit is contained in:
Stefan Wehrmeyer 2022-02-15 21:09:09 +01:00
commit 19170142c3
20 changed files with 894 additions and 0 deletions

View file

@ -0,0 +1,3 @@
__version__ = "0.0.1"
default_app_config = "froide_govplan.apps.FroideGovPlanConfig"

82
froide_govplan/admin.py Normal file
View file

@ -0,0 +1,82 @@
from django import forms
from django.contrib import admin
from django.urls import reverse_lazy
from tinymce.widgets import TinyMCE
from froide.helper.widgets import TagAutocompleteWidget
from .models import Government, GovernmentPlan, GovernmentPlanUpdate
class GovernmentPlanAdminForm(forms.ModelForm):
class Meta:
model = GovernmentPlan
fields = "__all__"
widgets = {
"categories": TagAutocompleteWidget(
autocomplete_url=reverse_lazy("api:category-autocomplete")
),
}
class GovernmentPlanUpdateAdminForm(forms.ModelForm):
class Meta:
model = GovernmentPlanUpdate
fields = "__all__"
widgets = {
"content": TinyMCE(attrs={'cols': 80, 'rows': 30})
}
class GovernmentAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("name",)}
list_display = ("name", "public", "start_date", "end_date")
list_filter = ("public",)
class GovernmentPlanAdmin(admin.ModelAdmin):
form = GovernmentPlanAdminForm
save_on_top = True
prepopulated_fields = {"slug": ("title",)}
raw_id_fields = ("responsible_publicbody",)
list_display = (
"title",
"public",
"status",
"rating",
)
list_filter = ("status", "rating", "public", "government")
class GovernmentPlanUpdateAdmin(admin.ModelAdmin):
form = GovernmentPlanUpdateAdminForm
raw_id_fields = ("user", "foirequest")
list_display = (
"plan",
"user",
"timestamp",
"status",
"rating",
"public",
)
list_filter = (
"status",
"public",
)
search_fields = ("title", "plan__title",)
date_hierarchy = "timestamp"
def get_queryset(self, request):
qs = super().get_queryset(request)
qs = qs.prefetch_related(
"plan",
"user",
)
return qs
admin.site.register(Government, GovernmentAdmin)
admin.site.register(GovernmentPlan, GovernmentPlanAdmin)
admin.site.register(GovernmentPlanUpdate, GovernmentPlanUpdateAdmin)

7
froide_govplan/apps.py Normal file
View file

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class FroideGovPlanConfig(AppConfig):
name = "froide_govplan"
verbose_name = _("GovPlan App")

View file

@ -0,0 +1,13 @@
from django.utils.translation import gettext_lazy as _
from cms.app_base import CMSApp
from cms.apphook_pool import apphook_pool
@apphook_pool.register
class GovPlanCMSApp(CMSApp):
name = _("GovPlan CMS App")
app_name = "govplan"
def get_urls(self, page=None, language=None, **kwargs):
return ["froide_govplan.urls"]

View file

@ -0,0 +1,24 @@
from django.utils.translation import gettext_lazy as _
from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool
from .models import PLUGIN_TEMPLATES, GovernmentPlansCMSPlugin
@plugin_pool.register_plugin
class GovernmentPlansPlugin(CMSPluginBase):
name = _("Government plans")
model = GovernmentPlansCMSPlugin
filter_horizontal = ("categories",)
cache = True
def get_render_template(self, context, instance, placeholder):
return instance.template or PLUGIN_TEMPLATES[0][0]
def render(self, context, instance, placeholder):
context = super().render(context, instance, placeholder)
context["object_list"] = instance.get_plans(
context["request"], published_only=False
)
return context

View file

@ -0,0 +1,149 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-02-15 22:13+0100\n"
"PO-Revision-Date: 2022-02-15 22:18+0100\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Last-Translator: Stefan Wehrmeyer <stefan.wehrmeyer@okfn.de>\n"
"Language-Team: \n"
"X-Generator: Poedit 2.3\n"
#: apps.py:7
msgid "GovPlan App"
msgstr ""
#: cms_apps.py:9
msgid "GovPlan CMS App"
msgstr ""
#: cms_plugins.py:11 models.py:109
msgid "Government plans"
msgstr "Regierungsvorhaben"
#: models.py:23
msgid "not started"
msgstr "nicht begonnen"
#: models.py:24
msgid "started"
msgstr "begonnen"
#: models.py:25
msgid "partially implemented"
msgstr "teilweise umgesetzt"
#: models.py:26
msgid "implemented"
msgstr "umgesetzt"
#: models.py:27
msgid "deferred"
msgstr "verschoben"
#: models.py:31
msgid "terrible"
msgstr "sehr schlecht"
#: models.py:32
msgid "bad"
msgstr "schlecht"
#: models.py:33
msgid "OK"
msgstr "mittelmäßig"
#: models.py:34
msgid "good"
msgstr "gut"
#: models.py:35
msgid "excellent"
msgstr "sehr gut"
#: models.py:52
msgid "Government"
msgstr "Regierung"
#: models.py:53
msgid "Governments"
msgstr "Regierungen"
#: models.py:66
msgid "Categorized Government Plan"
msgstr "Kategorisiertes Regierungsvorhaben"
#: models.py:67
msgid "Categorized Government Plans"
msgstr "Kategorisierte Regierungsvorhaben"
#: models.py:79
msgid "image"
msgstr "Bild"
#: models.py:94 models.py:180
msgid "categories"
msgstr "Kategorien"
#: models.py:108
msgid "Government plan"
msgstr "Regierungsvorhaben"
#: models.py:160
msgid "Plan update"
msgstr "Entwicklung beim Vorhaben"
#: models.py:161
msgid "Plan updates"
msgstr "Entwicklungen beim Vorhaben"
#: models.py:170
msgid "Normal"
msgstr "Standard"
#: models.py:184
msgid "number of plans"
msgstr "Anzahl der Vorhaben"
#: models.py:184
msgid "0 means all the plans"
msgstr "0 heißt all Vorhaben"
#: models.py:187
msgid "offset"
msgstr "Start"
#: models.py:189
msgid "number of plans to skip from top of list"
msgstr "Anzahl der Vorhaben, die ausgelassen werden"
#: models.py:192
msgid "template"
msgstr "Template"
#: models.py:196
msgid "template used to display the plugin"
msgstr "Template, mit dem das Plugin angezeigt wird"
#: models.py:215
msgid "All matching plans"
msgstr "Alle zutreffenden Vorhaben"
#: models.py:216
#, python-format
msgid "%s matching plans"
msgstr "%s zutreffenden Vorhaben"
#: urls.py:10
msgctxt "url part"
msgid "<slug:gov>/plan/<slug:plan>/"
msgstr "<slug:gov>/vorhaben/<slug:plan>/"

View file

@ -0,0 +1,108 @@
# Generated by Django 3.2.8 on 2022-02-15 19:55
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import filer.fields.image
import taggit.managers
class Migration(migrations.Migration):
initial = True
dependencies = [
('publicbody', '0039_publicbody_alternative_emails'),
('auth', '0012_alter_user_first_name_max_length'),
('foirequest', '0054_alter_foirequest_options'),
migrations.swappable_dependency(settings.FILER_IMAGE_MODEL),
('organization', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='CategorizedGovernmentPlan',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
options={
'verbose_name': 'Categorized Government Plan',
'verbose_name_plural': 'Categorized Government Plans',
},
),
migrations.CreateModel(
name='Government',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('slug', models.SlugField(max_length=255, unique=True)),
('public', models.BooleanField(default=False)),
('description', models.TextField(blank=True)),
('start_date', models.DateField(blank=True, null=True)),
('end_date', models.DateField(blank=True, null=True)),
('planning_document', models.URLField(blank=True)),
('jurisdiction', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='publicbody.jurisdiction')),
],
options={
'verbose_name': 'Government',
'verbose_name_plural': 'Governments',
},
),
migrations.CreateModel(
name='GovernmentPlan',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('slug', models.SlugField(max_length=255, unique=True)),
('description', models.TextField(blank=True)),
('public', models.BooleanField(default=False)),
('status', models.CharField(choices=[('not_started', 'not started'), ('started', 'started'), ('partially_implemented', 'partially implemented'), ('implemented', 'implemented'), ('deferred', 'deferred')], default='needs_approval', max_length=25)),
('rating', models.IntegerField(blank=True, choices=[(1, 'terrible'), (2, 'bad'), (3, 'OK'), (4, 'good'), (5, 'excellent')], null=True)),
('reference', models.CharField(blank=True, max_length=255)),
('categories', taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='froide_govplan.CategorizedGovernmentPlan', to='publicbody.Category', verbose_name='categories')),
('government', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='froide_govplan.government')),
('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='auth.group')),
('image', filer.fields.image.FilerImageField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.FILER_IMAGE_MODEL, verbose_name='image')),
('responsible_publicbody', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='publicbody.publicbody')),
],
options={
'verbose_name': 'Government plan',
'verbose_name_plural': 'Government plans',
'ordering': ('reference', 'title'),
},
),
migrations.CreateModel(
name='GovernmentPlanUpdate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
('title', models.CharField(blank=True, max_length=1024)),
('content', models.TextField(blank=True)),
('status', models.CharField(blank=True, choices=[('not_started', 'not started'), ('started', 'started'), ('partially_implemented', 'partially implemented'), ('implemented', 'implemented'), ('deferred', 'deferred')], default='', max_length=25)),
('rating', models.IntegerField(blank=True, choices=[(1, 'terrible'), (2, 'bad'), (3, 'OK'), (4, 'good'), (5, 'excellent')], null=True)),
('public', models.BooleanField(default=False)),
('foirequest', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='foirequest.foirequest')),
('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='organization.organization')),
('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='updates', to='froide_govplan.governmentplan')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Plan update',
'verbose_name_plural': 'Plan updates',
'ordering': ('-timestamp',),
'get_latest_by': 'timestamp',
},
),
migrations.AddField(
model_name='categorizedgovernmentplan',
name='content_object',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='froide_govplan.governmentplan'),
),
migrations.AddField(
model_name='categorizedgovernmentplan',
name='tag',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='categorized_governmentplan', to='publicbody.category'),
),
]

View file

@ -0,0 +1,42 @@
# Generated by Django 3.2.8 on 2022-02-15 20:13
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('publicbody', '0039_publicbody_alternative_emails'),
('organization', '0001_initial'),
('cms', '0022_auto_20180620_1551'),
('froide_govplan', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='governmentplan',
name='organization',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='organization.organization'),
),
migrations.AddField(
model_name='governmentplanupdate',
name='url',
field=models.URLField(blank=True),
),
migrations.CreateModel(
name='GovernmentPlansCMSPlugin',
fields=[
('cmsplugin_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='froide_govplan_governmentplanscmsplugin', serialize=False, to='cms.cmsplugin')),
('count', models.PositiveIntegerField(default=1, help_text='0 means all the plans', verbose_name='number of plans')),
('offset', models.PositiveIntegerField(default=0, help_text='number of plans to skip from top of list', verbose_name='offset')),
('template', models.CharField(blank=True, choices=[('froide_govplan/plugins/default.html', 'Normal')], help_text='template used to display the plugin', max_length=250, verbose_name='template')),
('categories', models.ManyToManyField(blank=True, to='publicbody.Category', verbose_name='categories')),
('government', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='froide_govplan.government')),
],
options={
'abstract': False,
},
bases=('cms.cmsplugin',),
),
]

View file

243
froide_govplan/models.py Normal file
View file

@ -0,0 +1,243 @@
from django.conf import settings
from django.contrib.auth.models import Group
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from filer.fields.image import FilerImageField
from taggit.managers import TaggableManager
from taggit.models import TaggedItemBase
from froide.foirequest.models import FoiRequest
from froide.organization.models import Organization
from froide.publicbody.models import Category, Jurisdiction, PublicBody
try:
from cms.models.pluginmodel import CMSPlugin
except ImportError:
CMSPlugin = None
class PlanStatus(models.TextChoices):
NOT_STARTED = ("not_started", _("not started"))
STARTED = ("started", _("started"))
PARTIALLY_IMPLEMENTED = ("partially_implemented", _("partially implemented"))
IMPLEMENTED = ("implemented", _("implemented"))
DEFERRED = ("deferred", _("deferred"))
class PlanRating(models.IntegerChoices):
TERRIBLE = 1, _("terrible")
BAD = 2, _("bad")
OK = 3, _("OK")
GOOD = 4, _("good")
EXCELLENT = 5, _("excellent")
class Government(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
public = models.BooleanField(default=False)
jurisdiction = models.ForeignKey(Jurisdiction, null=True, on_delete=models.SET_NULL)
description = models.TextField(blank=True)
start_date = models.DateField(null=True, blank=True)
end_date = models.DateField(null=True, blank=True)
planning_document = models.URLField(blank=True)
class Meta:
verbose_name = _("Government")
verbose_name_plural = _("Governments")
def __str__(self):
return self.name
class CategorizedGovernmentPlan(TaggedItemBase):
tag = models.ForeignKey(
Category, on_delete=models.CASCADE, related_name="categorized_governmentplan"
)
content_object = models.ForeignKey("GovernmentPlan", on_delete=models.CASCADE)
class Meta:
verbose_name = _("Categorized Government Plan")
verbose_name_plural = _("Categorized Government Plans")
class GovernmentPlan(models.Model):
government = models.ForeignKey(Government, on_delete=models.CASCADE)
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
image = FilerImageField(
null=True,
blank=True,
default=None,
verbose_name=_("image"),
on_delete=models.SET_NULL,
)
description = models.TextField(blank=True)
public = models.BooleanField(default=False)
status = models.CharField(
max_length=25, choices=PlanStatus.choices, default="needs_approval"
)
rating = models.IntegerField(choices=PlanRating.choices, null=True, blank=True)
reference = models.CharField(max_length=255, blank=True)
categories = TaggableManager(
through=CategorizedGovernmentPlan, verbose_name=_("categories"), blank=True
)
responsible_publicbody = models.ForeignKey(
PublicBody, null=True, blank=True, on_delete=models.SET_NULL
)
organization = models.ForeignKey(
Organization, null=True, blank=True, on_delete=models.SET_NULL
)
group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.SET_NULL)
class Meta:
ordering = ("reference", "title")
verbose_name = _("Government plan")
verbose_name_plural = _("Government plans")
def __str__(self):
return 'GovernmentPlan "%s" (#%s)' % (self.title, self.pk)
def get_absolute_url(self):
return reverse("govplan:plan", kwargs={
"gov": self.government.slug,
"plan": self.slug
})
def get_absolute_domain_url(self):
return settings.SITE_URL + self.get_absolute_url()
def get_reference_link(self):
if self.reference.startswith("https://"):
return self.reference
return "{}#{}".format(
self.government.planning_document,
self.reference
)
class GovernmentPlanUpdate(models.Model):
plan = models.ForeignKey(
GovernmentPlan, on_delete=models.CASCADE, related_name="updates"
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL
)
organization = models.ForeignKey(
Organization, null=True, blank=True, on_delete=models.SET_NULL
)
timestamp = models.DateTimeField(default=timezone.now)
title = models.CharField(max_length=1024, blank=True)
content = models.TextField(blank=True)
url = models.URLField(blank=True)
status = models.CharField(
max_length=25, choices=PlanStatus.choices, default="", blank=True
)
rating = models.IntegerField(choices=PlanRating.choices, null=True, blank=True)
public = models.BooleanField(default=False)
foirequest = models.ForeignKey(
FoiRequest, null=True, blank=True, on_delete=models.SET_NULL
)
class Meta:
ordering = ("-timestamp",)
get_latest_by = "timestamp"
verbose_name = _("Plan update")
verbose_name_plural = _("Plan updates")
def __str__(self):
return "Plan Update (%s)" % (self.pk,)
if CMSPlugin:
PLUGIN_TEMPLATES = [
("froide_govplan/plugins/default.html", _("Normal")),
]
class GovernmentPlansCMSPlugin(CMSPlugin):
"""
CMS Plugin for displaying latest articles
"""
government = models.ForeignKey(Government, null=True, blank=True, on_delete=models.SET_NULL)
categories = models.ManyToManyField(
Category, verbose_name=_("categories"), blank=True
)
count = models.PositiveIntegerField(
_("number of plans"), default=1, help_text=_("0 means all the plans")
)
offset = models.PositiveIntegerField(
_("offset"),
default=0,
help_text=_("number of plans to skip from top of list"),
)
template = models.CharField(
_("template"),
blank=True,
max_length=250,
choices=PLUGIN_TEMPLATES,
help_text=_("template used to display the plugin"),
)
@property
def render_template(self):
"""
Override render_template to use
the template_to_render attribute
"""
return self.template_to_render
def copy_relations(self, old_instance):
"""
Duplicate ManyToMany relations on plugin copy
"""
self.categories.set(old_instance.categories.all())
def __str__(self):
if self.count == 0:
return str(_("All matching plans"))
return _("%s matching plans") % self.count
def get_plans(self, request, published_only=True):
if (
published_only
or not request
or not getattr(request, "toolbar", False)
or not request.toolbar.edit_mode_active
):
plans = GovernmentPlan.objects.filter(public=True)
else:
plans = GovernmentPlan.objects.all()
filters = {}
if self.government:
filters["government"] = self.government
cat_list = self.categories.all().values_list("id", flat=True)
if cat_list:
filters["categories__in"] = cat_list
plans = plans.filter(**filters).distinct()
plans = plans.prefetch_related(
"categories", "government", "organization"
)
if self.count == 0:
return plans[self.offset:]
return plans[self.offset : self.offset + self.count]

View file

@ -0,0 +1,81 @@
{% extends CMS_TEMPLATE %}
{% load i18n %}
{% load markup %}
{% load cms_tags %}
{% block title %}{{ object.title }}{% endblock %}
{% block app_body %}
<div class="container mt-3 mb-5">
<div class="jumbotron">
<small class="badge badge-light">Vorhaben der {{ object.government.name }}</small>
<h1 class="display-4 mt-0">
{{ object.title }}
</h1>
</div>
{% if object.image %}
{# this should not be a dependency! #}
{% include "fds_blog/includes/_picture.html" with picture=object.image %}
{% endif %}
<p>Status: {{ object.get_status_display }}</p>
{% if object.rating %}
<p>Rating: {{ object.get_rating_display }}</p>
{% endif %}
{{ object.description | safe }}
<p>
<a href="{{ object.get_reference_link }}">Quelle</a>
</p>
{% if object.responsible_publicbody %}
<p>
Federführung:
<a href="{{ object.responsible_publicbody.get_absolute_url }}">
{{ object.responsible_publicbody.name }}
</a>
</p>
{% endif %}
{% if object.organization %}
<p>
Dieses Vorhaben wird beobachtet von
<a href="{{ object.organization.website }}">
{{ object.organization.name }}
</a>
</p>
{% endif %}
<h3>Neueste Entwicklungen</h3>
{% for update in updates %}
<h4>{{ update.title }}</h4>
<time>{{ update.timestamp|date:"DATE_FORMAT" }}</time>
<small>von {% if update.user %}{{ update.user.get_full_name }}{% endif %}{% if update.organization %}({{ update.organization.name }}){% endif %}
</small>
{{ update.content|markdown }}
{% if update.url %}
<a href="{{ update.url }}">Mehr lesen</a>
{% endif %}
{% if update.foirequest %}
<p>
Relevante Anfrage:
<a href="{{ update.foirequest.get_absolute_url }}">
{{ update.foirequest.title }}
</a>
</p>
{% endif %}
{% empty %}
Keine Entwicklungen
{% endfor %}
</div>
{% endblock %}

View file

@ -0,0 +1,9 @@
<ul>
{% for object in object_list %}
<li>
<a href="{{ object.get_absolute_url }}">
{{ object.title }}
</a>
</li>
{% endfor %}
</ul>

14
froide_govplan/urls.py Normal file
View file

@ -0,0 +1,14 @@
from django.urls import path
from django.utils.translation import pgettext_lazy
from .views import GovPlanDetailView
app_name = "govplan"
urlpatterns = [
path(
pgettext_lazy("url part", "<slug:gov>/plan/<slug:plan>/"),
GovPlanDetailView.as_view(),
name="plan",
),
]

18
froide_govplan/views.py Normal file
View file

@ -0,0 +1,18 @@
from django.views.generic import DetailView
from .models import GovernmentPlan
class GovPlanDetailView(DetailView):
slug_url_kwarg = "plan"
template_name = "froide_govplan/detail.html"
def get_queryset(self):
return GovernmentPlan.objects.filter(public=True)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["updates"] = self.object.updates.filter(public=True).order_by(
"-timestamp"
)
return context