diff --git a/src/privatim/__init__.py b/src/privatim/__init__.py index 352c524..a2ec1c1 100644 --- a/src/privatim/__init__.py +++ b/src/privatim/__init__.py @@ -21,10 +21,7 @@ __version__ = '0.0.0' -from typing import Any, TYPE_CHECKING - -from privatim.views.profile import user_pic_url - +from typing import Any, TYPE_CHECKING, Iterable if TYPE_CHECKING: from _typeshed.wsgi import WSGIApplication from privatim.cli.upgrade import UpgradeContext @@ -69,7 +66,14 @@ def includeme(config: Configurator) -> None: config.set_default_csrf_options(require_csrf=True) config.add_request_method(authenticated_user, 'user', property=True) - config.add_request_method(user_pic_url, 'user_pic', property=True) + + def profile_pic(request: 'IRequest') -> str: + user = request.user + if not user: + return '' + return request.route_url('download_general_file', id=user.picture.id) + + config.add_request_method(profile_pic, 'profile_pic', property=True) config.add_request_method(MessageQueue, 'messages', reify=True) def add_action_menu_entry( @@ -90,6 +94,19 @@ def add_action_menu_entry( reify=True ) + def add_action_menu_entries( + request: 'IRequest', + entries: Iterable[tuple[str, str]], + ) -> None: + if not hasattr(request, 'action_menu_entries'): + request.action_menu_entries = [] + for title, url in entries: + request.action_menu_entries.append(ActionMenuEntry(title, url)) + + config.add_request_method( + lambda request: partial(add_action_menu_entries, request), + 'add_action_menu_entries', reify=True) + def main( global_config: Any, **settings: Any diff --git a/src/privatim/forms/agenda_item_form.py b/src/privatim/forms/agenda_item_form.py index 99069d8..3418abb 100644 --- a/src/privatim/forms/agenda_item_form.py +++ b/src/privatim/forms/agenda_item_form.py @@ -1,6 +1,9 @@ +from sqlalchemy import select from wtforms import StringField from wtforms import validators +from wtforms.fields.choices import RadioField from wtforms.fields.simple import TextAreaField +from wtforms.validators import ValidationError from privatim.forms.core import Form from privatim.i18n import _ @@ -41,3 +44,45 @@ def populate_obj(self, obj: 'AgendaItem') -> None: # type:ignore[override] super().populate_obj(obj) for name, field in self._fields.items(): field.populate_obj(obj, name) + + +class AgendaItemCopyForm(Form): + + def __init__( + self, + context: Meeting, + request: 'IRequest', + ) -> None: + + self._title = _('Select Destionation for Agenda Item') + + super().__init__( + request.POST, + obj=context, + meta={'context': context, 'request': request}, + ) + + all_meetings_for_choices = [ + (str(meeting.id), meeting.name) + # valid destination are all meetings except the one from which + # we are copying from + for meeting in request.dbsession.execute( + select(Meeting).where(Meeting.id != context.id) + ).scalars().all() + ] + if not all_meetings_for_choices: + self.copy_to.validators.append( + lambda form, field: ValidationError( + _('No valid destination meetings available.') + ) + ) + self.copy_to.choices = all_meetings_for_choices + + copy_to = RadioField( + label=_('Copy to'), validators=[validators.DataRequired()] + ) + + def populate_obj(self, obj: 'AgendaItem') -> None: # type:ignore[override] + super().populate_obj(obj) + for name, field in self._fields.items(): + field.populate_obj(obj, name) diff --git a/src/privatim/forms/consultation_form.py b/src/privatim/forms/consultation_form.py index 56ac585..6ccbf1e 100644 --- a/src/privatim/forms/consultation_form.py +++ b/src/privatim/forms/consultation_form.py @@ -2,7 +2,6 @@ from privatim.forms.constants import CANTONS_SHORT from privatim.forms.core import Form -from wtforms import StringField from wtforms.fields.choices import SelectField from wtforms.fields.simple import TextAreaField from wtforms.validators import DataRequired @@ -52,17 +51,43 @@ def __init__( ] self.status.choices = translated_choices - title = StringField( + # If editing, populate the secondary_tags field + # if context and context.secondary_tags: + # self.secondary_tags.process_data(context.secondary_tags) + + title = TextAreaField( _('Title'), validators=[DataRequired()], ) - description = TextAreaField(_('Description')) - recommendation = StringField(_('Recommendation')) + + # Beschreibung + description = TextAreaField( + _('Description'), + render_kw={'rows': 6}, + ) + # Empfehlung + recommendation = TextAreaField( + _('Recommendation'), + render_kw={'rows': 6}, + ) + + # new Prüfergebnis + evaluation_result = TextAreaField( + _('Evaluation Result'), + render_kw={'rows': 6}, + ) + + # new: Beschluss + decision = TextAreaField( + _('Decision'), + render_kw={'rows': 6}, + ) + status = SelectField( _('Status'), choices=[] ) - cantons = SearchableSelectField( + secondary_tags = SearchableSelectField( _('Cantons'), choices=[('', '')] + CANTONS_SHORT, validators=[ diff --git a/src/privatim/forms/fields/fields.py b/src/privatim/forms/fields/fields.py index 4bf8114..efbf483 100644 --- a/src/privatim/forms/fields/fields.py +++ b/src/privatim/forms/fields/fields.py @@ -10,7 +10,7 @@ from wtforms.validators import DataRequired from wtforms.validators import InputRequired from wtforms.fields import FieldList -from wtforms.fields.choices import SelectField +from wtforms.fields.choices import SelectMultipleField from wtforms.fields.simple import FileField from wtforms.widgets.core import Select from werkzeug.datastructures import MultiDict @@ -159,8 +159,10 @@ def process_formdata(self, valuelist: list['RawFormValue']) -> None: self.data = sedate.replace_timezone(self.data, self.timezone) -class SearchableSelectField(SelectField): - """A multiple select field with tom-select.js support. +class SearchableSelectField(SelectMultipleField): + + """ + A multiple select field with tom-select.js support. Note: This is unrelated to PostgreSQL full-text search, which also uses the term 'searchable'. @@ -171,7 +173,11 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any: init_tom_select.need() return super().__call__(*args, **kwargs) - widget = ChosenSelectWidget(multiple=True) + def process_data(self, value: list[object]) -> None: + if value: + self.data = [ + str(v.id) if hasattr(v, 'id') else str(v) for v in value + ] class UploadField(FileField): diff --git a/src/privatim/forms/filter_form.py b/src/privatim/forms/filter_form.py new file mode 100644 index 0000000..3b53819 --- /dev/null +++ b/src/privatim/forms/filter_form.py @@ -0,0 +1,67 @@ +from wtforms import SelectField, BooleanField, DateField + +from privatim.forms.constants import cantons_named +from privatim.forms.core import Form +from wtforms.validators import Optional +from privatim.i18n import _ + + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pyramid.interfaces import IRequest + from wtforms import Field + + +def render_filter_field(field: 'Field') -> str: + if isinstance(field, BooleanField): + return field(class_="form-check-input") + else: + return field(class_="form-control") + + +class FilterForm(Form): + def __init__( + self, + request: 'IRequest', + ) -> None: + + self._title = _('Filter') + session = request.dbsession + super().__init__(request.POST, meta={'dbsession': session}) + + def get_type_fields(self) -> list[BooleanField]: + return [self.consultation, self.meeting, self.comment] + + def get_date_fields(self) -> list[tuple[str, DateField]]: + return [('datumVon', self.start_date), ('datumBis', self.end_date)] + + canton: SelectField = SelectField( + _('Kanton'), + choices=[('all', _('all'))] + cantons_named, + validators=[Optional()], + render_kw={'class': 'form-select', 'id': 'kanton'}, + ) + + consultation: BooleanField = BooleanField( + _('Consultation'), + render_kw={'class': 'form-check-input', 'id': 'vernehmlassung'}, + ) + meeting: BooleanField = BooleanField( + _('Meeting'), render_kw={'class': 'form-check-input', 'id': 'sitzung'} + ) + comment: BooleanField = BooleanField( + _('Comment'), + render_kw={'class': 'form-check-input', 'id': 'kommentar'}, + ) + + start_date: DateField = DateField( + _('Date from'), + validators=[Optional()], + render_kw={'class': 'form-control', 'id': 'datumVon'}, + ) + end_date: DateField = DateField( + _('Date to'), + validators=[Optional()], + render_kw={'class': 'form-control', 'id': 'datumBis'}, + ) diff --git a/src/privatim/forms/meeting_form.py b/src/privatim/forms/meeting_form.py index a9b55c3..3ad2bbd 100644 --- a/src/privatim/forms/meeting_form.py +++ b/src/privatim/forms/meeting_form.py @@ -52,18 +52,19 @@ def __init__( ) attendees = SearchableSelectField( - _('Attendees'), + _('Members'), validators=[InputRequired()], ) def validate_name(self, field: 'Field') -> None: - session = self.meta.dbsession - stmt = select(Meeting).where(Meeting.name == field.data) - meeting = session.execute(stmt).scalar() - if meeting: - raise validators.ValidationError(_( - 'A meeting with this name already exists.' - )) + if self._title == _('Add Meeting'): + session = self.meta.dbsession + stmt = select(Meeting).where(Meeting.name == field.data) + meeting = session.execute(stmt).scalar() + if meeting: + raise validators.ValidationError(_( + 'A meeting with this name already exists.' + )) def populate_obj(self, obj: Meeting) -> None: # type:ignore[override] for name, field in self._fields.items(): diff --git a/src/privatim/forms/working_group_forms.py b/src/privatim/forms/working_group_forms.py index 0b53497..3fd99a3 100644 --- a/src/privatim/forms/working_group_forms.py +++ b/src/privatim/forms/working_group_forms.py @@ -46,3 +46,5 @@ def __init__( leader: SelectField = SelectField(_('Leader')) members: SearchableSelectField = SearchableSelectField(_('Members')) + + chairman_contact: StringField = StringField(_('Contact Chairman')) diff --git a/src/privatim/layouts/layout.pt b/src/privatim/layouts/layout.pt index 354f013..193bd6c 100644 --- a/src/privatim/layouts/layout.pt +++ b/src/privatim/layouts/layout.pt @@ -13,11 +13,10 @@ -${panel('navbar')} - + ${panel('navbar')}
-
+
${panel('flash')} @@ -31,7 +30,7 @@ ${panel('navbar')}
-${panel('footer')} + ${panel('footer')} diff --git a/src/privatim/layouts/macros.pt b/src/privatim/layouts/macros.pt index bdd6467..b03b07b 100644 --- a/src/privatim/layouts/macros.pt +++ b/src/privatim/layouts/macros.pt @@ -12,7 +12,7 @@
avatar
avatar -
+
avatar
- ${comment_dict['comment'].user.fullname or comment_dict['comment'].user.email} + ${flattened_comment['comment'].user.fullname or flattened_comment['comment'].user.email}

- ${layout.format_date(comment_dict['comment'].created, 'relative')} + ${layout.format_date(flattened_comment['comment'].created, 'relative')}

- ${comment_dict['comment'].content} + ${flattened_comment['comment'].content}

- - -
-
+
+
diff --git a/src/privatim/layouts/navbar.pt b/src/privatim/layouts/navbar.pt index afcfa80..d36b395 100644 --- a/src/privatim/layouts/navbar.pt +++ b/src/privatim/layouts/navbar.pt @@ -45,7 +45,8 @@
-
+ +
- + +