views.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. # -*- coding: utf-8 -*-
  2. from flask import request, redirect, url_for, abort, \
  3. render_template, flash, json, session, Response, Markup, \
  4. current_app, Blueprint
  5. from flask.ext.babel import gettext as _, get_locale
  6. from flask.ext.mail import Message
  7. from sqlalchemy.sql import func, asc
  8. import itsdangerous
  9. import docutils.core
  10. import ispformat.specs
  11. from datetime import datetime
  12. import locale
  13. locale.setlocale(locale.LC_ALL, '')
  14. from time import time
  15. import os.path
  16. from . import forms, utils
  17. from .constants import STEPS, STEPS_LABELS, LOCALES_FLAGS
  18. from . import db, cache, mail
  19. from .models import ISP, ISPWhoosh, CoveredArea, RegisteredOffice
  20. from .crawler import WebValidator, PrettyValidator
  21. ispdb = Blueprint('ispdb', __name__)
  22. @ispdb.route('/')
  23. def home():
  24. return render_template('index.html', active_button="home")
  25. @ispdb.route('/isp/')
  26. def project_list():
  27. return render_template('project_list.html', projects=ISP.query.filter_by(is_disabled=False).order_by(asc(func.lower(ISP.name))))
  28. @ispdb.app_errorhandler(404)
  29. def page_not_found(e):
  30. return render_template('404.html'), 404
  31. @ispdb.app_errorhandler(500)
  32. def internal_error(e):
  33. return render_template('500.html'), 500
  34. # this needs to be cached
  35. @ispdb.route('/isp/map_data.json', methods=['GET'])
  36. def isp_map_data():
  37. isps = ISP.query.filter_by(is_disabled=False)
  38. data = []
  39. for isp in isps:
  40. d = dict(isp.json)
  41. for k in d.keys():
  42. if k not in ('name', 'shortname', 'coordinates'):
  43. del d[k]
  44. d['id'] = isp.id
  45. d['ffdn_member'] = isp.is_ffdn_member
  46. d['popup'] = render_template('map_popup.html', isp=isp)
  47. data.append(d)
  48. return Response(json.dumps(data), mimetype='application/json')
  49. @ispdb.route('/isp/find_near.json', methods=['GET'])
  50. def isp_find_near():
  51. lat = request.args.get('lat')
  52. lon = request.args.get('lon')
  53. try:
  54. lat = float(lat)
  55. lon = float(lon)
  56. except (ValueError, TypeError):
  57. abort(400)
  58. q = CoveredArea.containing((lat, lon))\
  59. .options(db.joinedload('isp'))
  60. res = [[{
  61. 'isp_id': ca.isp_id,
  62. 'area': {
  63. 'id': ca.id,
  64. 'name': ca.name,
  65. }
  66. } for ca in q]]
  67. dst = RegisteredOffice.point.distance(db.func.MakePoint(lon, lat), 1).label('distance')
  68. q = db.session.query(RegisteredOffice, dst)\
  69. .options(db.joinedload('isp'))\
  70. .order_by('distance ASC')\
  71. .limit(2)
  72. res.append([{
  73. 'distance': d,
  74. 'isp_id': r.isp.id,
  75. } for r, d in q])
  76. return Response(json.dumps(res))
  77. @ispdb.route('/isp/<projectid>/covered_areas.json', methods=['GET'])
  78. def isp_covered_areas(projectid):
  79. p = ISP.query.filter_by(id=projectid, is_disabled=False)\
  80. .options(db.joinedload('covered_areas'),
  81. db.defer('covered_areas.area'),
  82. db.undefer('covered_areas.area_geojson'))\
  83. .scalar()
  84. if not p:
  85. abort(404)
  86. cas = []
  87. for ca in p.covered_areas:
  88. cas.append({
  89. 'id': ca.id,
  90. 'name': ca.name,
  91. 'area': json.loads(ca.area_geojson) if ca.area_geojson else None
  92. })
  93. return Response(json.dumps(cas), mimetype='application/json')
  94. @ispdb.route('/isp/<projectid>/')
  95. def project(projectid):
  96. p = ISP.query.filter_by(id=projectid, is_disabled=False).first()
  97. if not p:
  98. abort(404)
  99. return render_template('project_detail.html', project_row=p, project=p.json)
  100. @ispdb.route('/isp/<projectid>/edit', methods=['GET', 'POST'])
  101. def edit_project(projectid):
  102. MAX_TOKEN_AGE = 3600
  103. isp = ISP.query.filter_by(id=projectid, is_disabled=False).first_or_404()
  104. sess_token = session.get('edit_tokens', {}).get(isp.id)
  105. if 'token' in request.args:
  106. s = itsdangerous.URLSafeTimedSerializer(current_app.secret_key, salt='edit')
  107. try:
  108. r = s.loads(request.args['token'], max_age=MAX_TOKEN_AGE,
  109. return_timestamp=True)
  110. except:
  111. abort(403)
  112. if r[0] != isp.id:
  113. abort(403)
  114. tokens = session.setdefault('edit_tokens', {})
  115. session.modified = True # ITS A TARP
  116. tokens[r[0]] = r[1]
  117. # refresh page, without the token in the url
  118. return redirect(url_for('.edit_project', projectid=r[0]))
  119. elif (sess_token is None or (datetime.utcnow() - sess_token).total_seconds() > MAX_TOKEN_AGE):
  120. return redirect(url_for('.gen_edit_token', projectid=isp.id))
  121. if isp.is_local:
  122. form = forms.ProjectForm.edit_json(isp)
  123. if form.validate_on_submit():
  124. isp.name = form.name.data
  125. isp.shortname = form.shortname.data or None
  126. isp.json = form.to_json(isp.json)
  127. isp.tech_email = form.tech_email.data
  128. db.session.add(isp)
  129. db.session.commit()
  130. flash(_(u'Project modified'), 'info')
  131. return redirect(url_for('.project', projectid=isp.id))
  132. return render_template('edit_project_form.html', form=form)
  133. else:
  134. form = forms.ProjectJSONForm(obj=isp)
  135. if form.validate_on_submit():
  136. isp.tech_email = form.tech_email.data
  137. url = utils.make_ispjson_url(form.json_url.data)
  138. isp.json_url = url
  139. db.session.add(isp)
  140. db.session.commit()
  141. flash(_(u'Project modified'), 'info')
  142. return redirect(url_for('.project', projectid=isp.id))
  143. return render_template('edit_project_json_form.html', form=form)
  144. @ispdb.route('/isp/<projectid>/gen_edit_token', methods=['GET', 'POST'])
  145. def gen_edit_token(projectid):
  146. isp = ISP.query.filter_by(id=projectid, is_disabled=False).first_or_404()
  147. form = forms.RequestEditToken()
  148. if form.validate_on_submit(): # validated
  149. if form.tech_email.data == isp.tech_email:
  150. s = itsdangerous.URLSafeTimedSerializer(current_app.secret_key, salt='edit')
  151. token = s.dumps(isp.id)
  152. msg = Message("Edit request of your ISP", sender=current_app.config['EMAIL_SENDER'])
  153. msg.body = """
  154. Hello,
  155. You are receiving this message because your are listed as technical contact for "%s" on the FFDN ISP database.
  156. Someone asked to edit your ISP's data in our database. If it's not you, please ignore this message.
  157. To proceed to the editing form, please click on the following link:
  158. %s?token=%s
  159. Note: the link is only valid for one hour from the moment we send you this email.
  160. Thanks,
  161. The FFDN ISP Database team
  162. https://db.ffdn.org
  163. """.strip() % (isp.complete_name,
  164. url_for('.edit_project', projectid=isp.id, _external=True),
  165. token)
  166. msg.add_recipient(isp.tech_email)
  167. mail.send(msg)
  168. # if the email provided is not the correct one, we still redirect
  169. flash(_(u'If you provided the correct email adress, '
  170. 'you must will receive a message shortly (check your spam folder)'), 'info')
  171. return redirect(url_for('.project', projectid=isp.id))
  172. return render_template('gen_edit_token.html', form=form)
  173. @ispdb.route('/add-a-project', methods=['GET'])
  174. def add_project():
  175. return render_template('add_project.html')
  176. @ispdb.route('/isp/create/form', methods=['GET', 'POST'])
  177. def create_project_form():
  178. form = forms.ProjectForm()
  179. if form.validate_on_submit():
  180. isp = ISP()
  181. isp.name = form.name.data
  182. isp.shortname = form.shortname.data or None
  183. isp.tech_email = form.tech_email.data
  184. isp.json = form.to_json(isp.json)
  185. db.session.add(isp)
  186. db.session.commit()
  187. flash(_(u'Project created'), 'info')
  188. return redirect(url_for('.project', projectid=isp.id))
  189. return render_template('add_project_form.html', form=form)
  190. @ispdb.route('/isp/create/validator', methods=['GET'])
  191. def json_url_validator():
  192. if 'form_json' not in session or \
  193. session['form_json'].get('validated', False):
  194. abort(403)
  195. v = session['form_json'].get('validator')
  196. if v is not None:
  197. if v > time() - 5:
  198. abort(429)
  199. else:
  200. session['form_json']['validator'] = time()
  201. validator = WebValidator(session._get_current_object(), 'form_json')
  202. return Response(utils.stream_with_ctx_and_exc(
  203. validator(session['form_json']['url'])
  204. ), mimetype="text/event-stream")
  205. @ispdb.route('/isp/create', methods=['GET', 'POST'])
  206. def create_project_json():
  207. form = forms.ProjectJSONForm()
  208. if form.validate_on_submit():
  209. url = utils.make_ispjson_url(form.json_url.data)
  210. session['form_json'] = {'url': url, 'tech_email': form.tech_email.data}
  211. return render_template('project_json_validator.html')
  212. return render_template('add_project_json_form.html', form=form)
  213. @ispdb.route('/isp/create/confirm', methods=['POST'])
  214. def create_project_json_confirm():
  215. if 'form_json' in session and session['form_json'].get('validated', False):
  216. if not forms.is_url_unique(session['form_json']['url']):
  217. abort(409)
  218. jdict = session['form_json']['jdict']
  219. isp = ISP()
  220. isp.name = jdict['name']
  221. if 'shortname' in jdict:
  222. isp.shortname = jdict['shortname']
  223. isp.json_url = session['form_json']['url']
  224. isp.json = jdict
  225. isp.tech_email = session['form_json']['tech_email']
  226. isp.last_update_attempt = session['form_json']['last_update']
  227. isp.last_update_success = session['form_json']['last_update']
  228. isp.next_update = session['form_json']['next_update']
  229. isp.cache_info = session['form_json']['cache_info']
  230. del session['form_json']
  231. db.session.add(isp)
  232. db.session.commit()
  233. flash(_(u'Project created'), 'info')
  234. return redirect(url_for('.project', projectid=isp.id))
  235. else:
  236. return redirect(url_for('.create_project_json'))
  237. @ispdb.route('/isp/reactivate-validator', methods=['GET'])
  238. def reactivate_validator():
  239. if 'form_reactivate' not in session or \
  240. session['form_reactivate'].get('validated', False):
  241. abort(403)
  242. p = ISP.query.get(session['form_reactivate']['isp_id'])
  243. if not p:
  244. abort(403)
  245. v = session['form_reactivate'].get('validator')
  246. if v is not None:
  247. if v > time() - 5:
  248. abort(429)
  249. else:
  250. session['form_reactivate']['validator'] = time()
  251. validator = PrettyValidator(session._get_current_object(), 'form_reactivate')
  252. return Response(utils.stream_with_ctx_and_exc(
  253. validator(p.json_url, p.cache_info or {})
  254. ), mimetype="text/event-stream")
  255. @ispdb.route('/isp/<projectid>/reactivate', methods=['GET', 'POST'])
  256. def reactivate_isp(projectid):
  257. """
  258. Allow to reactivate an ISP after it has been disabled
  259. because of problems with the JSON file.
  260. """
  261. p = ISP.query.filter(ISP.id == projectid, ISP.is_disabled == False,
  262. ISP.update_error_strike >= 3).first_or_404()
  263. if request.method == 'GET':
  264. key = request.args.get('key')
  265. try:
  266. s = itsdangerous.URLSafeSerializer(current_app.secret_key,
  267. salt='reactivate')
  268. d = s.loads(key)
  269. except Exception:
  270. abort(403)
  271. if (len(d) != 2 or d[0] != p.id or d[1] != str(p.last_update_attempt)):
  272. abort(403)
  273. session['form_reactivate'] = {'isp_id': p.id}
  274. return render_template('reactivate_validator.html', isp=p)
  275. else:
  276. if 'form_reactivate' not in session or \
  277. not session['form_reactivate'].get('validated', False):
  278. abort(409)
  279. p = ISP.query.get(session['form_reactivate']['isp_id'])
  280. p.json = session['form_reactivate']['jdict']
  281. p.cache_info = session['form_reactivate']['cache_info']
  282. p.last_update_attempt = session['form_reactivate']['last_update']
  283. p.last_update_success = p.last_update_attempt
  284. p.update_error_strike = 0
  285. db.session.add(p)
  286. db.session.commit()
  287. flash(_(u'Automatic updates activated'), 'info')
  288. return redirect(url_for('.project', projectid=p.id))
  289. @ispdb.route('/search', methods=['GET', 'POST'])
  290. def search():
  291. terms = request.args.get('q')
  292. if not terms:
  293. return redirect(url_for('.home'))
  294. res = ISPWhoosh.search(terms)
  295. return render_template('search_results.html', results=res, search_terms=terms)
  296. @ispdb.route('/format', methods=['GET'])
  297. def format():
  298. parts = cache.get('format-spec')
  299. if parts is None:
  300. spec = open(ispformat.specs.versions[0.1]).read()
  301. overrides = {
  302. 'initial_header_level': 3,
  303. }
  304. parts = docutils.core.publish_parts(
  305. spec,
  306. source_path=os.path.dirname(ispformat.specs.versions[0.1]),
  307. destination_path=None, writer_name='html',
  308. settings_overrides=overrides
  309. )
  310. cache.set('format-spec', parts, timeout=60 * 60 * 24)
  311. return render_template('format_spec.html', spec=Markup(parts['html_body']))
  312. @ispdb.route('/api/v1/', methods=['GET'])
  313. def api():
  314. return render_template('api.html')
  315. @ispdb.route('/humans.txt', methods=['GET'])
  316. def humans():
  317. import os.path
  318. authors_file = os.path.join(os.path.dirname(__file__), '../AUTHORS')
  319. return Response(open(authors_file), mimetype='text/plain; charset=utf-8')
  320. @ispdb.route('/site.js', methods=['GET'])
  321. def site_js():
  322. l = get_locale()
  323. js_i18n = cache.get('site_js_%s' % (l,))
  324. if not js_i18n:
  325. js_i18n = render_template('site.js')
  326. cache.set('site_js_%s' % (l,), js_i18n, timeout=60 * 60)
  327. r = Response(js_i18n, headers={
  328. 'Content-type': 'application/javascript',
  329. 'Cache-control': 'private, max-age=3600'
  330. })
  331. r.add_etag()
  332. r.make_conditional(request)
  333. return r
  334. @ispdb.route('/locale_selector', methods=['GET', 'POST'])
  335. def locale_selector():
  336. l = current_app.config['LANGUAGES']
  337. if request.method == 'POST' and request.form.get('locale') in l:
  338. resp = redirect(url_for('.home'))
  339. resp.set_cookie('locale', request.form['locale'])
  340. return resp
  341. return render_template('locale_selector.html', locales=(
  342. (code, LOCALES_FLAGS[code], name) for code, name in l.iteritems()
  343. ))
  344. #------
  345. # Filters
  346. @ispdb.app_template_filter('step_to_label')
  347. def step_to_label(step):
  348. if step:
  349. return u"<a href='#' data-toggle='tooltip' data-placement='right' title='" + STEPS[step] + "'><span class='badge badge-" + STEPS_LABELS[step] + "'>" + str(step) + "</span></a>"
  350. else:
  351. return u'-'
  352. @ispdb.app_template_filter('stepname')
  353. def stepname(step):
  354. return STEPS[step]
  355. @ispdb.app_template_filter('js_str')
  356. def json_filter(v):
  357. return Markup(json.dumps(unicode(v)))
  358. @ispdb.app_template_filter('locale_flag')
  359. def locale_flag(l):
  360. return LOCALES_FLAGS.get(str(l), '_unknown')
  361. @ispdb.app_template_global('current_locale')
  362. def current_locale():
  363. return get_locale()