A web framework for aspiring media terrorists – PRE-ALPHA – DO NOT DEPLOY!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

507 lines
14KB

  1. # -*- coding: utf-8 -*-
  2. # external imports
  3. import time
  4. import copy
  5. import functools
  6. import collections
  7. import peewee
  8. import werkzeug
  9. import click
  10. import flask
  11. # parent imports
  12. #import poobrains
  13. from poobrains import app, request
  14. import poobrains.errors
  15. import poobrains.helpers
  16. import poobrains.rendering
  17. # internal imports
  18. from . import fields
  19. from . import types
  20. class FormMeta(poobrains.helpers.MetaCompatibility, poobrains.helpers.ClassOrInstanceBound):
  21. def __new__(cls, name, bases, attrs):
  22. return super(FormMeta, cls).__new__(cls, name, bases, attrs)
  23. def __setattr__(cls, name, value):
  24. return super(FormMeta, cls).__setattr__(name, value)
  25. class BaseForm(poobrains.rendering.Renderable, metaclass=FormMeta):
  26. __metaclass__ = FormMeta
  27. class Meta:
  28. abstract = True
  29. _external_fields = None
  30. custom_id = None
  31. fields = None
  32. controls = None
  33. prefix = None
  34. name = None
  35. title = None
  36. def __new__(cls, *args, **kw):
  37. # instance = super(BaseForm, cls).__new__(cls, *args, **kw) # 2.7
  38. instance = super(BaseForm, cls).__new__(cls)
  39. instance.fields = poobrains.helpers.CustomOrderedDict()
  40. instance.controls = poobrains.helpers.CustomOrderedDict()
  41. clone_attributes = []
  42. for attr_name in dir(instance):
  43. attr = getattr(instance, attr_name)
  44. if isinstance(attr, fields.BaseField) or isinstance(attr, Fieldset) or isinstance(attr, Button): # FIXME: This should be doable with just one check
  45. clone_attributes.append((attr_name, attr))
  46. for (attr_name, attr) in sorted(clone_attributes, key=lambda x: getattr(x[1], '_created')): # Get elements in the same order they were defined in, as noted by _created property
  47. kw = {}
  48. for propname in attr._meta.clone_props:
  49. value = getattr(attr, propname)
  50. if not callable(value):
  51. value = copy.deepcopy(value)
  52. elif propname in ('default', 'choices', 'value'):
  53. value = value() # handle arguments to Fields that are optionally callable; ex: Checkbox(default=lambda: bool(random.randint(0,1)))
  54. kw[propname] = value
  55. kw['name'] = attr_name
  56. clone = attr.__class__(**kw)
  57. setattr(instance, attr_name, clone)
  58. return instance
  59. def __init__(self, prefix=None, name=None, title=None, custom_id=None, **kwargs):
  60. self._external_fields = []
  61. super(BaseForm, self).__init__(**kwargs)
  62. self.name = name if name else self.__class__.__name__
  63. if title:
  64. self.title = title
  65. elif not self.title: # Only use the fallback if title has been supplied neither to __init__ nor in class definition
  66. self.title = self.__class__.__name__
  67. self.prefix = prefix
  68. self.custom_id = custom_id
  69. def __setattr__(self, name, value):
  70. if name == 'name' and isinstance(value, str):
  71. assert not '.' in value, "Form names *must* not contain dots: %s" % value
  72. super(BaseForm, self).__setattr__(name, value)
  73. elif name == 'prefix':
  74. super(BaseForm, self).__setattr__(name, value)
  75. if value:
  76. child_prefix = "%s.%s" % (value, self.name)
  77. else:
  78. child_prefix = self.name
  79. for field in self.fields.values():
  80. field.prefix = child_prefix
  81. for button in self.controls.values():
  82. button.prefix = child_prefix
  83. elif isinstance(value, fields.BaseField) or isinstance(value, Fieldset):
  84. value.name = name
  85. #value.prefix = "%s.%s" % (self.prefix, self.name) if self.prefix else self.name
  86. #self.fields[name] = value
  87. self.add_field(value)
  88. elif isinstance(value, Button):
  89. value.name = name
  90. value.prefix = "%s.%s" % (self.prefix, self.name) if self.prefix else self.name
  91. self.controls[name] = value
  92. else:
  93. super(BaseForm, self).__setattr__(name, value)
  94. def __iter__(self):
  95. """
  96. Iterate over this forms renderable fields.
  97. """
  98. for field in self.fields.values():
  99. if isinstance(field, (fields.Field, Fieldset)) and field.name not in self._external_fields:
  100. yield field
  101. @property
  102. def renderable_fields(self):
  103. return [field for field in self]
  104. @property
  105. def fieldsets(self):
  106. return [field for field in self if isinstance(field, Fieldset)]
  107. @property
  108. def ref_id(self):
  109. """ HTML 'id' attribute (to enable assigning fields outside that <form> element). """
  110. if self.custom_id:
  111. return self.custom_id
  112. if self.prefix:
  113. return "%s-%s" % (self.prefix.replace('.', '-'), self.name)
  114. return self.name
  115. def empty(self): # TODO: find out why I didn't make this @property
  116. for field in self:
  117. if not field.empty:
  118. return False
  119. return True
  120. @property
  121. def readonly(self):
  122. for field in self:
  123. if not field.readonly:
  124. return False
  125. return True
  126. #def _add_external_field(self, field):
  127. def add_field(self, field, external=False):
  128. """
  129. Add a field which is to be rendered outside of this form, but processed by it.
  130. Fields like this can be created by passing a Form object to the Field constructor.
  131. """
  132. if isinstance(field, fields.Checkbox) and field.name in self.fields and type(field) == type(self.fields[field.name]): # checkboxes/radio inputs can pop up multiple times, but belong to the same name
  133. self.fields[field.name].choices.extend(field.choices)
  134. else:
  135. if self.prefix:
  136. field.prefix = "%s.%s" % (self.prefix, self.name)
  137. else:
  138. field.prefix = self.name
  139. self.fields[field.name] = field
  140. if external:
  141. self._external_fields.append(field.name)
  142. def bind(self, values, files):
  143. if not values is None:
  144. compound_error = poobrains.errors.CompoundError()
  145. actionable_fields = [f for f in self]
  146. actionable_fields += [self.fields[name] for name in self._external_fields]
  147. for field in actionable_fields:
  148. if not field.readonly:
  149. source = files if isinstance(field, fields.File) else values
  150. if not field.name in source:
  151. if field.multi or isinstance(field, Fieldset):
  152. field_values = werkzeug.datastructures.MultiDict()
  153. elif field.type == types.BOOL:
  154. field_values = False # boolean values via checkbox can only ever be implied false. yay html!
  155. else:
  156. field_values = ''
  157. elif field.multi:
  158. field_values = source.getlist(field.name)
  159. else:
  160. field_values = source[field.name]
  161. try:
  162. if isinstance(field, Fieldset):
  163. sub_files = files[field.name] if field.name in files else werkzeug.datastructures.MultiDict()
  164. field.bind(field_values, sub_files)
  165. else:
  166. field.bind(field_values)
  167. except poobrains.errors.CompoundError as ce:
  168. for e in ce.errors:
  169. compound_error.append(e)
  170. for name, control in self.controls.items():
  171. if isinstance(control, Button):
  172. control.value = values.get(name, False)
  173. if len(compound_error):
  174. raise compound_error
  175. def render_fields(self):
  176. """
  177. Render fields of this form which have not yet been rendered.
  178. """
  179. rendered_fields = u''
  180. for field in self:
  181. if not field.rendered:
  182. rendered_fields += field.render()
  183. return rendered_fields
  184. def render_controls(self):
  185. """
  186. Render controls for this form.
  187. TODO: Do we *want* to filter out already rendered controls, like we do with fields?
  188. """
  189. rendered_controls = u''
  190. for control in self.controls.values():
  191. rendered_controls += control.render()
  192. return rendered_controls
  193. def templates(self, mode=None):
  194. tpls = []
  195. for x in [self.__class__] + self.__class__.ancestors():
  196. name = x.__name__.lower()
  197. if issubclass(x, BaseForm):
  198. tpls.append('form/%s.jinja' % name)
  199. if mode:
  200. tpls.append('form/%s-%s.jinja' % (name, mode))
  201. else:
  202. tpls.append('%s.jinja' % name)
  203. if mode:
  204. tpls.append('%s-%s.jinja' % (name, mode))
  205. return tpls
  206. def process(self, submit):
  207. raise NotImplementedError("%s.process not implemented." % self.__class__.__name__)
  208. class Form(BaseForm):
  209. method = None
  210. action = None
  211. class Meta:
  212. abstract = True
  213. def __init__(self, prefix=None, name=None, title=None, method=None, action=None, custom_id=None, **kwargs):
  214. super(Form, self).__init__(prefix=prefix, name=name, title=title, custom_id=custom_id, **kwargs)
  215. self.method = method if method else 'POST'
  216. self.action = action if action else ''
  217. @classmethod
  218. def class_view(cls, mode='full', **kwargs):
  219. instance = cls(**kwargs)
  220. return instance.view(mode)
  221. def validate(self):
  222. pass
  223. @poobrains.helpers.themed
  224. def view(self, mode='full', **kwargs):
  225. """
  226. view function to be called in a flask request context
  227. """
  228. if request.method == self.method:
  229. validation_error = None
  230. binding_error = None
  231. values = request.form.get(self.name, werkzeug.datastructures.MultiDict())
  232. files = request.files.get(self.name, werkzeug.datastructures.FileMultiDict())
  233. try:
  234. self.bind(values, files)
  235. self.validate()
  236. try:
  237. return self.process(request.form['submit'][len(self.ref_id)+1:])
  238. except poobrains.errors.CompoundError as e:
  239. for error in e.errors:
  240. flask.flash(error.message, 'error')
  241. except poobrains.errors.CompoundError as e:
  242. for error in e.errors:
  243. flask.flash(str(error), 'error')
  244. except poobrains.errors.ValidationError as e:
  245. flask.flash(str(e), 'error')
  246. if e.field:
  247. self.fields[e.field].append(e)
  248. if not request.form['submit'].startswith(self.ref_id): # means the right form was submitted, should be implied by the path tho…
  249. app.logger.error("Form %s: submit button of another form used: %s" % (self.name, request.form['submit']))
  250. flask.flash("The form you just used might be broken. Bug someone if this problem persists.", 'error')
  251. return self
  252. class Fieldset(BaseForm):
  253. errors = None
  254. readonly = None
  255. rendered = None
  256. multi = False
  257. _default = werkzeug.datastructures.MultiDict()
  258. class Meta:
  259. abstract = True
  260. clone_props = ['name', 'title']
  261. def __new__(cls, *args, **kwargs):
  262. instance = super(Fieldset, cls).__new__(cls, *args, **kwargs)
  263. instance._created = time.time()
  264. return instance
  265. def __init__(self, *args, **kw):
  266. self.rendered = False
  267. self.readonly = False
  268. self.errors = []
  269. super(Fieldset, self).__init__(*args, **kw)
  270. def render(self, mode=None):
  271. self.rendered = True
  272. return super(Fieldset, self).render(mode)
  273. def process(self, submit, instance):
  274. """
  275. Parameters:
  276. * instance: A `Renderable` instance pertaining to the parent form.
  277. Usually an `Administerable` that was saved before fieldsets
  278. are processed.
  279. """
  280. raise NotImplementedError("%s.process not implemented." % self.__class__.__name__)
  281. def __setattr__(self, name, value):
  282. if name == 'value':
  283. for field in self.fields.values():
  284. if hasattr(value, field.name):
  285. field.value = getattr(value, field.name)
  286. elif name == 'name' and isinstance(value, str):
  287. assert not '.' in value, "Form Field names *must* not contain dots: %s" % value
  288. super(Fieldset, self).__setattr__(name, value)
  289. else:
  290. super(Fieldset, self).__setattr__(name, value)
  291. class ProxyFieldset(Fieldset):
  292. """ A fieldset wrapping a form object """
  293. form = None
  294. def __init__(self, form, **kwargs):
  295. super(ProxyFieldset, self).__init__(**kwargs)
  296. self.form = form
  297. self.title = form.title
  298. self.fields = form.fields # TODO: is this done by reference? will probably fuck up if not.
  299. def bind(self, values, files):
  300. self.form.bind(values, files)
  301. def validate(self):
  302. self.form.validate()
  303. def process(self, submit, instance):
  304. self.form.process(submit)
  305. class Button(poobrains.rendering.Renderable):
  306. name = None
  307. type = None
  308. value = None
  309. label = None
  310. class Meta:
  311. clone_props = ['type', 'name', 'value', 'label']
  312. def __new__(cls, *args, **kwargs):
  313. #instance = super(Button, cls).__new__(cls, *args, **kwargs)
  314. instance = super(Button, cls).__new__(cls)
  315. instance._created = time.time()
  316. return instance
  317. def __init__(self, type, name=None, value=None, label=None):
  318. super(Button, self).__init__()
  319. self.name = name
  320. self.type = type
  321. self.value = value
  322. self.label = label
  323. def templates(self, mode=None):
  324. return ['form/button-%s.jinja' % self.type, 'form/button.jinja']