Commit aecbcd9c authored by Felix Schlösser / TinTin's avatar Felix Schlösser / TinTin
Browse files

added reopened motions for improved workflow

parent 7e5acdab
Pipeline #148267 failed with stages
in 12 seconds
......@@ -6,14 +6,16 @@ from .models import *
@admin.register(Decision)
class DecisionAdmin(admin.ModelAdmin):
pass
list_display = ["motion", "meeting", "voting_result"]
list_filter = ["_voting_result",]
@admin.register(BoardDecision)
class BoardDecisionAdmin(admin.ModelAdmin):
pass
list_display = ["motion", "meeting", "voting_result"]
list_filter = ["_voting_result",]
@admin.register(ExtendedBoardDecision)
class ExtendenBoardDecisionAdmin(admin.ModelAdmin):
pass
list_display = ["motion", "meeting", "voting_result"]
list_filter = ["_voting_result",]
# Generated by Django 4.0.2 on 2022-06-06 23:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('decisions', '0002_initial'),
]
operations = [
migrations.RenameField(
model_name='boarddecision',
old_name='voting_result',
new_name='_voting_result',
),
migrations.RenameField(
model_name='decision',
old_name='voting_result',
new_name='_voting_result',
),
migrations.RenameField(
model_name='extendedboarddecision',
old_name='voting_result',
new_name='_voting_result',
),
]
from django.db import models
from django.db import models, transaction
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError, ObjectDoesNotExist
......@@ -21,7 +21,7 @@ class AbstractDecision(TimeStampedModel):
("REJECTED", _("abgelehnt")),
]
voting_result = StatusField(
_voting_result = StatusField(
choices_name="VOTING_RESULT_CHOICES", verbose_name=_("Anstimmungsergebnis")
)
note = models.TextField(
......@@ -37,7 +37,15 @@ class AbstractDecision(TimeStampedModel):
abstract = True
def __str__(self):
return f"{ self.created.strftime('%x') }: { self.motion.title } "
return f"{ self.motion.title }, {self.voting_result}"
@property
def voting_result(self):
return self.get__voting_result_display()
def get_voting_result_str(self):
on = _("auf")
return f" {self.voting_result} {on} {self.agenda_item.meeting}"
@property
def motion(self):
......@@ -47,16 +55,39 @@ class AbstractDecision(TimeStampedModel):
def meeting(self):
return self.agenda_item.meeting
def get_raw_voting_result(self):
return self._voting_result
def clean(self):
if self.created < self.agenda_item.meeting.datetime:
raise ValidationError(
f"Beschlüsse können erst nach Beginn der Sitzung ({self.agenda_item.meeting.datetime}) erstellt werden.",
code="decision-before-meeting-begin",
)
if self.created > self.agenda_item.meeting.datetime + timedelta(hours=24):
raise ValidationError(
f"Beschlüsse können nur in den ersten 24h nach dem Beginn der zugehörigen Sitzung ({self.agenda_item.meeting.datetime}) erstellt werden.",
code="decision-24h-after-meeting-begin",
)
# Dont allow decisions to be changed retroactivly, but allow a time window to allow for corrections.
if datetime.now() > self.created + timedelta(hours=24):
if self.modified > self.created + timedelta(hours=24):
if self.created != self.modified:
raise ValidationError(
f"24h nach Erstellung werden Beschlüssene vom System eingefrohren und können nicht mehr verändert werden. \
Wenn du glaubst, dass sich hierbei um einen Fehler handelt, wende dich an den Administrator.",
code="editing-forzen-motion",
Wenn du glaubst, dass sich hierbei um einen Fehler handelt, wende dich an den Administrator.",
code="editing-forzen-decision",
)
def save(self, *args, **kwargs):
# Set the motion state to 'DECIDED'
with transaction.atomic():
motion = self.agenda_item.motion
motion.decide()
motion.save()
super().save(*args, **kwargs)
class Decision(AbstractDecision):
......@@ -67,9 +98,10 @@ class Decision(AbstractDecision):
verbose_name_plural = _("AStA-Beschlüsse")
class BoardDecision(AbstractDecision):
agenda_item = models.OneToOneField("meetings.BoardMeetingAgendaItem", on_delete=models.PROTECT)
agenda_item = models.OneToOneField(
"meetings.BoardMeetingAgendaItem", on_delete=models.PROTECT
)
class Meta(AbstractDecision.Meta):
verbose_name = _("Vorstandsbeschluss")
......@@ -77,10 +109,10 @@ class BoardDecision(AbstractDecision):
class ExtendedBoardDecision(AbstractDecision):
agenda_item = models.OneToOneField("meetings.ExtendedBoardMeetingAgendaItem", on_delete=models.PROTECT)
agenda_item = models.OneToOneField(
"meetings.ExtendedBoardMeetingAgendaItem", on_delete=models.PROTECT
)
class Meta(BoardDecision.Meta):
verbose_name = _("erweiterter Vorstandsbeschluss")
verbose_name_plural = _("erweiterter Vorstandsbeschlüsse")
......@@ -9,7 +9,11 @@ register = template.Library()
@register.simple_tag
def voting_result_tag(voting_result, class_list=''):
voting_result_string = voting_result.lower().capitalize()
if not voting_result:
raise ValueError("No voting result string provided for tag.")
# This convoluted way because .capitalize() and .title() methods dont to what I want.
voting_result_str = voting_result[0].upper() + voting_result[1:]
if voting_result.upper() in "ACCEPTED, ANGENOMMEN":
color_class = "is-success"
......@@ -21,7 +25,7 @@ def voting_result_tag(voting_result, class_list=''):
color_class = "is-danger"
else:
class_name = "is-black"
voting_result_string = _("Unbekannt")
logging.error(f"Unknown voting result: {voting_result}")
voting_result_string = _("Ungültig")
logging.error(f"Invalid voting result: {voting_result}")
return mark_safe(f"<span class='tag is-family-code {color_class} {class_list}'>{voting_result_string}</span>")
return mark_safe(f"<span class='tag is-family-code {color_class} {class_list}'>{voting_result_str}</span>")
......@@ -8,7 +8,10 @@ class AgendaItemManager(models.Manager):
the existing agenda item, further reducing work for the meeting moderation.
"""
previous_item = meeting.agenda_items.latest('sort_order')
next_sort_order = previous_item.sort_order + 1
if previous_item:
next_sort_order = previous_item.sort_order + 1
else:
next_sort_order = 1
next_item = self.create(
sort_order=next_sort_order, meeting=meeting, motion=motion
......
# Generated by Django 4.0.2 on 2022-06-06 23:53
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('motions', '0017_alter_historicalmotion_title_and_more'),
('meetings', '0002_alter_format_is_favorite'),
]
operations = [
migrations.AlterField(
model_name='agendaitem',
name='motion',
field=models.ForeignKey(limit_choices_to=models.Q(('_status', 'OPEN'), ('_status', 'CHANGED'), ('_status', 'ADJOURNED'), _connector='OR'), on_delete=django.db.models.deletion.PROTECT, related_name='%(class)ss', to='motions.motion', verbose_name='Antrag'),
),
migrations.AlterField(
model_name='agendaitem',
name='sort_order',
field=models.IntegerField(editable=False, verbose_name='Reihenfolge'),
),
migrations.AlterField(
model_name='boardmeeting',
name='first_invitation_send_datetime',
field=models.DateTimeField(editable=False, null=True, unique=True, verbose_name='Versandzeitpunkt der ersten Sitzungseinladung'),
),
migrations.AlterField(
model_name='boardmeeting',
name='last_invitation_send_datetime',
field=models.DateTimeField(editable=False, null=True, unique=True, verbose_name='Versandzeitpunkt der letzten Sitzungseinladung'),
),
migrations.AlterField(
model_name='boardmeetingagendaitem',
name='motion',
field=models.ForeignKey(limit_choices_to=models.Q(('_status', 'OPEN'), ('_status', 'CHANGED'), ('_status', 'ADJOURNED'), _connector='OR'), on_delete=django.db.models.deletion.PROTECT, related_name='%(class)ss', to='motions.motion', verbose_name='Antrag'),
),
migrations.AlterField(
model_name='boardmeetingagendaitem',
name='sort_order',
field=models.IntegerField(editable=False, verbose_name='Reihenfolge'),
),
migrations.AlterField(
model_name='extendedboardmeeting',
name='first_invitation_send_datetime',
field=models.DateTimeField(editable=False, null=True, unique=True, verbose_name='Versandzeitpunkt der ersten Sitzungseinladung'),
),
migrations.AlterField(
model_name='extendedboardmeeting',
name='last_invitation_send_datetime',
field=models.DateTimeField(editable=False, null=True, unique=True, verbose_name='Versandzeitpunkt der letzten Sitzungseinladung'),
),
migrations.AlterField(
model_name='extendedboardmeetingagendaitem',
name='motion',
field=models.ForeignKey(limit_choices_to=models.Q(('_status', 'OPEN'), ('_status', 'CHANGED'), ('_status', 'ADJOURNED'), _connector='OR'), on_delete=django.db.models.deletion.PROTECT, related_name='%(class)ss', to='motions.motion', verbose_name='Antrag'),
),
migrations.AlterField(
model_name='extendedboardmeetingagendaitem',
name='sort_order',
field=models.IntegerField(editable=False, verbose_name='Reihenfolge'),
),
migrations.AlterField(
model_name='meeting',
name='first_invitation_send_datetime',
field=models.DateTimeField(editable=False, null=True, unique=True, verbose_name='Versandzeitpunkt der ersten Sitzungseinladung'),
),
migrations.AlterField(
model_name='meeting',
name='last_invitation_send_datetime',
field=models.DateTimeField(editable=False, null=True, unique=True, verbose_name='Versandzeitpunkt der letzten Sitzungseinladung'),
),
]
# Generated by Django 4.0.2 on 2022-06-07 00:57
import committee_transparency.utils.datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('meetings', '0003_alter_agendaitem_motion_alter_agendaitem_sort_order_and_more'),
]
operations = [
migrations.AlterField(
model_name='agendaitem',
name='sort_order',
field=models.IntegerField(default=1, editable=False, verbose_name='Reihenfolge'),
),
migrations.AlterField(
model_name='boardmeeting',
name='time',
field=models.TimeField(default=committee_transparency.utils.datetime.get_current_time_plus_2_minutes, verbose_name='Uhrzeit'),
),
migrations.AlterField(
model_name='boardmeetingagendaitem',
name='sort_order',
field=models.IntegerField(default=1, editable=False, verbose_name='Reihenfolge'),
),
migrations.AlterField(
model_name='extendedboardmeeting',
name='time',
field=models.TimeField(default=committee_transparency.utils.datetime.get_current_time_plus_2_minutes, verbose_name='Uhrzeit'),
),
migrations.AlterField(
model_name='extendedboardmeetingagendaitem',
name='sort_order',
field=models.IntegerField(default=1, editable=False, verbose_name='Reihenfolge'),
),
]
......@@ -15,9 +15,10 @@ import logging
from committee_transparency.utils.datetime import (
get_next_meeting_date,
get_current_date,
get_current_date_plus_one_year,
get_current_time,
get_current_time_plus_2_minutes,
get_current_datetime,
get_current_date_plus_one_year,
)
from committee_transparency.utils.locale import set_locale
......@@ -28,6 +29,7 @@ from committee_transparency.decisions.models import (
ExtendedBoardDecision,
)
from django.conf import settings
from .managers import AgendaItemManager
from .validators import present_or_future_date
......@@ -315,16 +317,14 @@ class AbstractMeeting(TimeStampedModel):
present_or_future_date,
],
)
time = models.TimeField(default=get_current_time, verbose_name=_("Uhrzeit"))
time = models.TimeField(default=get_current_time_plus_2_minutes, verbose_name=_("Uhrzeit"))
first_invitation_send_datetime = models.DateTimeField(
blank=True,
null=True,
unique=True,
editable=False,
verbose_name=_("Versandzeitpunkt der ersten Sitzungseinladung"),
)
last_invitation_send_datetime = models.DateTimeField(
blank=True,
null=True,
unique=True,
editable=False,
......@@ -375,33 +375,54 @@ class AbstractMeeting(TimeStampedModel):
}
)
# motion_list = [item.motion for item in self.agenda_items.all()]
# previous_motion = self.agenda_items.filter(id=self.id).get().motion
# old_motion_list = motion_list.copy()
# motion_list.remove(previous_motion)
# motion_list.append(self.motion)
# motion_set = set(motion_list)
elif self.datetime < self.created:
raise ValidationError(
{
"time": _(
f"Sitzungszeitpunkt muss nach dem Erstellungszeitpunkt ({self.created}) liegen."
),
"date": "",
}
)
# Dont allow meetings to be changed retroactivly, but allow a time window to allow for corrections.
if datetime.now() > self.datetime + timedelta(hours=24):
if self.created != self.modified:
raise ValidationError(
f"24h nach Begin der Sitzung ({self.datetime}) werden Sitzungen vom System eingefrohren und können nicht mehr verändert werden. \
Wenn du glaubst, dass sich hierbei um einen Fehler handelt, wende dich an den Administrator.",
code="editing-forzen-meeting",
)
# if len(motion_set) != len(old_motion_list):
# raise ValidationError(
# _("Die einzelnen Tagesordnungspunkte einer Sitzung müssen verschieden sein."),
# code="agenda-items-unique",
if not settings.DEBUG:
# Deactivate during Debug such that I can create meetings without agenda items from the django admin.
motion_list = [item.motion for item in self.agenda_items.all()]
previous_motion = self.agenda_items.filter(id=self.id).get().motion
old_motion_list = motion_list.copy()
motion_list.remove(previous_motion)
motion_list.append(self.motion)
motion_set = set(motion_list)
# )
if len(motion_set) != len(old_motion_list):
raise ValidationError(
_("Die einzelnen Tagesordnungspunkte einer Sitzung müssen verschieden sein."),
code="agenda-items-unique",
# sort_order_list = [item.sort_order for item in self.agenda_items.all()]
# previous_sort_order = self.agenda_items.filter(id=self.id).get().sort_order
# old_sort_order_list = sort_order_list.copy()
# sort_order_list.remove(previous_sort_order)
# sort_order_list.append(self.sort_order)
# sort_order_set = set(sort_order_list)
)
# if len(sort_order_set) != len(sort_order_list):
# raise ValidationError(
# _("Die Ordinalzahlen der Tagesordnungspunkte einer Sitzung müssen verschieden sein."),
# code="agenda-items-unique",
sort_order_list = [item.sort_order for item in self.agenda_items.all()]
previous_sort_order = self.agenda_items.filter(id=self.id).get().sort_order
old_sort_order_list = sort_order_list.copy()
sort_order_list.remove(previous_sort_order)
sort_order_list.append(self.sort_order)
sort_order_set = set(sort_order_list)
# )
if len(sort_order_set) != len(sort_order_list):
raise ValidationError(
_("Die Ordinalzahlen der Tagesordnungspunkte einer Sitzung müssen verschieden sein."),
code="agenda-items-unique",
)
def __str__(self):
......@@ -409,13 +430,18 @@ class AbstractMeeting(TimeStampedModel):
date = self.date.strftime("%x")
date_str = _(f"vom { date }")
return f"{ self._meta.verbose_name } { date_str }"
return f"{ self._meta.verbose_name.title() } { date_str }"
@property
def begin_time(self):
return self.time
@property
def datetime(self):
date_and_time = datetime.combine(self.date, self.time)
return date_and_time
def send_meeting_invitation(self):
if not self.first_invitation_send_datetime:
# If this is the first invitation for this meeting
......@@ -483,9 +509,9 @@ class Meeting(AbstractMeeting):
next_item = AgendaItem.objects.create_agenda_item(self, motion)
return next_item
class BoardMeeting(AbstractMeeting):
pass
class BoardMeeting(AbstractMeeting):
class Meta(AbstractMeeting.Meta):
verbose_name = _("Vorstandssitzung")
verbose_name_plural = _("Vorstandssitzungen")
......@@ -519,7 +545,7 @@ class BoardMeeting(AbstractMeeting):
return self._boardmeetingagedaitems
def add_agenda_item(self, motion):
next_item = BoardMeetingAgendaItem.objects.create_decision(self, has_passed, note)
next_item = BoardMeetingAgendaItem.objects.create_agenda_item(self, has_passed, note)
return next_item
class ExtendedBoardMeeting(AbstractMeeting):
......@@ -558,7 +584,7 @@ class ExtendedBoardMeeting(AbstractMeeting):
return self._extendedboardmeetingagendaitems
def add_agenda_item(self, motion):
next_item = ExtendedBoardMeetingAgendaItem.objects.create_decision(self, has_passed, note)
next_item = ExtendedBoardMeetingAgendaItem.objects.create_agenda_item(self, has_passed, note)
return next_item
......@@ -566,7 +592,8 @@ class ExtendedBoardMeeting(AbstractMeeting):
class AbstractAgendaItem(TimeStampedModel):
sort_order = models.IntegerField(
default=0,
editable=False,
default = 1,
verbose_name=_("Reihenfolge"),
)
......@@ -574,7 +601,7 @@ class AbstractAgendaItem(TimeStampedModel):
"motions.Motion",
related_name="%(class)ss",
on_delete=models.PROTECT,
limit_choices_to=(models.Q(_status="OPEN") | models.Q(_status="CHANGED")),
limit_choices_to=(models.Q(_status="OPEN") | models.Q(_status="CHANGED") | models.Q(_status="ADJOURNED") | models.Q(_status="REOPENED")),
verbose_name=_("Antrag"),
)
......@@ -631,6 +658,32 @@ class AbstractAgendaItem(TimeStampedModel):
return item
return None
def clean(self):
if self.created > self.meeting.datetime + timedelta(hours=24):
raise ValidationError( {
'meeting': f"Tagesordnungspunkte können spätestens noch in den ersten 24h nach dem Beginn der zugehörigen Sitzung ({self.meeting.datetime}) erstellt werden.",
}
)
# Dont allow agenda items to be changed retroactivly, but allow a time window to allow for corrections.
if self.modified > self.created + timedelta(hours=24):
if self.created != self.modified:
raise ValidationError(
f"24h nach Erstellung werden Tagesordnungspunkte vom System eingefrohren und können nicht mehr verändert werden. \
Wenn du glaubst, dass sich hierbei um einen Fehler handelt, wende dich an den Administrator.",
code="editing-forzen-agenda-item",
)
def save(self, *args, **kwargs):
# Only one can be the favorite
with transaction.atomic():
agenda_items = self.__class__.objects.filter(meeting=self.meeting)
if agenda_items.exists():
max_sort_order = agenda_items.last().sort_order
self.sort_order = max_sort_order +1
super().save(*args, **kwargs)
else:
super().save(*args, **kwargs)
class AgendaItem(AbstractAgendaItem):
......
from django.contrib import admin
# Register your models here.
from django.apps import AppConfig
class MembersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'members'
from django.db import models
# Create your models here.
from django.test import TestCase
# Create your tests here.
from django.shortcuts import render
# Create your views here.
......@@ -14,7 +14,7 @@ class MotionForm(forms.ModelForm):
class Meta:
model = Motion
exclude = ('requestor', '_status')
exclude = ['requestor', '_status']
widgets = {
"title": forms.TextInput(
......@@ -118,7 +118,7 @@ die Vielfalt der Mate-basierten Kalt- und Warmgetränke aus Südamerika und Euro
),
}
),
"amount_euro": forms.NumberInput(attrs={"placeholder": 50.0, "step": 1.0}),
"amount_euro": forms.NumberInput(attrs={"placeholder": 50.0}),
"revenue_source": forms.Textarea(
attrs={
"rows": 3,
......@@ -145,9 +145,8 @@ class MotionUpdateForm(MotionForm):
if not self.instance.is_draft:
self.fields["title"].disabled = True
class Meta(MotionForm.Meta):
pass
exclude = ['requestor', '_status', 'gdpr_consent']
def clean(self):
inital_title = self.initial["title"]
......@@ -170,12 +169,13 @@ class FinancialRequestUpdateForm(FinancialRequestForm):
self.fields["title"].disabled = True