style front page updates, support FOIRequests
This commit is contained in:
parent
c56689893b
commit
e3d283f463
7 changed files with 239 additions and 97 deletions
|
|
@ -22,6 +22,7 @@ from .models import (
|
||||||
GovernmentPlanFollower,
|
GovernmentPlanFollower,
|
||||||
GovernmentPlanSection,
|
GovernmentPlanSection,
|
||||||
GovernmentPlanUpdate,
|
GovernmentPlanUpdate,
|
||||||
|
FOIRequest,
|
||||||
)
|
)
|
||||||
|
|
||||||
User = auth.get_user_model()
|
User = auth.get_user_model()
|
||||||
|
|
@ -345,12 +346,22 @@ class GovernmentPlanSectionAdmin(SortableAdminMixin, admin.ModelAdmin):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FOIRequestAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("title", "government_plan", "url", "created_at")
|
||||||
|
list_filter = ("government_plan",)
|
||||||
|
search_fields = ("title", "government_plan__title")
|
||||||
|
date_hierarchy = "created_at"
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Government, GovernmentAdmin)
|
admin.site.register(Government, GovernmentAdmin)
|
||||||
admin.site.register(GovernmentPlan, GovernmentPlanAdmin)
|
admin.site.register(GovernmentPlan, GovernmentPlanAdmin)
|
||||||
admin.site.register(GovernmentPlanUpdate, GovernmentPlanUpdateAdmin)
|
admin.site.register(GovernmentPlanUpdate, GovernmentPlanUpdateAdmin)
|
||||||
admin.site.register(GovernmentPlanSection, GovernmentPlanSectionAdmin)
|
admin.site.register(GovernmentPlanSection, GovernmentPlanSectionAdmin)
|
||||||
admin.site.register(GovernmentPlanFollower, FollowerAdmin)
|
admin.site.register(GovernmentPlanFollower, FollowerAdmin)
|
||||||
|
admin.site.register(FOIRequest, FOIRequestAdmin)
|
||||||
|
|
||||||
|
|
||||||
govplan_admin_site = GovPlanAdminSite(name="govplanadmin")
|
govplan_admin_site = GovPlanAdminSite(name="govplanadmin")
|
||||||
|
govplan_admin_site.register(FOIRequest, FOIRequestAdmin)
|
||||||
govplan_admin_site.register(GovernmentPlan, GovernmentPlanAdmin)
|
govplan_admin_site.register(GovernmentPlan, GovernmentPlanAdmin)
|
||||||
govplan_admin_site.register(GovernmentPlanUpdate, GovernmentPlanUpdateAdmin)
|
govplan_admin_site.register(GovernmentPlanUpdate, GovernmentPlanUpdateAdmin)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Generated by Django 5.1.12 on 2025-09-18 20:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('froide_govplan', '0014_remove_governmentplansection_content_placeholder'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='categorizedgovernmentplan',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='government',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='governmentplan',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='governmentplanfollower',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='governmentplansection',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='governmentplanupdate',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
]
|
||||||
30
froide_govplan/migrations/0016_foirequest.py
Normal file
30
froide_govplan/migrations/0016_foirequest.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 5.1.12 on 2025-09-19 12:36
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('froide_govplan', '0015_alter_categorizedgovernmentplan_id_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FOIRequest',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=1024, verbose_name='title')),
|
||||||
|
('created_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created at')),
|
||||||
|
('government_plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='foi_requests', to='froide_govplan.governmentplan', verbose_name='government plan')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'FOI request',
|
||||||
|
'verbose_name_plural': 'FOI requests',
|
||||||
|
'ordering': ('-created_at',),
|
||||||
|
'get_latest_by': 'created_at',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
18
froide_govplan/migrations/0017_foirequest_url.py
Normal file
18
froide_govplan/migrations/0017_foirequest_url.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.1.12 on 2025-09-19 12:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('froide_govplan', '0016_foirequest'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='foirequest',
|
||||||
|
name='url',
|
||||||
|
field=models.CharField(blank=True, max_length=1024, verbose_name='URL'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -321,23 +321,29 @@ class GovernmentPlan(models.Model):
|
||||||
return "govplan:plan@{}".format(self.pk)
|
return "govplan:plan@{}".format(self.pk)
|
||||||
|
|
||||||
def get_related_foirequests(self):
|
def get_related_foirequests(self):
|
||||||
if FoiRequest is None:
|
return FOIRequest.objects.filter(government_plan=self).order_by('-created_at')
|
||||||
return []
|
|
||||||
if not self.responsible_publicbody:
|
|
||||||
return []
|
|
||||||
if hasattr(self, "_related_foirequests"):
|
|
||||||
return self._related_foirequests
|
|
||||||
|
|
||||||
self._related_foirequests = (
|
|
||||||
FoiRequest.objects.filter(
|
|
||||||
visibility=FoiRequest.VISIBILITY.VISIBLE_TO_PUBLIC,
|
class FOIRequest(models.Model):
|
||||||
public_body=self.responsible_publicbody,
|
title = models.CharField(max_length=1024, verbose_name=_("title"))
|
||||||
)
|
government_plan = models.ForeignKey(
|
||||||
.filter(tags__name=conf.GOVPLAN_NAME)
|
'GovernmentPlan',
|
||||||
.filter(reference=self.get_foirequest_reference())
|
on_delete=models.CASCADE,
|
||||||
.order_by("-created_at")
|
related_name='foi_requests',
|
||||||
)
|
verbose_name=_("government plan"),
|
||||||
return self._related_foirequests
|
)
|
||||||
|
url = models.CharField(max_length=1024, blank=True, verbose_name=_("URL"))
|
||||||
|
created_at = models.DateTimeField(default=timezone.now, verbose_name=_("created at"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
get_latest_by = "created_at"
|
||||||
|
verbose_name = _("FOI request")
|
||||||
|
verbose_name_plural = _("FOI requests")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.title} - {self.government_plan.title}"
|
||||||
|
|
||||||
|
|
||||||
class GovernmentPlanUpdate(models.Model):
|
class GovernmentPlanUpdate(models.Model):
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@
|
||||||
{% with foirequest=object.get_recent_foirequest %}
|
{% with foirequest=object.get_recent_foirequest %}
|
||||||
<dt>Anfrage</dt>
|
<dt>Anfrage</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{% include "foirequest/snippets/request_item_mini.html" with object=foirequest %}
|
<a href="{{ foirequest.url }}" class="text-decoration-none">Aktuell laufende Anfrage vom {{ foirequest.created_at|date:"d.m.Y" }}</a>
|
||||||
</dd>
|
</dd>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<style>
|
<style>
|
||||||
.govplan-update .card-body div.tight-margin {
|
.govplan-update .card-body div.tight-margin {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
overflow: hidden; /* make sure content doesn’t spill */
|
||||||
}
|
}
|
||||||
|
|
||||||
.govplan-update .card-header h3 {
|
.govplan-update .card-header h3 {
|
||||||
|
|
@ -10,6 +11,37 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#myCarousel .card {
|
||||||
|
height: 380px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#myCarousel .card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Multiline ellipsis for content */
|
||||||
|
#myCarousel .tight-margin {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#myCarousel .tight-margin p,
|
||||||
|
#myCarousel .tight-margin div {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3; /* number of lines to show */
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
#myCarousel .badge {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Update tiles on start page -->
|
<!-- Update tiles on start page -->
|
||||||
|
|
@ -17,100 +49,102 @@
|
||||||
|
|
||||||
<div class="py-5 bg-body-tertiary">
|
<div class="py-5 bg-body-tertiary">
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h1 class="text-body-emphasis">Aktuelles</h1>
|
<h1 class="text-body-emphasis">Aktuelles</h1>
|
||||||
<p class="fs-5 col-md-8">
|
<p class="fs-5 col-md-8">
|
||||||
Hier finden Sie aktuelle Artikel und Links zu Blogs zu den Fortschritten der Stadtverwaltung Karlsruhe.
|
Hier finden Sie aktuelle Artikel und Links zu Blogs zu den Fortschritten der Stadtverwaltung Karlsruhe.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="myCarousel" class="carousel slide" data-bs-ride="carousel">
|
<div id="myCarousel" class="carousel slide" data-bs-ride="carousel">
|
||||||
<div class="carousel-inner w-100">
|
<div class="carousel-inner w-100">
|
||||||
|
|
||||||
{% for update in updates %}
|
{% for update in updates %}
|
||||||
<div class="carousel-item {% if forloop.first %}active{% endif %}">
|
<div class="carousel-item {% if forloop.first %}active{% endif %}">
|
||||||
<div class="card col-md-4">
|
<div class="card col-md-4">
|
||||||
<a href="{{ update.get_absolute_url }}" class="text-body text-decoration-none">
|
<a href="{{ update.get_absolute_url }}" class="text-body text-decoration-none">
|
||||||
<div class="card-header p-4 tight-margin text-start">
|
<div class="card-header p-4 tight-margin text-start">
|
||||||
{% if show_context %}
|
|
||||||
<span class="badge text-bg-light text-decoration-none me-2 mb-2">
|
|
||||||
{{ update.plan.get_section }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if show_context %}
|
|
||||||
<h3 class="h5 mt-0 ellipsis">{{ update.plan }}</h3>
|
|
||||||
{% else %}
|
|
||||||
<h3 class="h4 mt-0 ellipsis">{{ update.title }}</h3>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="small">
|
|
||||||
<time datetime="{{ update.timestamp|date:'c' }}">{{ update.timestamp|date:"DATE_FORMAT" }}</time>
|
|
||||||
|
|
||||||
{% if update.user or update.organization %}
|
|
||||||
<span>
|
|
||||||
von {{ update.user.get_full_name }}{% if update.user and update.organization %},{% endif %}
|
|
||||||
{{ update.organization.name }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div class="card-body tight-margin text-start p-4 d-flex flex-column">
|
|
||||||
|
|
||||||
{% if update.content or show_context %}
|
|
||||||
<div class="tight-margin">
|
|
||||||
{% if show_context %}
|
|
||||||
<h4 class="h5">{{ update.title }}</h4>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% with update.content|markdown as content %}
|
|
||||||
{% if show_context %}
|
{% if show_context %}
|
||||||
{{ content|truncatewords_html:15 }}
|
<h3 class="h5 mt-0 ellipsis">{{ update.plan }}</h3>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ content }}
|
<h3 class="h4 mt-0 ellipsis">{{ update.title }}</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
<div class="small">
|
||||||
</div>
|
<time datetime="{{ update.timestamp|date:'c' }}">{{ update.timestamp|date:"DATE_FORMAT" }}</time>
|
||||||
{% endif %}
|
{% if update.user or update.organization %}
|
||||||
|
<span>
|
||||||
{% if update.url or update.foirequest or show_context %}
|
von {{ update.user.get_full_name }}{% if update.user and update.organization %},{% endif %}
|
||||||
<div class="{% if update.content %}box-card-links{% else %}d-flex mt-auto{% endif %} mt-auto">
|
{{ update.organization.name }}
|
||||||
{% if show_context %}
|
</span>
|
||||||
<a href="{{ update.get_absolute_url }}" class="action-link">→ zum Vorhaben</a>
|
{% endif %}
|
||||||
{% else %}
|
</div>
|
||||||
{% if update.url %}
|
</div>
|
||||||
<a href="{{ update.url }}" class="action-link me-3" target="_blank" rel="noopener">→ mehr auf {{ update.get_url_domain }} lesen…</a>
|
</a>
|
||||||
{% endif %}
|
<div class="card-body tight-margin text-start p-4 d-flex flex-column">
|
||||||
|
|
||||||
{% if update.foirequest %}
|
{% if update.content or show_context %}
|
||||||
<a href="{{ update.foirequest.get_absolute_url }}" class="action-link">→ zur Anfrage</a>
|
<div class="tight-margin flex-grow-1">
|
||||||
|
{% if show_context %}
|
||||||
|
<h4 class="h5">{{ update.title }}</h4>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% with update.content|markdown as content %}
|
||||||
|
{% if show_context %}
|
||||||
|
{{ content|truncatewords_html:15 }}
|
||||||
|
{% else %}
|
||||||
|
{{ content }}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if update.url or update.foirequest or show_context %}
|
||||||
|
<div class="{% if update.content %}box-card-links{% else %}d-flex mt-auto{% endif %} mt-auto">
|
||||||
|
{% if show_context %}
|
||||||
|
<a href="{{ update.get_absolute_url }}" class="action-link">→ zum Vorhaben</a>
|
||||||
|
{% else %}
|
||||||
|
{% if update.url %}
|
||||||
|
<a href="{{ update.url }}" class="action-link me-3" target="_blank" rel="noopener">
|
||||||
|
→ mehr auf {{ update.get_url_domain }} lesen…
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if update.foirequest %}
|
||||||
|
<a href="{{ update.foirequest.get_absolute_url }}" class="action-link">→ zur Anfrage</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if show_context %}
|
||||||
|
<div class="d-md-flex mt-2 align-items-center mt-3">
|
||||||
|
<ul class="list-unstyled d-flex m-0">
|
||||||
|
{% for cat in update.plan.categories.all %}
|
||||||
|
<li>
|
||||||
|
<span class="badge bg-secondary text-decoration-none me-2 mb-2">
|
||||||
|
#{{ cat.name }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
|
<button class="carousel-control-prev" type="button" data-bs-target="#myCarousel" data-bs-slide="prev">
|
||||||
|
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||||
|
<span class="visually-hidden">Previous</span>
|
||||||
|
</button>
|
||||||
|
<button class="carousel-control-next" type="button" data-bs-target="#myCarousel" data-bs-slide="next">
|
||||||
|
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||||
|
<span class="visually-hidden">Next</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="carousel-control-prev" type="button" data-bs-target="#myCarousel" data-bs-slide="prev">
|
|
||||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
|
||||||
<span class="visually-hidden">Previous</span>
|
|
||||||
</button>
|
|
||||||
<button class="carousel-control-next" type="button" data-bs-target="#myCarousel" data-bs-slide="next">
|
|
||||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
|
||||||
<span class="visually-hidden">Next</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Update tiles on single page -->
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
<div class="py-3" style="margin-top: 1rem; margin-bottom: -2rem;">
|
<div class="py-3" style="margin-top: 1rem; margin-bottom: -2rem;">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue