style front page updates, support FOIRequests

This commit is contained in:
Jonas Heinrich 2025-09-19 14:50:42 +02:00
parent c56689893b
commit e3d283f463
7 changed files with 239 additions and 97 deletions

View file

@ -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)

View file

@ -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'),
),
]

View 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',
},
),
]

View 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'),
),
]

View file

@ -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):

View file

@ -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 %}

View file

@ -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 doesnt 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>
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.url or update.foirequest or show_context %} {% if update.content or show_context %}
<div class="{% if update.content %}box-card-links{% else %}d-flex mt-auto{% endif %} mt-auto"> <div class="tight-margin flex-grow-1">
{% if show_context %} {% if show_context %}
<a href="{{ update.get_absolute_url }}" class="action-link">→ zum Vorhaben</a> <h4 class="h5">{{ update.title }}</h4>
{% 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 %}
{% with update.content|markdown as content %}
{% if show_context %}
{{ content|truncatewords_html:15 }}
{% else %}
{{ content }}
{% endif %}
{% endwith %}
</div>
{% endif %} {% endif %}
</div>
{% 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> </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;">