PageRenderTime 302ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/ckan/forms/common.py

https://bitbucket.org/philippkueng/ckan-liip
Python | 992 lines | 966 code | 14 blank | 12 comment | 22 complexity | b691e8911ef434ff0813f6a373f68c0f MD5 | raw file
  1. import re
  2. import logging
  3. from formalchemy import helpers as fa_h
  4. import formalchemy
  5. import genshi
  6. from pylons.templating import render_genshi as render
  7. from pylons import c, config
  8. from pylons.i18n import _, ungettext, N_, gettext
  9. from ckan.lib.helpers import literal
  10. from ckan.authz import Authorizer
  11. import ckan.model as model
  12. import ckan.lib.helpers as h
  13. import ckan.lib.field_types as field_types
  14. import ckan.misc
  15. import ckan.lib.dictization.model_save as model_save
  16. log = logging.getLogger(__name__)
  17. name_match = re.compile('[a-z0-9_\-]*$')
  18. def name_validator(val, field=None):
  19. # check basic textual rules
  20. min_length = 2
  21. if len(val) < min_length:
  22. raise formalchemy.ValidationError(_('Name must be at least %s characters long') % min_length)
  23. if not name_match.match(val):
  24. raise formalchemy.ValidationError(_('Name must be purely lowercase alphanumeric (ascii) characters and these symbols: -_'))
  25. def package_exists(val):
  26. if model.Session.query(model.Package).autoflush(False).filter_by(name=val).count():
  27. return True
  28. return False
  29. def package_name_validator(val, field=None):
  30. name_validator(val, field)
  31. # we disable autoflush here since may get used in dataset preview
  32. pkgs = model.Session.query(model.Package).autoflush(False).filter_by(name=val)
  33. for pkg in pkgs:
  34. if pkg != field.parent.model:
  35. raise formalchemy.ValidationError(_('Dataset name already exists in database'))
  36. def group_exists(val):
  37. if model.Session.query(model.Group).autoflush(False).filter_by(name=val).count():
  38. return True
  39. return False
  40. def group_name_validator(val, field=None):
  41. name_validator(val, field)
  42. # we disable autoflush here since may get used in dataset preview
  43. groups = model.Session.query(model.Group).autoflush(False).filter_by(name=val)
  44. for group in groups:
  45. if group != field.parent.model:
  46. raise formalchemy.ValidationError(_('Group name already exists in database'))
  47. def field_readonly_renderer(key, value, newline_reqd=False):
  48. if value is None:
  49. value = ''
  50. html = literal('<p>%s</p>') % value
  51. if newline_reqd:
  52. html += literal('<br/>')
  53. return html
  54. class CoreField(formalchemy.fields.Field):
  55. '''A field which can sync to a core field in the model.
  56. Use this for overriding AttributeFields when you want to be able
  57. to set a default value without having to change the sqla Column default.'''
  58. def sync(self):
  59. if not self.is_readonly():
  60. setattr(self.model, self.name, self._deserialize())
  61. class DateTimeFieldRenderer(formalchemy.fields.DateTimeFieldRenderer):
  62. def render_readonly(self, **kwargs):
  63. return field_readonly_renderer(self.field.key,
  64. formalchemy.fields.DateTimeFieldRenderer.render_readonly(self, **kwargs))
  65. class CheckboxFieldRenderer(formalchemy.fields.CheckBoxFieldRenderer):
  66. def render_readonly(self, **kwargs):
  67. value = u'yes' if self.raw_value else u'no'
  68. return field_readonly_renderer(self.field.key, value)
  69. class TextRenderer(formalchemy.fields.TextFieldRenderer):
  70. def render_readonly(self, **kwargs):
  71. return field_readonly_renderer(self.field.key, self.raw_value)
  72. class SelectFieldRenderer(formalchemy.fields.SelectFieldRenderer):
  73. def render_readonly(self, **kwargs):
  74. return field_readonly_renderer(self.field.key,
  75. formalchemy.fields.SelectFieldRenderer.render_readonly(self, **kwargs))
  76. class TextAreaRenderer(formalchemy.fields.TextAreaFieldRenderer):
  77. def render_readonly(self, **kwargs):
  78. return field_readonly_renderer(self.field.key, self.raw_value)
  79. class TextExtraRenderer(formalchemy.fields.TextFieldRenderer):
  80. def render(self, **kwargs):
  81. kwargs['size'] = '40'
  82. return fa_h.text_field(self.name, value=self.value, maxlength=self.length, **kwargs)
  83. def render_readonly(self, **kwargs):
  84. return field_readonly_renderer(self.field.key, self.value)
  85. # Common fields paired with their renderer and maybe validator
  86. class ConfiguredField(object):
  87. '''A parent class for a form field and its configuration.
  88. Derive specific field classes which should contain:
  89. * a formalchemy Field class
  90. * a formalchemy Renderer class
  91. * possible a field validator method
  92. * a get_configured method which returns the Field configured to use
  93. the Renderer (and validator if it is used)
  94. '''
  95. def __init__(self, name, **kwargs):
  96. self.name = name
  97. self.kwargs = kwargs
  98. class RegExValidatingField(ConfiguredField):
  99. '''Inherit from this for fields that need a regex validator.
  100. @param validate_re - ("regex", "equivalent format but human readable")
  101. '''
  102. def __init__(self, name, validate_re=None, **kwargs):
  103. super(RegExValidatingField, self).__init__(name, **kwargs)
  104. self._validate_re = validate_re
  105. if validate_re:
  106. assert isinstance(validate_re, tuple)
  107. assert isinstance(validate_re[0], str) # reg ex
  108. assert isinstance(validate_re[1], (str, unicode)) # user readable format
  109. def get_configured(self, field):
  110. if self._validate_re:
  111. field = field.validate(self.validate_re)
  112. return field
  113. def validate_re(self, value, field=None):
  114. if value:
  115. match = re.match(self._validate_re[0], value)
  116. if not match:
  117. raise formalchemy.ValidationError(_('Value does not match required format: %s') % self._validate_re[1])
  118. class RegExRangeValidatingField(RegExValidatingField):
  119. '''Validates a range field (each value is validated on the same regex)'''
  120. def validate_re(self, values, field=None):
  121. for value in values:
  122. RegExValidatingField.validate_re(self, value, field=field)
  123. class TextExtraField(RegExValidatingField):
  124. '''A form field for basic text in an "extras" field.'''
  125. def get_configured(self):
  126. field = self.TextExtraField(self.name).with_renderer(self.TextExtraRenderer, **self.kwargs)
  127. return RegExValidatingField.get_configured(self, field)
  128. class TextExtraField(formalchemy.Field):
  129. def __init__(self, *args, **kwargs):
  130. self._null_option = (_('(None)'), u'')
  131. super(self.__class__, self).__init__(*args, **kwargs)
  132. @property
  133. def raw_value(self):
  134. return self.model.extras.get(self.name)
  135. def sync(self):
  136. if not self.is_readonly():
  137. pkg = self.model
  138. val = self._deserialize() or u''
  139. pkg.extras[self.name] = val
  140. class TextExtraRenderer(TextExtraRenderer):
  141. pass
  142. class TextAreaExtraField(RegExValidatingField):
  143. '''A form field for basic text in an "extras" field.'''
  144. def get_configured(self):
  145. field = self.TextAreaExtraField(self.name).with_renderer(self.TextAreaRenderer, **self.kwargs)
  146. return RegExValidatingField.get_configured(self, field)
  147. class TextAreaExtraField(formalchemy.Field):
  148. def __init__(self, *args, **kwargs):
  149. super(self.__class__, self).__init__(*args, **kwargs)
  150. @property
  151. def raw_value(self):
  152. return self.model.extras.get(self.name)
  153. def sync(self):
  154. if not self.is_readonly():
  155. pkg = self.model
  156. val = self._deserialize() or u''
  157. pkg.extras[self.name] = val
  158. class TextAreaRenderer(TextAreaRenderer):
  159. pass
  160. class DateExtraField(ConfiguredField):
  161. '''A form field for DateType data stored in an 'extra' field.'''
  162. def get_configured(self):
  163. return self.DateExtraFieldField(self.name).with_renderer(self.DateExtraRenderer).validate(field_types.DateType.form_validator)
  164. class DateExtraFieldField(formalchemy.Field):
  165. @property
  166. def raw_value(self):
  167. db_date = self.model.extras.get(self.name)
  168. if db_date:
  169. return field_types.DateType.db_to_form(db_date)
  170. else:
  171. return None
  172. def sync(self):
  173. if not self.is_readonly():
  174. form_date = self._deserialize()
  175. date_db = field_types.DateType.form_to_db(form_date, may_except=False)
  176. self.model.extras[self.name] = date_db
  177. class DateExtraRenderer(TextExtraRenderer):
  178. def __init__(self, field):
  179. super(DateExtraField.DateExtraRenderer, self).__init__(field)
  180. def render_readonly(self, **kwargs):
  181. return field_readonly_renderer(self.field.key, self.value)
  182. class DateRangeExtraField(ConfiguredField):
  183. '''A form field for two DateType fields, representing a date range,
  184. stored in 'extra' fields.'''
  185. def get_configured(self):
  186. return self.DateRangeField(self.name).with_renderer(self.DateRangeRenderer).validate(self.validator)
  187. def validator(self, form_date_tuple, field=None):
  188. assert isinstance(form_date_tuple, (tuple, list)), form_date_tuple
  189. from_, to_ = form_date_tuple
  190. return field_types.DateType.form_validator(from_) and \
  191. field_types.DateType.form_validator(to_)
  192. class DateRangeField(formalchemy.Field):
  193. @property
  194. def raw_value(self):
  195. extras = self.model.extras
  196. from_ = extras.get(self.name + '-from', u'')
  197. to = extras.get(self.name + '-to', u'')
  198. from_form = field_types.DateType.db_to_form(from_)
  199. to_form = field_types.DateType.db_to_form(to)
  200. return (from_form, to_form)
  201. @property
  202. def is_collection(self):
  203. # Become a collection to allow two values to be passed around.
  204. return True
  205. def sync(self):
  206. if not self.is_readonly():
  207. pkg = self.model
  208. vals = self._deserialize() or u''
  209. pkg.extras[self.name + '-from'] = field_types.DateType.form_to_db(vals[0], may_except=False)
  210. pkg.extras[self.name + '-to'] = field_types.DateType.form_to_db(vals[1], may_except=False)
  211. class DateRangeRenderer(formalchemy.fields.FieldRenderer):
  212. def render(self, **kwargs):
  213. from_, to = self.value
  214. from_html = fa_h.text_field(self.name + '-from', value=from_, class_="medium-width", **kwargs)
  215. to_html = fa_h.text_field(self.name + '-to', value=to, class_="medium-width", **kwargs)
  216. html = '%s - %s' % (from_html, to_html)
  217. return html
  218. def render_readonly(self, **kwargs):
  219. from_, to = self.value or (u'', u'')
  220. if to:
  221. val_str = '%s - %s' % (from_, to)
  222. else:
  223. val_str = '%s' % from_
  224. return field_readonly_renderer(self.field.key, val_str)
  225. def _serialized_value(self):
  226. # interpret params like this:
  227. # 'Dataset--temporal_coverage-from', u'4/12/2009'
  228. param_val_from = self.params.get(self.name + '-from', u'')
  229. param_val_to = self.params.get(self.name + '-to', u'')
  230. return param_val_from, param_val_to
  231. class TextRangeExtraField(RegExRangeValidatingField):
  232. '''A form field for two TextType fields, representing a range,
  233. stored in 'extra' fields.'''
  234. def get_configured(self):
  235. field = self.TextRangeField(self.name).with_renderer(self.TextRangeRenderer)
  236. return RegExRangeValidatingField.get_configured(self, field)
  237. class TextRangeField(formalchemy.Field):
  238. def sync(self):
  239. if not self.is_readonly():
  240. pkg = self.model
  241. vals = self._deserialize() or u''
  242. pkg.extras[self.name + '-from'] = vals[0]
  243. pkg.extras[self.name + '-to'] = vals[1]
  244. class TextRangeRenderer(formalchemy.fields.FieldRenderer):
  245. def _get_value(self):
  246. #TODO Refactor this into field raw_value, like in DateRangeExtraField
  247. extras = self.field.parent.model.extras
  248. if self.value:
  249. from_form, to_form = self.value
  250. else:
  251. from_ = extras.get(self.field.name + '-from') or u''
  252. to = extras.get(self.field.name + '-to') or u''
  253. from_form = from_
  254. to_form = to
  255. return (from_form, to_form)
  256. def render(self, **kwargs):
  257. from_, to = self._get_value()
  258. from_html = fa_h.text_field(self.name + '-from', value=from_, class_="medium-width", **kwargs)
  259. to_html = fa_h.text_field(self.name + '-to', value=to, class_="medium-width", **kwargs)
  260. html = '%s - %s' % (from_html, to_html)
  261. return html
  262. def render_readonly(self, **kwargs):
  263. val = self._get_value()
  264. if not val:
  265. val = u'', u''
  266. from_, to = val
  267. if to:
  268. val_str = '%s - %s' % (from_, to)
  269. else:
  270. val_str = '%s' % from_
  271. return field_readonly_renderer(self.field.key, val_str)
  272. def _serialized_value(self):
  273. param_val_from = self.params.get(self.name + '-from', u'')
  274. param_val_to = self.params.get(self.name + '-to', u'')
  275. return param_val_from, param_val_to
  276. def deserialize(self):
  277. return self._serialized_value()
  278. class ResourcesField(ConfiguredField):
  279. '''A form field for multiple dataset resources.'''
  280. def __init__(self, name, hidden_label=False, fields_required=None):
  281. super(ResourcesField, self).__init__(name)
  282. self._hidden_label = hidden_label
  283. self.fields_required = fields_required or set(['url'])
  284. assert isinstance(self.fields_required, set)
  285. def resource_validator(self, val, field=None):
  286. resources_data = val
  287. assert isinstance(resources_data, list)
  288. not_nothing_regex = re.compile('\S')
  289. errormsg = _('Dataset resource(s) incomplete.')
  290. not_nothing_validator = formalchemy.validators.regex(not_nothing_regex,
  291. errormsg)
  292. for resource_data in resources_data:
  293. assert isinstance(resource_data, dict)
  294. for field in self.fields_required:
  295. value = resource_data.get(field, '')
  296. not_nothing_validator(value, field)
  297. def get_configured(self):
  298. field = self.ResourcesField(self.name).with_renderer(self.ResourcesRenderer).validate(self.resource_validator)
  299. field._hidden_label = self._hidden_label
  300. field.fields_required = self.fields_required
  301. field.set(multiple=True)
  302. return field
  303. class ResourcesField(formalchemy.Field):
  304. def sync(self):
  305. if not self.is_readonly():
  306. pkg = self.model
  307. res_dicts = self._deserialize() or []
  308. pkg.update_resources(res_dicts, autoflush=False)
  309. def requires_label(self):
  310. return not self._hidden_label
  311. requires_label = property(requires_label)
  312. @property
  313. def raw_value(self):
  314. # need this because it is a property
  315. return getattr(self.model, self.name)
  316. def is_required(self, field_name=None):
  317. if not field_name:
  318. return False
  319. else:
  320. return field_name in self.fields_required
  321. class ResourcesRenderer(formalchemy.fields.FieldRenderer):
  322. def render(self, **kwargs):
  323. c.resources = self.value or []
  324. # [:] does a copy, so we don't change original
  325. c.resources = c.resources[:]
  326. c.resources.extend([None])
  327. c.id = self.name
  328. c.columns = model.Resource.get_columns()
  329. c.field = self.field
  330. c.fieldset = self.field.parent
  331. return render('package/form_resources.html')
  332. def stringify_value(self, v):
  333. # actually returns dict here for _value
  334. # multiple=True means v is a Resource
  335. res_dict = {}
  336. if v:
  337. assert isinstance(v, model.Resource)
  338. for col in model.Resource.get_columns() + ['id']:
  339. res_dict[col] = getattr(v, col)
  340. return res_dict
  341. def _serialized_value(self):
  342. package = self.field.parent.model
  343. params = self.params
  344. new_resources = []
  345. rest_key = self.name
  346. # REST param format
  347. # e.g. 'Dataset-1-resources': [{u'url':u'http://ww...
  348. if params.has_key(rest_key) and any(params.getall(rest_key)):
  349. new_resources = params.getall(rest_key)[:] # copy, so don't edit orig
  350. # formalchemy form param format
  351. # e.g. 'Dataset-1-resources-0-url': u'http://ww...'
  352. row = 0
  353. # The base columns historically defaulted to empty strings
  354. # not None (Null). This is why they are seperate here.
  355. base_columns = ['url', 'format', 'description', 'hash', 'id']
  356. while True:
  357. if not params.has_key('%s-%i-url' % (self.name, row)):
  358. break
  359. new_resource = {}
  360. blank_row = True
  361. for col in model.Resource.get_columns() + ['id']:
  362. if col in base_columns:
  363. value = params.get('%s-%i-%s' % (self.name, row, col), u'')
  364. else:
  365. value = params.get('%s-%i-%s' % (self.name, row, col))
  366. new_resource[col] = value
  367. if col != 'id' and value:
  368. blank_row = False
  369. if not blank_row:
  370. new_resources.append(new_resource)
  371. row += 1
  372. return new_resources
  373. class TagField(ConfiguredField):
  374. '''A form field for tags'''
  375. def get_configured(self):
  376. return self.TagField(self.name).with_renderer(self.TagEditRenderer).validate(self.tag_name_validator)
  377. class TagField(formalchemy.Field):
  378. @property
  379. def raw_value(self):
  380. tag_objects = self.model.get_tags()
  381. tag_names = [tag.name for tag in tag_objects]
  382. return tag_names
  383. def sync(self):
  384. if not self.is_readonly():
  385. self._update_tags()
  386. def _update_tags(self):
  387. pkg = self.model
  388. updated_tags = set(self._deserialize())
  389. existing_tags = set(self.raw_value)
  390. for tag in updated_tags - existing_tags:
  391. pkg.add_tag_by_name(tag, autoflush=False)
  392. tags_to_delete = existing_tags - updated_tags
  393. for pkgtag in pkg.package_tags:
  394. if pkgtag.tag.name in tags_to_delete:
  395. pkgtag.delete()
  396. @property
  397. def is_collection(self):
  398. # Become a collection to allow value to be a list of tag strings
  399. return True
  400. class TagEditRenderer(formalchemy.fields.FieldRenderer):
  401. def render(self, **kwargs):
  402. kwargs['value'] = ', '.join(self.value)
  403. kwargs['size'] = 60
  404. api_url = config.get('ckan.api_url', '/').rstrip('/')
  405. tagcomplete_url = api_url + h.url_for(controller='api',
  406. action='tag_autocomplete', id=None, ver=2)
  407. kwargs['data-tagcomplete-url'] = tagcomplete_url
  408. kwargs['data-tagcomplete-queryparam'] = 'incomplete'
  409. kwargs['class'] = 'long tagComplete'
  410. html = literal(fa_h.text_field(self.name, **kwargs))
  411. return html
  412. def _tag_links(self):
  413. tags = self.value
  414. tag_links = [h.link_to(tagname, h.url_for(controller='tag', action='read', id=tagname)) for tagname in tags]
  415. return literal(', '.join(tag_links))
  416. def render_readonly(self, **kwargs):
  417. tag_links = self._tag_links()
  418. return field_readonly_renderer(self.field.key, tag_links)
  419. def _serialized_value(self):
  420. # despite being a collection, there is only one field to get
  421. # the values from
  422. tags_as_string = self.params.getone(self.name).strip()
  423. if tags_as_string == "":
  424. return []
  425. tags = map(lambda s: s.strip(), tags_as_string.split(','))
  426. return tags
  427. tagname_match = re.compile('[^"]*$') # already split on commas
  428. def tag_name_validator(self, val, field):
  429. for tag in val:
  430. # formalchemy deserializes an empty string into None.
  431. # This happens if the tagstring gets split on commas, and
  432. # there's an empty string in the resulting list.
  433. # e.g. "tag1,,tag2" ; " ,tag1" and "tag1," will all result
  434. # in an empty tag name.
  435. if tag is None:
  436. tag = u'' # let the minimum length validator take care of it.
  437. min_length = 2
  438. if len(tag) < min_length:
  439. raise formalchemy.ValidationError(_('Tag "%s" length is less than minimum %s') % (tag, min_length))
  440. if not self.tagname_match.match(tag):
  441. raise formalchemy.ValidationError(_('Tag "%s" must not contain any quotation marks: "') % (tag))
  442. class ExtrasField(ConfiguredField):
  443. '''A form field for arbitrary "extras" dataset data.'''
  444. def __init__(self, name, hidden_label=False):
  445. super(ExtrasField, self).__init__(name)
  446. self._hidden_label = hidden_label
  447. def get_configured(self):
  448. field = self.ExtrasField(self.name).with_renderer(self.ExtrasRenderer).validate(self.extras_validator)
  449. field._hidden_label = self._hidden_label
  450. return field
  451. def extras_validator(self, val, field=None):
  452. val_dict = dict(val)
  453. for key, value in val:
  454. if value != val_dict[key]:
  455. raise formalchemy.ValidationError(_('Duplicate key "%s"') % key)
  456. if value and not key:
  457. # Note value is allowed to be None - REST way of deleting fields.
  458. raise formalchemy.ValidationError(_('Extra key-value pair: key is not set for value "%s".') % value)
  459. class ExtrasField(formalchemy.Field):
  460. @property
  461. def raw_value(self):
  462. return self.model.extras.items() or []
  463. @property
  464. def is_collection(self):
  465. # Become a collection to allow multiple values to be passed around.
  466. return True
  467. def sync(self):
  468. if not self.is_readonly():
  469. self._update_extras()
  470. def _update_extras(self):
  471. pkg = self.model
  472. extra_list = self._deserialize()
  473. current_extra_keys = pkg.extras.keys()
  474. extra_keys = []
  475. for key, value in extra_list:
  476. extra_keys.append(key)
  477. if key in current_extra_keys:
  478. if pkg.extras[key] != value:
  479. # edit existing extra
  480. pkg.extras[key] = value
  481. else:
  482. # new extra
  483. pkg.extras[key] = value
  484. for key in current_extra_keys:
  485. if key not in extra_keys:
  486. del pkg.extras[key]
  487. def requires_label(self):
  488. return not self._hidden_label
  489. requires_label = property(requires_label)
  490. class ExtrasRenderer(formalchemy.fields.FieldRenderer):
  491. @property
  492. def value(self):
  493. '''
  494. Override 'value' method to avoid stringifying each
  495. extra key-value pair.
  496. '''
  497. if not self.field.is_readonly() and self.params is not None:
  498. v = self.deserialize()
  499. else:
  500. v = None
  501. return v or self.field.model_value
  502. def render(self, **kwargs):
  503. extras = self.value
  504. html = ''
  505. field_values = []
  506. for key, value in extras:
  507. field_values.append({
  508. 'name':self.name + '-' + key,
  509. 'key':key.capitalize(),
  510. 'value':value,})
  511. for i in range(3):
  512. field_values.append({
  513. 'name':'%s-newfield%s' % (self.name, i)})
  514. c.fields = field_values
  515. html = render('package/form_extra_fields.html')
  516. return h.literal(html)
  517. def render_readonly(self, **kwargs):
  518. html_items = []
  519. for key, value in self.value:
  520. html_items.append(field_readonly_renderer(key, value))
  521. return html_items
  522. def deserialize(self):
  523. # Example params:
  524. # ('Dataset-1-extras', {...}) (via REST i/f)
  525. # ('Dataset-1-extras-genre', u'romantic novel'),
  526. # ('Dataset-1-extras-genre-checkbox', 'on')
  527. # ('Dataset-1-extras-newfield0-key', u'aaa'),
  528. # ('Dataset-1-extras-newfield0-value', u'bbb'),
  529. # TODO: This method is run multiple times per edit - cache results?
  530. if not hasattr(self, 'extras_re'):
  531. self.extras_re = re.compile('([a-zA-Z0-9-]*)-([a-f0-9-]*)-extras(?:-(.+))?$')
  532. self.newfield_re = re.compile('newfield(\d+)-(.*)')
  533. extra_fields = []
  534. for key, value in self.params.items():
  535. extras_match = self.extras_re.match(key)
  536. if not extras_match:
  537. continue
  538. key_parts = extras_match.groups()
  539. entity_name = key_parts[0]
  540. entity_id = key_parts[1]
  541. if key_parts[2] is None:
  542. if isinstance(value, dict):
  543. # simple dict passed into 'Dataset-1-extras' e.g. via REST i/f
  544. extra_fields.extend(value.items())
  545. elif key_parts[2].startswith('newfield'):
  546. newfield_match = self.newfield_re.match(key_parts[2])
  547. if not newfield_match:
  548. log.warn('Did not parse newfield correctly: %r', key_parts)
  549. continue
  550. new_field_index, key_or_value = newfield_match.groups()
  551. if key_or_value == 'key':
  552. new_key = value
  553. value_key = '%s-%s-extras-newfield%s-value' % (entity_name, entity_id, new_field_index)
  554. new_value = self.params.get(value_key, '')
  555. if new_key or new_value:
  556. extra_fields.append((new_key, new_value))
  557. elif key_or_value == 'value':
  558. # if it doesn't have a matching key, add it to extra_fields anyway for
  559. # validation to fail
  560. key_key = '%s-%s-extras-newfield%s-key' % (entity_name, entity_id, new_field_index)
  561. if not self.params.has_key(key_key):
  562. extra_fields.append(('', value))
  563. else:
  564. log.warn('Expected key or value for newfield: %r' % key)
  565. elif key_parts[2].endswith('-checkbox'):
  566. continue
  567. else:
  568. # existing field
  569. key = key_parts[2].decode('utf8')
  570. checkbox_key = '%s-%s-extras-%s-checkbox' % (entity_name, entity_id, key)
  571. delete = self.params.get(checkbox_key, '') == 'on'
  572. if not delete:
  573. extra_fields.append((key, value))
  574. return extra_fields
  575. class GroupSelectField(ConfiguredField):
  576. '''A form field for selecting groups'''
  577. def __init__(self, name, allow_empty=True, multiple=True, user_editable_groups=None):
  578. super(GroupSelectField, self).__init__(name)
  579. self.allow_empty = allow_empty
  580. self.multiple = multiple
  581. assert user_editable_groups is not None
  582. self.user_editable_groups = user_editable_groups
  583. def get_configured(self):
  584. field = self.GroupSelectionField(self.name, self.allow_empty).with_renderer(self.GroupSelectEditRenderer)
  585. field.set(multiple=self.multiple)
  586. field.user_editable_groups = self.user_editable_groups
  587. return field
  588. class GroupSelectionField(formalchemy.Field):
  589. def __init__(self, name, allow_empty):
  590. formalchemy.Field.__init__(self, name)
  591. self.allow_empty = allow_empty
  592. def sync(self):
  593. if not self.is_readonly():
  594. self._update_groups()
  595. def _update_groups(self):
  596. new_group_ids = self._deserialize() or []
  597. group_dicts = [dict(id = group_id) for
  598. group_id in new_group_ids]
  599. context = {'model': model, 'session': model.Session}
  600. model_save.package_membership_list_save(
  601. group_dicts, self.parent.model, context)
  602. def requires_label(self):
  603. return False
  604. requires_label = property(requires_label)
  605. class GroupSelectEditRenderer(formalchemy.fields.FieldRenderer):
  606. def _get_value(self, **kwargs):
  607. return self.field.parent.model.get_groups()
  608. def _get_user_editable_groups(self):
  609. return self.field.user_editable_groups
  610. def render(self, **kwargs):
  611. # Get groups which are editable by the user.
  612. editable_groups = self._get_user_editable_groups()
  613. # Get groups which are already selected.
  614. selected_groups = self._get_value()
  615. # Make checkboxes HTML from selected groups.
  616. checkboxes_html = ''
  617. checkbox_action = '<input type="checkbox" name="%(name)s" checked="checked" value="%(id)s" />'
  618. checkbox_noaction = '&nbsp;'
  619. checkbox_template = '''
  620. <dt>
  621. %(action)s
  622. </dt>
  623. <dd>
  624. <label for="%(name)s">%(title)s</label><br/>
  625. </dd>
  626. '''
  627. for group in selected_groups:
  628. checkbox_context = {
  629. 'id': group.id,
  630. 'name': self.name + '-' + group.id,
  631. 'title': group.display_name
  632. }
  633. action = checkbox_noaction
  634. if group in editable_groups:
  635. context = {
  636. 'id': group.id,
  637. 'name': self.name + '-' + group.id
  638. }
  639. action = checkbox_action % context
  640. # Make checkbox HTML from a group.
  641. checkbox_context = {
  642. 'action': action,
  643. 'name': self.name + '-' + group.id,
  644. 'title': group.display_name
  645. }
  646. checkbox_html = checkbox_template % checkbox_context
  647. checkboxes_html += checkbox_html
  648. # Infer addable groups, subtract selected from editable groups.
  649. addable_groups = []
  650. for group in editable_groups:
  651. if group not in selected_groups:
  652. addable_groups.append(group)
  653. # Construct addable options from addable groups.
  654. options = []
  655. if len(addable_groups):
  656. if self.field.allow_empty or len(selected_groups):
  657. options.append(('', _('(None)')))
  658. for group in addable_groups:
  659. options.append((group.id, group.display_name))
  660. # Make select HTML.
  661. if len(options):
  662. new_name = self.name + '-new'
  663. select_html = h.select(new_name, None, options)
  664. else:
  665. # Todo: Translation call.
  666. select_html = _("Cannot add any groups.")
  667. # Make the field HTML.
  668. field_template = '''
  669. <dl> %(checkboxes)s
  670. <dt>
  671. %(label)s
  672. </dt>
  673. <dd> %(select)s
  674. </dd>
  675. </dl>
  676. '''
  677. field_context = {
  678. 'checkboxes': checkboxes_html,
  679. 'select': select_html,
  680. 'label': _("Group"),
  681. }
  682. field_html = field_template % field_context
  683. # Convert to literals.
  684. return h.literal(field_html)
  685. def render_readonly(self, **kwargs):
  686. return field_readonly_renderer(self.field.key, self._get_value())
  687. def _serialized_value(self):
  688. name = self.name.encode('utf-8')
  689. return [v for k, v in self.params.items() if k.startswith(name)]
  690. def deserialize(self):
  691. # Return groups which have just been selected by the user.
  692. new_group_ids = self._serialized_value()
  693. if new_group_ids and isinstance(new_group_ids, list):
  694. # Either...
  695. if len(new_group_ids) == 1:
  696. # Convert [['id1', 'id2', ...]] into ['id1,' 'id2', ...].
  697. nested_value = new_group_ids[0]
  698. if isinstance(new_group_ids, list):
  699. new_group_ids = nested_value
  700. # Or...
  701. else:
  702. # Convert [['id1'], ['id2'], ...] into ['id1,' 'id2', ...].
  703. for (i, nested_value) in enumerate(new_group_ids):
  704. if nested_value and isinstance(nested_value, list):
  705. if len(nested_value) > 1:
  706. msg = _("Can't derived new group selection from serialized value structured like this: %s") % nested_value
  707. raise Exception, msg
  708. new_group_ids[i] = nested_value[0]
  709. # Todo: Decide on the structure of a multiple-group selection.
  710. if new_group_ids and isinstance(new_group_ids, basestring):
  711. new_group_ids = [new_group_ids]
  712. return new_group_ids
  713. class SelectExtraField(TextExtraField):
  714. '''A form field for text suggested from from a list of options, that is
  715. stored in an "extras" field.'''
  716. def __init__(self, name, options, allow_empty=True):
  717. self.options = options[:]
  718. self.is_required = not allow_empty
  719. # ensure options have key and value, not just a value
  720. for i, option in enumerate(self.options):
  721. if not isinstance(option, (tuple, list)):
  722. self.options[i] = (option, option)
  723. super(SelectExtraField, self).__init__(name)
  724. def get_configured(self):
  725. field = self.TextExtraField(self.name, options=self.options)
  726. field_configured = field.with_renderer(self.SelectRenderer).validate(self.validate)
  727. if self.is_required:
  728. field_configured = field_configured.required()
  729. return field_configured
  730. def validate(self, value, field=None):
  731. if not value:
  732. # if value is required then this is checked by 'required' validator
  733. return
  734. if value not in [id_ for label, id_ in self.options]:
  735. raise formalchemy.ValidationError('Value %r is not one of the options.' % id_)
  736. class SelectRenderer(formalchemy.fields.SelectFieldRenderer):
  737. def _serialized_value(self):
  738. return self.params.get(self.name, u'')
  739. def render(self, options, **kwargs):
  740. # @param options - an iterable of (label, value)
  741. if not self.field.is_required():
  742. options = list(options)
  743. if options and isinstance(options[0], (tuple, list)):
  744. null_option = self.field._null_option
  745. else:
  746. null_option = self.field._null_option[1]
  747. options.insert(0, self.field._null_option)
  748. return formalchemy.fields.SelectFieldRenderer.render(self, options, **kwargs)
  749. def render_readonly(self, **kwargs):
  750. return field_readonly_renderer(self.field.key, self.value)
  751. class SuggestedTextExtraField(TextExtraField):
  752. '''A form field for text suggested from from a list of options, that is
  753. stored in an "extras" field.'''
  754. def __init__(self, name, options, default=None):
  755. self.options = options[:]
  756. self.default = default
  757. # ensure options have key and value, not just a value
  758. for i, option in enumerate(self.options):
  759. if not isinstance(option, (tuple, list)):
  760. self.options[i] = (option, option)
  761. super(SuggestedTextExtraField, self).__init__(name)
  762. def get_configured(self):
  763. field = self.TextExtraField(self.name, options=self.options)
  764. field.default = self.default
  765. return field.with_renderer(self.SelectRenderer)
  766. class SelectRenderer(formalchemy.fields.FieldRenderer):
  767. def render(self, options, **kwargs):
  768. selected = self.value
  769. if selected is None:
  770. selected = self.field.default
  771. options = [('', '')] + options + [(_('other - please specify'), 'other')]
  772. option_keys = [key for value, key in options]
  773. if selected in option_keys:
  774. select_field_selected = selected
  775. text_field_value = u''
  776. elif selected:
  777. select_field_selected = u'other'
  778. text_field_value = selected or u''
  779. else:
  780. select_field_selected = u''
  781. text_field_value = u''
  782. fa_version_nums = formalchemy.__version__.split('.')
  783. # Requires FA 1.3.2 onwards for this select i/f
  784. html = literal(fa_h.select(self.name, select_field_selected,
  785. options, class_="short", **kwargs))
  786. other_name = self.name+'-other'
  787. html += literal('<label class="inline" for="%s">%s: %s</label>') % (other_name, _('Other'), literal(fa_h.text_field(other_name, value=text_field_value, class_="medium-width", **kwargs)))
  788. return html
  789. def render_readonly(self, **kwargs):
  790. return field_readonly_renderer(self.field.key, self.value)
  791. def _serialized_value(self):
  792. main_value = self.params.get(self.name, u'')
  793. other_value = self.params.get(self.name + '-other', u'')
  794. return other_value if main_value in ('', 'other') else main_value
  795. class CheckboxExtraField(TextExtraField):
  796. '''A form field for a checkbox value, stored in an "extras" field as
  797. "yes" or "no".'''
  798. def get_configured(self):
  799. return self.CheckboxExtraField(self.name).with_renderer(self.CheckboxExtraRenderer)
  800. class CheckboxExtraField(formalchemy.fields.Field):
  801. @property
  802. def raw_value(self):
  803. extras = self.model.extras
  804. return u'yes' if extras.get(self.name) == u'yes' else u'no'
  805. def sync(self):
  806. if not self.is_readonly():
  807. store_value = u'yes' if self._deserialize() else u'no'
  808. self.model.extras[self.name] = store_value
  809. class CheckboxExtraRenderer(formalchemy.fields.CheckBoxFieldRenderer):
  810. def render(self, **kwargs):
  811. kwargs['size'] = '40'
  812. bool_value = (self.value == u'yes')
  813. return fa_h.check_box(self.name, u'yes', checked=bool_value, **kwargs)
  814. return fa_h.text_field(self.name, value=bool_value, maxlength=self.length, **kwargs)
  815. def render_readonly(self, **kwargs):
  816. return field_readonly_renderer(self.field.key, self.value)
  817. class PackageNameField(ConfiguredField):
  818. def get_configured(self):
  819. return self.PackageNameField(self.name).with_renderer(self.PackageNameRenderer)
  820. class PackageNameField(formalchemy.Field):
  821. pass
  822. class PackageNameRenderer(formalchemy.fields.FieldRenderer):
  823. pass
  824. class UserNameField(ConfiguredField):
  825. def get_configured(self):
  826. return self.UserNameField(self.name).with_renderer(self.UserNameRenderer)
  827. class UserNameField(formalchemy.Field):
  828. pass
  829. class UserNameRenderer(formalchemy.fields.FieldRenderer):
  830. pass
  831. def prettify(field_name):
  832. '''Generates a field label based on the field name.
  833. Used by the FormBuilder in method set_label_prettifier.
  834. Also does i18n.'''
  835. field_name = field_name.capitalize()
  836. field_name = field_name.replace('_', ' ')
  837. return _(field_name)