├── project
├── __init__.py
├── templates
│ ├── snippets
│ │ └── meta.html
│ ├── header_reduced.html
│ ├── header.html
│ ├── cms
│ │ └── page.html
│ └── base.html
├── asgi.py
├── wsgi.py
├── urls.py
└── settings.py
├── froide_govplan
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── import_govplan.py
├── migrations
│ ├── __init__.py
│ ├── 0014_remove_governmentplansection_content_placeholder.py
│ ├── 0008_governmentplan_proposals.py
│ ├── 0011_governmentplan_properties.py
│ ├── 0010_governmentplanscmsplugin_extra_classes.py
│ ├── 0013_government_active.py
│ ├── 0007_auto_20220314_1422.py
│ ├── 0006_sections_cms_plugin.py
│ ├── 0009_governmentplanupdatescmsplugin.py
│ ├── 0002_auto_20220215_2113.py
│ ├── 0005_governmentplansection.py
│ ├── 0012_government_georegion_alter_government_jurisdiction_and_more.py
│ ├── 0004_auto_20220311_2330.py
│ ├── 0001_initial.py
│ └── 0003_auto_20220228_1051.py
├── templatetags
│ ├── __init__.py
│ └── govplan.py
├── __init__.py
├── conf.py
├── templates
│ ├── froide_govplan
│ │ ├── plugins
│ │ │ ├── default.html
│ │ │ ├── time_used.html
│ │ │ ├── progress.html
│ │ │ ├── progress_row.html
│ │ │ ├── search.html
│ │ │ ├── sections.html
│ │ │ ├── card_cols.html
│ │ │ └── updates.html
│ │ ├── base.html
│ │ ├── admin
│ │ │ ├── accept_proposal.html
│ │ │ └── _proposal.html
│ │ ├── section.html
│ │ └── detail.html
│ └── admin
│ │ └── froide_govplan
│ │ ├── governmentplanupdate
│ │ └── change_form.html
│ │ └── governmentplan
│ │ └── change_form.html
├── cms_apps.py
├── apps.py
├── urls.py
├── auth.py
├── utils.py
├── cms_toolbars.py
├── cms_plugins.py
├── api_views.py
├── configuration.py
├── plan_importer.py
├── views.py
├── forms.py
├── admin.py
├── locale
│ └── de
│ │ └── LC_MESSAGES
│ │ └── django.po
└── models.py
├── .gitignore
├── MANIFEST.in
├── .pre-commit-config.yaml
├── docker-compose.yml
├── manage.py
├── pyproject.toml
├── LICENSE
└── README.md
/project/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/froide_govplan/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/froide_govplan/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/project/templates/snippets/meta.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/froide_govplan/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/froide_govplan/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/froide_govplan/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = "froide_govplan.apps.FroideGovPlanConfig"
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.mo
3 | *.egg-info/
4 | __pycache__
5 | .vscode/
6 | .venv/
7 | postgres_data/
8 | .ruff_cache/
9 |
--------------------------------------------------------------------------------
/project/templates/header_reduced.html:
--------------------------------------------------------------------------------
1 | {% extends "header.html" %}
2 |
3 | {% block nav_search %}{% endblock %}
4 |
5 | {% block nav %}{% endblock nav %}
6 |
--------------------------------------------------------------------------------
/project/templates/header.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md LICENSE
2 | recursive-include froide_govplan/templates *
3 | recursive-include froide_govplan/static *
4 | recursive-include froide_govplan/locale *.po
5 |
--------------------------------------------------------------------------------
/froide_govplan/conf.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 |
3 | GOVPLAN_ENABLE_FOIREQUEST = getattr(settings, "GOVPLAN_ENABLE_FOIREQUEST", True)
4 | GOVPLAN_NAME = getattr(settings, "GOVPLAN_NAME", "GovPlan")
5 |
--------------------------------------------------------------------------------
/project/templates/cms/page.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% load cms_tags %}
4 |
5 | {% block body %}
6 | {% block app_body %}
7 | {% placeholder "content" %}
8 | {% endblock %}
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/froide_govplan/templates/froide_govplan/plugins/default.html:
--------------------------------------------------------------------------------
1 |
8 |
{{ object.title }}
9 |
10 |
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/froide_govplan/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | from django.utils.translation import gettext_lazy as _
3 |
4 |
5 | class FroideGovPlanConfig(AppConfig):
6 | name = "froide_govplan"
7 | verbose_name = _("GovPlan App")
8 |
9 | def ready(self):
10 | from froide.api import api_router
11 | from froide.follow.configuration import follow_registry
12 |
13 | from .api_views import GovernmentPlanViewSet
14 | from .configuration import GovernmentPlanFollowConfiguration
15 |
16 | follow_registry.register(GovernmentPlanFollowConfiguration())
17 |
18 | api_router.register(
19 | r"governmentplan", GovernmentPlanViewSet, basename="governmentplan"
20 | )
21 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == "__main__":
22 | main()
23 |
--------------------------------------------------------------------------------
/froide_govplan/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from django.utils.translation import pgettext_lazy
3 |
4 | from .views import (
5 | GovPlanDetailView,
6 | GovPlanProposeUpdateView,
7 | GovPlanSectionDetailView,
8 | search,
9 | )
10 |
11 | app_name = "govplan"
12 |
13 | urlpatterns = [
14 | path("search/", search, name="search"),
15 | path(
16 | pgettext_lazy("url part", "
/plan//"),
17 | GovPlanDetailView.as_view(),
18 | name="plan",
19 | ),
20 | path(
21 | pgettext_lazy("url part", "/plan//propose-update/"),
22 | GovPlanProposeUpdateView.as_view(),
23 | name="propose_planupdate",
24 | ),
25 | path(
26 | pgettext_lazy("url part", "//"),
27 | GovPlanSectionDetailView.as_view(),
28 | name="section",
29 | ),
30 | ]
31 |
--------------------------------------------------------------------------------
/froide_govplan/templates/froide_govplan/plugins/time_used.html:
--------------------------------------------------------------------------------
1 | {% load govplan %}
2 |
3 | {% load i18n %}
4 |
5 |
6 | {% with gov=instance.government %}
7 |
8 |
9 | {% blocktranslate count days=gov.days_left %}One day left{% plural %}{{ days }} days left{% endblocktranslate %}
10 |
11 |
12 |
18 | {% endwith %}
19 |
20 |
--------------------------------------------------------------------------------
/froide_govplan/auth.py:
--------------------------------------------------------------------------------
1 | from django.db.models import Q
2 |
3 | from .models import GovernmentPlan
4 |
5 |
6 | def has_limited_access(user):
7 | if not user.is_authenticated:
8 | return True
9 | return not user.has_perm("froide_govplan.add_governmentplan")
10 |
11 |
12 | def get_allowed_plans(request):
13 | if not has_limited_access(request.user):
14 | return GovernmentPlan.objects.all()
15 | groups = request.user.groups.all()
16 | return GovernmentPlan.objects.filter(group__in=groups).distinct()
17 |
18 |
19 | def get_visible_plans(request):
20 | if not has_limited_access(request.user):
21 | return GovernmentPlan.objects.all()
22 | if request.user.is_authenticated:
23 | groups = request.user.groups.all()
24 | return GovernmentPlan.objects.filter(
25 | Q(public=True) | Q(group__in=groups)
26 | ).distinct()
27 | return GovernmentPlan.objects.filter(public=True)
28 |
--------------------------------------------------------------------------------
/froide_govplan/templates/froide_govplan/plugins/progress.html:
--------------------------------------------------------------------------------
1 | {% load govplan %}
2 |
3 | {% get_plan_progress object_list as progress %}
4 |
5 |
6 |
{{ progress.count }} Vorhaben
7 |
8 |
9 | {% for section in progress.sections %}
10 |
11 |
12 | {% endfor %}
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "froide_govplan"
3 | readme = "README.md"
4 | description = ""
5 | license = { file = "LICENSE" }
6 | requires-python = ">=3.10"
7 | classifiers = [
8 | "Development Status :: 5 - Production/Stable",
9 | "Framework :: Django",
10 | "Intended Audience :: Developers",
11 | "License :: OSI Approved :: MIT License",
12 | "Operating System :: OS Independent",
13 | "Programming Language :: Python",
14 | "Programming Language :: Python :: 3.10",
15 | "Programming Language :: Python :: 3.12",
16 | "Topic :: Utilities",
17 | ]
18 | version = "0.0.1"
19 | dependencies = [
20 | "django-cms",
21 | "djangocms-alias",
22 | "django-filer",
23 | "psycopg[binary]",
24 | "django-admin-sortable2",
25 | "nh3",
26 | "django-tinymce",
27 | "django-oauth-toolkit",
28 | "django-mfa3",
29 | ]
30 |
31 | [build-system]
32 | requires = ["setuptools"]
33 | build-backend = "setuptools.build_meta"
34 |
35 | [tool.setuptools.packages.find]
36 | include = ["froide_govplan*"]
37 |
--------------------------------------------------------------------------------
/froide_govplan/templates/froide_govplan/plugins/progress_row.html:
--------------------------------------------------------------------------------
1 | {% load govplan %}
2 |
3 | {% get_plan_progress object_list as progress %}
4 |
5 |
6 |
7 | {{ progress.count }} Vorhaben
8 |
9 |
10 |
11 | {% for section in progress.sections %}
12 |
13 |
14 | {% endfor %}
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/froide_govplan/management/commands/import_govplan.py:
--------------------------------------------------------------------------------
1 | import csv
2 | import json
3 |
4 | from django.core.management.base import BaseCommand
5 |
6 | from ...models import Government
7 | from ...plan_importer import PlanImporter
8 |
9 |
10 | class Command(BaseCommand):
11 | help = "Loads public bodies"
12 |
13 | def add_arguments(self, parser):
14 | parser.add_argument("government", type=str)
15 | parser.add_argument("json_mapping", type=str)
16 | parser.add_argument("filename", type=str)
17 |
18 | def handle(self, *args, **options):
19 | government = Government.objects.get(slug=options["government"])
20 |
21 | with open(options["json_mapping"]) as f:
22 | col_mapping = json.load(f)
23 |
24 | importer = PlanImporter(government, col_mapping=col_mapping)
25 |
26 | filename = options["filename"]
27 | with open(filename) as csv_file:
28 | reader = csv.DictReader(csv_file)
29 | importer.import_rows(reader)
30 |
31 | self.stdout.write("Import done.\n")
32 |
--------------------------------------------------------------------------------
/froide_govplan/utils.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import quote, urlencode
2 |
3 | from django.conf import settings
4 | from django.urls import reverse
5 |
6 | from . import conf
7 |
8 |
9 | def make_request_url(plan, publicbody):
10 | pb_slug = publicbody.slug
11 | url = reverse("foirequest-make_request", kwargs={"publicbody_slug": pb_slug})
12 | subject = "Stand des Regierungsvorhabens „{}“".format(plan.title)
13 | if len(subject) > 250:
14 | subject = subject[:250] + "..."
15 | body = "Dokumente, die den Stand des Regierungsvorhabens zum Thema {} (siehe Koalitionsvertrag), dokumentieren.".format(
16 | plan.title
17 | )
18 | query = {
19 | "subject": subject.encode("utf-8"),
20 | "body": body,
21 | "ref": plan.get_foirequest_reference(),
22 | "tags": conf.GOVPLAN_NAME,
23 | }
24 |
25 | hide_features = ["hide_public", "hide_similar", "hide_draft"]
26 |
27 | query.update({f: b"1" for f in hide_features})
28 | query = urlencode(query, quote_via=quote)
29 | return "%s%s?%s" % (settings.SITE_URL, url, query)
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2022 FragDenStaat.de / Stefan Wehrmeyer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/project/urls.py:
--------------------------------------------------------------------------------
1 | """project URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/4.1/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.conf import settings
17 | from django.conf.urls.static import static
18 | from django.contrib import admin
19 | from django.urls import include, path
20 |
21 | urlpatterns = [
22 | path(
23 | "follow/",
24 | include("froide.follow.urls", namespace="follow"),
25 | ),
26 | path("admin/", admin.site.urls),
27 | path("", include("cms.urls")),
28 | ]
29 |
30 | if settings.DEBUG:
31 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
32 |
--------------------------------------------------------------------------------
/froide_govplan/migrations/0007_auto_20220314_1422.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.12 on 2022-03-14 13:22
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("froide_govplan", "0006_sections_cms_plugin"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name="governmentplanscmsplugin",
15 | name="template",
16 | field=models.CharField(
17 | blank=True,
18 | choices=[
19 | ("froide_govplan/plugins/default.html", "Normal"),
20 | ("froide_govplan/plugins/progress.html", "Progress"),
21 | ("froide_govplan/plugins/card_cols.html", "Card columns"),
22 | ("froide_govplan/plugins/search.html", "Search"),
23 | ],
24 | help_text="template used to display the plugin",
25 | max_length=250,
26 | verbose_name="template",
27 | ),
28 | ),
29 | migrations.AlterField(
30 | model_name="governmentplanupdate",
31 | name="url",
32 | field=models.URLField(blank=True, max_length=1024, verbose_name="URL"),
33 | ),
34 | ]
35 |
--------------------------------------------------------------------------------
/froide_govplan/cms_toolbars.py:
--------------------------------------------------------------------------------
1 | from django.urls import reverse
2 | from django.utils.translation import gettext_lazy as _
3 |
4 | from cms.toolbar_base import CMSToolbar
5 | from cms.toolbar_pool import toolbar_pool
6 |
7 | from . import conf
8 |
9 |
10 | class GovPlanToolbar(CMSToolbar):
11 | def populate(self):
12 | if not self.is_current_app:
13 | return
14 | menu = self.toolbar.get_or_create_menu("govplan-menu", conf.GOVPLAN_NAME)
15 |
16 | url = reverse(
17 | "admin:app_list",
18 | kwargs={"app_label": "froide_govplan"},
19 | current_app="govplanadmin",
20 | )
21 | menu.add_link_item(_("Edit plans and updates"), url=url)
22 |
23 | if hasattr(self.request, "govplan"):
24 | govplan = self.request.govplan
25 | url = reverse(
26 | "admin:froide_govplan_governmentplan_change",
27 | args=(govplan.pk,),
28 | current_app="govplanadmin",
29 | )
30 | menu.add_link_item(_("Edit government plan"), url=url)
31 | url = reverse(
32 | "admin:froide_govplan_governmentplanupdate_add",
33 | current_app="govplanadmin",
34 | )
35 | url = "{}?plan={}".format(url, govplan.id)
36 | menu.add_link_item(_("Add update"), url=url)
37 |
38 |
39 | toolbar_pool.register(GovPlanToolbar)
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Froide GovPlan
2 |
3 | A Django app that allows tracking government plans. Deployed at: https://fragdenstaat.de/koalitionstracker/
4 |
5 |
6 | ## Install stand-alone
7 |
8 | Requires [GDAL/Geos for GeoDjango](https://docs.djangoproject.com/en/4.1/ref/contrib/gis/install/geolibs/).
9 |
10 | ```bash
11 | # Start a Postgres server with Postgis
12 | docker compose up -d
13 | # Setup virtualenv
14 | python3 -m venv .venv
15 | source .venv/bin/activate
16 | # Install dependencies
17 | pip install -e git+https://github.com/okfde/froide.git@main#egg=froide
18 | pip install -e .
19 | # Setup initial database
20 | ./manage.py migrate
21 | # Create admin user
22 | ./manage.py createsuperuser
23 | # Start development server
24 | ./manage.py runserver
25 | ```
26 |
27 |
28 | 1. Go to http://localhost:8000/admin/
29 | 2. Setup a homepage in the CMS: http://localhost:8000/admin/cms/page/
30 | 3. Setup a page (could be the homepage) and then choose under advanced setting the Govplan app as application.
31 | 4. Publish that page
32 | 5. Setup a government and plans via the admin.
33 |
34 | ## Possible next steps
35 |
36 | - Use the `project` directory as a blueprint for an app that uses this repo as a depdency.
37 | - Setup [djangocms-text-ckeditor](https://github.com/django-cms/djangocms-text-ckeditor), [djangocms-frontend](https://github.com/django-cms/djangocms-frontend) and other CMS components/
38 | - Use Django apps for social authentication.
39 | - Override templates in your custom project.
40 |
--------------------------------------------------------------------------------
/froide_govplan/templatetags/govplan.py:
--------------------------------------------------------------------------------
1 | from django import template
2 |
3 | from froide_govplan.models import STATUS_CSS, PlanStatus
4 |
5 | register = template.Library()
6 |
7 |
8 | PROGRESS_ORDER = [
9 | PlanStatus.IMPLEMENTED,
10 | PlanStatus.PARTIALLY_IMPLEMENTED,
11 | PlanStatus.STARTED,
12 | PlanStatus.NOT_STARTED,
13 | PlanStatus.DEFERRED,
14 | ]
15 |
16 |
17 | @register.simple_tag
18 | def get_plan_progress(object_list):
19 | sections = []
20 | for value in PROGRESS_ORDER:
21 | label = value.label
22 | value = str(value)
23 | status_count = len([x for x in object_list if x.status == value])
24 | percentage = (
25 | 0 if len(object_list) == 0 else status_count / len(object_list) * 100
26 | )
27 | sections.append(
28 | {
29 | "count": status_count,
30 | "name": str(value),
31 | "label": label,
32 | "css_class": STATUS_CSS[value],
33 | "percentage": round(percentage),
34 | "css_percentage": str(percentage),
35 | }
36 | )
37 |
38 | value, label
39 | return {"count": len(object_list), "sections": sections}
40 |
41 |
42 | @register.inclusion_tag("froide_govplan/plugins/progress.html")
43 | def get_section_progress(section):
44 | return {"object_list": section.get_plans()}
45 |
46 |
47 | @register.filter
48 | def addquotes(text):
49 | return f"„{str.strip(text)}“"
50 |
--------------------------------------------------------------------------------
/froide_govplan/templates/froide_govplan/plugins/search.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
19 |
20 |
34 |
--------------------------------------------------------------------------------
/froide_govplan/migrations/0006_sections_cms_plugin.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.12 on 2022-03-14 12:10
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ("cms", "0022_auto_20180620_1551"),
11 | ("froide_govplan", "0005_governmentplansection"),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name="GovernmentPlanSectionsCMSPlugin",
17 | fields=[
18 | (
19 | "cmsplugin_ptr",
20 | models.OneToOneField(
21 | auto_created=True,
22 | on_delete=django.db.models.deletion.CASCADE,
23 | parent_link=True,
24 | primary_key=True,
25 | related_name="froide_govplan_governmentplansectionscmsplugin",
26 | serialize=False,
27 | to="cms.cmsplugin",
28 | ),
29 | ),
30 | (
31 | "government",
32 | models.ForeignKey(
33 | blank=True,
34 | null=True,
35 | on_delete=django.db.models.deletion.SET_NULL,
36 | to="froide_govplan.government",
37 | ),
38 | ),
39 | ],
40 | options={
41 | "abstract": False,
42 | },
43 | bases=("cms.cmsplugin",),
44 | ),
45 | ]
46 |
--------------------------------------------------------------------------------
/froide_govplan/templates/froide_govplan/plugins/sections.html:
--------------------------------------------------------------------------------
1 | {% load thumbnail %}
2 | {% load govplan %}
3 |
12 |
13 | {% for section in sections %}
14 |
35 | {% endfor %}
36 |
37 |
--------------------------------------------------------------------------------
/project/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}{% load static %}{% load cms_tags %}{% load sekizai_tags %}
2 |
3 |
4 |
5 |
6 |
7 |
8 | {% block title %}{% page_attribute "page_title" %} - {{ SITE_NAME }}{% endblock %}
9 |
10 | {% block header_font %}{% endblock %}
11 |
12 | {% block css %}
13 |
14 | {% block extra_css %}
15 | {% endblock %}
16 | {% endblock %}
17 | {% render_block "css" %}
18 |
19 | {% block meta %}
20 | {# Translators: meta description #}
21 | {% endblock %}
22 |
23 | {% block extra_head %}
24 | {% endblock %}
25 |
26 |
27 | {% cms_toolbar %}
28 | {% block body_tag %}{% block body %}{% block main %}{% endblock %}{% endblock %}{% endblock %}
29 | {% block extra_footer %}{% endblock %}
30 |
31 | {% block scripts %}
32 |
33 | {% endblock %}
34 | {% render_block "js" %}
35 |
36 | {% block below_scripts %}{% endblock %}
37 |
38 |
39 |
--------------------------------------------------------------------------------
/froide_govplan/templates/froide_govplan/plugins/card_cols.html:
--------------------------------------------------------------------------------
1 | {% load thumbnail %}
2 | {% load i18n %}
3 | {% load govplan %}
4 | {% if object_list|length == 0 %}
5 | {% trans "Could not find any results. Try different keywords or browse the categories." %}
6 | {% else %}
7 |
8 | {% for object in object_list %}
9 |
33 | {% endfor %}
34 |
35 | {% endif %}
36 |
--------------------------------------------------------------------------------
/froide_govplan/templates/froide_govplan/admin/_proposal.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load form_helper %}
3 | {% load permission_helper %}
4 |
5 |
78 |
--------------------------------------------------------------------------------
/froide_govplan/cms_plugins.py:
--------------------------------------------------------------------------------
1 | from django.utils.translation import gettext_lazy as _
2 |
3 | from cms.plugin_base import CMSPluginBase
4 | from cms.plugin_pool import plugin_pool
5 |
6 | from .models import (
7 | PLUGIN_TEMPLATES,
8 | GovernmentPlansCMSPlugin,
9 | GovernmentPlanSection,
10 | GovernmentPlanSectionsCMSPlugin,
11 | GovernmentPlanUpdatesCMSPlugin,
12 | PlanStatus,
13 | )
14 |
15 |
16 | @plugin_pool.register_plugin
17 | class GovernmentPlansPlugin(CMSPluginBase):
18 | name = _("Government plans")
19 | model = GovernmentPlansCMSPlugin
20 | filter_horizontal = ("categories",)
21 | cache = True
22 |
23 | def get_render_template(self, context, instance, placeholder):
24 | return instance.template or PLUGIN_TEMPLATES[0][0]
25 |
26 | def render(self, context, instance, placeholder):
27 | context = super().render(context, instance, placeholder)
28 | context["plugin"] = instance
29 | context["status_list"] = PlanStatus.choices
30 | context["object_list"] = instance.get_plans(
31 | context["request"], published_only=False
32 | )
33 | return context
34 |
35 |
36 | @plugin_pool.register_plugin
37 | class GovernmentPlanSectionsPlugin(CMSPluginBase):
38 | name = _("Government plan sections")
39 | model = GovernmentPlanSectionsCMSPlugin
40 | render_template = "froide_govplan/plugins/sections.html"
41 |
42 | def render(self, context, instance, placeholder):
43 | context = super().render(context, instance, placeholder)
44 |
45 | if instance.government_id:
46 | sections = GovernmentPlanSection.objects.filter(
47 | government_id=instance.government_id
48 | )
49 | else:
50 | sections = GovernmentPlanSection.objects.all()
51 |
52 | sections = sections.select_related("government")
53 |
54 | context["sections"] = sections
55 |
56 | return context
57 |
58 |
59 | @plugin_pool.register_plugin
60 | class GovernmentPlanUpdatesPlugin(CMSPluginBase):
61 | name = _("Government plan updates")
62 | model = GovernmentPlanUpdatesCMSPlugin
63 | render_template = "froide_govplan/plugins/updates.html"
64 |
65 | def render(self, context, instance, placeholder):
66 | context = super().render(context, instance, placeholder)
67 | context["updates"] = instance.get_updates(
68 | context["request"], published_only=False
69 | )
70 | context["show_context"] = True
71 | return context
72 |
--------------------------------------------------------------------------------
/froide_govplan/migrations/0009_governmentplanupdatescmsplugin.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.12 on 2022-03-17 14:54
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ("cms", "0022_auto_20180620_1551"),
11 | ("publicbody", "0039_publicbody_alternative_emails"),
12 | ("froide_govplan", "0008_governmentplan_proposals"),
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name="GovernmentPlanUpdatesCMSPlugin",
18 | fields=[
19 | (
20 | "cmsplugin_ptr",
21 | models.OneToOneField(
22 | auto_created=True,
23 | on_delete=django.db.models.deletion.CASCADE,
24 | parent_link=True,
25 | primary_key=True,
26 | related_name="froide_govplan_governmentplanupdatescmsplugin",
27 | serialize=False,
28 | to="cms.cmsplugin",
29 | ),
30 | ),
31 | (
32 | "count",
33 | models.PositiveIntegerField(
34 | default=1,
35 | help_text="0 means all the updates",
36 | verbose_name="number of updates",
37 | ),
38 | ),
39 | (
40 | "offset",
41 | models.PositiveIntegerField(
42 | default=0,
43 | help_text="number of updates to skip from top of list",
44 | verbose_name="offset",
45 | ),
46 | ),
47 | (
48 | "categories",
49 | models.ManyToManyField(
50 | blank=True, to="publicbody.Category", verbose_name="categories"
51 | ),
52 | ),
53 | (
54 | "government",
55 | models.ForeignKey(
56 | blank=True,
57 | null=True,
58 | on_delete=django.db.models.deletion.SET_NULL,
59 | to="froide_govplan.government",
60 | ),
61 | ),
62 | ],
63 | options={
64 | "abstract": False,
65 | },
66 | bases=("cms.cmsplugin",),
67 | ),
68 | ]
69 |
--------------------------------------------------------------------------------
/froide_govplan/api_views.py:
--------------------------------------------------------------------------------
1 | from django_filters import rest_framework as filters
2 | from rest_framework import serializers, viewsets
3 |
4 | from .models import Government, GovernmentPlan, GovernmentPlanUpdate
5 |
6 |
7 | class GovernmentSerializer(serializers.ModelSerializer):
8 | class Meta:
9 | model = Government
10 | fields = ("id", "name", "slug", "start_date", "end_date", "planning_document")
11 |
12 |
13 | class GovernmentPlanSerializer(serializers.ModelSerializer):
14 | site_url = serializers.CharField(source="get_absolute_domain_url")
15 | updates = serializers.SerializerMethodField()
16 |
17 | class Meta:
18 | model = GovernmentPlan
19 | fields = (
20 | "id",
21 | "site_url",
22 | "government",
23 | "title",
24 | "slug",
25 | "description",
26 | "quote",
27 | "due_date",
28 | "measure",
29 | "status",
30 | "rating",
31 | "properties",
32 | "updates",
33 | )
34 |
35 | def get_updates(self, obj):
36 | return GovernmentPlanUpdateSerializer(
37 | obj.updates.all(), read_only=True, many=True, context=self.context
38 | ).data
39 |
40 |
41 | class GovernmentPlanUpdateSerializer(serializers.ModelSerializer):
42 | site_url = serializers.CharField(source="get_absolute_domain_url")
43 |
44 | class Meta:
45 | model = GovernmentPlanUpdate
46 | fields = (
47 | "timestamp",
48 | "title",
49 | "content",
50 | "site_url",
51 | "url",
52 | "status",
53 | "rating",
54 | )
55 |
56 |
57 | class GovernmentPlanFilter(filters.FilterSet):
58 | government = filters.ModelChoiceFilter(
59 | queryset=Government.objects.filter(public=True)
60 | )
61 | properties = filters.CharFilter(method="properties_filter")
62 |
63 | class Meta:
64 | model = GovernmentPlan
65 | fields = (
66 | "government",
67 | "status",
68 | "rating",
69 | "properties",
70 | )
71 |
72 | def properties_filter(self, queryset, name, value):
73 | try:
74 | key, value = value.split(":", 1)
75 | except ValueError:
76 | return queryset.filter(properties__has_key=value)
77 |
78 | return queryset.filter(**{"properties__%s__contains" % key: value})
79 |
80 |
81 | class GovernmentPlanViewSet(viewsets.ReadOnlyModelViewSet):
82 | serializer_class = GovernmentPlanSerializer
83 | filterset_class = GovernmentPlanFilter
84 |
85 | def get_queryset(self):
86 | return (
87 | GovernmentPlan.objects.filter(public=True)
88 | .select_related("government")
89 | .prefetch_related("updates")
90 | )
91 |
--------------------------------------------------------------------------------
/froide_govplan/templates/froide_govplan/section.html:
--------------------------------------------------------------------------------
1 | {% extends "froide_govplan/base.html" %}
2 | {% load i18n %}
3 | {% load markup %}
4 | {% load cms_tags %}
5 | {% load follow_tags %}
6 | {% load thumbnail %}
7 | {% block title %}
8 | {{ object.title }}
9 | {% endblock title %}
10 | {% block meta %}
11 | {% include "snippets/meta.html" %}
12 | {% endblock meta %}
13 | {% block app_body %}
14 |
15 |
16 |
17 |
18 |
19 |
20 | {% if not government.active %}
21 |
22 | Dieser Themenbereich gehörte zur {{ government.name }} und wird nicht mehr aktualisiert.
23 |
24 | {% endif %}
25 |
{{ object.title }}
26 |
27 | {% blocktranslate with section=object.title %}
28 | Here you can find all plans of the section “{{ section }}”, which the coalition constituted in their agreement. On the curresponding detail pages, you'll get more information, stay up to date with news or submit changes.
29 | {% endblocktranslate %}
30 |
31 |
32 |
33 |
34 | {% include "froide_govplan/plugins/progress_row.html" with object_list=plans %}
35 |
36 | nicht begonnen
37 | begonnen
38 | teilweise umgesetzt
39 | umgesetzt
40 | verschoben
41 |
42 | {% include "froide_govplan/plugins/time_used.html" with instance=object %}
43 |
44 |
45 |
46 |
47 | {% include "froide_govplan/plugins/card_cols.html" with object_list=plans %}
48 |
49 | {% endblock app_body %}
50 |
--------------------------------------------------------------------------------
/froide_govplan/configuration.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from typing import Iterator
3 |
4 | from django.utils.translation import gettext_lazy as _
5 | from froide.follow.configuration import FollowConfiguration
6 | from froide.helper.notifications import Notification, TemplatedEvent
7 |
8 | from .admin import has_limited_access
9 | from .models import GovernmentPlan, GovernmentPlanFollower, GovernmentPlanUpdate
10 |
11 |
12 | class GovernmentPlanFollowConfiguration(FollowConfiguration):
13 | model = GovernmentPlanFollower
14 | title: str = _("Government plans")
15 | slug: str = "govplan"
16 | follow_message: str = _("You are now following this plan.")
17 | unfollow_message: str = _("You are not following this plan anymore.")
18 | confirm_email_message: str = _(
19 | "Check your emails and click the confirmation link in order to follow this government plan."
20 | )
21 | action_labels = {
22 | "follow": _("Follow plan"),
23 | "follow_q": _("Follow plan?"),
24 | "unfollow": _("Unfollow plan"),
25 | "following": _("Following plan"),
26 | "follow_description": _(
27 | "You will get notifications via email when something new happens with this plan. You can unsubscribe anytime."
28 | ),
29 | }
30 |
31 | def get_content_object_queryset(self, request):
32 | if has_limited_access(request.user):
33 | return GovernmentPlan.objects.filter(public=True)
34 | return GovernmentPlan.objects.all()
35 |
36 | def can_follow(self, content_object, user, request=None):
37 | return (
38 | content_object.government.active
39 | and content_object.public
40 | or not has_limited_access(user)
41 | )
42 |
43 | def get_batch_updates(
44 | self, start: datetime, end: datetime
45 | ) -> Iterator[Notification]:
46 | yield from get_plan_updates(start, end)
47 |
48 | def get_confirm_follow_message(self, content_object):
49 | return _(
50 | "please confirm that you want to follow the plan “{title}” by clicking this link:"
51 | ).format(title=content_object.title)
52 |
53 | def email_changed(self, user):
54 | # Move all confirmed email subscriptions of new email
55 | # to user except own requests
56 | self.model.objects.filter(email=user.email, confirmed=True).update(
57 | email="", user=user
58 | )
59 |
60 |
61 | def get_plan_updates(start: datetime, end: datetime):
62 | plan_updates = GovernmentPlanUpdate.objects.filter(
63 | public=True, timestamp__gte=start, timestamp__lt=end
64 | ).select_related("plan")
65 |
66 | for plan_update in plan_updates:
67 | yield Notification(
68 | section=_("Government Plans"),
69 | event_type="planupdate",
70 | object=plan_update.plan,
71 | object_label=plan_update.plan.title,
72 | timestamp=plan_update.timestamp,
73 | event=make_plan_event(plan_update.plan),
74 | user_id=None,
75 | )
76 |
77 |
78 | def make_plan_event(plan):
79 | return TemplatedEvent(
80 | _("An update was posted for the government plan “{title}”."),
81 | title=plan.title,
82 | )
83 |
--------------------------------------------------------------------------------
/froide_govplan/templates/froide_govplan/plugins/updates.html:
--------------------------------------------------------------------------------
1 | {% load markup %}
2 | {% for update in updates %}
3 | {% if wrapper_classes %}{% endif %}
62 | {% endfor %}
63 |
--------------------------------------------------------------------------------
/froide_govplan/migrations/0002_auto_20220215_2113.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.8 on 2022-02-15 20:13
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ("publicbody", "0039_publicbody_alternative_emails"),
11 | ("organization", "0001_initial"),
12 | ("cms", "0022_auto_20180620_1551"),
13 | ("froide_govplan", "0001_initial"),
14 | ]
15 |
16 | operations = [
17 | migrations.AddField(
18 | model_name="governmentplan",
19 | name="organization",
20 | field=models.ForeignKey(
21 | blank=True,
22 | null=True,
23 | on_delete=django.db.models.deletion.SET_NULL,
24 | to="organization.organization",
25 | ),
26 | ),
27 | migrations.AddField(
28 | model_name="governmentplanupdate",
29 | name="url",
30 | field=models.URLField(blank=True),
31 | ),
32 | migrations.CreateModel(
33 | name="GovernmentPlansCMSPlugin",
34 | fields=[
35 | (
36 | "cmsplugin_ptr",
37 | models.OneToOneField(
38 | auto_created=True,
39 | on_delete=django.db.models.deletion.CASCADE,
40 | parent_link=True,
41 | primary_key=True,
42 | related_name="froide_govplan_governmentplanscmsplugin",
43 | serialize=False,
44 | to="cms.cmsplugin",
45 | ),
46 | ),
47 | (
48 | "count",
49 | models.PositiveIntegerField(
50 | default=1,
51 | help_text="0 means all the plans",
52 | verbose_name="number of plans",
53 | ),
54 | ),
55 | (
56 | "offset",
57 | models.PositiveIntegerField(
58 | default=0,
59 | help_text="number of plans to skip from top of list",
60 | verbose_name="offset",
61 | ),
62 | ),
63 | (
64 | "template",
65 | models.CharField(
66 | blank=True,
67 | choices=[("froide_govplan/plugins/default.html", "Normal")],
68 | help_text="template used to display the plugin",
69 | max_length=250,
70 | verbose_name="template",
71 | ),
72 | ),
73 | (
74 | "categories",
75 | models.ManyToManyField(
76 | blank=True, to="publicbody.Category", verbose_name="categories"
77 | ),
78 | ),
79 | (
80 | "government",
81 | models.ForeignKey(
82 | blank=True,
83 | null=True,
84 | on_delete=django.db.models.deletion.SET_NULL,
85 | to="froide_govplan.government",
86 | ),
87 | ),
88 | ],
89 | options={
90 | "abstract": False,
91 | },
92 | bases=("cms.cmsplugin",),
93 | ),
94 | ]
95 |
--------------------------------------------------------------------------------
/froide_govplan/migrations/0005_governmentplansection.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.12 on 2022-03-14 10:14
2 |
3 | import django.db.models.deletion
4 | from django.conf import settings
5 | from django.db import migrations, models
6 |
7 | import cms.models.fields
8 | import filer.fields.image
9 |
10 |
11 | class Migration(migrations.Migration):
12 |
13 | dependencies = [
14 | ("publicbody", "0039_publicbody_alternative_emails"),
15 | migrations.swappable_dependency(settings.FILER_IMAGE_MODEL),
16 | ("cms", "0022_auto_20180620_1551"),
17 | ("froide_govplan", "0004_auto_20220311_2330"),
18 | ]
19 |
20 | operations = [
21 | migrations.CreateModel(
22 | name="GovernmentPlanSection",
23 | fields=[
24 | (
25 | "id",
26 | models.AutoField(
27 | auto_created=True,
28 | primary_key=True,
29 | serialize=False,
30 | verbose_name="ID",
31 | ),
32 | ),
33 | ("title", models.CharField(max_length=255, verbose_name="title")),
34 | (
35 | "slug",
36 | models.SlugField(max_length=255, unique=True, verbose_name="slug"),
37 | ),
38 | (
39 | "description",
40 | models.TextField(blank=True, verbose_name="description"),
41 | ),
42 | (
43 | "icon",
44 | models.CharField(
45 | blank=True,
46 | help_text='Enter an icon name from the FontAwesome 4 icon set ',
47 | max_length=50,
48 | verbose_name="Icon",
49 | ),
50 | ),
51 | ("order", models.PositiveIntegerField(default=0)),
52 | ("featured", models.DateTimeField(blank=True, null=True)),
53 | (
54 | "categories",
55 | models.ManyToManyField(blank=True, to="publicbody.Category"),
56 | ),
57 | (
58 | "content_placeholder",
59 | cms.models.fields.PlaceholderField(
60 | editable=False,
61 | null=True,
62 | on_delete=django.db.models.deletion.CASCADE,
63 | slotname="content",
64 | to="cms.placeholder",
65 | ),
66 | ),
67 | (
68 | "government",
69 | models.ForeignKey(
70 | on_delete=django.db.models.deletion.CASCADE,
71 | to="froide_govplan.government",
72 | verbose_name="government",
73 | ),
74 | ),
75 | (
76 | "image",
77 | filer.fields.image.FilerImageField(
78 | blank=True,
79 | default=None,
80 | null=True,
81 | on_delete=django.db.models.deletion.SET_NULL,
82 | to=settings.FILER_IMAGE_MODEL,
83 | verbose_name="image",
84 | ),
85 | ),
86 | ],
87 | options={
88 | "verbose_name": "Government plan section",
89 | "verbose_name_plural": "Government plan sections",
90 | "ordering": ("order", "title"),
91 | },
92 | ),
93 | ]
94 |
--------------------------------------------------------------------------------
/froide_govplan/migrations/0012_government_georegion_alter_government_jurisdiction_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.4 on 2023-02-03 10:00
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ("cms", "0022_auto_20180620_1551"),
11 | ("publicbody", "0043_merge_20221019_1020"),
12 | ("georegion", "0011_georegion_invalid_on"),
13 | ("froide_govplan", "0011_governmentplan_properties"),
14 | ]
15 |
16 | operations = [
17 | migrations.AddField(
18 | model_name="government",
19 | name="georegion",
20 | field=models.ForeignKey(
21 | blank=True,
22 | null=True,
23 | on_delete=django.db.models.deletion.SET_NULL,
24 | to="georegion.georegion",
25 | verbose_name="georegion",
26 | ),
27 | ),
28 | migrations.AlterField(
29 | model_name="government",
30 | name="jurisdiction",
31 | field=models.ForeignKey(
32 | blank=True,
33 | null=True,
34 | on_delete=django.db.models.deletion.SET_NULL,
35 | to="publicbody.jurisdiction",
36 | verbose_name="jurisdiction",
37 | ),
38 | ),
39 | migrations.AlterField(
40 | model_name="governmentplanscmsplugin",
41 | name="cmsplugin_ptr",
42 | field=models.OneToOneField(
43 | auto_created=True,
44 | on_delete=django.db.models.deletion.CASCADE,
45 | parent_link=True,
46 | primary_key=True,
47 | related_name="%(app_label)s_%(class)s",
48 | serialize=False,
49 | to="cms.cmsplugin",
50 | ),
51 | ),
52 | migrations.AlterField(
53 | model_name="governmentplanscmsplugin",
54 | name="template",
55 | field=models.CharField(
56 | blank=True,
57 | choices=[
58 | ("froide_govplan/plugins/default.html", "Normal"),
59 | ("froide_govplan/plugins/progress.html", "Progress"),
60 | ("froide_govplan/plugins/progress_row.html", "Progress Row"),
61 | ("froide_govplan/plugins/time_used.html", "Time used"),
62 | ("froide_govplan/plugins/card_cols.html", "Card columns"),
63 | ("froide_govplan/plugins/search.html", "Search"),
64 | ],
65 | help_text="template used to display the plugin",
66 | max_length=250,
67 | verbose_name="template",
68 | ),
69 | ),
70 | migrations.AlterField(
71 | model_name="governmentplansectionscmsplugin",
72 | name="cmsplugin_ptr",
73 | field=models.OneToOneField(
74 | auto_created=True,
75 | on_delete=django.db.models.deletion.CASCADE,
76 | parent_link=True,
77 | primary_key=True,
78 | related_name="%(app_label)s_%(class)s",
79 | serialize=False,
80 | to="cms.cmsplugin",
81 | ),
82 | ),
83 | migrations.AlterField(
84 | model_name="governmentplanupdatescmsplugin",
85 | name="cmsplugin_ptr",
86 | field=models.OneToOneField(
87 | auto_created=True,
88 | on_delete=django.db.models.deletion.CASCADE,
89 | parent_link=True,
90 | primary_key=True,
91 | related_name="%(app_label)s_%(class)s",
92 | serialize=False,
93 | to="cms.cmsplugin",
94 | ),
95 | ),
96 | ]
97 |
--------------------------------------------------------------------------------
/froide_govplan/migrations/0004_auto_20220311_2330.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.12 on 2022-03-11 22:30
2 |
3 | import django.db.models.deletion
4 | import django.utils.timezone
5 | from django.conf import settings
6 | from django.db import migrations, models
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13 | ("froide_govplan", "0003_auto_20220228_1051"),
14 | ]
15 |
16 | operations = [
17 | migrations.AlterField(
18 | model_name="governmentplanscmsplugin",
19 | name="template",
20 | field=models.CharField(
21 | blank=True,
22 | choices=[
23 | ("froide_govplan/plugins/default.html", "Normal"),
24 | ("froide_govplan/plugins/progress.html", "Progress"),
25 | ("froide_govplan/plugins/card_cols.html", "Card columns"),
26 | ],
27 | help_text="template used to display the plugin",
28 | max_length=250,
29 | verbose_name="template",
30 | ),
31 | ),
32 | migrations.CreateModel(
33 | name="GovernmentPlanFollower",
34 | fields=[
35 | (
36 | "id",
37 | models.AutoField(
38 | auto_created=True,
39 | primary_key=True,
40 | serialize=False,
41 | verbose_name="ID",
42 | ),
43 | ),
44 | ("email", models.CharField(blank=True, max_length=255)),
45 | ("confirmed", models.BooleanField(default=False)),
46 | (
47 | "timestamp",
48 | models.DateTimeField(
49 | default=django.utils.timezone.now,
50 | verbose_name="Timestamp of Following",
51 | ),
52 | ),
53 | ("context", models.JSONField(blank=True, null=True)),
54 | (
55 | "content_object",
56 | models.ForeignKey(
57 | on_delete=django.db.models.deletion.CASCADE,
58 | related_name="followers",
59 | to="froide_govplan.governmentplan",
60 | verbose_name="Government plan",
61 | ),
62 | ),
63 | (
64 | "user",
65 | models.ForeignKey(
66 | blank=True,
67 | null=True,
68 | on_delete=django.db.models.deletion.CASCADE,
69 | to=settings.AUTH_USER_MODEL,
70 | verbose_name="User",
71 | ),
72 | ),
73 | ],
74 | options={
75 | "verbose_name": "Government plan follower",
76 | "verbose_name_plural": "Government plan followers",
77 | "ordering": ("-timestamp",),
78 | "get_latest_by": "timestamp",
79 | "abstract": False,
80 | },
81 | ),
82 | migrations.AddConstraint(
83 | model_name="governmentplanfollower",
84 | constraint=models.UniqueConstraint(
85 | condition=models.Q(("user__isnull", False)),
86 | fields=("content_object", "user"),
87 | name="unique_user_follower_froide_govplan_governmentplanfollower",
88 | ),
89 | ),
90 | migrations.AddConstraint(
91 | model_name="governmentplanfollower",
92 | constraint=models.UniqueConstraint(
93 | condition=models.Q(("user__isnull", True)),
94 | fields=("content_object", "email"),
95 | name="unique_email_follower_froide_govplan_governmentplanfollower",
96 | ),
97 | ),
98 | ]
99 |
--------------------------------------------------------------------------------
/froide_govplan/plan_importer.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import re
3 |
4 | from django.template.defaultfilters import slugify
5 |
6 | from froide.publicbody.models import Category, PublicBody
7 |
8 | from .models import GovernmentPlan, GovernmentPlanSection
9 |
10 |
11 | class PlanImporter(object):
12 | def __init__(self, government, col_mapping=None):
13 | if col_mapping is None:
14 | col_mapping = {}
15 | self.col_mapping = col_mapping
16 | self.government = government
17 | self.post_save_list = []
18 |
19 | def import_rows(self, reader):
20 | for row in reader:
21 | self.import_row(row)
22 |
23 | def import_row(self, row):
24 | print("importing", row)
25 | title = row[self.col_mapping["title"]]
26 | if not title:
27 | return
28 | plan = GovernmentPlan.objects.filter(
29 | government=self.government, title=title
30 | ).first()
31 |
32 | if not plan:
33 | plan = GovernmentPlan(government=self.government)
34 |
35 | self.post_save_list = []
36 | for col, row_col in self.col_mapping.items():
37 | method_name = "handle_{}".format(col)
38 | if hasattr(self, method_name):
39 | getattr(self, method_name)(plan, row[row_col])
40 | else:
41 | setattr(plan, col, row[row_col])
42 | plan.save()
43 | for func in self.post_save_list:
44 | func(plan)
45 |
46 | def handle_title(self, plan, title):
47 | plan.title = title
48 | plan.slug = slugify(title)
49 |
50 | def handle_categories(self, plan, category_name):
51 | categories = [
52 | x.strip() for x in re.split(r", | & | und ", category_name) if x.strip()
53 | ]
54 | self.make_section(category_name, "-".join(categories), categories)
55 | if categories:
56 | self.post_save_list.append(lambda p: p.categories.set(*categories))
57 |
58 | def make_section(self, section_name, section_slug, categories):
59 | slug = slugify(section_slug)
60 | section, _created = GovernmentPlanSection.objects.get_or_create(
61 | slug=slug,
62 | defaults={
63 | "government": self.government,
64 | "title": section_name,
65 | },
66 | )
67 | section.categories.set([self.get_category(c) for c in categories])
68 |
69 | def get_category(self, cat_name):
70 | return Category.objects.get(name=cat_name)
71 |
72 | def handle_reference(self, plan, reference):
73 | plan.reference = ", ".join(re.split(r"\s*[,/]\s*", reference))
74 |
75 | def handle_responsible_publicbody(self, plan, pb):
76 | if not pb.strip():
77 | return
78 | pb = PublicBody.objects.get(
79 | jurisdiction=self.government.jurisdiction,
80 | other_names__iregex=r"(\W|^){}(\W|$)".format(pb),
81 | )
82 | plan.responsible_publicbody = pb
83 |
84 | def handle_due_date(self, plan, date_descr):
85 | if not date_descr.strip():
86 | return
87 |
88 | def parse_date(date_descr):
89 | match = re.search(r"(\d{4})", date_descr)
90 | if not match:
91 | return
92 | year = int(match.group(1))
93 | if "Mitte" in date_descr:
94 | return datetime.date(year, 7, 1)
95 | if "Ende" in date_descr:
96 | return datetime.date(year, 12, 1)
97 | if "Anfang" in date_descr:
98 | return datetime.date(year, 3, 1)
99 | if "Innerhalb" in date_descr:
100 | return datetime.date(year, 12, 31)
101 | if "Juni" in date_descr:
102 | return datetime.date(year, 6, 1)
103 | return datetime.date(year, 1, 1)
104 |
105 | plan.due_date = parse_date(date_descr)
106 |
107 | def handle_status(self, plan, status):
108 | status = status.strip()
109 | if not status or status == "noch nicht umgesetzt":
110 | status = "not_started"
111 | if status == "umgesetzt":
112 | status = "implemented"
113 | if status == "begonnen":
114 | status = "started"
115 | plan.status = status
116 |
--------------------------------------------------------------------------------
/froide_govplan/views.py:
--------------------------------------------------------------------------------
1 | from django.contrib import messages
2 | from django.contrib.auth.mixins import LoginRequiredMixin
3 | from django.shortcuts import get_object_or_404, redirect, render
4 | from django.utils.translation import gettext_lazy as _
5 | from django.views.generic import DetailView, UpdateView
6 | from froide.helper.breadcrumbs import BreadcrumbView
7 |
8 | from .auth import get_visible_plans
9 | from .forms import GovernmentPlanUpdateProposalForm
10 | from .models import Government, GovernmentPlan, GovernmentPlanSection
11 |
12 |
13 | class GovernmentMixin(BreadcrumbView):
14 | def dispatch(self, *args, **kwargs):
15 | self.get_government()
16 | return super().dispatch(*args, **kwargs)
17 |
18 | def get_government(self):
19 | filter_kwarg = {}
20 | if not self.request.user.is_authenticated or not self.request.user.is_staff:
21 | filter_kwarg["public"] = True
22 | self.government = get_object_or_404(
23 | Government, slug=self.kwargs["gov"], **filter_kwarg
24 | )
25 |
26 | def get_context_data(self, **kwargs):
27 | context = super().get_context_data(**kwargs)
28 | context["government"] = self.government
29 | return context
30 |
31 | def get_breadcrumbs(self, context):
32 | breadcrumbs = []
33 | if "request" in context:
34 | request = context["request"]
35 |
36 | title = request.current_page.get_title()
37 | url = request.current_page.get_absolute_url()
38 | breadcrumbs.append((title, url))
39 |
40 | breadcrumbs.append((self.government.name, self.government.get_absolute_url()))
41 | return breadcrumbs
42 |
43 |
44 | class GovPlanSectionDetailView(GovernmentMixin, DetailView):
45 | slug_url_kwarg = "section"
46 | template_name = "froide_govplan/section.html"
47 |
48 | def get_queryset(self):
49 | return GovernmentPlanSection.objects.filter(
50 | government=self.government
51 | ).select_related("government")
52 |
53 | def get_context_data(self, **kwargs):
54 | context = super().get_context_data(**kwargs)
55 | queryset = get_visible_plans(self.request)
56 | context["plans"] = context["object"].get_plans(queryset=queryset)
57 | return context
58 |
59 | def get_breadcrumbs(self, context):
60 | return super().get_breadcrumbs(context) + [
61 | (self.object.title, self.object.get_absolute_url())
62 | ]
63 |
64 |
65 | class GovPlanDetailView(GovernmentMixin, DetailView):
66 | slug_url_kwarg = "plan"
67 | template_name = "froide_govplan/detail.html"
68 |
69 | def get_queryset(self):
70 | qs = GovernmentPlan.objects.filter(government=self.government)
71 | if self.request.user.is_authenticated and self.request.user.is_staff:
72 | return qs
73 | return qs.filter(public=True).select_related(
74 | "responsible_publicbody", "organization"
75 | )
76 |
77 | def get_context_data(self, **kwargs):
78 | context = super().get_context_data(**kwargs)
79 | context["updates"] = self.object.updates.filter(public=True).order_by(
80 | "-timestamp"
81 | )
82 | context["section"] = self.object.get_section()
83 | if self.request.user.is_authenticated:
84 | context["update_proposal_form"] = GovernmentPlanUpdateProposalForm()
85 | # For CMS toolbar
86 | self.request.govplan = self.object
87 | return context
88 |
89 | def get_breadcrumbs(self, context):
90 | obj = context["object"]
91 | section = context["section"]
92 |
93 | breadcrumbs = super().get_breadcrumbs(context)
94 |
95 | if section:
96 | breadcrumbs.append((section.title, section.get_absolute_url()))
97 |
98 | return breadcrumbs + [
99 | (obj.title, obj.get_absolute_url()),
100 | ]
101 |
102 |
103 | class GovPlanProposeUpdateView(GovernmentMixin, LoginRequiredMixin, UpdateView):
104 | slug_url_kwarg = "plan"
105 | form_class = GovernmentPlanUpdateProposalForm
106 |
107 | def get(self, request, *args, **kwargs):
108 | self.object = self.get_object()
109 | return redirect(self.object)
110 |
111 | def get_queryset(self):
112 | qs = GovernmentPlan.objects.filter(
113 | government=self.government, government__active=True
114 | )
115 | if self.request.user.is_authenticated and self.request.user.is_staff:
116 | return qs
117 | return qs.filter(public=True)
118 |
119 | def get_success_url(self):
120 | return self.object.get_absolute_url()
121 |
122 | def form_valid(self, form):
123 | form.save(self.object, self.request.user)
124 | messages.add_message(
125 | self.request,
126 | messages.INFO,
127 | _(
128 | "Thank you for your proposal. We will send you an email when it has been approved."
129 | ),
130 | )
131 | return redirect(self.object)
132 |
133 | def form_invalid(self, form):
134 | messages.add_message(
135 | self.request,
136 | messages.ERROR,
137 | _("There's been an error with your form submission."),
138 | )
139 | return redirect(self.object)
140 |
141 |
142 | def search(request):
143 | q = request.GET.get("q", "")
144 | plans = GovernmentPlan.objects.filter(public=True)
145 | if q:
146 | plans = GovernmentPlan.objects.search(q, qs=plans)
147 |
148 | if request.GET.get("government"):
149 | try:
150 | gov_id = int(request.GET["government"])
151 | plans = plans.filter(government_id=gov_id)
152 | except ValueError:
153 | pass
154 | if request.GET.get("status"):
155 | try:
156 | status = request.GET["status"]
157 | plans = plans.filter(status=status)
158 | except ValueError:
159 | pass
160 |
161 | if q:
162 | # limit when there's a search
163 | plans = plans[:20]
164 | return render(
165 | request, "froide_govplan/plugins/card_cols.html", {"object_list": plans}
166 | )
167 |
--------------------------------------------------------------------------------
/project/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for project project.
3 |
4 | Generated by 'django-admin startproject' using Django 4.1.6.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.1/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/4.1/ref/settings/
11 | """
12 |
13 | import os
14 | from pathlib import Path
15 |
16 | from django.utils.translation import gettext_lazy as _
17 |
18 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
19 | BASE_DIR = Path(__file__).resolve().parent.parent
20 | PROJECT_DIR = Path(__file__).resolve().parent
21 |
22 | # Quick-start development settings - unsuitable for production
23 | # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
24 |
25 | # SECURITY WARNING: keep the secret key used in production secret!
26 | SECRET_KEY = "django-insecure-8qh(ha_fw#3mc#y+rtd^se+e$-+n5%1o*d3%3p0d379q1zxypu"
27 |
28 | # SECURITY WARNING: don't run with debug turned on in production!
29 | DEBUG = True
30 |
31 | ALLOWED_HOSTS = []
32 |
33 |
34 | # Application definition
35 |
36 | INSTALLED_APPS = [
37 | # "djangocms_admin_style",
38 | "django.contrib.admin",
39 | "django.contrib.auth",
40 | "django.contrib.sites",
41 | "django.contrib.contenttypes",
42 | "django.contrib.sessions",
43 | "django.contrib.messages",
44 | "django.contrib.staticfiles",
45 | "django.contrib.humanize",
46 | "django.contrib.gis",
47 | # Froide apps
48 | "froide_govplan.apps.FroideGovPlanConfig",
49 | "froide.georegion",
50 | "froide.publicbody",
51 | "froide.follow",
52 | "froide.organization",
53 | "froide.team",
54 | "froide.helper",
55 | # Third party apps
56 | "easy_thumbnails",
57 | "filer",
58 | "sekizai",
59 | "cms",
60 | "djangocms_alias",
61 | "menus",
62 | "treebeard",
63 | "taggit",
64 | "tinymce",
65 | ]
66 |
67 | MIDDLEWARE = [
68 | "django.middleware.security.SecurityMiddleware",
69 | "django.middleware.locale.LocaleMiddleware", # needs to be before CommonMiddleware
70 | "django.contrib.sessions.middleware.SessionMiddleware",
71 | "django.middleware.common.CommonMiddleware",
72 | "django.middleware.csrf.CsrfViewMiddleware",
73 | "django.contrib.auth.middleware.AuthenticationMiddleware",
74 | "django.contrib.messages.middleware.MessageMiddleware",
75 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
76 | "cms.middleware.user.CurrentUserMiddleware",
77 | "cms.middleware.page.CurrentPageMiddleware",
78 | "cms.middleware.toolbar.ToolbarMiddleware",
79 | "cms.middleware.language.LanguageCookieMiddleware",
80 | ]
81 |
82 | ROOT_URLCONF = "project.urls"
83 |
84 | TEMPLATES = [
85 | {
86 | "BACKEND": "django.template.backends.django.DjangoTemplates",
87 | "DIRS": [
88 | PROJECT_DIR / "templates",
89 | ],
90 | "APP_DIRS": True,
91 | "OPTIONS": {
92 | "context_processors": [
93 | "django.template.context_processors.debug",
94 | "django.template.context_processors.request",
95 | "django.contrib.auth.context_processors.auth",
96 | "django.contrib.messages.context_processors.messages",
97 | "sekizai.context_processors.sekizai",
98 | "cms.context_processors.cms_settings",
99 | "froide.helper.context_processors.site_settings",
100 | ],
101 | },
102 | },
103 | ]
104 |
105 | WSGI_APPLICATION = "project.wsgi.application"
106 |
107 |
108 | # Database
109 | # https://docs.djangoproject.com/en/4.1/ref/settings/#databases
110 |
111 | DATABASES = {
112 | "default": {
113 | "ENGINE": "django.contrib.gis.db.backends.postgis", # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
114 | # "NAME": "fragdenstaat_de",
115 | "NAME": "govplan",
116 | "USER": "govplan",
117 | "PASSWORD": "govplan",
118 | "HOST": "localhost", # Set to empty string for localhost. Not used with sqlite3.
119 | "PORT": "5432", # Set to empty string for default. Not used with sqlite3.
120 | }
121 | }
122 | GDAL_LIBRARY_PATH = os.environ.get("GDAL_LIBRARY_PATH")
123 | GEOS_LIBRARY_PATH = os.environ.get("GEOS_LIBRARY_PATH")
124 |
125 |
126 | # Password validation
127 | # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
128 |
129 | AUTH_PASSWORD_VALIDATORS = [
130 | {
131 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
132 | },
133 | {
134 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
135 | },
136 | {
137 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
138 | },
139 | {
140 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
141 | },
142 | ]
143 |
144 |
145 | # Internationalization
146 | # https://docs.djangoproject.com/en/4.1/topics/i18n/
147 |
148 | LANGUAGE_CODE = "de"
149 | LANGUAGES = [("de", _("German"))]
150 |
151 |
152 | TIME_ZONE = "UTC"
153 |
154 | USE_I18N = True
155 |
156 | USE_TZ = True
157 |
158 |
159 | # Static files (CSS, JavaScript, Images)
160 | # https://docs.djangoproject.com/en/4.1/howto/static-files/
161 |
162 | STATIC_URL = "static/"
163 |
164 | # Default primary key field type
165 | # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
166 |
167 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
168 | # AUTH_USER_MODEL = "account.User"
169 |
170 | TAGGIT_CASE_INSENSITIVE = True
171 | TAGGIT_STRIP_UNICODE_WHEN_SLUGIFYING = True
172 |
173 | LOGIN_URL = "admin:login"
174 | X_FRAME_OPTIONS = "SAMEORIGIN"
175 |
176 | # Django CMS
177 |
178 | CMS_TEMPLATES = [
179 | ("froide_govplan/base.html", "Govplan base template"),
180 | ]
181 |
182 | # Froide
183 |
184 | SITE_ID = 1
185 | SITE_NAME = "GovPlan"
186 | SITE_URL = "http://localhost:8000"
187 | SITE_EMAIL = "info@example.com"
188 | SITE_LOGO = ""
189 |
190 | FROIDE_CONFIG = {
191 | "bounce_enabled": False,
192 | "bounce_format": "bounce+{token}@example.com",
193 | "bounce_max_age": 60 * 60 * 24 * 14, # 14 days
194 | "unsubscribe_enabled": False,
195 | "unsubscribe_format": "unsub+{token}@example.com",
196 | }
197 |
198 | ELASTICSEARCH_INDEX_PREFIX = "govplan"
199 | CMS_CONFIRM_VERSION4 = True
200 |
201 | # Govplan settings
202 |
203 | GOVPLAN_NAME = "GovPlan"
204 | GOVPLAN_ENABLE_FOIREQUEST = False
205 |
--------------------------------------------------------------------------------
/froide_govplan/forms.py:
--------------------------------------------------------------------------------
1 | import copy
2 |
3 | import nh3
4 | from django import forms
5 | from django.conf import settings
6 | from django.contrib.auth import get_user_model
7 | from django.utils import timezone
8 | from django.utils.safestring import mark_safe
9 | from django.utils.translation import gettext_lazy as _
10 | from froide.helper.widgets import BootstrapSelect
11 | from tinymce.widgets import TinyMCE
12 |
13 | from .models import GovernmentPlan, GovernmentPlanUpdate, PlanRating, PlanStatus
14 |
15 | ALLOWED_TAGS = {
16 | "a",
17 | "strong",
18 | "b",
19 | "i",
20 | "em",
21 | "ul",
22 | "ol",
23 | "li",
24 | "p",
25 | "h3",
26 | "h4",
27 | "h5",
28 | "blockquote",
29 | }
30 |
31 |
32 | class BleachField(forms.CharField):
33 | """Bleach form field"""
34 |
35 | def to_python(self, value):
36 | """
37 | Strips any dodgy HTML tags from the input.
38 | Mark the return value as template safe.
39 | """
40 | if value in self.empty_values:
41 | return self.empty_value
42 | cleaned_html = nh3.clean(value, tags=ALLOWED_TAGS, link_rel="noopener")
43 | return mark_safe(cleaned_html)
44 |
45 |
46 | class GovernmentPlanForm(forms.ModelForm):
47 | description = BleachField(
48 | required=False, widget=TinyMCE(attrs={"cols": 80, "rows": 30})
49 | )
50 |
51 | class Meta:
52 | model = GovernmentPlan
53 | fields = "__all__"
54 |
55 |
56 | class GovernmentPlanUpdateForm(forms.ModelForm):
57 | content = BleachField(
58 | required=False, widget=TinyMCE(attrs={"cols": 80, "rows": 30})
59 | )
60 |
61 | class Meta:
62 | model = GovernmentPlanUpdate
63 | fields = "__all__"
64 |
65 |
66 | class GovernmentPlanUpdateProposalForm(forms.ModelForm):
67 | title = forms.CharField(
68 | label=_("title"),
69 | help_text=_("Summarize the update in a title."),
70 | widget=forms.TextInput(
71 | attrs={
72 | "class": "form-control",
73 | }
74 | ),
75 | )
76 | content = forms.CharField(
77 | required=False,
78 | label=_("details"),
79 | help_text=_("Optionally give more details."),
80 | widget=forms.Textarea(attrs={"class": "form-control", "rows": "3"}),
81 | )
82 | url = forms.URLField(
83 | label=_("source URL"),
84 | help_text=_("Please give provide a link."),
85 | widget=forms.URLInput(
86 | attrs={"class": "form-control", "placeholder": "https://"}
87 | ),
88 | )
89 | status = forms.ChoiceField(
90 | label=_("status"),
91 | help_text=_("Has the status of the plan changed?"),
92 | choices=[("", "---")] + PlanStatus.choices,
93 | required=False,
94 | widget=BootstrapSelect,
95 | )
96 | rating = forms.TypedChoiceField(
97 | label=_("rating"),
98 | help_text=_("What's your rating of the current implementation?"),
99 | choices=[("", "---")] + PlanRating.choices,
100 | coerce=int,
101 | empty_value=None,
102 | required=False,
103 | widget=BootstrapSelect,
104 | )
105 |
106 | class Meta:
107 | model = GovernmentPlanUpdate
108 | fields = (
109 | "title",
110 | "content",
111 | "url",
112 | "status",
113 | "rating",
114 | )
115 |
116 | def __init__(self, *args, **kwargs):
117 | super().__init__(*args, **kwargs)
118 |
119 | def save(self, plan, user):
120 | """
121 | This doesn't save instance, but saves
122 | the change proposal.
123 | """
124 | data = self.cleaned_data
125 | plan.proposals = plan.proposals or {}
126 | plan.proposals[user.id] = {
127 | "data": data,
128 | "timestamp": timezone.now().isoformat(),
129 | }
130 | plan.save(update_fields=["proposals"])
131 | return plan
132 |
133 |
134 | class GovernmentPlanUpdateAcceptProposalForm(GovernmentPlanUpdateProposalForm):
135 | def __init__(self, *args, **kwargs):
136 | self.plan = kwargs.pop("plan")
137 | super().__init__(*args, **kwargs)
138 |
139 | def get_proposals(self):
140 | data = copy.deepcopy(self.plan.proposals)
141 | user_ids = self.plan.proposals.keys()
142 | user_map = {
143 | str(u.id): u for u in get_user_model().objects.filter(id__in=user_ids)
144 | }
145 | status_dict = dict(PlanStatus.choices)
146 | rating_dict = dict(PlanRating.choices)
147 | for user_id, v in data.items():
148 | v["user"] = user_map[user_id]
149 | data[user_id]["data"]["rating_label"] = rating_dict.get(
150 | data[user_id]["data"]["rating"]
151 | )
152 | data[user_id]["data"]["status_label"] = status_dict.get(
153 | data[user_id]["data"]["status"]
154 | )
155 | return data
156 |
157 | def save(
158 | self,
159 | proposal_id=None,
160 | delete_proposals=None,
161 | ):
162 | update = super(forms.ModelForm, self).save(commit=False)
163 | update.plan = self.plan
164 |
165 | if delete_proposals is None:
166 | delete_proposals = []
167 | if proposal_id:
168 | proposals = self.get_proposals()
169 | proposal_user = proposals[proposal_id]["user"]
170 | proposal_user.send_mail(
171 | _("Your proposal for the plan “%s” was accepted") % self.plan.title,
172 | _(
173 | "Hello,\n\nA moderator has accepted your proposal for an update to the plan "
174 | "“{title}”. An update will be published soon.\n\nAll the Best,\n{site_name}"
175 | ).format(title=self.plan.title, site_name=settings.SITE_NAME),
176 | priority=False,
177 | )
178 | user_org = proposal_user.organization_set.all().first()
179 | if user_org:
180 | update.organization = user_org
181 | delete_proposals.append(proposal_id)
182 |
183 | self.delete_proposals(delete_proposals)
184 | update.save()
185 | return update
186 |
187 | def delete_proposals(self, delete_proposals):
188 | for pid in delete_proposals:
189 | if pid in self.plan.proposals:
190 | del self.plan.proposals[pid]
191 | if not self.plan.proposals:
192 | self.plan.proposals = None
193 | self.plan.save(update_fields=["proposals"])
194 |
--------------------------------------------------------------------------------
/froide_govplan/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.8 on 2022-02-15 19:55
2 |
3 | import django.db.models.deletion
4 | import django.utils.timezone
5 | from django.conf import settings
6 | from django.db import migrations, models
7 |
8 | import filer.fields.image
9 | import taggit.managers
10 |
11 | from .. import conf
12 |
13 |
14 | class Migration(migrations.Migration):
15 |
16 | initial = True
17 |
18 | dependencies = (
19 | [
20 | ("publicbody", "0039_publicbody_alternative_emails"),
21 | ("auth", "0012_alter_user_first_name_max_length"),
22 | ]
23 | + (
24 | [
25 | ("foirequest", "0054_alter_foirequest_options"),
26 | ]
27 | if conf.GOVPLAN_ENABLE_FOIREQUEST
28 | else []
29 | )
30 | + [
31 | migrations.swappable_dependency(settings.FILER_IMAGE_MODEL),
32 | ("organization", "0001_initial"),
33 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
34 | ]
35 | )
36 |
37 | operations = [
38 | migrations.CreateModel(
39 | name="CategorizedGovernmentPlan",
40 | fields=[
41 | (
42 | "id",
43 | models.AutoField(
44 | auto_created=True,
45 | primary_key=True,
46 | serialize=False,
47 | verbose_name="ID",
48 | ),
49 | ),
50 | ],
51 | options={
52 | "verbose_name": "Categorized Government Plan",
53 | "verbose_name_plural": "Categorized Government Plans",
54 | },
55 | ),
56 | migrations.CreateModel(
57 | name="Government",
58 | fields=[
59 | (
60 | "id",
61 | models.AutoField(
62 | auto_created=True,
63 | primary_key=True,
64 | serialize=False,
65 | verbose_name="ID",
66 | ),
67 | ),
68 | ("name", models.CharField(max_length=255)),
69 | ("slug", models.SlugField(max_length=255, unique=True)),
70 | ("public", models.BooleanField(default=False)),
71 | ("description", models.TextField(blank=True)),
72 | ("start_date", models.DateField(blank=True, null=True)),
73 | ("end_date", models.DateField(blank=True, null=True)),
74 | ("planning_document", models.URLField(blank=True)),
75 | (
76 | "jurisdiction",
77 | models.ForeignKey(
78 | null=True,
79 | on_delete=django.db.models.deletion.SET_NULL,
80 | to="publicbody.jurisdiction",
81 | ),
82 | ),
83 | ],
84 | options={
85 | "verbose_name": "Government",
86 | "verbose_name_plural": "Governments",
87 | },
88 | ),
89 | migrations.CreateModel(
90 | name="GovernmentPlan",
91 | fields=[
92 | (
93 | "id",
94 | models.AutoField(
95 | auto_created=True,
96 | primary_key=True,
97 | serialize=False,
98 | verbose_name="ID",
99 | ),
100 | ),
101 | ("title", models.CharField(max_length=255)),
102 | ("slug", models.SlugField(max_length=255, unique=True)),
103 | ("description", models.TextField(blank=True)),
104 | ("public", models.BooleanField(default=False)),
105 | (
106 | "status",
107 | models.CharField(
108 | choices=[
109 | ("not_started", "not started"),
110 | ("started", "started"),
111 | ("partially_implemented", "partially implemented"),
112 | ("implemented", "implemented"),
113 | ("deferred", "deferred"),
114 | ],
115 | default="needs_approval",
116 | max_length=25,
117 | ),
118 | ),
119 | (
120 | "rating",
121 | models.IntegerField(
122 | blank=True,
123 | choices=[
124 | (1, "terrible"),
125 | (2, "bad"),
126 | (3, "OK"),
127 | (4, "good"),
128 | (5, "excellent"),
129 | ],
130 | null=True,
131 | ),
132 | ),
133 | ("reference", models.CharField(blank=True, max_length=255)),
134 | (
135 | "categories",
136 | taggit.managers.TaggableManager(
137 | blank=True,
138 | help_text="A comma-separated list of tags.",
139 | through="froide_govplan.CategorizedGovernmentPlan",
140 | to="publicbody.Category",
141 | verbose_name="categories",
142 | ),
143 | ),
144 | (
145 | "government",
146 | models.ForeignKey(
147 | on_delete=django.db.models.deletion.CASCADE,
148 | to="froide_govplan.government",
149 | ),
150 | ),
151 | (
152 | "group",
153 | models.ForeignKey(
154 | blank=True,
155 | null=True,
156 | on_delete=django.db.models.deletion.SET_NULL,
157 | to="auth.group",
158 | ),
159 | ),
160 | (
161 | "image",
162 | filer.fields.image.FilerImageField(
163 | blank=True,
164 | default=None,
165 | null=True,
166 | on_delete=django.db.models.deletion.SET_NULL,
167 | to=settings.FILER_IMAGE_MODEL,
168 | verbose_name="image",
169 | ),
170 | ),
171 | (
172 | "responsible_publicbody",
173 | models.ForeignKey(
174 | blank=True,
175 | null=True,
176 | on_delete=django.db.models.deletion.SET_NULL,
177 | to="publicbody.publicbody",
178 | ),
179 | ),
180 | ],
181 | options={
182 | "verbose_name": "Government plan",
183 | "verbose_name_plural": "Government plans",
184 | "ordering": ("reference", "title"),
185 | },
186 | ),
187 | migrations.CreateModel(
188 | name="GovernmentPlanUpdate",
189 | fields=[
190 | (
191 | "id",
192 | models.AutoField(
193 | auto_created=True,
194 | primary_key=True,
195 | serialize=False,
196 | verbose_name="ID",
197 | ),
198 | ),
199 | ("timestamp", models.DateTimeField(default=django.utils.timezone.now)),
200 | ("title", models.CharField(blank=True, max_length=1024)),
201 | ("content", models.TextField(blank=True)),
202 | (
203 | "status",
204 | models.CharField(
205 | blank=True,
206 | choices=[
207 | ("not_started", "not started"),
208 | ("started", "started"),
209 | ("partially_implemented", "partially implemented"),
210 | ("implemented", "implemented"),
211 | ("deferred", "deferred"),
212 | ],
213 | default="",
214 | max_length=25,
215 | ),
216 | ),
217 | (
218 | "rating",
219 | models.IntegerField(
220 | blank=True,
221 | choices=[
222 | (1, "terrible"),
223 | (2, "bad"),
224 | (3, "OK"),
225 | (4, "good"),
226 | (5, "excellent"),
227 | ],
228 | null=True,
229 | ),
230 | ),
231 | ("public", models.BooleanField(default=False)),
232 | ]
233 | + (
234 | [
235 | (
236 | "foirequest",
237 | models.ForeignKey(
238 | blank=True,
239 | null=True,
240 | on_delete=django.db.models.deletion.SET_NULL,
241 | to="foirequest.foirequest",
242 | ),
243 | )
244 | ]
245 | if conf.GOVPLAN_ENABLE_FOIREQUEST
246 | else []
247 | )
248 | + [
249 | (
250 | "organization",
251 | models.ForeignKey(
252 | blank=True,
253 | null=True,
254 | on_delete=django.db.models.deletion.SET_NULL,
255 | to="organization.organization",
256 | ),
257 | ),
258 | (
259 | "plan",
260 | models.ForeignKey(
261 | on_delete=django.db.models.deletion.CASCADE,
262 | related_name="updates",
263 | to="froide_govplan.governmentplan",
264 | ),
265 | ),
266 | (
267 | "user",
268 | models.ForeignKey(
269 | blank=True,
270 | null=True,
271 | on_delete=django.db.models.deletion.SET_NULL,
272 | to=settings.AUTH_USER_MODEL,
273 | ),
274 | ),
275 | ],
276 | options={
277 | "verbose_name": "Plan update",
278 | "verbose_name_plural": "Plan updates",
279 | "ordering": ("-timestamp",),
280 | "get_latest_by": "timestamp",
281 | },
282 | ),
283 | migrations.AddField(
284 | model_name="categorizedgovernmentplan",
285 | name="content_object",
286 | field=models.ForeignKey(
287 | on_delete=django.db.models.deletion.CASCADE,
288 | to="froide_govplan.governmentplan",
289 | ),
290 | ),
291 | migrations.AddField(
292 | model_name="categorizedgovernmentplan",
293 | name="tag",
294 | field=models.ForeignKey(
295 | on_delete=django.db.models.deletion.CASCADE,
296 | related_name="categorized_governmentplan",
297 | to="publicbody.category",
298 | ),
299 | ),
300 | ]
301 |
--------------------------------------------------------------------------------
/froide_govplan/admin.py:
--------------------------------------------------------------------------------
1 | from adminsortable2.admin import SortableAdminMixin
2 | from django.contrib import admin, auth
3 | from django.contrib.auth.models import Group
4 | from django.shortcuts import get_object_or_404, redirect, render
5 | from django.urls import path, reverse, reverse_lazy
6 | from django.utils.translation import gettext_lazy as _
7 | from froide.follow.admin import FollowerAdmin
8 | from froide.helper.admin_utils import make_choose_object_action, make_emptyfilter
9 | from froide.helper.widgets import TagAutocompleteWidget
10 | from froide.organization.models import Organization
11 |
12 | from . import conf
13 | from .auth import get_allowed_plans, has_limited_access
14 | from .forms import (
15 | GovernmentPlanForm,
16 | GovernmentPlanUpdateAcceptProposalForm,
17 | GovernmentPlanUpdateForm,
18 | )
19 | from .models import (
20 | Government,
21 | GovernmentPlan,
22 | GovernmentPlanFollower,
23 | GovernmentPlanSection,
24 | GovernmentPlanUpdate,
25 | )
26 |
27 | User = auth.get_user_model()
28 |
29 |
30 | class GovPlanAdminSite(admin.AdminSite):
31 | site_header = conf.GOVPLAN_NAME
32 | site_url = None
33 |
34 |
35 | class GovernmentPlanAdminForm(GovernmentPlanForm):
36 | class Meta:
37 | model = GovernmentPlan
38 | fields = "__all__"
39 | widgets = {
40 | "categories": TagAutocompleteWidget(
41 | autocomplete_url=reverse_lazy("api:category-autocomplete")
42 | ),
43 | }
44 |
45 |
46 | class GovernmentAdmin(admin.ModelAdmin):
47 | prepopulated_fields = {"slug": ("name",)}
48 | list_display = ("name", "public", "start_date", "end_date")
49 | list_filter = ("public",)
50 | raw_id_fields = ("georegion",)
51 |
52 |
53 | def execute_assign_organization(admin, request, queryset, action_obj):
54 | queryset.update(organization=action_obj)
55 |
56 |
57 | def execute_assign_group(admin, request, queryset, action_obj):
58 | queryset.update(group=action_obj)
59 |
60 |
61 | PLAN_ACTIONS = {
62 | "assign_organization": make_choose_object_action(
63 | Organization, execute_assign_organization, _("Assign organization...")
64 | ),
65 | "assign_group": make_choose_object_action(
66 | Group, execute_assign_group, _("Assign permission group...")
67 | ),
68 | }
69 |
70 |
71 | class GovernmentPlanAdmin(admin.ModelAdmin):
72 | form = GovernmentPlanForm
73 |
74 | save_on_top = True
75 | prepopulated_fields = {"slug": ("title",)}
76 | search_fields = ("title",)
77 | raw_id_fields = ("responsible_publicbody",)
78 |
79 | actions = ["make_public"]
80 |
81 | def get_queryset(self, request):
82 | qs = get_allowed_plans(request)
83 | qs = qs.prefetch_related(
84 | "categories",
85 | "organization",
86 | "group",
87 | )
88 | return qs
89 |
90 | def get_actions(self, request):
91 | actions = super().get_actions(request)
92 | if not has_limited_access(request.user):
93 | admin_actions = {
94 | action: (
95 | func,
96 | action,
97 | func.short_description,
98 | )
99 | for action, func in PLAN_ACTIONS.items()
100 | }
101 | actions.update(admin_actions)
102 | return actions
103 |
104 | def get_urls(self):
105 | urls = super().get_urls()
106 | my_urls = [
107 | path(
108 | "/accept-proposal/",
109 | self.admin_site.admin_view(self.accept_proposal),
110 | name="froide_govplan-plan_accept_proposal",
111 | ),
112 | ]
113 | return my_urls + urls
114 |
115 | def get_list_display(self, request):
116 | list_display = [
117 | "title",
118 | "public",
119 | "status",
120 | "rating",
121 | "organization",
122 | "get_categories",
123 | ]
124 | if not has_limited_access(request.user):
125 | list_display.append("group")
126 | return list_display
127 |
128 | def get_list_filter(self, request):
129 | list_filter = [
130 | "status",
131 | "rating",
132 | "public",
133 | ]
134 | if not has_limited_access(request.user):
135 | list_filter.extend(
136 | [
137 | make_emptyfilter(
138 | "proposals", _("Has change proposals"), empty_value=None
139 | ),
140 | "organization",
141 | "group",
142 | "government",
143 | "categories",
144 | ]
145 | )
146 | return list_filter
147 |
148 | def get_fields(self, request, obj=None):
149 | if has_limited_access(request.user):
150 | return (
151 | "title",
152 | "slug",
153 | "description",
154 | "quote",
155 | "public",
156 | "due_date",
157 | "measure",
158 | "status",
159 | "rating",
160 | "reference",
161 | )
162 | return super().get_fields(request, obj=obj)
163 |
164 | def get_categories(self, obj):
165 | """
166 | Return the categories linked in HTML.
167 | """
168 | categories = [category.name for category in obj.categories.all()]
169 | return ", ".join(categories)
170 |
171 | get_categories.short_description = _("category(s)")
172 |
173 | def make_public(self, request, queryset):
174 | queryset.update(public=True)
175 |
176 | make_public.short_description = _("Make public")
177 |
178 | def accept_proposal(self, request, pk):
179 | obj = get_object_or_404(self.get_queryset(request), pk=pk)
180 | plan_url = reverse(
181 | "admin:froide_govplan_governmentplan_change",
182 | args=(obj.pk,),
183 | current_app=self.admin_site.name,
184 | )
185 | if not obj.proposals:
186 | return redirect(plan_url)
187 | if request.method == "POST":
188 | proposals = obj.proposals or {}
189 | proposal_id = request.POST.get("proposal_id")
190 | delete_proposals = request.POST.getlist("proposal_delete")
191 | update = None
192 | if proposal_id:
193 | data = proposals[proposal_id]["data"]
194 | form = GovernmentPlanUpdateAcceptProposalForm(data=data, plan=obj)
195 | if form.is_valid():
196 | update = form.save(
197 | proposal_id=proposal_id,
198 | delete_proposals=delete_proposals,
199 | )
200 | else:
201 | form = GovernmentPlanUpdateAcceptProposalForm(data={}, plan=obj)
202 | form.delete_proposals(delete_proposals)
203 |
204 | if update is None:
205 | self.message_user(request, _("The proposal has been deleted."))
206 |
207 | return redirect(plan_url)
208 |
209 | self.message_user(
210 | request,
211 | _("An unpublished update has been created."),
212 | )
213 | update_url = reverse(
214 | "admin:froide_govplan_governmentplanupdate_change",
215 | args=(update.pk,),
216 | current_app=self.admin_site.name,
217 | )
218 | return redirect(update_url)
219 | else:
220 | form = GovernmentPlanUpdateAcceptProposalForm(plan=obj)
221 |
222 | opts = self.model._meta
223 | context = {
224 | "form": form,
225 | "proposals": form.get_proposals(),
226 | "object": obj,
227 | "app_label": opts.app_label,
228 | "opts": opts,
229 | }
230 | return render(
231 | request,
232 | "froide_govplan/admin/accept_proposal.html",
233 | context,
234 | )
235 |
236 |
237 | class GovernmentPlanUpdateAdmin(admin.ModelAdmin):
238 | form = GovernmentPlanUpdateForm
239 | save_on_top = True
240 | raw_id_fields = ["user"] + (
241 | ["foirequest"] if conf.GOVPLAN_ENABLE_FOIREQUEST else []
242 | )
243 | date_hierarchy = "timestamp"
244 | search_fields = ("title", "content")
245 | list_display = (
246 | "title",
247 | "timestamp",
248 | "plan",
249 | "user",
250 | "status",
251 | "rating",
252 | "public",
253 | )
254 | list_filter = (
255 | "status",
256 | "public",
257 | "organization",
258 | )
259 | search_fields = (
260 | "title",
261 | "plan__title",
262 | )
263 | date_hierarchy = "timestamp"
264 |
265 | def get_queryset(self, request):
266 | qs = super().get_queryset(request)
267 | qs = qs.prefetch_related(
268 | "plan",
269 | "user",
270 | )
271 | if has_limited_access(request.user):
272 | qs = qs.filter(plan__in=get_allowed_plans(request))
273 | return qs
274 |
275 | def save_model(self, request, obj, form, change):
276 | limited = has_limited_access(request.user)
277 | if not change and limited:
278 | # When added by a limited user,
279 | # autofill user and organization
280 | obj.user = request.user
281 | if obj.plan.organization:
282 | user_has_org = request.user.organization_set.all().filter(pk=1).exists()
283 | if user_has_org:
284 | obj.organization = obj.plan.organization
285 |
286 | res = super().save_model(request, obj, form, change)
287 |
288 | obj.plan.update_from_updates()
289 |
290 | return res
291 |
292 | def get_fields(self, request, obj=None):
293 | if has_limited_access(request.user):
294 | return (
295 | "plan",
296 | "title",
297 | "timestamp",
298 | "content",
299 | "url",
300 | "status",
301 | "rating",
302 | "public",
303 | )
304 | return super().get_fields(request, obj=obj)
305 |
306 | def formfield_for_foreignkey(self, db_field, request, **kwargs):
307 | if db_field.name == "plan":
308 | if has_limited_access(request.user):
309 | kwargs["queryset"] = get_allowed_plans(request)
310 | return super().formfield_for_foreignkey(db_field, request, **kwargs)
311 |
312 | def user_in_obj_group(self, request, obj):
313 | if not obj.plan.group_id:
314 | return False
315 | user = request.user
316 | return User.objects.filter(pk=user.pk, groups=obj.plan.group_id).exists()
317 |
318 | def has_view_permission(self, request, obj=None):
319 | if obj and self.user_in_obj_group(request, obj):
320 | return True
321 | return super().has_view_permission(request, obj=obj)
322 |
323 | def has_add_permission(self, request):
324 | return super().has_add_permission(request)
325 |
326 | def has_change_permission(self, request, obj=None):
327 | if obj and self.user_in_obj_group(request, obj):
328 | return True
329 | return super().has_change_permission(request, obj=obj)
330 |
331 |
332 | class GovernmentPlanSectionAdmin(SortableAdminMixin, admin.ModelAdmin):
333 | save_on_top = True
334 | prepopulated_fields = {"slug": ("title",)}
335 | search_fields = ("title",)
336 | raw_id_fields = ("categories",)
337 | list_display = (
338 | "title",
339 | "featured",
340 | )
341 | list_filter = (
342 | "featured",
343 | "categories",
344 | "government",
345 | )
346 |
347 |
348 | admin.site.register(Government, GovernmentAdmin)
349 | admin.site.register(GovernmentPlan, GovernmentPlanAdmin)
350 | admin.site.register(GovernmentPlanUpdate, GovernmentPlanUpdateAdmin)
351 | admin.site.register(GovernmentPlanSection, GovernmentPlanSectionAdmin)
352 | admin.site.register(GovernmentPlanFollower, FollowerAdmin)
353 |
354 | govplan_admin_site = GovPlanAdminSite(name="govplanadmin")
355 | govplan_admin_site.register(GovernmentPlan, GovernmentPlanAdmin)
356 | govplan_admin_site.register(GovernmentPlanUpdate, GovernmentPlanUpdateAdmin)
357 |
--------------------------------------------------------------------------------
/froide_govplan/templates/froide_govplan/detail.html:
--------------------------------------------------------------------------------
1 | {% extends "froide_govplan/base.html" %}
2 | {% load i18n %}
3 | {# TODO: i18n for all strings #}
4 | {% load markup %}
5 | {% load cms_tags %}
6 | {% load follow_tags %}
7 | {% load govplan %}
8 | {% load form_helper %}
9 | {% load content_helper %}
10 | {% load thumbnail %}
11 | {% block title %}
12 | {{ object.title }}
13 | {% endblock title %}
14 | {% block meta %}
15 | {% include "snippets/meta.html" %}
16 | {% endblock meta %}
17 | {% block app_body %}
18 |
19 |
20 |
21 |
22 | {% if not government.active %}
23 |
Dieses Vorhaben gehörte zur {{ government.name }} und wird nicht mehr aktualisiert.
24 | {% endif %}
25 |
{{ object.title }}
26 |
27 |
28 |
29 | {{ object.get_status_display }}
30 |
31 | {% for cat in object.categories.all %}
32 |
33 | {{ cat.name }}
35 |
36 | {% endfor %}
37 |
38 | {% if government.active %}
39 |
{% show_follow "govplan" object %}
40 | {% endif %}
41 |
42 |
43 |
44 |
45 | {% if object.quote %}
46 | Ausschnitt aus dem Koalitionsvertrag
47 |
48 |
49 | {{ object.quote | addquotes | markdown }}
50 |
51 | {% with refs=object.get_reference_links %}
52 | {% if refs %}
53 |
54 |
55 | {% if refs|length > 1 %}
56 | Quellen:
57 | {% else %}
58 | Quelle:
59 | {% endif %}
60 |
61 | {% for ref in refs %}
62 | {{ forloop.counter }}
63 | {% endfor %}
64 |
65 | {% endif %}
66 | {% endwith %}
67 | {% endif %}
68 |
69 | {% if object.description %}
70 |
71 |
Unsere Einschätzung
72 |
73 | {{ object.description | safe }}
74 |
75 |
76 | {% endif %}
77 |
78 |
79 |
144 |
145 |
146 |
147 |
148 |
149 | {% include "froide_govplan/plugins/updates.html" with wrapper_classes="col col-12 col-lg-6 d-flex mb-4" %}
150 | {% if government.active %}
151 |
152 |
153 |
154 |
155 |
Neue Entwicklung melden
156 |
157 |
158 | {% if request.user.is_authenticated %}
159 |
Gibt es Neuigkeiten zu diesem Vorhaben, die wir noch nicht erfasst haben?
160 |
Entwicklung melden
164 |
169 |
170 |
171 |
178 |
179 |
185 |
186 |
187 |
188 |
189 | {% else %}
190 | Bitte
melden Sie sich an , um einen Änderungsvorschlag einzureichen.
191 | {% endif %}
192 |
193 |
194 |
195 |
196 | {% endif %}
197 |
198 |
199 | {% endblock app_body %}
200 |
--------------------------------------------------------------------------------
/froide_govplan/migrations/0003_auto_20220228_1051.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.12 on 2022-02-28 09:51
2 |
3 | import django.db.models.deletion
4 | import django.utils.timezone
5 | from django.conf import settings
6 | from django.db import migrations, models
7 |
8 | from .. import conf
9 |
10 |
11 | class Migration(migrations.Migration):
12 |
13 | dependencies = [
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ("publicbody", "0039_publicbody_alternative_emails"),
16 | ("organization", "0001_initial"),
17 | ("auth", "0012_alter_user_first_name_max_length"),
18 | ("froide_govplan", "0002_auto_20220215_2113"),
19 | ] + (
20 | [("foirequest", "0054_alter_foirequest_options")]
21 | if conf.GOVPLAN_ENABLE_FOIREQUEST
22 | else []
23 | )
24 |
25 | operations = (
26 | [
27 | migrations.AddField(
28 | model_name="governmentplan",
29 | name="due_date",
30 | field=models.DateField(blank=True, null=True, verbose_name="due date"),
31 | ),
32 | migrations.AddField(
33 | model_name="governmentplan",
34 | name="measure",
35 | field=models.CharField(
36 | blank=True, max_length=255, verbose_name="measure"
37 | ),
38 | ),
39 | migrations.AddField(
40 | model_name="governmentplan",
41 | name="quote",
42 | field=models.TextField(blank=True, verbose_name="quote"),
43 | ),
44 | migrations.AlterField(
45 | model_name="government",
46 | name="description",
47 | field=models.TextField(blank=True, verbose_name="description"),
48 | ),
49 | migrations.AlterField(
50 | model_name="government",
51 | name="end_date",
52 | field=models.DateField(blank=True, null=True, verbose_name="end date"),
53 | ),
54 | migrations.AlterField(
55 | model_name="government",
56 | name="jurisdiction",
57 | field=models.ForeignKey(
58 | null=True,
59 | on_delete=django.db.models.deletion.SET_NULL,
60 | to="publicbody.jurisdiction",
61 | verbose_name="jurisdiction",
62 | ),
63 | ),
64 | migrations.AlterField(
65 | model_name="government",
66 | name="name",
67 | field=models.CharField(max_length=255, verbose_name="name"),
68 | ),
69 | migrations.AlterField(
70 | model_name="government",
71 | name="planning_document",
72 | field=models.URLField(blank=True, verbose_name="planning document"),
73 | ),
74 | migrations.AlterField(
75 | model_name="government",
76 | name="public",
77 | field=models.BooleanField(default=False, verbose_name="is public?"),
78 | ),
79 | migrations.AlterField(
80 | model_name="government",
81 | name="slug",
82 | field=models.SlugField(
83 | max_length=255, unique=True, verbose_name="slug"
84 | ),
85 | ),
86 | migrations.AlterField(
87 | model_name="government",
88 | name="start_date",
89 | field=models.DateField(
90 | blank=True, null=True, verbose_name="start date"
91 | ),
92 | ),
93 | migrations.AlterField(
94 | model_name="governmentplan",
95 | name="description",
96 | field=models.TextField(blank=True, verbose_name="description"),
97 | ),
98 | migrations.AlterField(
99 | model_name="governmentplan",
100 | name="government",
101 | field=models.ForeignKey(
102 | on_delete=django.db.models.deletion.CASCADE,
103 | to="froide_govplan.government",
104 | verbose_name="government",
105 | ),
106 | ),
107 | migrations.AlterField(
108 | model_name="governmentplan",
109 | name="group",
110 | field=models.ForeignKey(
111 | blank=True,
112 | null=True,
113 | on_delete=django.db.models.deletion.SET_NULL,
114 | to="auth.group",
115 | verbose_name="group",
116 | ),
117 | ),
118 | migrations.AlterField(
119 | model_name="governmentplan",
120 | name="organization",
121 | field=models.ForeignKey(
122 | blank=True,
123 | null=True,
124 | on_delete=django.db.models.deletion.SET_NULL,
125 | to="organization.organization",
126 | verbose_name="organization",
127 | ),
128 | ),
129 | migrations.AlterField(
130 | model_name="governmentplan",
131 | name="public",
132 | field=models.BooleanField(default=False, verbose_name="is public?"),
133 | ),
134 | migrations.AlterField(
135 | model_name="governmentplan",
136 | name="rating",
137 | field=models.IntegerField(
138 | blank=True,
139 | choices=[
140 | (1, "terrible"),
141 | (2, "bad"),
142 | (3, "OK"),
143 | (4, "good"),
144 | (5, "excellent"),
145 | ],
146 | null=True,
147 | verbose_name="rating",
148 | ),
149 | ),
150 | migrations.AlterField(
151 | model_name="governmentplan",
152 | name="reference",
153 | field=models.CharField(
154 | blank=True, max_length=255, verbose_name="reference"
155 | ),
156 | ),
157 | migrations.AlterField(
158 | model_name="governmentplan",
159 | name="responsible_publicbody",
160 | field=models.ForeignKey(
161 | blank=True,
162 | null=True,
163 | on_delete=django.db.models.deletion.SET_NULL,
164 | to="publicbody.publicbody",
165 | verbose_name="responsible public body",
166 | ),
167 | ),
168 | migrations.AlterField(
169 | model_name="governmentplan",
170 | name="slug",
171 | field=models.SlugField(
172 | max_length=255, unique=True, verbose_name="slug"
173 | ),
174 | ),
175 | migrations.AlterField(
176 | model_name="governmentplan",
177 | name="status",
178 | field=models.CharField(
179 | choices=[
180 | ("not_started", "not started"),
181 | ("started", "started"),
182 | ("partially_implemented", "partially implemented"),
183 | ("implemented", "implemented"),
184 | ("deferred", "deferred"),
185 | ],
186 | default="not_started",
187 | max_length=25,
188 | verbose_name="status",
189 | ),
190 | ),
191 | migrations.AlterField(
192 | model_name="governmentplan",
193 | name="title",
194 | field=models.CharField(max_length=255, verbose_name="title"),
195 | ),
196 | migrations.AlterField(
197 | model_name="governmentplanupdate",
198 | name="content",
199 | field=models.TextField(blank=True, verbose_name="content"),
200 | ),
201 | ]
202 | + (
203 | [
204 | migrations.AlterField(
205 | model_name="governmentplanupdate",
206 | name="foirequest",
207 | field=models.ForeignKey(
208 | blank=True,
209 | null=True,
210 | on_delete=django.db.models.deletion.SET_NULL,
211 | to="foirequest.foirequest",
212 | verbose_name="FOI request",
213 | ),
214 | ),
215 | ]
216 | if conf.GOVPLAN_ENABLE_FOIREQUEST
217 | else []
218 | )
219 | + [
220 | migrations.AlterField(
221 | model_name="governmentplanupdate",
222 | name="organization",
223 | field=models.ForeignKey(
224 | blank=True,
225 | null=True,
226 | on_delete=django.db.models.deletion.SET_NULL,
227 | to="organization.organization",
228 | verbose_name="organization",
229 | ),
230 | ),
231 | migrations.AlterField(
232 | model_name="governmentplanupdate",
233 | name="plan",
234 | field=models.ForeignKey(
235 | on_delete=django.db.models.deletion.CASCADE,
236 | related_name="updates",
237 | to="froide_govplan.governmentplan",
238 | verbose_name="plan",
239 | ),
240 | ),
241 | migrations.AlterField(
242 | model_name="governmentplanupdate",
243 | name="public",
244 | field=models.BooleanField(default=False, verbose_name="is public?"),
245 | ),
246 | migrations.AlterField(
247 | model_name="governmentplanupdate",
248 | name="rating",
249 | field=models.IntegerField(
250 | blank=True,
251 | choices=[
252 | (1, "terrible"),
253 | (2, "bad"),
254 | (3, "OK"),
255 | (4, "good"),
256 | (5, "excellent"),
257 | ],
258 | null=True,
259 | verbose_name="rating",
260 | ),
261 | ),
262 | migrations.AlterField(
263 | model_name="governmentplanupdate",
264 | name="status",
265 | field=models.CharField(
266 | blank=True,
267 | choices=[
268 | ("not_started", "not started"),
269 | ("started", "started"),
270 | ("partially_implemented", "partially implemented"),
271 | ("implemented", "implemented"),
272 | ("deferred", "deferred"),
273 | ],
274 | default="",
275 | max_length=25,
276 | verbose_name="status",
277 | ),
278 | ),
279 | migrations.AlterField(
280 | model_name="governmentplanupdate",
281 | name="timestamp",
282 | field=models.DateTimeField(
283 | default=django.utils.timezone.now, verbose_name="timestamp"
284 | ),
285 | ),
286 | migrations.AlterField(
287 | model_name="governmentplanupdate",
288 | name="title",
289 | field=models.CharField(
290 | blank=True, max_length=1024, verbose_name="title"
291 | ),
292 | ),
293 | migrations.AlterField(
294 | model_name="governmentplanupdate",
295 | name="url",
296 | field=models.URLField(blank=True, verbose_name="URL"),
297 | ),
298 | migrations.AlterField(
299 | model_name="governmentplanupdate",
300 | name="user",
301 | field=models.ForeignKey(
302 | blank=True,
303 | null=True,
304 | on_delete=django.db.models.deletion.SET_NULL,
305 | to=settings.AUTH_USER_MODEL,
306 | verbose_name="user",
307 | ),
308 | ),
309 | ]
310 | )
311 |
--------------------------------------------------------------------------------
/froide_govplan/locale/de/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: \n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2022-11-23 13:50+0100\n"
11 | "PO-Revision-Date: 2022-11-23 13:51+0100\n"
12 | "Last-Translator: Stefan Wehrmeyer \n"
13 | "Language-Team: \n"
14 | "Language: de\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
19 | "X-Generator: Poedit 3.1.1\n"
20 |
21 | #: froide_govplan/admin.py
22 | msgid "Assign organization..."
23 | msgstr "Organisation zuweisen..."
24 |
25 | #: froide_govplan/admin.py
26 | msgid "Assign permission group..."
27 | msgstr "Berechtigungsgruppe zuweisen..."
28 |
29 | #: froide_govplan/admin.py
30 | msgid "Has change proposals"
31 | msgstr "Hat Update-Vorschläge"
32 |
33 | #: froide_govplan/admin.py
34 | msgid "category(s)"
35 | msgstr "Kategorie(n)"
36 |
37 | #: froide_govplan/admin.py
38 | msgid "Make public"
39 | msgstr "Veröffentlichen"
40 |
41 | #: froide_govplan/admin.py
42 | msgid "The proposal has been deleted."
43 | msgstr "Der Vorschlag wurde gelöscht."
44 |
45 | #: froide_govplan/admin.py
46 | msgid "An unpublished update has been created."
47 | msgstr "Eine unveröffentlichtes Update wurde erstellt."
48 |
49 | #: froide_govplan/apps.py
50 | msgid "GovPlan App"
51 | msgstr "Regierungsvorhaben-App"
52 |
53 | #: froide_govplan/cms_apps.py
54 | msgid "GovPlan CMS App"
55 | msgstr "Regierungsvorhaben-CMS-App"
56 |
57 | #: froide_govplan/cms_plugins.py froide_govplan/configuration.py
58 | #: froide_govplan/models.py
59 | msgid "Government plans"
60 | msgstr "Regierungsvorhaben"
61 |
62 | #: froide_govplan/cms_plugins.py froide_govplan/models.py
63 | msgid "Government plan sections"
64 | msgstr "Regierungsvorhaben-Themenbereiche"
65 |
66 | #: froide_govplan/cms_plugins.py
67 | msgid "Government plan updates"
68 | msgstr "Entwicklungen bei Regierungsvorhaben"
69 |
70 | #: froide_govplan/cms_toolbars.py
71 | msgid "Edit plans and updates"
72 | msgstr "Übersicht öffnen"
73 |
74 | #: froide_govplan/cms_toolbars.py
75 | msgid "Edit government plan"
76 | msgstr "Bearbeite Regierungsvorhaben"
77 |
78 | #: froide_govplan/cms_toolbars.py
79 | msgid "Add update"
80 | msgstr "Neue Entwicklung hinzufügen"
81 |
82 | #: froide_govplan/configuration.py
83 | msgid "You are now following this plan."
84 | msgstr "Sie folgen jetzt diesem Vorhaben."
85 |
86 | #: froide_govplan/configuration.py
87 | msgid "You are not following this plan anymore."
88 | msgstr "Sie folgen diesem Vorhaben nicht mehr."
89 |
90 | #: froide_govplan/configuration.py
91 | msgid ""
92 | "Check your emails and click the confirmation link in order to follow this "
93 | "government plan."
94 | msgstr ""
95 | "Rufen Sie Ihre E-Mails ab und klicken Sie auf den Bestätigungs-Link um "
96 | "Benachrichtigungen für dieses Regierungsvorhaben zu erhalten."
97 |
98 | #: froide_govplan/configuration.py
99 | msgid "Follow plan"
100 | msgstr "Vorhaben folgen"
101 |
102 | #: froide_govplan/configuration.py
103 | msgid "Follow plan?"
104 | msgstr "Vorhaben folgen?"
105 |
106 | #: froide_govplan/configuration.py
107 | msgid "Unfollow plan"
108 | msgstr "Vorhaben entfolgen"
109 |
110 | #: froide_govplan/configuration.py
111 | msgid "Following plan"
112 | msgstr "Vorhaben folgend"
113 |
114 | #: froide_govplan/configuration.py
115 | msgid ""
116 | "You will get notifications via email when something new happens with this "
117 | "plan. You can unsubscribe anytime."
118 | msgstr ""
119 | "Sie erhalten Benachrichtigungen per E-Mail, sobald es Neuigkeiten zu diesem "
120 | "Vorhaben gibt. Sie können die Benachrichtigungen jederzeit abbestellen."
121 |
122 | #: froide_govplan/configuration.py
123 | #, python-brace-format
124 | msgid ""
125 | "please confirm that you want to follow the plan “{title}” by clicking this "
126 | "link:"
127 | msgstr ""
128 | "Bitte bestätige, dass du dem Regierungsvorhaben „{title}“ folgen willst, in "
129 | "dem du diesen Link anklickst:"
130 |
131 | #: froide_govplan/configuration.py
132 | msgid "Government Plans"
133 | msgstr "Regierungsvorhaben"
134 |
135 | #: froide_govplan/configuration.py
136 | #, python-brace-format
137 | msgid "An update was posted for the government plan “{title}”."
138 | msgstr "Es gibt Neuigkeiten zum Regierungsvorhaben „{title}”."
139 |
140 | #: froide_govplan/forms.py froide_govplan/models.py
141 | msgid "title"
142 | msgstr "Titel"
143 |
144 | #: froide_govplan/forms.py
145 | msgid "Summarize the update in a title."
146 | msgstr "Fasse die Entwicklung in einem Titel zusammen."
147 |
148 | #: froide_govplan/forms.py
149 | msgid "details"
150 | msgstr "Details"
151 |
152 | #: froide_govplan/forms.py
153 | msgid "Optionally give more details."
154 | msgstr "Optional kannst du noch mehr Details angeben."
155 |
156 | #: froide_govplan/forms.py
157 | msgid "source URL"
158 | msgstr "Beleg-URL"
159 |
160 | #: froide_govplan/forms.py
161 | msgid "Please give provide a link."
162 | msgstr "Bitte gib einen Link an."
163 |
164 | #: froide_govplan/forms.py froide_govplan/models.py
165 | msgid "status"
166 | msgstr "Status"
167 |
168 | #: froide_govplan/forms.py
169 | msgid "Has the status of the plan changed?"
170 | msgstr "Hat sich der Status des Vorhabens geändert?"
171 |
172 | #: froide_govplan/forms.py froide_govplan/models.py
173 | msgid "rating"
174 | msgstr "Bewertung"
175 |
176 | #: froide_govplan/forms.py
177 | msgid "What's your rating of the current implementation?"
178 | msgstr "Wie würdest du die aktuelle Umsetzung bewerten?"
179 |
180 | #: froide_govplan/forms.py
181 | #, python-format
182 | msgid "Your proposal for the plan “%s” was accepted"
183 | msgstr "Ihr Vorschlag für das Vorhaben „%s“ wurde angenommen"
184 |
185 | #: froide_govplan/forms.py
186 | #, python-brace-format
187 | msgid ""
188 | "Hello,\n"
189 | "\n"
190 | "A moderator has accepted your proposal for an update to the plan “{title}”. "
191 | "An update will be published soon.\n"
192 | "\n"
193 | "All the Best,\n"
194 | "{site_name}"
195 | msgstr ""
196 | "Hallo,\n"
197 | "\n"
198 | "eine Moderationsperson hat Ihren Vorschlag für eine Aktualisierung des "
199 | "Regierungsvorhabens „{title}“ angenommen. Die neue Entwicklung wird "
200 | "demnächst veröffentlicht.\n"
201 | "\n"
202 | "Beste Grüße\n"
203 | "{site_name}"
204 |
205 | #: froide_govplan/models.py
206 | msgid "not started"
207 | msgstr "nicht begonnen"
208 |
209 | #: froide_govplan/models.py
210 | msgid "started"
211 | msgstr "begonnen"
212 |
213 | #: froide_govplan/models.py
214 | msgid "partially implemented"
215 | msgstr "teilweise umgesetzt"
216 |
217 | #: froide_govplan/models.py
218 | msgid "implemented"
219 | msgstr "umgesetzt"
220 |
221 | #: froide_govplan/models.py
222 | msgid "deferred"
223 | msgstr "verschoben"
224 |
225 | #: froide_govplan/models.py
226 | msgid "terrible"
227 | msgstr "sehr schlecht"
228 |
229 | #: froide_govplan/models.py
230 | msgid "bad"
231 | msgstr "schlecht"
232 |
233 | #: froide_govplan/models.py
234 | msgid "OK"
235 | msgstr "mittelmäßig"
236 |
237 | #: froide_govplan/models.py
238 | msgid "good"
239 | msgstr "gut"
240 |
241 | #: froide_govplan/models.py
242 | msgid "excellent"
243 | msgstr "sehr gut"
244 |
245 | #: froide_govplan/models.py
246 | msgid "name"
247 | msgstr "Name"
248 |
249 | #: froide_govplan/models.py
250 | msgid "slug"
251 | msgstr "URL-Kürzel"
252 |
253 | #: froide_govplan/models.py
254 | msgid "is public?"
255 | msgstr "Ist öffentlich?"
256 |
257 | #: froide_govplan/models.py
258 | msgid "jurisdiction"
259 | msgstr "Zuständigkeit"
260 |
261 | #: froide_govplan/models.py
262 | msgid "description"
263 | msgstr "Beschreibung"
264 |
265 | #: froide_govplan/models.py
266 | msgid "start date"
267 | msgstr "Startdatum"
268 |
269 | #: froide_govplan/models.py
270 | msgid "end date"
271 | msgstr "Enddatum"
272 |
273 | #: froide_govplan/models.py
274 | msgid "planning document"
275 | msgstr "Planungsdokument"
276 |
277 | #: froide_govplan/models.py
278 | msgid "Government"
279 | msgstr "Regierung"
280 |
281 | #: froide_govplan/models.py
282 | msgid "Governments"
283 | msgstr "Regierungen"
284 |
285 | #: froide_govplan/models.py
286 | msgid "Categorized Government Plan"
287 | msgstr "Kategorisiertes Regierungsvorhaben"
288 |
289 | #: froide_govplan/models.py
290 | msgid "Categorized Government Plans"
291 | msgstr "Kategorisierte Regierungsvorhaben"
292 |
293 | #: froide_govplan/models.py
294 | msgid "government"
295 | msgstr "Regierung"
296 |
297 | #: froide_govplan/models.py
298 | msgid "image"
299 | msgstr "Bild"
300 |
301 | #: froide_govplan/models.py
302 | msgid "quote"
303 | msgstr "Zitat"
304 |
305 | #: froide_govplan/models.py
306 | msgid "due date"
307 | msgstr "Frist"
308 |
309 | #: froide_govplan/models.py
310 | msgid "measure"
311 | msgstr "Art des Vorhabens"
312 |
313 | #: froide_govplan/models.py
314 | msgid "reference"
315 | msgstr "Fundstelle"
316 |
317 | #: froide_govplan/models.py
318 | msgid "categories"
319 | msgstr "Kategorien"
320 |
321 | #: froide_govplan/models.py
322 | msgid "responsible public body"
323 | msgstr "verantwortliche Behörde"
324 |
325 | #: froide_govplan/models.py
326 | msgid "organization"
327 | msgstr "Organisation"
328 |
329 | #: froide_govplan/models.py
330 | msgid "group"
331 | msgstr "Gruppe"
332 |
333 | #: froide_govplan/models.py
334 | msgid "Government plan"
335 | msgstr "Regierungsvorhaben"
336 |
337 | #: froide_govplan/models.py
338 | msgid "plan"
339 | msgstr "Vorhaben"
340 |
341 | #: froide_govplan/models.py
342 | msgid "user"
343 | msgstr "Nutzer:in"
344 |
345 | #: froide_govplan/models.py
346 | msgid "timestamp"
347 | msgstr "Zeitpunkt"
348 |
349 | #: froide_govplan/models.py
350 | msgid "content"
351 | msgstr "Inhalt"
352 |
353 | #: froide_govplan/models.py
354 | msgid "URL"
355 | msgstr "URL"
356 |
357 | #: froide_govplan/models.py
358 | msgid "FOI request"
359 | msgstr "IFG-Anfrage"
360 |
361 | #: froide_govplan/models.py
362 | msgid "Plan update"
363 | msgstr "Entwicklung bei Vorhaben"
364 |
365 | #: froide_govplan/models.py
366 | msgid "Plan updates"
367 | msgstr "Entwicklungen bei Vorhaben"
368 |
369 | #: froide_govplan/models.py
370 | msgid "Government plan follower"
371 | msgstr "Regierungsvorhaben-Follower:in"
372 |
373 | #: froide_govplan/models.py
374 | msgid "Government plan followers"
375 | msgstr "Regierungsvorhaben-Follower:innen"
376 |
377 | #: froide_govplan/models.py
378 | msgid "Icon"
379 | msgstr "Icon"
380 |
381 | #: froide_govplan/models.py
382 | msgid ""
383 | "Enter an icon name from the FontAwesome 4 icon set "
385 | msgstr ""
386 | "Gib ein Icon-Namen aus dem FontAwesome 4 Icon-Set an"
388 |
389 | #: froide_govplan/models.py
390 | msgid "Government plan section"
391 | msgstr "Regierungsvorhaben-Themenbereich"
392 |
393 | #: froide_govplan/models.py
394 | msgid "Normal"
395 | msgstr "Standard"
396 |
397 | #: froide_govplan/models.py
398 | msgid "Progress"
399 | msgstr "Fortschritt"
400 |
401 | #: froide_govplan/models.py
402 | msgid "Progress Row"
403 | msgstr "Fortschritt Zeile"
404 |
405 | #: froide_govplan/models.py
406 | msgid "Time used"
407 | msgstr "Vergangene Zeit"
408 |
409 | #: froide_govplan/models.py
410 | msgid "Card columns"
411 | msgstr "Karten-Spalten"
412 |
413 | #: froide_govplan/models.py
414 | #: froide_govplan/templates/froide_govplan/plugins/search.html
415 | msgid "Search"
416 | msgstr "Suchen"
417 |
418 | #: froide_govplan/models.py
419 | msgid "number of plans"
420 | msgstr "Anzahl der Vorhaben"
421 |
422 | #: froide_govplan/models.py
423 | msgid "0 means all the plans"
424 | msgstr "0 bedeutet alle Vorhaben"
425 |
426 | #: froide_govplan/models.py
427 | msgid "offset"
428 | msgstr "Start"
429 |
430 | #: froide_govplan/models.py
431 | msgid "number of plans to skip from top of list"
432 | msgstr "Anzahl der Vorhaben, die ausgelassen werden"
433 |
434 | #: froide_govplan/models.py
435 | msgid "template"
436 | msgstr "Template"
437 |
438 | #: froide_govplan/models.py
439 | msgid "template used to display the plugin"
440 | msgstr "Template, mit dem das Plugin angezeigt wird"
441 |
442 | #: froide_govplan/models.py
443 | msgid "All matching plans"
444 | msgstr "Alle zutreffenden Vorhaben"
445 |
446 | #: froide_govplan/models.py
447 | #, python-format
448 | msgid "%s matching plans"
449 | msgstr "%s zutreffenden Vorhaben"
450 |
451 | #: froide_govplan/models.py
452 | msgid "number of updates"
453 | msgstr "Anzahl der Entwicklungen"
454 |
455 | #: froide_govplan/models.py
456 | msgid "0 means all the updates"
457 | msgstr "0 bedeutet alle Entwicklungen"
458 |
459 | #: froide_govplan/models.py
460 | msgid "number of updates to skip from top of list"
461 | msgstr "Anzahl der Entwicklungen, die ausgelassen werden"
462 |
463 | #: froide_govplan/models.py
464 | msgid "All matching updates"
465 | msgstr "Alle zutreffenden Entwicklungen"
466 |
467 | #: froide_govplan/models.py
468 | #, python-format
469 | msgid "%s matching updates"
470 | msgstr "%s zutreffende Entwicklungen"
471 |
472 | #: froide_govplan/templates/admin/froide_govplan/governmentplan/change_form.html
473 | msgid "See update proposals"
474 | msgstr "Update-Vorschläge ansehen"
475 |
476 | #: froide_govplan/templates/froide_govplan/admin/_proposal.html
477 | msgid "Field"
478 | msgstr "Feld"
479 |
480 | #: froide_govplan/templates/froide_govplan/admin/_proposal.html
481 | msgid "Proposal"
482 | msgstr "Vorschlag"
483 |
484 | #: froide_govplan/templates/froide_govplan/admin/_proposal.html
485 | msgid "Turn into update draft"
486 | msgstr "In Update-Entwurf umwandeln"
487 |
488 | #: froide_govplan/templates/froide_govplan/admin/_proposal.html
489 | msgid "Delete proposal"
490 | msgstr "Vorschlag löschen"
491 |
492 | #: froide_govplan/templates/froide_govplan/admin/_proposal.html
493 | msgid "Delete"
494 | msgstr "Löschen"
495 |
496 | #: froide_govplan/templates/froide_govplan/admin/accept_proposal.html
497 | msgid "Create update draft"
498 | msgstr "Update-Entwurf anlegen"
499 |
500 | #: froide_govplan/templates/froide_govplan/detail.html
501 | #: froide_govplan/templates/froide_govplan/plugins/search.html
502 | msgid "Close"
503 | msgstr "Schließen"
504 |
505 | #: froide_govplan/templates/froide_govplan/plugins/card_cols.html
506 | msgid ""
507 | "Could not find any results. Try different keywords or browse the categories."
508 | msgstr ""
509 | "Keine Ergebnisse gefunden. Versuchen Sie es mit anderen Stichworten oder "
510 | "durchsuchen Sie passende Kategorien."
511 |
512 | #: froide_govplan/templates/froide_govplan/plugins/search.html
513 | msgid "Search query"
514 | msgstr "Suchbegriff"
515 |
516 | #: froide_govplan/templates/froide_govplan/plugins/search.html
517 | msgid "Filter status"
518 | msgstr "Status filtern"
519 |
520 | #: froide_govplan/templates/froide_govplan/plugins/search.html
521 | msgid "Search results"
522 | msgstr "Suchergebnisse"
523 |
524 | #: froide_govplan/templates/froide_govplan/plugins/time_used.html
525 | #, python-format
526 | msgid "One day left"
527 | msgid_plural "%(days)s days left"
528 | msgstr[0] "Ein Tag übrig"
529 | msgstr[1] "%(days)s Tage übrig"
530 |
531 | #: froide_govplan/templates/froide_govplan/section.html
532 | #, python-format
533 | msgid ""
534 | "\n"
535 | " Here you can find all plans of the section “%(section)s”, "
536 | "which the coalition constituted in their agreement. On the curresponding "
537 | "detail pages, you'll get more information, stay up to date with news or "
538 | "submit changes.\n"
539 | " "
540 | msgstr ""
541 | "\n"
542 | " Hier finden Sie alle Vorhaben aus dem Bereich „%(section)s“, welche die "
543 | "Regierungskoalition im Koalitionsvertrag festgelegt hat. Auf den jeweiligen "
544 | "Detailseiten erhalten Sie mehr Informationen, können Neuigkeiten abonnieren "
545 | "oder Änderungen einreichen. "
546 |
547 | #: froide_govplan/urls.py
548 | msgctxt "url part"
549 | msgid "/plan//"
550 | msgstr "/vorhaben//"
551 |
552 | #: froide_govplan/urls.py
553 | msgctxt "url part"
554 | msgid "/plan//_og/"
555 | msgstr "/vorhaben//_og/"
556 |
557 | #: froide_govplan/urls.py
558 | msgctxt "url part"
559 | msgid "/plan//propose-update/"
560 | msgstr "/vorhaben//entwicklung-melden/"
561 |
562 | #: froide_govplan/urls.py
563 | msgctxt "url part"
564 | msgid "//"
565 | msgstr "//"
566 |
567 | #: froide_govplan/urls.py
568 | msgctxt "url part"
569 | msgid "//_og/"
570 | msgstr "//_og/"
571 |
572 | #: froide_govplan/views.py
573 | msgid ""
574 | "Thank you for your proposal. We will send you an email when it has been "
575 | "approved."
576 | msgstr ""
577 | "Danke für Deinen Vorschlag. Wir senden Dir eine E-Mail, sobald dein Eintrag "
578 | "veröffentlicht wird."
579 |
580 | #: froide_govplan/views.py
581 | msgid "There's been an error with your form submission."
582 | msgstr "Es gab einen Fehler in Ihrer Formulareingabe."
583 |
584 | #~ msgid ""
585 | #~ "Please give a reason for not accepting this proposal. It will be send to "
586 | #~ "the user who made the proposal."
587 | #~ msgstr ""
588 | #~ "Bitte geben Sie einen Grund an, warum Sie diese Einreichung nicht "
589 | #~ "angenommen haben. Der/die Einreicher/in wird darüber informiert."
590 |
591 | #~ msgid "We could not accept this update because..."
592 | #~ msgstr "Wir konnten diese Einreichung nicht annehmen, weil..."
593 |
594 | #~ msgid "Delete proposed update"
595 | #~ msgstr "Vorgeschlagene Entwicklung löschen"
596 |
597 | #~ msgid "Search all government plans…"
598 | #~ msgstr "Durchsuche alle Regierungsvorhaben…"
599 |
600 | #~ msgid "GovPlan"
601 | #~ msgstr "Regierungsvorhaben"
602 |
--------------------------------------------------------------------------------
/froide_govplan/models.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import re
3 | from datetime import timedelta
4 | from urllib.parse import urlparse
5 |
6 | from django.conf import settings
7 | from django.contrib.auth.models import Group
8 | from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector
9 | from django.db import models
10 | from django.urls import reverse
11 | from django.utils import timezone
12 | from django.utils.functional import cached_property
13 | from django.utils.translation import gettext_lazy as _
14 | from filer.fields.image import FilerImageField
15 | from froide.follow.models import Follower
16 | from froide.georegion.models import GeoRegion
17 | from froide.organization.models import Organization
18 | from froide.publicbody.models import Category, Jurisdiction, PublicBody
19 | from taggit.managers import TaggableManager
20 | from taggit.models import TaggedItemBase
21 |
22 | from . import conf
23 | from .utils import make_request_url
24 |
25 | try:
26 | from cms.models.fields import PlaceholderRelationField
27 | from cms.utils.placeholder import get_placeholder_from_slot
28 | from cms.models.pluginmodel import CMSPlugin
29 | except ImportError:
30 | CMSPlugin = None
31 | PlaceholderRelationField = None
32 |
33 |
34 | if conf.GOVPLAN_ENABLE_FOIREQUEST:
35 | from froide.foirequest.models import FoiRequest
36 | else:
37 | FoiRequest = None
38 |
39 |
40 | class PlanStatus(models.TextChoices):
41 | NOT_STARTED = ("not_started", _("not started"))
42 | STARTED = ("started", _("started"))
43 | PARTIALLY_IMPLEMENTED = ("partially_implemented", _("partially implemented"))
44 | IMPLEMENTED = ("implemented", _("implemented"))
45 | DEFERRED = ("deferred", _("deferred"))
46 |
47 |
48 | STATUS_CSS = {
49 | PlanStatus.NOT_STARTED: "light",
50 | PlanStatus.STARTED: "primary",
51 | PlanStatus.PARTIALLY_IMPLEMENTED: "warning",
52 | PlanStatus.IMPLEMENTED: "success",
53 | PlanStatus.DEFERRED: "danger",
54 | }
55 |
56 |
57 | class PlanRating(models.IntegerChoices):
58 | TERRIBLE = 1, _("terrible")
59 | BAD = 2, _("bad")
60 | OK = 3, _("OK")
61 | GOOD = 4, _("good")
62 | EXCELLENT = 5, _("excellent")
63 |
64 |
65 | class Government(models.Model):
66 | name = models.CharField(max_length=255, verbose_name=_("name"))
67 | slug = models.SlugField(max_length=255, unique=True, verbose_name=_("slug"))
68 |
69 | public = models.BooleanField(default=False, verbose_name=_("is public?"))
70 | jurisdiction = models.ForeignKey(
71 | Jurisdiction,
72 | blank=True,
73 | null=True,
74 | on_delete=models.SET_NULL,
75 | verbose_name=_("jurisdiction"),
76 | )
77 | georegion = models.ForeignKey(
78 | GeoRegion,
79 | blank=True,
80 | null=True,
81 | on_delete=models.SET_NULL,
82 | verbose_name=_("georegion"),
83 | )
84 | description = models.TextField(blank=True, verbose_name=_("description"))
85 |
86 | start_date = models.DateField(null=True, blank=True, verbose_name=_("start date"))
87 | end_date = models.DateField(null=True, blank=True, verbose_name=_("end date"))
88 | active = models.BooleanField(default=True, verbose_name=_("active"))
89 |
90 | planning_document = models.URLField(blank=True, verbose_name=_("planning document"))
91 |
92 | class Meta:
93 | verbose_name = _("Government")
94 | verbose_name_plural = _("Governments")
95 |
96 | def __str__(self):
97 | return self.name
98 |
99 | def get_absolute_url(self):
100 | if self.planning_document:
101 | return self.planning_document
102 | # No planning document? Just return the base URL
103 | url = reverse("govplan:search")
104 | return url.rsplit("/", 2)[0]
105 |
106 | @property
107 | def days_available(self):
108 | if self.start_date is None:
109 | return 0
110 | if self.end_date is None:
111 | return 0
112 | return (self.end_date - self.start_date).days
113 |
114 | @property
115 | def days_left(self):
116 | if not self.end_date:
117 | return 0
118 | now = timezone.now().date()
119 | if now > self.end_date:
120 | return 0
121 | return (self.end_date - now).days
122 |
123 | def days_used_percentage(self) -> int:
124 | total_days = self.days_available
125 | if not total_days:
126 | return 0
127 | now = timezone.now().date()
128 | days_used = (now - self.start_date).days
129 | return int(days_used / total_days * 100)
130 |
131 |
132 | class CategorizedGovernmentPlan(TaggedItemBase):
133 | tag = models.ForeignKey(
134 | Category, on_delete=models.CASCADE, related_name="categorized_governmentplan"
135 | )
136 | content_object = models.ForeignKey("GovernmentPlan", on_delete=models.CASCADE)
137 |
138 | class Meta:
139 | verbose_name = _("Categorized Government Plan")
140 | verbose_name_plural = _("Categorized Government Plans")
141 |
142 |
143 | WORD_RE = re.compile(r"^\w+$", re.IGNORECASE)
144 |
145 |
146 | class GovernmentPlanManager(models.Manager):
147 | SEARCH_LANG = "german"
148 |
149 | def get_search_vector(self):
150 | fields = [
151 | ("title", "A"),
152 | ("description", "B"),
153 | ("quote", "B"),
154 | ]
155 | return functools.reduce(
156 | lambda a, b: a + b,
157 | [SearchVector(f, weight=w, config=self.SEARCH_LANG) for f, w in fields],
158 | )
159 |
160 | def search(self, query, qs=None):
161 | if not qs:
162 | qs = self.get_queryset()
163 | query = query.strip()
164 | if not query:
165 | return qs
166 | search_queries = []
167 | for q in query.split():
168 | q = q.strip()
169 | if not q:
170 | continue
171 | if WORD_RE.match(q):
172 | sq = SearchQuery(
173 | "{}:*".format(q), search_type="raw", config=self.SEARCH_LANG
174 | )
175 | else:
176 | sq = SearchQuery(q, search_type="plain", config=self.SEARCH_LANG)
177 | search_queries.append(sq)
178 |
179 | if not search_queries:
180 | return qs
181 | search_query = functools.reduce(lambda a, b: a & b, search_queries)
182 |
183 | search_vector = self.get_search_vector()
184 | qs = (
185 | qs.annotate(rank=SearchRank(search_vector, search_query))
186 | .filter(rank__gte=0.1)
187 | .order_by("-rank")
188 | )
189 | return qs
190 |
191 |
192 | class GovernmentPlan(models.Model):
193 | government = models.ForeignKey(
194 | Government, on_delete=models.CASCADE, verbose_name=_("government")
195 | )
196 | title = models.CharField(max_length=255, verbose_name=_("title"))
197 | slug = models.SlugField(max_length=255, unique=True, verbose_name=_("slug"))
198 |
199 | image = FilerImageField(
200 | null=True,
201 | blank=True,
202 | default=None,
203 | verbose_name=_("image"),
204 | on_delete=models.SET_NULL,
205 | )
206 |
207 | description = models.TextField(blank=True, verbose_name=_("description"))
208 | quote = models.TextField(blank=True, verbose_name=_("quote"))
209 | public = models.BooleanField(default=False, verbose_name=_("is public?"))
210 | due_date = models.DateField(null=True, blank=True, verbose_name=_("due date"))
211 | measure = models.CharField(max_length=255, blank=True, verbose_name=_("measure"))
212 |
213 | status = models.CharField(
214 | max_length=25,
215 | choices=PlanStatus.choices,
216 | default="not_started",
217 | verbose_name=_("status"),
218 | )
219 | rating = models.IntegerField(
220 | choices=PlanRating.choices, null=True, blank=True, verbose_name=_("rating")
221 | )
222 |
223 | reference = models.CharField(
224 | max_length=255, blank=True, verbose_name=_("reference")
225 | )
226 |
227 | categories = TaggableManager(
228 | through=CategorizedGovernmentPlan, verbose_name=_("categories"), blank=True
229 | )
230 | responsible_publicbody = models.ForeignKey(
231 | PublicBody,
232 | null=True,
233 | blank=True,
234 | on_delete=models.SET_NULL,
235 | verbose_name=_("responsible public body"),
236 | )
237 |
238 | organization = models.ForeignKey(
239 | Organization,
240 | null=True,
241 | blank=True,
242 | on_delete=models.SET_NULL,
243 | verbose_name=_("organization"),
244 | )
245 |
246 | group = models.ForeignKey(
247 | Group, null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("group")
248 | )
249 |
250 | proposals = models.JSONField(blank=True, null=True)
251 | properties = models.JSONField(blank=True, default=dict)
252 |
253 | objects = GovernmentPlanManager()
254 |
255 | class Meta:
256 | ordering = ("reference", "title")
257 | verbose_name = _("Government plan")
258 | verbose_name_plural = _("Government plans")
259 |
260 | def __str__(self):
261 | return self.title
262 |
263 | def get_absolute_url(self):
264 | return reverse(
265 | "govplan:plan", kwargs={"gov": self.government.slug, "plan": self.slug}
266 | )
267 |
268 | def get_absolute_domain_url(self):
269 | return settings.SITE_URL + self.get_absolute_url()
270 |
271 | def get_absolute_short_url(self): # Return long url as short url for update emails
272 | return self.get_absolute_url()
273 |
274 | def get_absolute_domain_short_url(self):
275 | return settings.SITE_URL + self.get_absolute_short_url()
276 |
277 | def get_reference_links(self):
278 | if self.reference.startswith("https://"):
279 | return [self.reference]
280 | refs = [x.strip() for x in self.reference.split(",")]
281 | return [
282 | "{}#p-{}".format(self.government.planning_document, ref) for ref in refs
283 | ]
284 |
285 | def get_section(self):
286 | return GovernmentPlanSection.objects.filter(
287 | categories__in=self.categories.all()
288 | ).first()
289 |
290 | def update_from_updates(self):
291 | last_status_update = (
292 | self.updates.all().filter(public=True).exclude(status="").first()
293 | )
294 | if last_status_update:
295 | self.status = last_status_update.status
296 | last_rating_update = (
297 | self.updates.all().filter(public=True).exclude(rating=None).first()
298 | )
299 | if last_rating_update:
300 | self.rating = last_rating_update.rating
301 | if last_status_update or last_rating_update:
302 | self.save()
303 |
304 | def get_status_css(self):
305 | return STATUS_CSS.get(self.status, "")
306 |
307 | def make_request_url(self):
308 | if not self.responsible_publicbody:
309 | return []
310 | return make_request_url(self, self.responsible_publicbody)
311 |
312 | def has_recent_foirequest(self):
313 | frs = self.get_related_foirequests()
314 | ago = timezone.now() - timedelta(days=90)
315 | return any(fr.created_at > ago for fr in frs)
316 |
317 | def get_recent_foirequest(self):
318 | return self.get_related_foirequests()[0]
319 |
320 | def get_foirequest_reference(self):
321 | return "govplan:plan@{}".format(self.pk)
322 |
323 | def get_related_foirequests(self):
324 | if FoiRequest is None:
325 | return []
326 | if not self.responsible_publicbody:
327 | return []
328 | if hasattr(self, "_related_foirequests"):
329 | return self._related_foirequests
330 |
331 | self._related_foirequests = (
332 | FoiRequest.objects.filter(
333 | visibility=FoiRequest.VISIBILITY.VISIBLE_TO_PUBLIC,
334 | public_body=self.responsible_publicbody,
335 | )
336 | .filter(tags__name=conf.GOVPLAN_NAME)
337 | .filter(reference=self.get_foirequest_reference())
338 | .order_by("-created_at")
339 | )
340 | return self._related_foirequests
341 |
342 |
343 | class GovernmentPlanUpdate(models.Model):
344 | plan = models.ForeignKey(
345 | GovernmentPlan,
346 | on_delete=models.CASCADE,
347 | related_name="updates",
348 | verbose_name=_("plan"),
349 | )
350 | user = models.ForeignKey(
351 | settings.AUTH_USER_MODEL,
352 | null=True,
353 | blank=True,
354 | on_delete=models.SET_NULL,
355 | verbose_name=_("user"),
356 | )
357 | organization = models.ForeignKey(
358 | Organization,
359 | null=True,
360 | blank=True,
361 | on_delete=models.SET_NULL,
362 | verbose_name=_("organization"),
363 | )
364 | timestamp = models.DateTimeField(default=timezone.now, verbose_name=_("timestamp"))
365 | title = models.CharField(max_length=1024, blank=True, verbose_name=_("title"))
366 | content = models.TextField(blank=True, verbose_name=_("content"))
367 | url = models.URLField(blank=True, max_length=1024, verbose_name=_("URL"))
368 |
369 | status = models.CharField(
370 | max_length=25,
371 | choices=PlanStatus.choices,
372 | default="",
373 | blank=True,
374 | verbose_name=_("status"),
375 | )
376 | rating = models.IntegerField(
377 | choices=PlanRating.choices, null=True, blank=True, verbose_name=_("rating")
378 | )
379 | public = models.BooleanField(default=False, verbose_name=_("is public?"))
380 |
381 | if FoiRequest:
382 | foirequest = models.ForeignKey(
383 | FoiRequest,
384 | null=True,
385 | blank=True,
386 | on_delete=models.SET_NULL,
387 | verbose_name=_("FOI request"),
388 | )
389 |
390 | class Meta:
391 | ordering = ("-timestamp",)
392 | get_latest_by = "timestamp"
393 | verbose_name = _("Plan update")
394 | verbose_name_plural = _("Plan updates")
395 |
396 | def __str__(self):
397 | return "{} - {} ({})".format(self.title, self.timestamp, self.plan)
398 |
399 | def get_absolute_url(self):
400 | return "{}#update-{}".format(
401 | reverse(
402 | "govplan:plan",
403 | kwargs={"gov": self.plan.government.slug, "plan": self.plan.slug},
404 | ),
405 | self.id,
406 | )
407 |
408 | def get_absolute_domain_url(self):
409 | return settings.SITE_URL + self.get_absolute_url()
410 |
411 | def get_url_domain(self):
412 | return urlparse(self.url).netloc or None
413 |
414 |
415 | class GovernmentPlanFollower(Follower):
416 | content_object = models.ForeignKey(
417 | GovernmentPlan,
418 | on_delete=models.CASCADE,
419 | related_name="followers",
420 | verbose_name=_("Government plan"),
421 | )
422 |
423 | class Meta(Follower.Meta):
424 | verbose_name = _("Government plan follower")
425 | verbose_name_plural = _("Government plan followers")
426 |
427 |
428 | class GovernmentPlanSection(models.Model):
429 | government = models.ForeignKey(
430 | Government, on_delete=models.CASCADE, verbose_name=_("government")
431 | )
432 |
433 | title = models.CharField(max_length=255, verbose_name=_("title"))
434 | slug = models.SlugField(max_length=255, unique=True, verbose_name=_("slug"))
435 |
436 | categories = models.ManyToManyField(Category, blank=True)
437 |
438 | description = models.TextField(blank=True, verbose_name=_("description"))
439 | image = FilerImageField(
440 | null=True,
441 | blank=True,
442 | default=None,
443 | verbose_name=_("image"),
444 | on_delete=models.SET_NULL,
445 | )
446 |
447 | icon = models.CharField(
448 | _("Icon"),
449 | max_length=50,
450 | blank=True,
451 | help_text=_(
452 | """Enter an icon name from the FontAwesome 4 icon set """
453 | ),
454 | )
455 | order = models.PositiveIntegerField(default=0)
456 | featured = models.DateTimeField(null=True, blank=True)
457 |
458 | if PlaceholderRelationField:
459 | placeholders = PlaceholderRelationField()
460 |
461 | class Meta:
462 | verbose_name = _("Government plan section")
463 | verbose_name_plural = _("Government plan sections")
464 | ordering = (
465 | "order",
466 | "title",
467 | )
468 |
469 | def __str__(self):
470 | return self.title
471 |
472 | def get_absolute_url(self):
473 | return reverse(
474 | "govplan:section",
475 | kwargs={"gov": self.government.slug, "section": self.slug},
476 | )
477 |
478 | def get_absolute_domain_url(self):
479 | return settings.SITE_URL + self.get_absolute_url()
480 |
481 | @cached_property
482 | def content_placeholder(self):
483 | return get_placeholder_from_slot(self.placeholders, "content")
484 |
485 | def get_plans(self, queryset=None):
486 | if queryset is None:
487 | queryset = GovernmentPlan.objects.filter(public=True)
488 |
489 | queryset = queryset.filter(
490 | categories__in=self.categories.all(), government_id=self.government_id
491 | )
492 | return queryset.distinct().order_by("title")
493 |
494 |
495 | if CMSPlugin:
496 | PLUGIN_TEMPLATES = [
497 | ("froide_govplan/plugins/default.html", _("Normal")),
498 | ("froide_govplan/plugins/progress.html", _("Progress")),
499 | ("froide_govplan/plugins/progress_row.html", _("Progress Row")),
500 | ("froide_govplan/plugins/time_used.html", _("Time used")),
501 | ("froide_govplan/plugins/card_cols.html", _("Card columns")),
502 | ("froide_govplan/plugins/search.html", _("Search")),
503 | ]
504 |
505 | class GovernmentPlansCMSPlugin(CMSPlugin):
506 | """
507 | CMS Plugin for displaying latest articles
508 | """
509 |
510 | government = models.ForeignKey(
511 | Government, null=True, blank=True, on_delete=models.SET_NULL
512 | )
513 | categories = models.ManyToManyField(
514 | Category, verbose_name=_("categories"), blank=True
515 | )
516 |
517 | count = models.PositiveIntegerField(
518 | _("number of plans"), default=1, help_text=_("0 means all the plans")
519 | )
520 | offset = models.PositiveIntegerField(
521 | _("offset"),
522 | default=0,
523 | help_text=_("number of plans to skip from top of list"),
524 | )
525 | template = models.CharField(
526 | _("template"),
527 | blank=True,
528 | max_length=250,
529 | choices=PLUGIN_TEMPLATES,
530 | help_text=_("template used to display the plugin"),
531 | )
532 | extra_classes = models.CharField(max_length=255, blank=True)
533 |
534 | @property
535 | def render_template(self):
536 | """
537 | Override render_template to use
538 | the template_to_render attribute
539 | """
540 | return self.template_to_render
541 |
542 | def copy_relations(self, old_instance):
543 | """
544 | Duplicate ManyToMany relations on plugin copy
545 | """
546 | self.categories.set(old_instance.categories.all())
547 |
548 | def __str__(self):
549 | if self.count == 0:
550 | return str(_("All matching plans"))
551 | return _("%s matching plans") % self.count
552 |
553 | def get_plans(self, request, published_only=True):
554 | if (
555 | published_only
556 | or not request
557 | or not getattr(request, "toolbar", False)
558 | or not request.toolbar.edit_mode_active
559 | ):
560 | plans = GovernmentPlan.objects.filter(public=True)
561 | else:
562 | plans = GovernmentPlan.objects.all()
563 |
564 | filters = {}
565 | if self.government_id:
566 | filters["government_id"] = self.government_id
567 |
568 | cat_list = self.categories.all().values_list("id", flat=True)
569 | if cat_list:
570 | filters["categories__in"] = cat_list
571 |
572 | plans = plans.filter(**filters).distinct()
573 | plans = plans.prefetch_related("categories", "government", "organization")
574 | if self.count == 0:
575 | return plans[self.offset :]
576 | return plans[self.offset : self.offset + self.count]
577 |
578 | class GovernmentPlanSectionsCMSPlugin(CMSPlugin):
579 | """
580 | CMS Plugin for displaying plan sections
581 | """
582 |
583 | government = models.ForeignKey(
584 | Government, null=True, blank=True, on_delete=models.SET_NULL
585 | )
586 |
587 | class GovernmentPlanUpdatesCMSPlugin(CMSPlugin):
588 | """
589 | CMS Plugin for displaying plan updates
590 | """
591 |
592 | government = models.ForeignKey(
593 | Government, null=True, blank=True, on_delete=models.SET_NULL
594 | )
595 | categories = models.ManyToManyField(
596 | Category, verbose_name=_("categories"), blank=True
597 | )
598 |
599 | count = models.PositiveIntegerField(
600 | _("number of updates"), default=1, help_text=_("0 means all the updates")
601 | )
602 | offset = models.PositiveIntegerField(
603 | _("offset"),
604 | default=0,
605 | help_text=_("number of updates to skip from top of list"),
606 | )
607 |
608 | def copy_relations(self, old_instance):
609 | """
610 | Duplicate ManyToMany relations on plugin copy
611 | """
612 | self.categories.set(old_instance.categories.all())
613 |
614 | def get_updates(self, request, published_only=True):
615 | # TODO: remove duplication with GovernmentPlansCMSPlugin.get_plans
616 | if (
617 | published_only
618 | or not request
619 | or not getattr(request, "toolbar", False)
620 | or not request.toolbar.edit_mode_active
621 | ):
622 | updates = GovernmentPlanUpdate.objects.filter(public=True)
623 | else:
624 | updates = GovernmentPlanUpdate.objects.all()
625 |
626 | updates = updates.order_by("-timestamp").prefetch_related(
627 | "plan", "plan__categories"
628 | )
629 |
630 | filters = {}
631 | if self.government_id:
632 | filters["plan__government_id"] = self.government_id
633 |
634 | cat_list = self.categories.all().values_list("id", flat=True)
635 | if cat_list:
636 | filters["plan__categories__in"] = cat_list
637 |
638 | updates = updates.filter(**filters).distinct()
639 | if self.count == 0:
640 | return updates[self.offset :]
641 | return updates[self.offset : self.offset + self.count]
642 |
643 | def __str__(self):
644 | if self.count == 0:
645 | return str(_("All matching updates"))
646 | return _("%s matching updates") % self.count
647 |
--------------------------------------------------------------------------------