Source code for org.forms.extensions
from functools import cached_property
from sedate import utcnow, to_timezone
from onegov.core.html_diff import render_html_diff
from onegov.form.extensions import FormExtension
from onegov.form.fields import HoneyPotField
from onegov.form.fields import TimezoneDateTimeField
from onegov.form.fields import UploadField
from onegov.form.submissions import prepare_for_submission
from onegov.form.validators import StrictOptional, ValidPhoneNumber
from onegov.gis import CoordinatesField
from onegov.org import _
from wtforms.fields import EmailField
from wtforms.fields import StringField
from wtforms.fields import TextAreaField
from wtforms.validators import DataRequired, InputRequired
from typing import TypeVar, TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Collection
from markupsafe import Markup
from onegov.form import Form
from wtforms import Field
[docs]
class CoordinatesFormExtension(FormExtension[FormT], name='coordinates'):
[docs]
def create(self) -> type[FormT]:
class CoordinatesForm(self.form_class): # type:ignore
coordinates = CoordinatesField(
label=_('Coordinates'),
description=_(
'The marker can be moved by dragging it with the mouse'
),
fieldset=_('Map'),
render_kw={'data-map-type': 'marker'}
)
return CoordinatesForm
[docs]
class SubmitterFormExtension(FormExtension[FormT], name='submitter'):
[docs]
def create(self) -> type[FormT]:
class SubmitterForm(self.form_class): # type:ignore
submitter = EmailField(
label=_('E-Mail'),
fieldset=_('Submitter'),
validators=[DataRequired()]
)
submitter_name = StringField(
label=_('Name'),
fieldset=_('Submitter'),
validators=[InputRequired()]
)
submitter_address = StringField(
label=_('Address'),
fieldset=_('Submitter'),
validators=[InputRequired()]
)
submitter_phone = StringField(
label=_('Phone'),
fieldset=_('Submitter'),
validators=[InputRequired(), ValidPhoneNumber()]
)
def on_request(self) -> None:
""" This is not an optimal solution defining this on a form
extension. However, this is the first of it's kind.
Don't forget to call super for the next one. =) """
if hasattr(super(), 'on_request'):
super().on_request()
if not hasattr(self.model, 'directory'):
fields: Collection[str] = []
else:
fields = self.model.directory.submitter_meta_fields or []
for field in ('name', 'address', 'phone'):
if f'submitter_{field}' not in fields:
self.delete_field(f'submitter_{field}')
@property
def submitter_meta(self) -> dict[str, str | None]:
def field_data(name: str) -> str | None:
field = getattr(self, name)
return field and field.data or None
return {
'submitter_name': field_data('submitter_name'),
'submitter_phone': field_data('submitter_phone'),
'submitter_address': field_data('submitter_address')
}
return SubmitterForm
[docs]
class CommentFormExtension(FormExtension[FormT], name='comment'):
[docs]
def create(self) -> type[FormT]:
class CommentForm(self.form_class): # type:ignore
comment = TextAreaField(
label=_('Comment'),
fieldset=_('Submitter'),
render_kw={'rows': 7}
)
return CommentForm
[docs]
class ChangeRequestFormExtension(FormExtension[FormT], name='change-request'):
[docs]
def create(self) -> type[FormT]:
# XXX circular import
from onegov.org.models.directory import ExtendedDirectoryEntry
prepare_for_submission(self.form_class, for_change_request=True)
class ChangeRequestForm(self.form_class): # type:ignore
@cached_property
def target(self) -> ExtendedDirectoryEntry | None:
# not all steps have this information set, for example, towards
# the end, the onegov.form submission code runs an extra
# validation, which we ignore, trusting that it all worked
# out earlier
if not getattr(self, 'model', None):
return None
return (
self.request.session.query(ExtendedDirectoryEntry)
.filter_by(id=self.model.meta['directory_entry'])
.first()
)
def is_different(self, field: 'Field') -> bool:
# if the target has been removed, stop
if not self.target:
return True
# after the changes have been applied, use the list of changes
if self.model.meta.get('changed'):
return field.id in self.model.meta['changed']
# ignore CSRF token
if field.id == 'csrf_token':
return False
# coordinates fields are provided through extension
if field.id == 'coordinates':
return field.data != self.target.coordinates
# upload fields differ if they are not empty
if isinstance(field, UploadField):
return field.data and True or False
# like coordinates, provided through extension
if field.id in ('publication_start', 'publication_end'):
if not field.data:
return False
return (
to_timezone(field.data, 'UTC')
!= getattr(self.target, field.id)
)
stored = self.target.values.get(field.id) or None
field_data = field.data or None
return stored != field_data
def render_original(
self,
field: 'Field',
from_model: bool = False
) -> 'Markup':
prev = field.data
try:
model = self.target
if model is not None:
field.data = (
model.values.get(field.id)
if not from_model
else getattr(model, field.id)
)
else:
field.data = None
return super().render_display(field)
finally:
field.data = prev
def render_display(self, field: 'Field') -> 'Markup | None':
if self.is_different(field):
proposed = super().render_display(field)
if not self.target:
return proposed
if field.id in ('csrf_token', 'coordinates'):
return proposed
if field.id in ('publication_start', 'publication_end'):
original = self.render_original(field, from_model=True)
return render_html_diff(original, proposed)
if field.id not in self.target.values:
return proposed
if isinstance(field, UploadField):
return proposed
original = self.render_original(field)
return render_html_diff(original, proposed)
return None
def ensure_changes(self) -> bool | None:
if not self.target:
return None
for name, field in self._fields.items():
if self.is_different(field):
return None
for name, field in self._fields.items():
if name == 'csrf_token':
continue
field.errors.append(
_('Please provide at least one change')
)
return False
return ChangeRequestForm
[docs]
class PublicationFormExtension(FormExtension[FormT], name='publication'):
"""Can be used with TimezonePublicationMixin or UTCDateTime type decorator.
"""
[docs]
def create(self, timezone: str = 'Europe/Zurich') -> type[FormT]:
tz = timezone
class PublicationForm(self.form_class): # type:ignore
publication_start = TimezoneDateTimeField(
label=_('Start'),
timezone=tz,
fieldset=_('Publication'),
validators=[StrictOptional()]
)
publication_end = TimezoneDateTimeField(
label=_('End'),
timezone=tz,
fieldset=_('Publication'),
validators=[StrictOptional()]
)
def ensure_publication_start_end(self) -> bool | None:
start = self.publication_start
end = self.publication_end
if not start or not end:
return None
if end.data and to_timezone(end.data, 'UTC') <= utcnow():
assert isinstance(self.publication_end.errors, list)
self.publication_end.errors.append(
_('Publication end must be in the future'))
return False
if not start.data or not end.data:
return None
if end.data <= start.data:
for field_name in ('publication_start', 'publication_end'):
field = getattr(self, field_name)
field.errors.append(
_('Publication start must be prior to end'))
return False
return None
return PublicationForm
[docs]
class HoneyPotFormExtension(FormExtension[FormT], name='honeypot'):
[docs]
def create(self) -> type[FormT]:
class HoneyPotForm(self.form_class): # type:ignore
duplicate_of = HoneyPotField()
def on_request(self) -> None:
if self.model and not getattr(self.model, 'honeypot', False):
self.delete_field('duplicate_of')
return HoneyPotForm