2
0

models.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923
  1. from collections import OrderedDict
  2. from importlib import import_module
  3. from itertools import zip_longest
  4. import json
  5. import os
  6. from pathlib import Path
  7. from PIL import Image
  8. import datetime
  9. from django.apps import apps
  10. from django.conf import settings
  11. from django.contrib import messages
  12. from django.contrib.contenttypes.fields import GenericForeignKey
  13. from django.contrib.contenttypes.models import ContentType
  14. from django.core.files.storage import default_storage
  15. from django.core.serializers.json import DjangoJSONEncoder
  16. from django.db.models import (
  17. CharField,
  18. TextField,
  19. DateTimeField,
  20. Model,
  21. ForeignKey,
  22. PROTECT,
  23. CASCADE,
  24. QuerySet,
  25. )
  26. from django.db.models.fields.files import FieldFile
  27. from django.db.models.signals import post_delete
  28. from django.dispatch import receiver
  29. from django.forms import Form, ImageField, FileField, URLField, EmailField
  30. from django.http import HttpResponseRedirect
  31. from django.template.response import TemplateResponse
  32. from django.utils.safestring import SafeData, mark_safe
  33. from django.utils.timezone import now
  34. from django.utils.translation import gettext_lazy as _
  35. from wagtail.models import Page
  36. from wagtail.contrib.forms.models import (
  37. AbstractForm,
  38. AbstractEmailForm,
  39. AbstractFormSubmission,
  40. )
  41. from .blocks import FormStepBlock, FormFieldBlock
  42. class Step:
  43. def __init__(self, steps, index, struct_child):
  44. self.steps = steps
  45. self.index = index
  46. block = getattr(struct_child, "block", None)
  47. if block is None:
  48. struct_child = []
  49. if isinstance(block, FormStepBlock):
  50. self.name = struct_child.value["name"]
  51. self.form_fields = struct_child.value["form_fields"]
  52. else:
  53. self.name = ""
  54. self.form_fields = struct_child
  55. @property
  56. def index1(self):
  57. return self.index + 1
  58. @property
  59. def url(self):
  60. return "%s?step=%s" % (self.steps.page.url, self.index1)
  61. def get_form_fields(self):
  62. form_fields = OrderedDict()
  63. field_blocks = self.form_fields
  64. for struct_child in field_blocks:
  65. block = struct_child.block
  66. if isinstance(block, FormFieldBlock):
  67. struct_value = struct_child.value
  68. field_name = block.get_slug(struct_value)
  69. form_fields[field_name] = block.get_field(struct_value)
  70. return form_fields
  71. def get_form_class(self):
  72. return type(
  73. "WagtailForm",
  74. self.steps.page.get_form_class_bases(),
  75. self.get_form_fields(),
  76. )
  77. def get_markups_and_bound_fields(self, form):
  78. for struct_child in self.form_fields:
  79. block = struct_child.block
  80. if isinstance(block, FormFieldBlock):
  81. struct_value = struct_child.value
  82. field_name = block.get_slug(struct_value)
  83. yield form[field_name], "field"
  84. else:
  85. yield mark_safe(struct_child), "markup"
  86. def __str__(self):
  87. if self.name:
  88. return self.name
  89. return _("Step %s") % self.index1
  90. @property
  91. def badge(self):
  92. return mark_safe('<span class="badge">%s/%s</span>') % (
  93. self.index1,
  94. len(self.steps),
  95. )
  96. def __html__(self):
  97. return "%s %s" % (self, self.badge)
  98. @property
  99. def is_active(self):
  100. return self.index == self.steps.current_index
  101. @property
  102. def is_last(self):
  103. return self.index1 == len(self.steps)
  104. @property
  105. def has_prev(self):
  106. return self.index > 0
  107. @property
  108. def has_next(self):
  109. return self.index1 < len(self.steps)
  110. @property
  111. def prev(self):
  112. if self.has_prev:
  113. return self.steps[self.index - 1]
  114. @property
  115. def next(self):
  116. if self.has_next:
  117. return self.steps[self.index + 1]
  118. def get_existing_data(self, raw=False):
  119. data = self.steps.get_existing_data()[self.index]
  120. fields = self.get_form_fields()
  121. if not raw:
  122. class FakeField:
  123. storage = self.steps.get_storage()
  124. for field_name, value in data.items():
  125. if field_name in fields and isinstance(
  126. fields[field_name], FileField
  127. ):
  128. data[field_name] = FieldFile(None, FakeField, value)
  129. return data
  130. @property
  131. def is_available(self):
  132. return self.prev is None or self.prev.get_existing_data(raw=True)
  133. class StreamFormJSONEncoder(DjangoJSONEncoder):
  134. def default(self, o):
  135. try:
  136. from phonenumber_field.phonenumber import PhoneNumber
  137. except ImportError:
  138. pass
  139. else:
  140. if isinstance(o, PhoneNumber):
  141. return str(o)
  142. return super().default(o)
  143. class Steps(list):
  144. def __init__(self, page, request=None):
  145. self.page = page
  146. # TODO: Make it possible to change the `form_fields` attribute.
  147. self.form_fields = page.form_fields
  148. self.request = request
  149. has_steps = any(
  150. isinstance(struct_child.block, FormStepBlock)
  151. for struct_child in self.form_fields
  152. )
  153. if has_steps:
  154. steps = [
  155. Step(self, i, form_field)
  156. for i, form_field in enumerate(self.form_fields)
  157. ]
  158. else:
  159. steps = [Step(self, 0, self.form_fields)]
  160. super().__init__(steps)
  161. def clamp_index(self, index: int):
  162. if index < 0:
  163. index = 0
  164. if index >= len(self):
  165. index = len(self) - 1
  166. while not self[index].is_available:
  167. index -= 1
  168. return index
  169. @property
  170. def current_index(self):
  171. return self.request.session.get(self.page.current_step_session_key, 0)
  172. @property
  173. def current(self):
  174. return self[self.current_index]
  175. @current.setter
  176. def current(self, new_index: int):
  177. if not isinstance(new_index, int):
  178. raise TypeError("Use an integer to set the new current step.")
  179. self.request.session[
  180. self.page.current_step_session_key
  181. ] = self.clamp_index(new_index)
  182. def forward(self, increment: int = 1):
  183. self.current = self.current_index + increment
  184. def backward(self, increment: int = 1):
  185. self.current = self.current_index - increment
  186. def get_submission(self):
  187. return self.page.get_submission(self.request)
  188. def get_existing_data(self):
  189. submission = self.get_submission()
  190. data = [] if submission is None else json.loads(submission.form_data)
  191. length_difference = len(self) - len(data)
  192. if length_difference > 0:
  193. data.extend([{}] * length_difference)
  194. return data
  195. def get_current_form(self):
  196. request = self.request
  197. if request.method == "POST":
  198. step_value = request.POST.get("step", "next")
  199. if step_value == "prev":
  200. self.backward()
  201. else:
  202. return self.current.get_form_class()(
  203. request.POST,
  204. request.FILES,
  205. initial=self.current.get_existing_data(),
  206. )
  207. return self.current.get_form_class()(
  208. initial=self.current.get_existing_data()
  209. )
  210. def get_storage(self):
  211. return self.page.get_storage()
  212. def save_files(self, form):
  213. submission = self.get_submission()
  214. for name, field in form.fields.items():
  215. if isinstance(field, FileField):
  216. file = form.cleaned_data[name]
  217. if file == form.initial.get(name, ""): # Nothing submitted.
  218. form.cleaned_data[name] = file.name
  219. continue
  220. if submission is not None:
  221. submission.delete_file(name)
  222. if not file: # 'Clear' was checked.
  223. form.cleaned_data[name] = ""
  224. continue
  225. directory = self.request.session.session_key
  226. storage = self.get_storage()
  227. Path(storage.path(directory)).mkdir(parents=True, exist_ok=True)
  228. path = storage.get_available_name(
  229. str(Path(directory) / file.name)
  230. )
  231. with storage.open(path, "wb+") as destination:
  232. for chunk in file.chunks():
  233. destination.write(chunk)
  234. form.cleaned_data[name] = path
  235. def update_data(self):
  236. form = self.get_current_form()
  237. if form.is_valid():
  238. form_data = self.get_existing_data()
  239. self.save_files(form)
  240. form_data[self.current_index] = form.cleaned_data
  241. form_data = json.dumps(form_data, cls=StreamFormJSONEncoder)
  242. is_complete = self.current.is_last
  243. submission = self.get_submission()
  244. submission.form_data = form_data
  245. if not submission.is_complete and is_complete:
  246. submission.status = submission.COMPLETE
  247. submission.save()
  248. if is_complete:
  249. self.current = 0
  250. else:
  251. self.forward()
  252. return is_complete
  253. return False
  254. class SessionFormSubmission(AbstractFormSubmission):
  255. session_key = CharField(max_length=40, null=True, default=None)
  256. user = ForeignKey(
  257. settings.AUTH_USER_MODEL,
  258. null=True,
  259. blank=True,
  260. related_name="+",
  261. on_delete=PROTECT,
  262. )
  263. thumbnails_by_path = TextField(default=json.dumps({}))
  264. last_modification = DateTimeField(_("last modification"), auto_now=True)
  265. INCOMPLETE = "incomplete"
  266. COMPLETE = "complete"
  267. REVIEWED = "reviewed"
  268. APPROVED = "approved"
  269. REJECTED = "rejected"
  270. STATUSES = (
  271. (INCOMPLETE, _("Not submitted")),
  272. (COMPLETE, _("In progress")),
  273. (REVIEWED, _("Under consideration")),
  274. (APPROVED, _("Approved")),
  275. (REJECTED, _("Rejected")),
  276. )
  277. status = CharField(max_length=10, choices=STATUSES, default=INCOMPLETE)
  278. class Meta:
  279. verbose_name = _("form submission")
  280. verbose_name_plural = _("form submissions")
  281. unique_together = (("page", "session_key"), ("page", "user"))
  282. abstract = True
  283. @property
  284. def is_complete(self):
  285. return self.status != self.INCOMPLETE
  286. @property
  287. def form_page(self):
  288. return self.page.specific
  289. def get_session(self):
  290. return import_module(settings.SESSION_ENGINE).SessionStore(
  291. session_key=self.session_key
  292. )
  293. def reset_step(self):
  294. session = self.get_session()
  295. try:
  296. del session[self.form_page.current_step_session_key]
  297. except KeyError:
  298. pass
  299. else:
  300. session.save()
  301. def get_storage(self):
  302. return self.form_page.get_storage()
  303. def get_thumbnail_path(self, path, width=64, height=64):
  304. if not path:
  305. return ""
  306. variant = "%s×%s" % (width, height)
  307. thumbnails_by_path = json.loads(self.thumbnails_by_path)
  308. thumbnails_paths = thumbnails_by_path.get(path)
  309. if thumbnails_paths is None:
  310. thumbnails_by_path[path] = {}
  311. else:
  312. thumbnail_path = thumbnails_paths.get(variant)
  313. if thumbnail_path is not None:
  314. return thumbnail_path
  315. path = Path(path)
  316. thumbnail_path = str(path.with_suffix(".%s%s" % (variant, path.suffix)))
  317. storage = self.get_storage()
  318. thumbnail_path = storage.get_available_name(thumbnail_path)
  319. thumbnail = Image.open(storage.path(path))
  320. thumbnail.thumbnail((width, height))
  321. thumbnail.save(storage.path(thumbnail_path))
  322. thumbnails_by_path[str(path)][variant] = thumbnail_path
  323. self.thumbnails_by_path = json.dumps(
  324. thumbnails_by_path, cls=StreamFormJSONEncoder
  325. )
  326. self.save()
  327. return thumbnail_path
  328. def get_fields(self, by_step=False):
  329. return self.form_page.get_form_fields(by_step=by_step)
  330. def get_existing_thumbnails(self, path):
  331. thumbnails_paths = json.loads(self.thumbnails_by_path).get(path, {})
  332. for thumbnail_path in thumbnails_paths.values():
  333. yield thumbnail_path
  334. def get_files_by_field(self):
  335. data = self.get_data(raw=True)
  336. files = {}
  337. for name, field in self.get_fields().items():
  338. if isinstance(field, FileField):
  339. path = data.get(name)
  340. if path:
  341. files[name] = [path] + list(
  342. self.get_existing_thumbnails(path)
  343. )
  344. return files
  345. def get_all_files(self):
  346. for paths in self.get_files_by_field().values():
  347. for path in paths:
  348. yield path
  349. def delete_file(self, field_name):
  350. thumbnails_by_path = json.loads(self.thumbnails_by_path)
  351. for path in self.get_files_by_field().get(field_name, ()):
  352. self.get_storage().delete(path)
  353. if path in thumbnails_by_path:
  354. del thumbnails_by_path[path]
  355. self.thumbnails_by_path = json.dumps(
  356. thumbnails_by_path, cls=StreamFormJSONEncoder
  357. )
  358. self.save()
  359. def render_email(self, value):
  360. return mark_safe('<a href="mailto:%s" target="_blank">%s</a>') % (
  361. value,
  362. value,
  363. )
  364. def render_link(self, value):
  365. return mark_safe('<a href="%s" target="_blank">%s</a>') % (value, value)
  366. def render_image(self, value):
  367. storage = self.get_storage()
  368. return mark_safe(
  369. '<a href="%s" target="_blank"><img src="%s" /></a>'
  370. ) % (storage.url(value), storage.url(self.get_thumbnail_path(value)))
  371. def render_file(self, value):
  372. return mark_safe('<a href="%s" target="_blank">%s</a>') % (
  373. self.get_storage().url(value),
  374. Path(value).name,
  375. )
  376. def format_value(self, field, value):
  377. if value is None or value == "":
  378. return "-"
  379. new_value = self.form_page.format_value(field, value)
  380. if new_value != value:
  381. return new_value
  382. if value is True:
  383. return "Yes"
  384. if value is False:
  385. return "No"
  386. if isinstance(value, (list, tuple)):
  387. return ", ".join([self.format_value(field, item) for item in value])
  388. if isinstance(value, datetime.date):
  389. return value
  390. if isinstance(field, EmailField):
  391. return self.render_email(value)
  392. if isinstance(field, URLField):
  393. return self.render_link(value)
  394. if isinstance(field, ImageField):
  395. return self.render_image(value)
  396. if isinstance(field, FileField):
  397. return self.render_file(value)
  398. if isinstance(value, SafeData) or hasattr(value, "__html__"):
  399. return value
  400. return str(value)
  401. def format_db_field(self, field_name, raw=False):
  402. method = getattr(self, "get_%s_display" % field_name, None)
  403. if method is not None:
  404. return method()
  405. value = getattr(self, field_name)
  406. if raw:
  407. return value
  408. return self.format_value(
  409. self._meta.get_field(field_name).formfield(), value
  410. )
  411. def get_steps_data(self, raw=False):
  412. steps_data = json.loads(self.form_data)
  413. if raw:
  414. return steps_data
  415. fields_and_data_iterator = zip_longest(
  416. self.get_fields(by_step=True), steps_data, fillvalue={}
  417. )
  418. return [
  419. OrderedDict(
  420. [
  421. (name, self.format_value(field, step_data.get(name)))
  422. for name, field in step_fields.items()
  423. ]
  424. )
  425. for step_fields, step_data in fields_and_data_iterator
  426. ]
  427. def get_extra_data(self, raw=False):
  428. return self.form_page.get_extra_data(self, raw=raw)
  429. def get_data(self, raw=False, add_metadata=True):
  430. steps_data = self.get_steps_data(raw=raw)
  431. form_data = {}
  432. form_data.update(self.get_extra_data(raw=raw))
  433. for step_data in steps_data:
  434. form_data.update(step_data)
  435. if add_metadata:
  436. form_data.update(
  437. status=self.format_db_field("status", raw=raw),
  438. user=self.format_db_field("user", raw=raw),
  439. submit_time=self.format_db_field("submit_time", raw=raw),
  440. last_modification=self.format_db_field(
  441. "last_modification", raw=raw
  442. ),
  443. )
  444. return form_data
  445. def steps_with_data_iterator(self, raw=False):
  446. for step, step_data_fields, step_data in zip(
  447. self.form_page.get_steps(),
  448. self.form_page.get_data_fields(by_step=True),
  449. self.get_steps_data(raw=raw),
  450. ):
  451. yield step, [
  452. (field_name, field_label, step_data[field_name])
  453. for field_name, field_label in step_data_fields
  454. ]
  455. @receiver(post_delete, sender=SessionFormSubmission)
  456. def delete_files(sender, **kwargs):
  457. instance = kwargs["instance"]
  458. instance.reset_step()
  459. storage = instance.get_storage()
  460. for path in instance.get_all_files():
  461. storage.delete(path)
  462. # Automatically deletes ancestor folders if empty.
  463. directory = Path(path)
  464. while directory.parent != Path(directory.root):
  465. directory = directory.parent
  466. try:
  467. subdirectories, files = storage.listdir(directory)
  468. except FileNotFoundError:
  469. continue
  470. if not subdirectories and not files:
  471. Path(storage.path(directory)).rmdir()
  472. class SubmissionRevisionQuerySet(QuerySet):
  473. def for_submission(self, submission):
  474. return self.filter(**self.model.get_filters_for(submission))
  475. def created(self):
  476. return self.filter(type=self.model.CREATED)
  477. def changed(self):
  478. return self.filter(type=self.model.CHANGED)
  479. def deleted(self):
  480. return self.filter(type=self.model.DELETED)
  481. class SubmissionRevision(Model):
  482. CREATED = "created"
  483. CHANGED = "changed"
  484. DELETED = "deleted"
  485. TYPES = (
  486. (CREATED, _("Created")),
  487. (CHANGED, _("Changed")),
  488. (DELETED, _("Deleted")),
  489. )
  490. type = CharField(max_length=7, choices=TYPES)
  491. created_at = DateTimeField(auto_now_add=True)
  492. submission_ct = ForeignKey("contenttypes.ContentType", on_delete=CASCADE)
  493. submission_id = TextField()
  494. submission = GenericForeignKey("submission_ct", "submission_id")
  495. data = TextField()
  496. summary = TextField()
  497. objects = SubmissionRevisionQuerySet.as_manager()
  498. class Meta:
  499. ordering = ("-created_at",)
  500. abstract = True
  501. @staticmethod
  502. def get_filters_for(submission):
  503. return {
  504. "submission_ct": ContentType.objects.get_for_model(
  505. submission._meta.model
  506. ),
  507. "submission_id": str(submission.pk),
  508. }
  509. @classmethod
  510. def diff_summary(cls, page, data1, data2):
  511. diff = []
  512. data_fields = page.get_data_fields()
  513. hidden_types = (tuple, list, dict)
  514. for k, label in data_fields:
  515. value1 = data1.get(k)
  516. value2 = data2.get(k)
  517. if value2 == value1 or not value1 and not value2:
  518. continue
  519. is_hidden = isinstance(value1, hidden_types) or isinstance(
  520. value2, hidden_types
  521. )
  522. # Escapes newlines as they are used as separator inside summaries.
  523. if isinstance(value1, str):
  524. value1 = value1.replace("\n", r"\n")
  525. if isinstance(value2, str):
  526. value2 = value2.replace("\n", r"\n")
  527. if value2 and not value1:
  528. diff.append(
  529. (
  530. (_("“%s” set.") % label)
  531. if is_hidden
  532. else (_("“%s” set to “%s”.")) % (label, value2)
  533. )
  534. )
  535. elif value1 and not value2:
  536. diff.append(_("“%s” unset.") % label)
  537. else:
  538. diff.append(
  539. (
  540. (_("“%s” changed.") % label)
  541. if is_hidden
  542. else (
  543. _("“%s” changed from “%s” to “%s”.")
  544. % (label, value1, value2)
  545. )
  546. )
  547. )
  548. return "\n".join(diff)
  549. @classmethod
  550. def create_from_submission(cls, submission, revision_type):
  551. page = submission.form_page
  552. try:
  553. previous = cls.objects.for_submission(submission).latest(
  554. "created_at"
  555. )
  556. except cls.DoesNotExist:
  557. previous_data = {}
  558. else:
  559. previous_data = previous.get_data()
  560. filters = cls.get_filters_for(submission)
  561. data = submission.get_data(raw=True, add_metadata=False)
  562. data["status"] = submission.status
  563. if revision_type == cls.CREATED:
  564. summary = _("Submission created.")
  565. elif revision_type == cls.DELETED:
  566. summary = _("Submission deleted.")
  567. else:
  568. summary = cls.diff_summary(page, previous_data, data)
  569. if not summary: # Nothing changed.
  570. return
  571. filters.update(
  572. type=revision_type,
  573. data=json.dumps(data, cls=StreamFormJSONEncoder),
  574. summary=summary,
  575. )
  576. return cls.objects.create(**filters)
  577. def get_data(self):
  578. return json.loads(self.data)
  579. # ORIGINAL NORIPYT CODE.
  580. # We don't want these receivers triggering.
  581. # @receiver(post_save)
  582. # def create_submission_changed_revision(sender, **kwargs):
  583. # if not issubclass(sender, SessionFormSubmission):
  584. # return
  585. # submission = kwargs['instance']
  586. # created = kwargs['created']
  587. # SubmissionRevision.create_from_submission(
  588. # submission, (SubmissionRevision.CREATED if created
  589. # else SubmissionRevision.CHANGED))
  590. # @receiver(post_delete)
  591. # def create_submission_deleted_revision(sender, **kwargs):
  592. # if not issubclass(sender, SessionFormSubmission):
  593. # return
  594. # submission = kwargs['instance']
  595. # SubmissionRevision.create_from_submission(submission,
  596. # SubmissionRevision.DELETED)
  597. class StreamFormMixin:
  598. preview_modes = Page.DEFAULT_PREVIEW_MODES
  599. @property
  600. def current_step_session_key(self):
  601. return "%s:step" % self.pk
  602. def get_steps(self, request=None):
  603. if not hasattr(self, "steps"):
  604. steps = Steps(self, request=request)
  605. if request is None:
  606. return steps
  607. self.steps = steps
  608. return self.steps
  609. def get_form_fields(self, by_step=False):
  610. if by_step:
  611. return [step.get_form_fields() for step in self.get_steps()]
  612. form_fields = OrderedDict()
  613. for step_fields in self.get_form_fields(by_step=True):
  614. form_fields.update(step_fields)
  615. return form_fields
  616. def get_context(self, request, *args, **kwargs):
  617. context = super().get_context(request, *args, **kwargs)
  618. self.steps = self.get_steps(request)
  619. step_value = request.GET.get("step")
  620. if step_value is not None and step_value.isdigit():
  621. self.steps.current = int(step_value) - 1
  622. form = self.steps.get_current_form()
  623. context.update(
  624. steps=self.steps,
  625. step=self.steps.current,
  626. form=form,
  627. markups_and_bound_fields=list(
  628. self.steps.current.get_markups_and_bound_fields(form)
  629. ),
  630. )
  631. return context
  632. def get_storage(self):
  633. return default_storage
  634. @staticmethod
  635. def get_form_class_bases():
  636. return (Form,)
  637. @staticmethod
  638. def get_submission_class():
  639. return SessionFormSubmission
  640. def get_submission(self, request):
  641. Submission = self.get_submission_class()
  642. if request.user.is_authenticated:
  643. user_submission = (
  644. Submission.objects.filter(user=request.user, page=self)
  645. .order_by("-pk")
  646. .first()
  647. )
  648. if user_submission is None:
  649. return Submission(user=request.user, page=self, form_data="[]")
  650. return user_submission
  651. user_submission = (
  652. Submission.objects.filter(
  653. session_key=request.session.session_key, page=self
  654. )
  655. .order_by("-pk")
  656. .first()
  657. )
  658. if user_submission is None:
  659. return Submission(
  660. session_key=request.session.session_key,
  661. page=self,
  662. form_data="[]",
  663. )
  664. return user_submission
  665. def get_success_url(self):
  666. form_complete_models = [
  667. model
  668. for model in apps.get_models()
  669. if issubclass(model, FormCompleteMixin)
  670. ]
  671. cts = ContentType.objects.get_for_models(*form_complete_models).values()
  672. first_child = self.get_children().filter(content_type__in=cts).first()
  673. if first_child is None:
  674. return self.url
  675. return first_child.url
  676. def serve_success(self, request, *args, **kwargs):
  677. url = self.get_success_url()
  678. if url == self.url:
  679. messages.success(request, _("Successfully submitted the form."))
  680. return HttpResponseRedirect(url)
  681. def serve(self, request, *args, **kwargs):
  682. context = self.get_context(request)
  683. form = context["form"]
  684. if request.method == "POST" and form.is_valid():
  685. is_complete = self.steps.update_data()
  686. if is_complete:
  687. return self.serve_success(request, *args, **kwargs)
  688. return HttpResponseRedirect(self.url)
  689. return Page.serve(self, request, *args, **kwargs)
  690. def get_data_fields(self, by_step=False, add_metadata=True):
  691. if by_step:
  692. return [
  693. [
  694. (field_name, field.label)
  695. for field_name, field in step_fields.items()
  696. ]
  697. for step_fields in self.get_form_fields(by_step=True)
  698. ]
  699. data_fields = []
  700. data_fields.extend(self.get_extra_data_fields())
  701. if add_metadata:
  702. data_fields.extend(
  703. (
  704. ("status", _("Status")),
  705. ("user", _("User")),
  706. ("submit_time", _("First modification")),
  707. ("last_modification", _("Last modification")),
  708. )
  709. )
  710. data_fields.extend(
  711. [
  712. (field_name, field_label)
  713. for step_data_fields in self.get_data_fields(by_step=True)
  714. for field_name, field_label in step_data_fields
  715. ]
  716. )
  717. return data_fields
  718. def get_extra_data_fields(self):
  719. return ()
  720. def get_extra_data(self, submission, raw=False):
  721. return {}
  722. def format_value(self, field, value):
  723. return value
  724. class ClosingFormMixin(Model):
  725. closing_at = DateTimeField()
  726. closed_template = None
  727. class Meta:
  728. abstract = True
  729. @property
  730. def is_closed(self):
  731. return now() > self.closing_at
  732. def get_closed_template(self, request, *args, **kwargs):
  733. if self.closed_template is None:
  734. template = self.get_template(request, *args, **kwargs)
  735. base, ext = os.path.splitext(template)
  736. return "%s_closed%s" % (base, ext)
  737. return self.closed_template
  738. def serve_closed(self, request, *args, **kwargs):
  739. return TemplateResponse(
  740. request,
  741. self.get_closed_template(request, *args, **kwargs),
  742. self.get_context(request, *args, **kwargs),
  743. )
  744. def serve(self, request, *args, **kwargs):
  745. if self.is_closed:
  746. return self.serve_closed(request, *args, **kwargs)
  747. return super().serve(request, *args, **kwargs)
  748. class FormCompleteMixin:
  749. def get_form_page(self):
  750. return self.get_parent().specific
  751. def serve(self, request, *args, **kwargs):
  752. form_page = self.get_form_page()
  753. if (
  754. isinstance(form_page, LoginRequiredMixin)
  755. and not request.user.is_authenticated()
  756. ):
  757. return HttpResponseRedirect(form_page.url)
  758. self.submission = form_page.get_submission(request)
  759. if (
  760. self.submission is not None
  761. and self.submission.is_complete
  762. or getattr(request, "is_preview", False)
  763. ):
  764. return super().serve(request, *args, **kwargs)
  765. return HttpResponseRedirect(form_page.url)
  766. def get_context(self, *args, **kwargs):
  767. context = super().get_context(*args, **kwargs)
  768. if hasattr(self, "submission"):
  769. context["submission"] = self.submission
  770. return context
  771. class LoginRequiredMixin:
  772. login_required_template = None
  773. def get_login_required_template(self, request, *args, **kwargs):
  774. if self.login_required_template is None:
  775. template = self.get_template(request, *args, **kwargs)
  776. base, ext = os.path.splitext(template)
  777. return "%s_login_required%s" % (base, ext)
  778. return self.login_required_template
  779. def serve_login_required(self, request, *args, **kwargs):
  780. return TemplateResponse(
  781. request,
  782. self.get_login_required_template(request, *args, **kwargs),
  783. self.get_context(request, *args, **kwargs),
  784. )
  785. def serve(self, request, *args, **kwargs):
  786. if not request.user.is_authenticated():
  787. return self.serve_login_required(request, *args, **kwargs)
  788. return super().serve(request, *args, **kwargs)
  789. class AbstractStreamForm(StreamFormMixin, AbstractForm):
  790. class Meta:
  791. abstract = True
  792. class AbstractEmailStreamForm(StreamFormMixin, AbstractEmailForm):
  793. class Meta:
  794. abstract = True