views.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. # -*- coding: utf-8 -*-
  2. from flask import request, g, redirect, url_for, abort, \
  3. render_template, flash, json, session, Response, escape
  4. from flask.ext.babel import gettext as _
  5. import requests
  6. from datetime import date, time, timedelta, datetime
  7. from urlparse import urlunsplit
  8. import locale
  9. locale.setlocale(locale.LC_ALL, '')
  10. import string
  11. import io
  12. from time import time
  13. from . import forms
  14. from .constants import *
  15. from . import app, db
  16. from .models import ISP
  17. from .schemavalidator import validate_isp
  18. @app.route('/')
  19. def home():
  20. return render_template('index.html', active_button="home")
  21. @app.route('/projects')
  22. def project_list():
  23. return render_template('project_list.html', projects=ISP.query.filter_by(is_disabled=False))
  24. # this needs to be cached
  25. @app.route('/isp/map_data.json', methods=['GET'])
  26. def isp_map_data():
  27. isps=ISP.query.filter_by(is_disabled=False)
  28. data=[]
  29. for isp in isps:
  30. d=dict(isp.json)
  31. for k in d.keys():
  32. if k not in ('name', 'shortname', 'coordinates'):
  33. del d[k]
  34. d['id']=isp.id
  35. d['ffdn_member']=isp.is_ffdn_member
  36. d['popup']=render_template('map_popup.html', isp=isp)
  37. data.append(d)
  38. return json.dumps(data)
  39. @app.route('/isp/<projectid>/covered_areas.json', methods=['GET'])
  40. def isp_covered_areas(projectid):
  41. p=ISP.query.filter_by(id=projectid, is_disabled=False).first()
  42. if not p:
  43. abort(404)
  44. return json.dumps(p.json['coveredAreas'])
  45. @app.route('/isp/<projectid>/')
  46. def project(projectid):
  47. p=ISP.query.filter_by(id=projectid, is_disabled=False).first()
  48. if not p:
  49. abort(404)
  50. return render_template('project_detail.html', project_row=p, project=p.json)
  51. @app.route('/isp/<projectid>/edit', methods=['GET', 'POST'])
  52. def edit_project(projectid):
  53. isp=ISP.query.filter_by(id=projectid, is_disabled=False).first()
  54. if not isp:
  55. abort(404)
  56. form = forms.ProjectForm.edit_json(isp.json)
  57. if form.validate_on_submit():
  58. isp.name = form.name.data
  59. isp.shortname = form.shortname.data or None
  60. isp.json=form.to_json(isp.json)
  61. db.session.add(isp)
  62. db.session.commit()
  63. flash(_(u'Project modified'), 'info')
  64. return redirect(url_for('project', projectid=isp.id))
  65. return render_template('project_form.html', form=form, project=isp)
  66. @app.route('/add-a-project', methods=['GET'])
  67. def add_project():
  68. return render_template('add_project.html')
  69. @app.route('/create/form', methods=['GET', 'POST'])
  70. def create_project_form():
  71. form = forms.ProjectForm()
  72. if form.validate_on_submit():
  73. isp=ISP()
  74. isp.name = form.name.data
  75. isp.shortname = form.shortname.data or None
  76. isp.json=form.to_json(isp.json)
  77. db.session.add(isp)
  78. db.session.commit()
  79. flash(_(u'Project created'), 'info')
  80. return redirect(url_for('project', projectid=isp.id))
  81. return render_template('project_form.html', form=form)
  82. @app.route('/create/json-url/validator', methods=['GET'])
  83. def json_url_validator():
  84. if 'form_json' not in session or \
  85. session['form_json'].get('validated', False):
  86. abort(403)
  87. v=session['form_json'].get('validator')
  88. if v is not None:
  89. if v > time()-5:
  90. abort(429)
  91. else:
  92. session['form_json']['validator']=time()
  93. validator=ValidateJSONURL(session=session._get_current_object())
  94. return Response(validator(session['form_json']['url']),
  95. mimetype="text/event-stream")
  96. class ValidateJSONURL(object):
  97. MAX_JSON_SIZE=1*1024*1024
  98. def __init__(self, **kwargs):
  99. self.__dict__.update(kwargs)
  100. def m(self, msg, evt=None):
  101. return '%sdata: %s\n\n'%('event: %s\n'%evt if evt else '', msg)
  102. def err(self, msg, *args):
  103. return self.m('<strong style="color: crimson">!</strong> %s'%msg, *args)
  104. def warn(self, msg):
  105. return self.m('<strong style="color: dodgerblue">@</strong> %s'%msg)
  106. def info(self, msg):
  107. return self.m('&ndash; %s'%msg)
  108. def abort(self, msg):
  109. return (self.m('<br />== <span style="color: crimson">%s</span>'%msg)+
  110. self.m(json.dumps({'closed': 1}), 'control'))
  111. def done_cb(self):
  112. self.session['form_json']['validated']=True
  113. self.session['form_json']['jdict']=self.jdict
  114. self.session.save()
  115. def __call__(self, url):
  116. yield self.m('Starting the validation process...')
  117. r=None
  118. try:
  119. yield self.m('* Attempting to retreive <strong>%s</strong>'%url)
  120. r=requests.get(url, verify='/etc/ssl/certs/ca-certificates.crt',
  121. headers={'User-Agent': 'FFDN DB validator'},
  122. stream=True, timeout=10)
  123. except requests.exceptions.SSLError as e:
  124. yield self.err('Unable to connect, SSL Error: <code style="color: #dd1144;">%s</code>'%escape(e))
  125. except requests.exceptions.ConnectionError as e:
  126. yield self.err('Unable to connect: <code style="color: #dd1144;">%s</code>'%e)
  127. except requests.exceptions.Timeout as e:
  128. yield self.err('Connection timeout')
  129. except requests.exceptions.TooManyRedirects as e:
  130. yield self.err('Too many redirects')
  131. except requests.exceptions.RequestException as e:
  132. yield self.err('Internal request exception')
  133. except Exception as e:
  134. yield self.err('Unexpected request exception')
  135. if r is None:
  136. yield self.abort('Connection could not be established, aborting')
  137. return
  138. yield self.info('Connection established')
  139. yield self.info('Response code: <strong>%s %s</strong>'%(escape(r.status_code), escape(r.reason)))
  140. try:
  141. r.raise_for_status()
  142. except requests.exceptions.HTTPError as e:
  143. yield cls.err('Response code indicates an error')
  144. yield cls.abort('Invalid response code')
  145. return
  146. yield self.info('Content type: <strong>%s</strong>'%(escape(r.headers.get('content-type', 'not defined'))))
  147. if not r.headers.get('content-type'):
  148. yield self.error('Content-type <strong>MUST</strong> be defined')
  149. yield self.abort('The file must have a proper content-type to continue')
  150. elif r.headers.get('content-type').lower() != 'application/json':
  151. yield self.warn('Content-type <em>SHOULD</em> be application/json')
  152. if not r.encoding:
  153. yield self.warn('Encoding not set. Assuming it\'s unicode, as per RFC4627 section 3')
  154. yield self.info('Content length: <strong>%s</strong>'%(escape(r.headers.get('content-length', 'not set'))))
  155. cl=r.headers.get('content-length')
  156. if not cl:
  157. yield self.warn('No content-length. Note that we will not process a file whose size exceed 1MiB')
  158. elif int(cl) > self.MAX_JSON_SIZE:
  159. yield self.abort('File too big ! File size must be less then 1MiB')
  160. yield self.info('Reading response into memory...')
  161. b=io.BytesIO()
  162. for d in r.iter_content(requests.models.CONTENT_CHUNK_SIZE):
  163. b.write(d)
  164. if b.tell() > self.MAX_JSON_SIZE:
  165. yield self.abort('File too big ! File size must be less then 1MiB')
  166. return
  167. r._content=b.getvalue()
  168. del b
  169. yield self.info('Successfully read %d bytes'%len(r.content))
  170. yield self.m('<br>* Parsing the JSON file')
  171. if not r.encoding:
  172. charset=requests.utils.guess_json_utf(r.content)
  173. if not charset:
  174. yield self.err('Unable to guess unicode charset')
  175. yield self.abort('The file MUST be unicode-encoded when no explicit charset is in the content-type')
  176. return
  177. yield self.info('Guessed charset: <strong>%s</strong>'%charset)
  178. try:
  179. txt=r.content.decode(r.encoding or charset)
  180. yield self.info('Successfully decoded file as %s'%escape(r.encoding or charset))
  181. except LookupError as e:
  182. yield self.err('Invalid/unknown charset: %s'%escape(e))
  183. yield self.abort('Charset error, Cannot continue')
  184. return
  185. except UnicodeDecodeError as e:
  186. yield self.err('Unicode decode error: %s'%e)
  187. yield self.abort('Charset error, cannot continue')
  188. return
  189. except Exception:
  190. yield self.abort('Unexpected charset error')
  191. return
  192. jdict=None
  193. try:
  194. jdict=json.loads(txt)
  195. except ValueError as e:
  196. yield self.err('Error while parsing JSON: %s'%escape(e))
  197. except Exception as e:
  198. yield self.err('Unexpected error while parsing JSON: %s'%escape(e))
  199. if not jdict:
  200. yield self.abort('Could not parse JSON')
  201. return
  202. yield self.info('JSON parsed successfully')
  203. yield self.m('<br />* Validating the JSON against the schema')
  204. v=list(validate_isp(jdict))
  205. if v:
  206. yield self.err('Errors: %s'%escape(str(v)))
  207. yield self.abort('Your JSON file does not follow the schema, please fix it')
  208. else:
  209. yield self.info('Done. No errors encountered \o')
  210. # check name uniqueness
  211. where = (ISP.name == jdict['name'])
  212. if 'shortname' in jdict and jdict['shortname']:
  213. where |= (ISP.shortname == jdict.get('shortname'))
  214. if ISP.query.filter(where).count() > 1:
  215. yield self.info('An ISP named %s already exist'%escape(
  216. jdict['name']+(' ('+jdict['shortname']+')' if jdict.get('shortname') else '')
  217. ))
  218. yield (self.m('<br />== <span style="color: forestgreen">All good ! You can click on Confirm now</span>')+
  219. self.m(json.dumps({'passed': 1}), 'control'))
  220. self.jdict=jdict
  221. self.done_cb()
  222. @app.route('/create/json-url', methods=['GET', 'POST'])
  223. def create_project_json():
  224. form = forms.ProjectJSONForm()
  225. if form.validate_on_submit():
  226. u=list(form.url.data)
  227. u[2]='/isp.json' # new path
  228. url=urlunsplit(u)
  229. session['form_json'] = {'url': url}
  230. return render_template('project_json_validator.html')
  231. return render_template('project_json_form.html', form=form)
  232. @app.route('/create/json-url/confirm', methods=['POST'])
  233. def create_project_json_confirm():
  234. if 'form_json' in session and session['form_json'].get('validated', False):
  235. if not forms.is_url_unique(session['form_json']['url']):
  236. abort(409)
  237. jdict=session['form_json']['jdict']
  238. isp=ISP()
  239. isp.name=jdict['name']
  240. isp.shortname=jdict['shortname']
  241. isp.url=session['form_json']['url']
  242. isp.json=jdict
  243. del session['form_json']
  244. db.session.add(isp)
  245. db.session.commit()
  246. flash(_(u'Project created'), 'info')
  247. return redirect(url_for('project', projectid=isp.id))
  248. else:
  249. return redirect(url_for('create_project_json'))
  250. @app.route('/search', methods=['GET', 'POST'])
  251. def search():
  252. if request.method == 'POST':
  253. pass
  254. return render_template('search.html')
  255. #------
  256. # Filters
  257. @app.template_filter('step_to_label')
  258. def step_to_label(step):
  259. if step:
  260. return u"<a href='#' rel='tooltip' data-placement='right' title='" + STEPS[step] + "'><span class='badge badge-" + STEPS_LABELS[step] + "'>" + str(step) + "</span></a>"
  261. else:
  262. return u'-'
  263. @app.template_filter('member_to_label')
  264. def member_to_label(is_member):
  265. if is_member:
  266. return u'<a href="#" rel="tooltip" data-placement="right" title="Membre de la Fédération FDN"><span class="label label-success">FFDN</span></a>'
  267. return ''
  268. @app.template_filter('stepname')
  269. def stepname(step):
  270. return STEPS[step]
  271. @app.template_filter('gpspart')
  272. def gpspart(gps, part):
  273. parts = gps.split(':');
  274. if part == 1:
  275. return parts[0]
  276. elif part == 2:
  277. return parts[1]
  278. return "";