backend.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import os
  4. import sys
  5. import sqlite3
  6. import urlparse
  7. import datetime
  8. import json
  9. from email import utils
  10. from os.path import join, dirname
  11. from bottle import route, run, static_file, request, template, FormsDict, redirect, response
  12. ORIENTATIONS = (
  13. ('N', 'Nord'),
  14. ('NO', 'Nord-Ouest'),
  15. ('O', 'Ouest'),
  16. ('SO', 'Sud-Ouest'),
  17. ('S', 'Sud'),
  18. ('SE', 'Sud-Est'),
  19. ('E', 'Est'),
  20. ('NE', 'Nord-Est'),
  21. )
  22. # Angular sector for each direction, written as (start, stop) in degrees
  23. ANGLES = {
  24. 'N': (-23, 22),
  25. 'NO': (292, 337),
  26. 'O': (247, 292),
  27. 'SO': (202, 247),
  28. 'S': (157, 202),
  29. 'SE': (112, 157),
  30. 'E': (67, 112),
  31. 'NE': (22, 67)
  32. }
  33. TABLE_NAME = 'contribs'
  34. DB_FILENAME = join(dirname(__file__), 'db.sqlite3')
  35. DB = sqlite3.connect(DB_FILENAME)
  36. DB_COLS = (
  37. ('id', 'INTEGER PRIMARY KEY'),
  38. ('name', 'TEXT'),
  39. ('contrib_type', 'TEXT'),
  40. ('latitude', 'REAL'),
  41. ('longitude', 'REAL'),
  42. ('phone', 'TEXT'),
  43. ('email', 'TEXT'),
  44. ('access_type', 'TEXT'),
  45. ('connect_local', 'INTEGER'),
  46. ('connect_internet', 'INTEGER'),
  47. ('bandwidth', 'REAL'),
  48. ('share_part', 'REAL'),
  49. ('floor', 'INTEGER'),
  50. ('floor_total', 'INTEGER'),
  51. ('orientations', 'TEXT'),
  52. ('roof', 'INTEGER'),
  53. ('comment', 'TEXT'),
  54. ('privacy_name', 'INTEGER'),
  55. ('privacy_email', 'INTEGER'),
  56. ('privacy_coordinates', 'INTEGER'),
  57. ('privacy_place_details', 'INTEGER'),
  58. ('privacy_comment', 'INTEGER'),
  59. ('date', 'TEXT'),
  60. )
  61. GEOJSON_NAME = 'public.json'
  62. @route('/')
  63. def home():
  64. redirect("/wifi-form")
  65. @route('/wifi-form')
  66. def show_wifi_form():
  67. return template('wifi-form', errors=None, data = FormsDict(),
  68. orientations=ORIENTATIONS, geojson=GEOJSON_NAME)
  69. def create_tabble(db, name, columns):
  70. col_defs = ','.join(['{} {}'.format(*i) for i in columns])
  71. db.execute('CREATE TABLE {} ({})'.format(name, col_defs))
  72. def save_to_db(db, dic):
  73. # SQLite is picky about encoding else
  74. tosave = {bytes(k):v.decode('utf-8') if isinstance(v,str) else v for k,v in dic.items()}
  75. tosave['date'] = utils.formatdate()
  76. return db.execute("""
  77. INSERT INTO {}
  78. (name, contrib_type, latitude, longitude, phone, email, access_type, connect_local, connect_internet, bandwidth, share_part, floor, floor_total, orientations, roof, comment,
  79. privacy_name, privacy_email, privacy_place_details, privacy_coordinates, privacy_comment, date)
  80. VALUES (:name, :contrib_type, :latitude, :longitude, :phone, :email, :access_type, :connect_local, :connect_internet, :bandwidth, :share_part, :floor, :floor_total, :orientations, :roof, :comment,
  81. :privacy_name, :privacy_email, :privacy_place_details, :privacy_coordinates, :privacy_comment, :date)
  82. """.format(TABLE_NAME), tosave)
  83. @route('/wifi-form', method='POST')
  84. def submit_wifi_form():
  85. required = ('name', 'contrib-type',
  86. 'latitude', 'longitude')
  87. required_or = (('email', 'phone'),)
  88. required_if = (
  89. ('contrib-type', 'share',('access-type', 'bandwidth',
  90. 'share-part')),
  91. )
  92. field_names = {
  93. 'name' : 'Nom/Pseudo',
  94. 'contrib-type': 'Type de participation',
  95. 'latitude' : 'Localisation',
  96. 'longitude' : 'Localisation',
  97. 'phone' : 'Téléphone',
  98. 'email' : 'Email',
  99. 'access-type' : 'Type de connexion',
  100. 'bandwidth' : 'Bande passante',
  101. 'share-part' : 'Débit partagé',
  102. 'floor' : 'Étage',
  103. 'floor_total' : 'Nombre d\'étages total'
  104. }
  105. errors = []
  106. for name in required:
  107. if (not request.forms.get(name)):
  108. errors.append((field_names[name], 'ce champ est requis'))
  109. for name_list in required_or:
  110. filleds = [True for name in name_list if request.forms.get(name)]
  111. if len(filleds) <= 0:
  112. errors.append((
  113. ' ou '.join([field_names[i] for i in name_list]),
  114. 'au moins un des de ces champs est requis'))
  115. for key, value, fields in required_if:
  116. if request.forms.get('key') == value:
  117. for name in fields:
  118. if not request.forms.get(name):
  119. errors.append(
  120. (field_names[name], 'ce champ est requis'))
  121. floor = request.forms.get('floor')
  122. floor_total = request.forms.get('floor_total')
  123. if floor and not floor_total:
  124. errors.append((field_names['floor_total'], "ce champ est requis"))
  125. elif not floor and floor_total:
  126. errors.append((field_names['floor'], "ce champ est requis"))
  127. elif floor and floor_total and (int(floor) > int(floor_total)):
  128. errors.append((field_names['floor'], "Étage supérieur au nombre total"))
  129. if errors:
  130. return template('wifi-form', errors=errors, data=request.forms,
  131. orientations=ORIENTATIONS, geojson=GEOJSON_NAME)
  132. else:
  133. d = request.forms
  134. save_to_db(DB, {
  135. 'name' : d.get('name'),
  136. 'contrib_type' : d.get('contrib-type'),
  137. 'latitude' : d.get('latitude'),
  138. 'longitude' : d.get('longitude'),
  139. 'phone' : d.get('phone'),
  140. 'email' : d.get('email'),
  141. 'phone' : d.get('phone'),
  142. 'access_type' : d.get('access-type'),
  143. 'connect_local' : 'local' in d.getall('connect-type'),
  144. 'connect_internet' : 'internet' in d.getall('connect-type'),
  145. 'bandwidth' : d.get('bandwidth'),
  146. 'share_part' : d.get('share-part'),
  147. 'floor' : d.get('floor'),
  148. 'floor_total' : d.get('floor_total'),
  149. 'orientations' : ','.join(d.getall('orientation')),
  150. 'roof' : d.get('roof'),
  151. 'comment' : d.get('comment'),
  152. 'privacy_name' : 'name' in d.getall('privacy'),
  153. 'privacy_email' : 'email' in d.getall('privacy'),
  154. 'privacy_place_details': 'place_details' in d.getall('privacy'),
  155. 'privacy_coordinates' : 'coordinates' in d.getall('privacy'),
  156. 'privacy_comment' : 'comment' in d.getall('privacy'),
  157. })
  158. DB.commit()
  159. # Rebuild GeoJSON
  160. build_geojson()
  161. return redirect(urlparse.urljoin(request.path,'thanks'))
  162. @route('/thanks')
  163. def wifi_form_thanks():
  164. return template('thanks')
  165. @route('/assets/<filename:path>')
  166. def send_asset(filename):
  167. return static_file(filename, root=join(dirname(__file__), 'assets'))
  168. @route('/legal')
  169. def legal():
  170. return template('legal')
  171. """
  172. Results Map
  173. """
  174. @route('/map')
  175. def public_map():
  176. return template('map', geojson=GEOJSON_NAME)
  177. @route('/public.json')
  178. def public_geojson():
  179. return static_file('public.json', root=join(dirname(__file__), 'json/'))
  180. """
  181. GeoJSON Functions
  182. """
  183. # Useful for merging angle intervals (orientations)
  184. def merge_intervals(l, wrap=360):
  185. """Merge a list of intervals, assuming the space is cyclic. The
  186. intervals should already by sorted by start value."""
  187. if l == []:
  188. return []
  189. result = list()
  190. # Transform the 2-tuple into a 2-list to be able to modify it
  191. result.append(list(l[0]))
  192. for (start, stop) in l:
  193. current = result[-1]
  194. if start > current[1]:
  195. result.append([start, stop])
  196. else:
  197. result[-1][1] = max(result[-1][1], stop)
  198. if len(result) == 1:
  199. return result
  200. # Handle the cyclicity by merging the ends if necessary
  201. last = result[-1]
  202. first = result[0]
  203. if first[0] <= last[1] - wrap:
  204. result[-1][1] = max(result[-1][1], first[1] + wrap)
  205. result.pop(0)
  206. return result
  207. def orientations_to_angle(orientations):
  208. """Return a list of (start, stop) angles from a list of orientations."""
  209. # Hack to make leaflet-semicircle happy (drawing a full circle only
  210. # works with (0, 360))
  211. if len(orientations) == 8:
  212. return [[0, 360]]
  213. angles = [ANGLES[orientation] for orientation in orientations]
  214. angles.sort(key=lambda (x, y): x)
  215. return merge_intervals(angles)
  216. # Save feature collection to a json file
  217. def save_featurecollection_json(id, features):
  218. with open('json/' + id + '.json', 'w') as outfile:
  219. json.dump({
  220. "type" : "FeatureCollection",
  221. "features" : features,
  222. "id" : id,
  223. }, outfile)
  224. # Build GeoJSON files from DB
  225. def build_geojson():
  226. # Read from DB
  227. DB.row_factory = sqlite3.Row
  228. cur = DB.execute("""
  229. SELECT * FROM {} ORDER BY id DESC
  230. """.format(TABLE_NAME))
  231. public_features = []
  232. private_features = []
  233. # Loop through results
  234. rows = cur.fetchall()
  235. for row in rows:
  236. orientations = row['orientations'].split(',')
  237. angles = orientations_to_angle(orientations)
  238. # Private JSON file
  239. private_features.append({
  240. "type" : "Feature",
  241. "geometry" : {
  242. "type": "Point",
  243. "coordinates": [row['longitude'], row['latitude']],
  244. },
  245. "id" : row['id'],
  246. "properties": {
  247. "name" : row['name'],
  248. "place" : {
  249. 'floor' : row['floor'],
  250. 'floor_total' : row['floor_total'],
  251. 'orientations' : orientations,
  252. 'angles' : angles,
  253. 'roof' : row['roof'],
  254. },
  255. "comment" : row['comment']
  256. }
  257. })
  258. # Bypass non-public points
  259. if not row['privacy_coordinates']:
  260. continue
  261. # Public JSON file
  262. public_feature = {
  263. "type" : "Feature",
  264. "geometry" : {
  265. "type": "Point",
  266. "coordinates": [row['longitude'], row['latitude']],
  267. },
  268. "id" : row['id'],
  269. "properties": {}
  270. }
  271. # Add optionnal variables
  272. if row['privacy_name']:
  273. public_feature['properties']['name'] = row['name']
  274. if row['privacy_comment']:
  275. public_feature['properties']['comment'] = row['comment']
  276. if row['privacy_place_details']:
  277. public_feature['properties']['place'] = {
  278. 'floor' : row['floor'],
  279. 'floor_total' : row['floor_total'],
  280. 'orientations' : orientations,
  281. 'angles' : angles,
  282. 'roof' : row['roof'],
  283. }
  284. # Add to public features list
  285. public_features.append(public_feature)
  286. # Build GeoJSON Feature Collection
  287. save_featurecollection_json('private', private_features)
  288. save_featurecollection_json('public', public_features)
  289. DEBUG = bool(os.environ.get('DEBUG', False))
  290. if __name__ == '__main__':
  291. if len(sys.argv) > 1:
  292. if sys.argv[1] == 'createdb':
  293. create_tabble(DB, TABLE_NAME, DB_COLS)
  294. if sys.argv[1] == 'buildgeojson':
  295. build_geojson()
  296. else:
  297. run(host='localhost', port=8080, reloader=DEBUG)
  298. DB.close()