Source code for org.views.form_definition
import morepath
from onegov.core.security import Private, Public
from onegov.core.utils import normalize_for_url
from onegov.form import FormCollection, FormDefinition
from onegov.form import FormRegistrationWindow
from onegov.gis import Coordinates
from onegov.org import _, OrgApp
from onegov.org.cli import close_ticket
from onegov.org.elements import Link
from onegov.org.forms import FormDefinitionForm
from onegov.org.forms.form_definition import FormDefinitionUrlForm
from onegov.org.layout import FormEditorLayout, FormSubmissionLayout
from onegov.org.models import BuiltinFormDefinition, CustomFormDefinition
from onegov.org.models.form import submission_deletable
from webob import exc
from typing import TypeVar, TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Iterable, Iterator
from onegov.core.layout import Layout
from onegov.core.types import RenderData
from onegov.form import Form, FormSubmission
from onegov.org.request import OrgRequest
from sqlalchemy.orm import Session
from webob import Response
[docs]
def get_form_class(
model: BuiltinFormDefinition | CustomFormDefinition | FormCollection,
request: 'OrgRequest'
) -> type[FormDefinitionForm]:
if isinstance(model, FormCollection):
model = CustomFormDefinition()
form_classes = {
'builtin': FormDefinitionForm,
'custom': FormDefinitionForm
}
return model.with_content_extensions(form_classes[model.type], request)
# FIXME: format_date should really be a utility function on the request
# first and on the layout second, passing around layouts in contexts
# where we don't actually always have one is weird
[docs]
def get_hints(
layout: 'Layout',
window: FormRegistrationWindow | None
) -> 'Iterator[tuple[str, str]]':
if not window:
return
if window.in_the_past:
yield 'stop', _('The registration has ended')
elif not window.enabled:
yield 'stop', _('The registration is closed')
if window.enabled and window.in_the_future:
yield 'date', _('The registration opens on ${day}, ${date}', mapping={
'day': layout.format_date(window.start, 'weekday_long'),
'date': layout.format_date(window.start, 'date_long')
})
if window.enabled and window.in_the_present:
yield 'date', _('The registration closes on ${day}, ${date}', mapping={
'day': layout.format_date(window.end, 'weekday_long'),
'date': layout.format_date(window.end, 'date_long')
})
if window.limit and window.overflow:
yield 'count', _("There's a limit of ${count} attendees", mapping={
'count': window.limit
})
if window.limit and not window.overflow:
spots = window.available_spots
if spots == 0:
yield 'stop', _('There are no spots left')
elif spots == 1:
yield 'count', _('There is one spot left')
else:
yield 'count', _('There are ${count} spots left', mapping={
'count': spots
})
[docs]
def handle_form_change_name(
form: 'FormDefinitionT',
session: 'Session',
new_name: str
) -> 'FormDefinitionT':
new_form = form.for_new_name(new_name)
session.add(new_form)
session.flush()
submissions = form.submissions
windows = form.registration_windows
with session.no_autoflush:
# This placed elsewhere will not work
form.submissions = []
form.registration_windows = []
# assigning the whole list directly will not work
for s in submissions:
s.name = new_name
new_form.submissions.append(s)
for w in windows:
w.name = new_name
new_form.registration_windows.append(w)
session.flush()
assert not form.submissions
assert not form.registration_windows
return new_form
@OrgApp.form(
model=FormDefinition, form=FormDefinitionUrlForm,
template='form.pt', permission=Private,
name='change-url'
)
[docs]
def handle_change_form_name(
self: FormDefinition,
request: 'OrgRequest',
form: FormDefinitionUrlForm,
layout: FormEditorLayout | None = None
) -> 'RenderData | Response':
"""Since the name used for the url is the primary key, we create a new
FormDefinition to make our live easier """
site_title = _('Change URL')
if form.submitted(request):
assert form.name.data is not None
new_form = handle_form_change_name(
self, request.session, form.name.data
)
request.session.delete(self)
return request.redirect(request.link(new_form))
elif not request.POST:
form.process(obj=self)
return {
'layout': layout or FormEditorLayout(self, request),
'form': form,
'title': site_title,
}
@OrgApp.form(
model=FormDefinition,
template='form.pt', permission=Public,
form=lambda self, request: self.form_class
)
[docs]
def handle_defined_form(
self: FormDefinition,
request: 'OrgRequest',
form: 'Form',
layout: FormSubmissionLayout | None = None
) -> 'RenderData | Response':
""" Renders the empty form and takes input, even if it's not valid, stores
it as a pending submission and redirects the user to the view that handles
pending submissions.
"""
collection = FormCollection(request.session)
if not self.current_registration_window:
spots = 0
enabled = True
else:
spots = 1
enabled = self.current_registration_window.accepts_submissions(spots)
if enabled and request.POST:
submission = collection.submissions.add(
self.name, form, state='pending', spots=spots)
return morepath.redirect(request.link(submission))
layout = layout or FormSubmissionLayout(self, request)
return {
'layout': layout,
'title': self.title,
'form': enabled and form,
'definition': self,
'form_width': 'small',
'lead': layout.linkify(self.meta.get('lead')),
'text': self.text,
'people': getattr(self, 'people', None),
'files': getattr(self, 'files', None),
'contact': getattr(self, 'contact_html', None),
'coordinates': getattr(self, 'coordinates', Coordinates()),
'hints': tuple(get_hints(layout, self.current_registration_window)),
'hints_callout': not enabled,
'button_text': _('Continue')
}
@OrgApp.form(
model=FormCollection,
name='new', template='form.pt',
permission=Private, form=get_form_class
)
[docs]
def handle_new_definition(
self: FormCollection,
request: 'OrgRequest',
form: FormDefinitionForm,
layout: FormEditorLayout | None = None
) -> 'RenderData | Response':
if form.submitted(request):
assert form.title.data is not None
assert form.definition.data is not None
if self.definitions.by_name(normalize_for_url(form.title.data)):
request.alert(_('A form with this name already exists'))
else:
definition = self.definitions.add(
title=form.title.data,
definition=form.definition.data,
type='custom'
)
form.populate_obj(definition)
request.success(_('Added a new form'))
return morepath.redirect(request.link(definition))
layout = layout or FormEditorLayout(self, request)
layout.breadcrumbs = [
Link(_('Homepage'), layout.homepage_url),
Link(_('Forms'), request.link(self)),
Link(_('New Form'), request.link(self, name='new'))
]
layout.edit_mode = True
return {
'layout': layout,
'title': _('New Form'),
'form': form,
'form_width': 'large',
}
@OrgApp.form(
model=FormDefinition,
template='form.pt', permission=Private,
form=get_form_class, name='edit'
)
[docs]
def handle_edit_definition(
self: FormDefinition,
request: 'OrgRequest',
form: FormDefinitionForm,
layout: FormEditorLayout | None = None
) -> 'RenderData | Response':
if form.submitted(request):
assert form.definition.data is not None
# why do we exclude definition here? we set it normally right after
# which is also what populate_obj should be doing
form.populate_obj(self, exclude={'definition'})
self.definition = form.definition.data
request.success(_('Your changes were saved'))
return morepath.redirect(request.link(self))
elif not request.POST:
form.process(obj=self)
collection = FormCollection(request.session)
layout = layout or FormEditorLayout(self, request)
layout.breadcrumbs = [
Link(_('Homepage'), layout.homepage_url),
Link(_('Forms'), request.link(collection)),
Link(self.title, request.link(self)),
Link(_('Edit'), request.link(self, name='edit'))
]
layout.edit_mode = True
return {
'layout': layout,
'title': self.title,
'form': form,
'form_width': 'large',
}
@OrgApp.view(
model=FormDefinition,
request_method='DELETE',
permission=Private
)
[docs]
def delete_form_definition(
self: FormDefinition,
request: 'OrgRequest'
) -> None:
"""
With introduction of cancelling submissions over the registration window,
we encourage the user to use this functionality to cancel all form
submissions through the ticket system.
This ensures all submissions are cancelled/denied and the tickets are
closed.In that case the ticket itself attached to the submission is
deletable.
If the customer wants to delete the form directly, we allow it now even
when there are completed submissions. In each case there is a ticket
associated with it we might take a snapshot before deleting it.
"""
request.assert_valid_csrf_token()
if self.type != 'custom':
raise exc.HTTPMethodNotAllowed()
def handle_ticket(submission: 'FormSubmission') -> None:
ticket = submission_deletable(submission, request.session)
if ticket is False:
raise exc.HTTPMethodNotAllowed()
if ticket is not True:
assert request.current_user is not None
close_ticket(ticket, request.current_user, request)
ticket.create_snapshot(request)
def handle_submissions(submissions: 'Iterable[FormSubmission]') -> None:
for s in submissions:
handle_ticket(s)
FormCollection(request.session).definitions.delete(
self.name,
with_submissions=True,
with_registration_windows=True,
handle_submissions=handle_submissions
)