Commit 7db5ab8a authored by Felix Schlösser's avatar Felix Schlösser 💬
Browse files

merge and voting front end

parents 920acbc5 e6185036
Pipeline #181937 failed with stages
in 14 seconds
......@@ -22,4 +22,9 @@ class Migration(migrations.Migration):
name='agendaitem',
unique_together=set(),
),
migrations.AlterField(
model_name='agendaitem',
name='quorum',
field=models.BooleanField(blank=True, default=None, null=True, verbose_name='Beschlussfähigkeit'),
),
]
# Generated by Django 4.1 on 2022-11-26 12:59
import committee_transparency.meetings.managers
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('meetings', '0026_alter_meeting_legislative_period'),
]
operations = [
migrations.AlterField(
model_name='meeting',
name='head',
field=models.ForeignKey(help_text='Die Sitzungsleitung moderiert die Sitzung, leitet die Abstimmungen und sorgt für eine angenehme Sitzungsathmosphäre.', limit_choices_to=models.Q(('is_member', True), ('is_board_member', True), _connector='OR'), on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to=settings.AUTH_USER_MODEL, verbose_name='Sitzungsleitung'),
),
migrations.AlterField(
model_name='meeting',
name='legislative_period',
field=models.ForeignKey(blank=True, default=committee_transparency.meetings.managers.LegislativePeriodManager.current, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='meetings.legislativeperiod', verbose_name='Legislaturperiode'),
),
migrations.AlterField(
model_name='meeting',
name='minutes_keeper',
field=models.ForeignKey(blank=True, help_text='Der/Die Protokollant:in fasst das Gesagte schriftlich zusammen und hält die Verläufe der Abstimmungen fest.', limit_choices_to=models.Q(('is_member', True), ('is_board_member', True), _connector='OR'), null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Protokollant:in'),
),
]
......@@ -333,8 +333,8 @@ class Meeting(TimeStampedModel):
"users.User",
on_delete=models.CASCADE,
limit_choices_to=(
models.Q(is_committee_member=True)
| models.Q(is_committee_board_member=True)
models.Q(is_member=True)
| models.Q(is_board_member=True)
),
related_name="%(class)ss",
verbose_name=_("Sitzungsleitung"),
......@@ -380,8 +380,8 @@ class Meeting(TimeStampedModel):
null=True,
on_delete=models.CASCADE,
limit_choices_to=(
models.Q(is_committee_member=True)
| models.Q(is_committee_board_member=True)
models.Q(is_member=True)
| models.Q(is_board_member=True)
),
related_name="+",
verbose_name="Protokollant:in",
......@@ -639,7 +639,7 @@ class AgendaItem(TimeStampedModel):
)
quorum = models.BooleanField(
blank=True, null=True, default=None, verbose_name=_("Beschlussfähig")
blank=True, null=True, default=None, verbose_name=_("Beschlussfähigkeit")
)
quorum_changed = MonitorField(
......@@ -663,25 +663,6 @@ class AgendaItem(TimeStampedModel):
self.end = datetime.now()
return self.request
def move_up(self):
items = self.meeting.agenda_items
with transaction.atomic():
item_directly_above = items.filter(sort_order=self.sort_order + 1).first()
if item_directly_above:
item_directly_above.sort_order = self.sort_order
item_directly_above.save()
self.sort_order += 1
self.save()
def move_down(self):
items = self.meeting.agenda_items
with transaction.atomic():
item_directly_below = items.filter(sort_order=self.sort_order - 1).first()
if item_directly_below:
item_directly_below.sort_order = self.sort_order
item_directly_below.save()
self.sort_order -= 1
self.save()
def get_next(self):
items = self.meeting.agenda_items
......@@ -718,6 +699,15 @@ class AgendaItem(TimeStampedModel):
code="editing-forzen-agenda-item",
)
# make sure that the quorum is only changed on the day of the meeting
if self.quorum_changed:
if self.quorum_changed < self.meeting.date_time:
raise ValidationError(
f"Die Beschlussfähigkeit eines Tagesordnungspunkts kann erst nach Beginn der zugehörigen Sitzung ({self.meeting.date_time}) festgestellt werden. Aktuell ({self.quorum_changed})",
code="quorum-change-begore-meeting-begin",
)
class Meta:
ordering = ["meeting", "sort_order"]
......
......@@ -47,6 +47,11 @@ urlpatterns += i18n_patterns(
AgendaItemDetail.as_view(),
name="agenda-item-detail",
),
path(
_("sitzung/<str:pk_m>/top/<str:pk_a>/beschlussfähigkeit"),
AgendaItemToggleQuorum.as_view(),
name="agenda-item-toggle-quorum",
),
path(
_("sitzung/<str:pk_m>/top/<str:pk_a>/beschließen"),
AgendaItemDecide.as_view(),
......
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponseNotAllowed, HttpResponseBadRequest
from django.views import generic
from django.views.generic.dates import (
ArchiveIndexView,
......@@ -12,14 +12,15 @@ from django.views.generic.dates import (
from django.views.defaults import bad_request
from django.utils.translation import get_language, gettext_lazy as _
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.views import RedirectURLMixin
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from committee_transparency.utils.mixins import FormsetMixin
from committee_transparency.utils.datetime import get_next_meeting_date_str, get_meeting_time
from committee_transparency.requests.models import Request
from committee_transparency.decisions.models import Decision
from .models import Meeting, AgendaItem, BoardMeeting
......@@ -109,7 +110,7 @@ class BoardMeetingDetail(generic.DetailView):
class MeetingMove(LoginRequiredMixin, SuccessMessageMixin, generic.UpdateView):
model = Meeting
form_class = MeetingMoveForm
template_name_suffix = "_move_form"
template_name_suffix = "_move_confirm"
success_message = _("Sitzung verschoben.")
......@@ -176,26 +177,70 @@ class AgendaItemDetail(generic.DetailView):
class AgendaItemDelete(SuccessMessageMixin, LoginRequiredMixin, generic.UpdateView):
class AgendaItemDelete(SuccessMessageMixin, UserPassesTestMixin, generic.UpdateView):
model = AgendaItem
template_name_suffix = "_modal_confirm"
success_message = _("TOP gelöscht.")
def test_func(self):
return self.request.user.is_voting_member
class AgendaItemDecide(generic.UpdateView):
model = AgendaItem
class AgendaItemDecide(SuccessMessageMixin, UserPassesTestMixin, generic.CreateView):
model = Decision
fields = [
"request",
]
success_message = _("TOP entschieden.")
def test_func(self):
return self.request.user.is_voting_member
class AgendaItemAdjourn(generic.UpdateView):
class AgendaItemAdjourn(SuccessMessageMixin, UserPassesTestMixin, generic.UpdateView):
model = AgendaItem
fields = [
"request",
]
success_message = _("TOP vertagt.")
def test_func(self):
return self.request.user.is_voting_member
class AgendaItemToggleQuorum(SuccessMessageMixin, UserPassesTestMixin, generic.UpdateView):
model = AgendaItem
fields = [
"quorum",
]
template_name_suffix = "_toggle_quorum_confirm"
success_message = _("Beschlussfähigkeit angepasst.")
pk_url_kwarg = 'pk_a'
def get_initial(self):
"""Set the ."""
logging.info(self.initial)
return {'quorum': not self.object.quorum}
def form_valid(self, form):
"""If the form is valid, save the associated model."""
if ("quorum", "toggle") in self.request.POST.items():
logging.info("Recieved valid toggle command")
self.object = self.get_object()
self.object.quorum = not self.object.quorum
logging.info(f"Valid submit command in: { self.request.POST }")
self.object.save()
return HttpResponseRedirect(self.get_success_url())
return super().form_valid(form)
def test_func(self):
return self.request.user.is_voting_member
......
{% extends 'two_columns.html' %}
{% load i18n hosts widget_tweaks static request_tags request_filters decision_tags humanize %}
{% block navbar_items %}
<a class="navbar-item" href="{% url 'meeting-upcoming-list' %}">{% translate "Aktuelle Sitzungen"|capfirst %}</a>
{% if meeting.is_past %}
<a class="navbar-item is-active" href="{% url 'archive' %}">{% translate "Archiv"|capfirst %}</a>
{% else %}
<a class="navbar-item " href="{% url 'archive' %}">{% translate "Archiv"|capfirst %}</a>
{% endif %}
{% endblock navbar_items %}
{% block main %}
<article>
<div class="columns is-vcentered is-mobile mb-0">
<div class="column py-0">
<p class="is-family-code has-text-weight-semibold has-text-primary is-size-5 pb-0">
{{ object.request|verbose_name|capfirst }}
{{ agendaitem.request|verbose_name|capfirst }}
</p>
</div>
<div class="column is-narrow py-0">
{% status_tag object.request.status 'is-rounded is-medium' %}
{% if object.request.is_reopened %}
{% status_tag agendaitem.request.status 'is-rounded is-medium' %}
{% if agendaitem.request.is_reopened %}
{% translate "Wiedereröffnet" as reopened %}
{% status_tag reopened 'is-rounded is-medium' %}
{% endif %}
......@@ -20,23 +29,23 @@
</div>
<h1 class='title mb-1'>
{{ object.request.title }}
{{ agendaitem.request.title }}
</h1>
<div class="tags mb-2">
{% for tag in object.request.tags.all %}
{% for tag in agendaitem.request.tags.all %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</div>
<p class="content">
{{ object.request.text }}
{{ agendaitem.request.text }}
</p>
{% if object.request.reasoning %}
<h2 class="title is-5 mb-1">{{ object.request|verbose_name_of:"reasoning"|capfirst }}</h2>
{% if agendaitem.request.reasoning %}
<h2 class="title is-5 mb-1">{{ agendaitem.request|verbose_name_of:"reasoning"|capfirst }}</h2>
<p class="content">
{{ object.request.reasoning }}
{{ agendaitem.request.reasoning }}
</p>
{% else %}
<p class="has-text-weight-medium is-italic mb-5">
......@@ -47,18 +56,18 @@
{% block additional_details %}{% endblock %}
{% if object.request.attachment %}
{% if agendaitem.request.attachment %}
<div class="block">
<h2 class="title is-5">{{ object.request|verbose_name_of:"attachment"|capfirst }}:</h2>
<h2 class="title is-5">{{ agendaitem.request|verbose_name_of:"attachment"|capfirst }}:</h2>
<a class="block button is-medium is-outlined {{ button_class }} mb-0"
href={{ object.request.attachment.url }}>
href={{ agendaitem.request.attachment.url }}>
<span class="icon is-small">
<i class="fas fa-file-download"></i>
</span>
<span>
{{ object.request.attachment.name |cut:"attachments/" |cut:"anhänge/"|truncatechars:"25" }}
({{ object.request.attachment.size|filesizeformat }})
{{ agendaitem.request.attachment.name |cut:"attachments/" |cut:"anhänge/"|truncatechars:"25" }}
({{ agendaitem.request.attachment.size|filesizeformat }})
</span>
</a>
</div>
......@@ -69,47 +78,205 @@
{% endblock main %}
{% block aside %}
{% if object.request.is_urgent %}
{% if agendaitem.request.is_urgent %}
<div class="message is-warning">
<div class="message-body">
<span class="has-text-weight-bold">{% translate "Dringlich"|capfirst %}:</span>
{{ object.request.urgency_justification }}
{{ agendaitem.request.urgency_justification }}
</div>
</div>
{% endif %}
<section class="box">
<h2 class="title is-5 mb-2">{{ object.request|verbose_name_of:"requestor"|capfirst }}</h2>
<h2 class="title is-5 mb-2">
{% now "Y-m-d" as today %}
{% if agendaitem.meeting.date|date:"Y-m-d" == today %}
{% translate "Aktuelle" %}
{% endif %}
{{ agendaitem.meeting|verbose_name }}
</h2>
{% if user == object.request.requestor or user.is_committee_member %}
<h3 class="is-family-code is-size-7">{{ object.request.requestor|verbose_name_of:"name"|capfirst }}</h3>
<p>{{ object.request.requestor }}</p>
{% if agendaitem.meeting.date|date:"Y-m-d" != today %}
<h3 class="is-family-code is-size-7">{{ agendaitem.meeting|verbose_name_of:"date"|capfirst }}</h3>
<p>{{ agendaitem.meeting.date }}</p>
<hr class="my-1">
{% endif %}
<h3 class="is-family-code is-size-7">{{ agendaitem.meeting|verbose_name_of:"head"|capfirst }}</h3>
<p>{{ agendaitem.meeting.head }}</p>
<hr class="my-1">
<h3 class="is-family-code is-size-7">{{ object.request.requestor|verbose_name_of:"email"|capfirst }}</h3>
<p>{{ object.request.requestor.email|default:"-" }}</p>
<h3 class="is-family-code is-size-7">{{ agendaitem.meeting|verbose_name_of:"minutes_keeper"|capfirst }}</h3>
<p>{{ agendaitem.meeting.minutes_keeper|default:"-" }}</p>
{% if agendaitem.meeting.is_running or agendaitem.meeting.is_past %}
<hr class="my-1">
<h3 class="is-family-code is-size-7">{{ agendaitem|verbose_name_of:"quorum"|capfirst }}</h3>
<div class="columns is-mobile mb-2">
<div class="column pb-0">
{% if agendaitem.quorum == None %}
<span class="icon-text">
<span class="icon">
<i class="fas fa-question-circle"></i>
</span>
<span>{% translate "unbekannt" %}</span>
</span>
{% elif agendaitem.quorum %}
<span class="icon-text has-text-success">
<span class="icon">
<i class="fas fa-users"></i>
</span>
<span>{% translate "gegeben" %}</span>
</span>
{% else %}
<span class="icon-text has-text-danger">
<span class="icon">
<i class="fas fa-users-slash"></i>
</span>
<span>{% translate "nicht gegeben" %}</span>
</span>
{% endif %}
</div>
<div class="column is-narrow pb-0 pt-1">
{% if agendaitem.meeting.is_running %}
<form action="{% url 'agenda-item-toggle-quorum' agendaitem.meeting.id agendaitem.id %}" method="post">
{% csrf_token %}
<input type="hidden" name="quorum" value="toggle">
<button type="submit" class="button is-small is-primary is-light"/>
<span class="icon">
<i class="fas fa-sync"></i>
</span>
</button>
</form>
{% endif %}
</div>
</div>
{% if agendaitem.quorum_changed %}
<hr class="my-1">
<h3 class="is-family-code is-size-7">{{ agendaitem|verbose_name_of:"quorum_changed"|capfirst }}</h3>
{% now "Y-m-d" as today %}
{% if agendaitem.quorum_changed|date:"Y-m-d" == today %}
<span class="icon-text">
<span class="icon">
<i class="far fa-clock"></i>
</span>
<span>{{ agendaitem.quorum_changed |time}}</span>
</span>
{% else %}
<p>{{ agendaitem.quorum_changed|date:"SHORT_DATETIME_FORMAT" }}</p>
{% endif %}
{% endif %}
{% endif %}
</section>
<section class="box">
<h2 class="title is-5 mb-2">{{ agendaitem.request|verbose_name_of:"requestor"|capfirst }}</h2>
{% if user == agendaitem.request.requestor or user.is_committee_member %}
<h3 class="is-family-code is-size-7">{{ agendaitem.request.requestor|verbose_name_of:"name"|capfirst }}</h3>
<p>{{ agendaitem.request.requestor }}</p>
<hr class="my-1">
<h3 class="is-family-code is-size-7">{{ agendaitem.request.requestor|verbose_name_of:"email"|capfirst }}</h3>
<p>{{ agendaitem.request.requestor.email|default:"-" }}</p>
<hr class="my-1">
<h3 class="is-family-code is-size-7">{{ object.request|verbose_name_of:"preferred_language"|capfirst }}</h3>
<p>{{ object.request.preferred_language|default:"-" }}</p>
<h3 class="is-family-code is-size-7">{{ agendaitem.request|verbose_name_of:"preferred_language"|capfirst }}</h3>
<p>{{ agendaitem.request.preferred_language|default:"-" }}</p>
<hr class="my-1">
{% else %}
<h3 class="is-family-code is-size-7">{{ object.request.requestor|verbose_name_of:"name"|capfirst }}</h3>
<p>{{ object.request.requestor.get_nickname_or_short_name }}</p>
<h3 class="is-family-code is-size-7">{{ agendaitem.request.requestor|verbose_name_of:"name"|capfirst }}</h3>
<p>{{ agendaitem.request.requestor.get_nickname_or_short_name }}</p>
<hr class="my-1">
{% endif %}
<h3 class="is-family-code is-size-7">{{ object.request|verbose_name_of:"group"|capfirst }}</h3>
<p>{{ object.request.group|capfirst }}</p>
<h3 class="is-family-code is-size-7">{{ agendaitem.request|verbose_name_of:"group"|capfirst }}</h3>
<p>{{ agendaitem.request.group|capfirst }}</p>
</section>
{% endblock aside %}
{% block nav %}
{% time_almost_equal object.request.created object.request.modified as request_not_modified %}
{% time_almost_equal object.request.modified object.request.status_modified as status_not_modified %}
{% if user == agendaitem.meeting.head and not agenda_item.request.is_decided %}
<section class="box mt-3">
<details>
{% spaceless %}
<summary class="title is-5 mb-0">
<span class="icon-text">
<span>{% translate 'Abstimmung' %}</span>
<span class="icon">
<i class="fas fa-vote-yea"></i>
</span>
</span>
</summary>
{% endspaceless %}
<div class="block mt-2">
<p class="content">
{% translate 'Wenn von den anwesenden, stimmberechtigten Mitgliedern mehr für den Antrag stimmen als dagegen, ist der Antrag angenommen.' %}<br>
{% translate 'Eine Vertagung ist nur möglich, wenn keiner der anwesenden, stimmberechtigten Mitgliedern wiederspricht.' %}
</p>
<div class="columns is-mobile ">
<div class="column">
<div class="field">
<div class="control">
<a class="button is-responsive is-primary is-light right" href="{% url 'agenda-item-adjourn' agendaitem.meeting.id agendaitem.request.id %}">
<span class="icon is-small">
<i class="fas fa-calendar-day"></i>
</span>
<span>{% translate "Vertagen"|capfirst %}</span>
</a>
</div>
</div>
</div>
<div class="column is-narrow">
<div class="field has-addons">
<div class="control">
<form action="{% url 'agenda-item-decide' agendaitem.meeting.id agendaitem.id %}" method="post">
{% csrf_token %}
<input type="hidden" name="decison" value="reject">
<button type="submit" class="button is-responsive is-danger" href="{% url 'agenda-item-decide"/>
<span class="icon">
<i class="fas fa-times"></i>
</span>
<span>{% translate "Ablehnen"|capfirst %}</span>
</button>
</form>
</div>
<div class="control">
<form action="{% url 'agenda-item-decide' agendaitem.meeting.id agendaitem.id %}" method="post">
{% csrf_token %}
<input type="hidden" name="decison" value="accept">
<button type="submit" class="button is-responsive is-success" href="{% url 'agenda-item-decide"/>
<span class="icon">
<i class="fas fa-check"></i>
</span>
<span>{% translate "Annehmen"|capfirst %}</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</details>
</section>
{% endif %}
{% time_almost_equal agendaitem.request.created agendaitem.request.modified as request_not_modified %}
{% time_almost_equal agendaitem.request.modified agendaitem.request.status_modified as status_not_modified %}
<hr class="mt-2 mb-1">
<div class="level has-text-centered-mobile mb-5">
......@@ -120,7 +287,7 @@
<div class="level-item">
<div>
<h3 class="is-family-code is-size-7 has-text-right">{% translate "Antrag erstellt" %}</h3>
<p class="has-text-right">{{ object.request.created |naturalday:"SHORT_DATE_FORMAT" }}</p>
<p class="has-text-right">{{ agendaitem.request.created |naturalday:"SHORT_DATE_FORMAT" }}</p>
</div>
</div>
</div>
......@@ -129,7 +296,7 @@
<div class="level-item">
<div>
<h3 class="is-family-code is-size-7">{% translate "Antrag erstellt" %}</h3>
<p>{{ object.request.created |naturalday:"SHORT_DATE_FORMAT" }}</p>
<p>{{ agendaitem.request.created |naturalday:"SHORT_DATE_FORMAT" }}</p>
</div>
</div>
</div>
......@@ -138,7 +305,7 @@
<div class="level-item m-0 p-0">
<div>
<h3 class="is-family-code is-size-7">{% translate "Status verändert" %}</h3>
<p>{{ object.request.status_changed |naturaltime }}</p>
<p>{{ agendaitem.request.status_changed|naturalday:"SHORT_DATE_FORMAT" }}</p>
</div>
</div>
</div>
......@@ -148,7 +315,7 @@
<div class="level-item">
<div>
<h3 class="is-family-code is-size-7 has-text-right">{% translate "Antrag verändert" %}</h3>
<p class="has-text-right">{{ object.request.modified |naturaltime }}</p>
<p class="has-text-right">{{ agendaitem.request.modified|date:"SHORT_DATETIME_FORMAT" }}</p>
</div>
</div>
</div>
......@@ -159,19 +326,12 @@
<nav class="level is-max-desktop">
<div class="level-left">
<div class="level-item">
<a class="button" href={% url 'meeting-detail' object.meeting.id %}>{% translate "Zurück"|capfirst %}</a>
<a class="button" href={% url 'meeting-detail' agendaitem.meeting.id %}>{% translate "Zurück"|capfirst %}</a>
</div>
</div>
<div class="level-right">
<div class="level-item">
{% if user == object.meeting.head %}