forms.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. from functools import partial
  2. import itertools
  3. import urlparse
  4. import json
  5. import collections
  6. from flask import current_app
  7. from flask.ext.wtf import Form
  8. from wtforms import Form as InsecureForm
  9. from wtforms import (TextField, DateField, DecimalField, IntegerField, SelectField,
  10. SelectMultipleField, FieldList, FormField)
  11. from wtforms.widgets import TextInput, ListWidget, html_params, HTMLString, CheckboxInput, Select, TextArea
  12. from wtforms.validators import (DataRequired, Optional, URL, Email, Length,
  13. NumberRange, ValidationError, StopValidation)
  14. from flask.ext.babel import lazy_gettext as _, gettext
  15. from babel.support import LazyProxy
  16. from ispformat.validator import validate_geojson
  17. from .constants import STEPS
  18. from .models import ISP
  19. from .utils import check_geojson_spatialite, filesize_fmt
  20. class InputListWidget(ListWidget):
  21. def __call__(self, field, **kwargs):
  22. kwargs.setdefault('id', field.id)
  23. html = ['<%s %s>' % (self.html_tag, html_params(**kwargs))]
  24. for subfield in field:
  25. html.append('<li>%s</li>' % (subfield()))
  26. html.append('</%s>' % self.html_tag)
  27. return HTMLString(''.join(html))
  28. class MultiCheckboxField(SelectMultipleField):
  29. """
  30. A multiple-select, except displays a list of checkboxes.
  31. Iterating the field will produce subfields, allowing custom rendering of
  32. the enclosed checkbox fields.
  33. """
  34. widget = ListWidget(prefix_label=False)
  35. option_widget = CheckboxInput()
  36. class MyFormField(FormField):
  37. @property
  38. def flattened_errors(self):
  39. return list(itertools.chain.from_iterable(self.errors.values()))
  40. class GeoJSONField(TextField):
  41. widget = TextArea()
  42. def process_formdata(self, valuelist):
  43. if valuelist and valuelist[0]:
  44. max_size = current_app.config['ISP_FORM_GEOJSON_MAX_SIZE']
  45. if len(valuelist[0]) > max_size:
  46. raise ValueError(_(u'JSON value too big, must be less than %(max_size)s',
  47. max_size=filesize_fmt(max_size)))
  48. try:
  49. self.data = json.loads(valuelist[0], object_pairs_hook=collections.OrderedDict)
  50. except Exception as e:
  51. raise ValueError(_(u'Not a valid JSON value'))
  52. elif valuelist and valuelist[0].strip() == '':
  53. self.data = None # if an empty string was passed, set data as None
  54. def _value(self):
  55. if self.raw_data:
  56. return self.raw_data[0]
  57. elif self.data is not None:
  58. return json.dumps(self.data)
  59. else:
  60. return ''
  61. def pre_validate(self, form):
  62. if self.data is not None:
  63. if not validate_geojson(self.data):
  64. raise StopValidation(_(u'Invalid GeoJSON, please check it'))
  65. if not check_geojson_spatialite(self.data):
  66. # TODO: log this
  67. raise StopValidation(_(u'GeoJSON not understood by database'))
  68. class Unique(object):
  69. """ validator that checks field uniqueness """
  70. def __init__(self, model, field, message=None, allow_edit=False):
  71. self.model = model
  72. self.field = field
  73. if not message:
  74. message = _(u'this element already exists')
  75. self.message = message
  76. def __call__(self, form, field):
  77. default=field.default() if callable(field.default) else field.default
  78. if field.object_data != default and field.object_data == field.data:
  79. return
  80. check = self.model.query.filter(self.field == field.data).first()
  81. if check:
  82. raise ValidationError(self.message)
  83. TECHNOLOGIES_CHOICES=(
  84. ('ftth', _('FTTH')),
  85. ('dsl', _('DSL')),
  86. ('wifi', _('Wi-Fi')),
  87. )
  88. class CoveredArea(InsecureForm):
  89. name = TextField(_(u'name'), widget=partial(TextInput(), class_='input-medium', placeholder=_(u'Area')))
  90. technologies = SelectMultipleField(_(u'technologies'), choices=TECHNOLOGIES_CHOICES,
  91. widget=partial(Select(True), **{'class': 'selectpicker', 'data-title': _(u'Technologies deployed')}))
  92. area = GeoJSONField(_('area'), widget=partial(TextArea(), class_='geoinput'))
  93. def validate(self, *args, **kwargs):
  94. r=super(CoveredArea, self).validate(*args, **kwargs)
  95. if bool(self.name.data) != bool(self.technologies.data):
  96. self._fields['name'].errors += [_(u'You must fill both fields')]
  97. r=False
  98. return r
  99. class OtherWebsites(InsecureForm):
  100. name = TextField(_(u'name'), widget=partial(TextInput(), class_='input-small', placeholder=_(u'Name')))
  101. url = TextField(_(u'url'), widget=partial(TextInput(), class_='input-medium', placeholder=_(u'URL')),
  102. validators=[Optional(), URL(require_tld=True)])
  103. STEP_CHOICES = [(k, LazyProxy(lambda k, s: u'%u - %s' % (k, s), k, STEPS[k], enable_cache=False)) for k in STEPS]
  104. class ProjectForm(Form):
  105. name = TextField(_(u'full name'), description=[_(u'E.g. French Data Network')],
  106. validators=[DataRequired(), Length(min=2), Unique(ISP, ISP.name)])
  107. shortname = TextField(_(u'short name'), description=[_(u'E.g. FDN')],
  108. validators=[Optional(), Length(min=2, max=15), Unique(ISP, ISP.shortname)])
  109. description = TextField(_(u'description'), description=[None, _(u'Short text describing the project')])
  110. logo_url = TextField(_(u'logo url'), validators=[Optional(), URL(require_tld=True)])
  111. website = TextField(_(u'website'), validators=[Optional(), URL(require_tld=True)])
  112. other_websites= FieldList(MyFormField(OtherWebsites, widget=partial(InputListWidget(), class_='formfield')),
  113. min_entries=1, widget=InputListWidget(),
  114. description=[None, _(u'Additional websites that you host (e.g. wiki, etherpad...)')])
  115. contact_email = TextField(_(u'contact email'), validators=[Optional(), Email()],
  116. description=[None, _(u'General contact email address')])
  117. main_ml = TextField(_(u'main mailing list'), validators=[Optional(), Email()],
  118. description=[None, _(u'Address of your main mailing list')])
  119. creation_date = DateField(_(u'creation date'), validators=[Optional()], widget=partial(TextInput(), placeholder=_(u'YYYY-mm-dd')),
  120. description=[None, _(u'Date at which the legal structure for your project was created')])
  121. chatrooms = FieldList(TextField(_(u'chatrooms')), min_entries=1, widget=InputListWidget(),
  122. description=[None, _(u'In URI form, e.g. <code>irc://irc.isp.net/#isp</code> or '+
  123. '<code>xmpp:isp@chat.isp.net?join</code>')])
  124. covered_areas = FieldList(MyFormField(CoveredArea, _('Covered Areas'), widget=partial(InputListWidget(), class_='formfield')),
  125. min_entries=1, widget=InputListWidget(),
  126. description=[None, _(u'Descriptive name of the covered areas and technologies deployed')])
  127. latitude = DecimalField(_(u'latitude'), validators=[Optional(), NumberRange(min=-90, max=90)],
  128. description=[None, _(u'Geographical coordinates of your registered office or usual meeting location.')])
  129. longitude = DecimalField(_(u'longitude'), validators=[Optional(), NumberRange(min=-180, max=180)])
  130. step = SelectField(_(u'progress step'), choices=STEP_CHOICES, coerce=int)
  131. member_count = IntegerField(_(u'members'), validators=[Optional(), NumberRange(min=0)],
  132. description=[None, _('Number of members')])
  133. subscriber_count = IntegerField(_(u'subscribers'), validators=[Optional(), NumberRange(min=0)],
  134. description=[None, _('Number of subscribers to an internet access')])
  135. tech_email = TextField(_('Email'), validators=[Email(), DataRequired()], description=[None,
  136. _('Technical contact, in case of problems with your submission')])
  137. def validate(self, *args, **kwargs):
  138. r=super(ProjectForm, self).validate(*args, **kwargs)
  139. if (self.latitude.data is None) != (self.longitude.data is None):
  140. self._fields['longitude'].errors += [_(u'You must fill both fields')]
  141. r=False
  142. return r
  143. def validate_covered_areas(self, field):
  144. if len(filter(lambda e: e['name'], field.data)) == 0:
  145. # not printed, whatever..
  146. raise ValidationError(_(u'You must specify at least one area'))
  147. geojson_size = sum([len(ca.area.raw_data[0]) for ca in self.covered_areas if ca.area.raw_data])
  148. print geojson_size
  149. max_size = current_app.config['ISP_FORM_GEOJSON_MAX_SIZE_TOTAL']
  150. if geojson_size > max_size:
  151. # TODO: XXX This is not printed !
  152. raise ValidationError(gettext(u'The size of all GeoJSON data combined must not exceed %(max_size)s',
  153. max_size=filesize_fmt(max_size)))
  154. def to_json(self, json=None):
  155. if json is None:
  156. json={}
  157. json['name'] = self.name.data
  158. def optstr(k, v):
  159. if k in json or v:
  160. json[k]=v
  161. def optlist(k, v):
  162. if k in json or len(v):
  163. json[k]=v
  164. def transform_covered_areas(cas):
  165. for ca in cas:
  166. if not ca['name']:
  167. continue
  168. if 'area' in ca and ca['area'] is None:
  169. del ca['area']
  170. yield ca
  171. optstr('shortname', self.shortname.data)
  172. optstr('description', self.description.data)
  173. optstr('logoURL', self.logo_url.data)
  174. optstr('website', self.website.data)
  175. optstr('otherWebsites', dict(((w['name'], w['url']) for w in self.other_websites.data if w['name'])))
  176. optstr('email', self.contact_email.data)
  177. optstr('mainMailingList', self.main_ml.data)
  178. optstr('creationDate', self.creation_date.data.isoformat() if self.creation_date.data else None)
  179. optstr('progressStatus', self.step.data)
  180. optstr('memberCount', self.member_count.data)
  181. optstr('subscriberCount', self.subscriber_count.data)
  182. optlist('chatrooms', filter(bool, self.chatrooms.data)) # remove empty strings
  183. optstr('coordinates', {'latitude': self.latitude.data, 'longitude': self.longitude.data}
  184. if self.latitude.data else {})
  185. optlist('coveredAreas', list(transform_covered_areas(self.covered_areas.data)))
  186. return json
  187. @classmethod
  188. def edit_json(cls, isp):
  189. json=isp.json
  190. obj=type('abject', (object,), {})()
  191. def set_attr(attr, itemk=None, d=json):
  192. if itemk is None:
  193. itemk=attr
  194. if itemk in d:
  195. setattr(obj, attr, d[itemk])
  196. set_attr('name')
  197. set_attr('shortname')
  198. set_attr('description')
  199. set_attr('logo_url', 'logoURL')
  200. set_attr('website')
  201. set_attr('contact_email', 'email')
  202. set_attr('main_ml', 'mainMailingList')
  203. set_attr('creation_date', 'creationDate')
  204. if hasattr(obj, 'creation_date'):
  205. obj.creation_date=ISP.str2date(obj.creation_date)
  206. set_attr('step', 'progressStatus')
  207. set_attr('member_count', 'memberCount')
  208. set_attr('subscriber_count', 'subscriberCount')
  209. set_attr('chatrooms', 'chatrooms')
  210. if 'coordinates' in json:
  211. set_attr('latitude', d=json['coordinates'])
  212. set_attr('longitude', d=json['coordinates'])
  213. if 'otherWebsites' in json:
  214. setattr(obj, 'other_websites', [{'name': n, 'url': w} for n, w in json['otherWebsites'].iteritems()])
  215. set_attr('covered_areas', 'coveredAreas')
  216. obj.tech_email=isp.tech_email
  217. return cls(obj=obj)
  218. class URLField(TextField):
  219. def _value(self):
  220. if isinstance(self.data, basestring):
  221. return self.data
  222. elif self.data is None:
  223. return ''
  224. else:
  225. return urlparse.urlunsplit(self.data)
  226. def process_formdata(self, valuelist):
  227. if valuelist:
  228. try:
  229. self.data = urlparse.urlsplit(valuelist[0])
  230. except:
  231. self.data = None
  232. raise ValidationError(_(u'Invalid URL'))
  233. def is_url_unique(url):
  234. if isinstance(url, basestring):
  235. url=urlparse.urlsplit(url)
  236. t=list(url)
  237. t[2]=''
  238. u1=urlparse.urlunsplit(t)
  239. t[0]='http' if t[0] == 'https' else 'https'
  240. u2=urlparse.urlunsplit(t)
  241. if ISP.query.filter(ISP.json_url.startswith(u1) | ISP.json_url.startswith(u2)).count() > 0:
  242. return False
  243. return True
  244. class ProjectJSONForm(Form):
  245. json_url = URLField(_(u'base url'), description=[_(u'E.g. https://isp.com/'),
  246. _(u'A ressource implementing our JSON-Schema specification '+
  247. 'must exist at path /isp.json')])
  248. tech_email = TextField(_(u'Email'), validators=[Email()], description=[None,
  249. _(u'Technical contact, in case of problems')])
  250. def validate_json_url(self, field):
  251. if not field.data.netloc:
  252. raise ValidationError(_(u'Invalid URL'))
  253. if field.data.scheme not in ('http', 'https'):
  254. raise ValidationError(_(u'Invalid URL (must be HTTP(S))'))
  255. if not field.object_data and not is_url_unique(field.data):
  256. raise ValidationError(_(u'This URL is already in our database'))
  257. class RequestEditToken(Form):
  258. tech_email = TextField(_(u'Tech Email'), validators=[Email()], description=[None,
  259. _(u'The Technical contact you provided while registering')])