26 Commits ade50e0dac ... ab7eccdabc

Auteur SHA1 Message Date
  pitchum ab7eccdabc Return HTTP status 405 instead of 500 + stacktrace + email. il y a 6 ans
  pitchum 6e290ffee3 Bugfix: cron_task fails when last_update_success is null in DB. il y a 6 ans
  pitchum 4d3c9822fa Allow switching between manual and isp.json updates. il y a 6 ans
  zorun b7567eec85 Merge branch 'realmaster' of pitchum/ffdn-db into master il y a 7 ans
  pitchum 0f6c1645d5 bugfix: crontab script fails when field 'covered_areas' is empty in isp.json. il y a 7 ans
  pitchum c22119f428 Catch errors in post-commit hook to prevent side-effects. il y a 7 ans
  zorun 74f19e23a5 Merge branch 'realmaster' of pitchum/ffdn-db into master il y a 7 ans
  pitchum ab57756b95 Fix libspatialite.so: undefined symbol: sqlite3_spatialite_init il y a 7 ans
  pitchum faf23b0eea Cron task: error syncing one ISP should not break the whole process. il y a 7 ans
  bram 79762bd2bb Merge branch 'fix-requests-requirements' of FFDN/ffdn-db into master il y a 7 ans
  Baptiste Jonglez 1a4d8553b8 Do not assert when an ISP changes its name (fix #14) il y a 7 ans
  opi f55ecd8f24 Fix requests version in requirements il y a 7 ans
  opi 8e65db30b0 Ajout de dependences manquantes pour une installation sur Debian il y a 7 ans
  sebian e74d639d19 Merge branch 'simplify-readme' of neodarz/ffdn-db into master il y a 7 ans
  sebian 4b63521988 Merge branch 'fix-readme-deps' of FFDN/ffdn-db into master il y a 7 ans
  neodarz 1e5459ac3b Simplification de la procédure d'installation et de déploiement il y a 7 ans
  opi 98ec7fe1e9 Ajout de dependences manquantes pour une installation sur Debian il y a 7 ans
  pitchum ade50e0dac Fix 'cannot create column with spatialite -> unexpected metadata layout' il y a 7 ans
  pitchum 91b02bbb93 Replace Flask-Cache with Flask-Caching. il y a 7 ans
  pitchum d2a6d5fb89 Upgraded Flask-based dependencies. il y a 7 ans
  pitchum 2120d3f8b1 Manual 2to3 on pseudo-HTML file. il y a 7 ans
  pitchum a21e544851 More gitignore. il y a 7 ans
  pitchum 58a0fe3d58 upgraded geoalchemy -> geoalchemy2 il y a 7 ans
  pitchum 73580928fc 2to3 --nobackups --write . il y a 7 ans
  pitchum ce10d6f667 Fix libspatialite.so: undefined symbol: sqlite3_spatialite_init il y a 7 ans
  pitchum 4139ac5295 Progressively upgrading python dependencies. il y a 7 ans

+ 0 - 3
.gitignore

@@ -1,6 +1,3 @@
 *~
 *.pyc
 .*.swp
-*.sqlite
-src/
-whoosh/

+ 10 - 3
README.md

@@ -4,28 +4,35 @@ ffdnispdb
 ``ffdnispdb`` is a website designed to display all the ISPs implementing the
 ``ispschema`` specification.
 
-## How to install & develop
+## How to install
 
 ``ffdnispdb`` requires python2.
 
 Third-party dependencies:
  * ``sqlite``
  * ``libspatialite``
+ * ``build-essential``
+ * ``python-dev``
 
 On a Debian Jessie system, do:
 
-    apt install sqlite3 libspatialite-dev libsqlite3-mod-spatialite
+    apt install sqlite3 libspatialite-dev build-essential python-dev
+
+On a Debian Stretch system, do:
+
+    apt install sqlite3 libsqlite3-mod-spatialite build-essential python-dev
 
 Preferably in a virtualenv, run:
 
     pip install -r requirements.txt
     python manage.py db create
 
+### Develop deployement
 To start the development server, run:
 
     python manage.py runserver
 
-## Production deployment
+### Production deployment
 To deploy this application, we recommend the use of gunicorn/gevent.
 We strongly discourage you to use a synchronous WSGI server, since the app uses
 ``Server-sent events``.

+ 18 - 8
ffdnispdb/__init__.py

@@ -1,10 +1,10 @@
 # -*- coding: utf-8 -*-
 
 from flask import Flask, g, current_app, request
-from flask_babel import Babel
-from flask_sqlalchemy import SQLAlchemy, event
-from flask_mail import Mail
-from flask_caching import Cache
+from flask.ext.babel import Babel
+from flask.ext.sqlalchemy import SQLAlchemy, event
+from flask.ext.mail import Mail
+from flask.ext.cache import Cache
 from .sessions import MySessionInterface
 
 
@@ -15,9 +15,9 @@ cache = Cache()
 mail = Mail()
 
 def get_locale():
-    if request.cookies.get('locale') in list(current_app.config['LANGUAGES'].keys()):
+    if request.cookies.get('locale') in current_app.config['LANGUAGES'].keys():
         return request.cookies.get('locale')
-    return request.accept_languages.best_match(list(current_app.config['LANGUAGES'].keys()), 'en')
+    return request.accept_languages.best_match(current_app.config['LANGUAGES'].keys(), 'en')
 
 
 def create_app(config={}):
@@ -34,11 +34,21 @@ def create_app(config={}):
     db.init_app(app)
 
     with app.app_context():
+        @event.listens_for(db.engine, "first_connect")
+        def connect(sqlite, connection_rec):
+            sqlite.enable_load_extension(True)
+            try:
+                sqlite.execute('select load_extension("libspatialite")')
+                current_app.spatialite_modulename = 'libspatialite'
+            except Exception as e:
+                sqlite.execute('select load_extension("mod_spatialite")')
+                current_app.spatialite_modulename = 'mod_spatialite'
+            sqlite.enable_load_extension(False)
+
         @event.listens_for(db.engine, "connect")
         def connect(sqlite, connection_rec):
             sqlite.enable_load_extension(True)
-            sqlite.execute('select load_extension("mod_spatialite")')
-            sqlite.execute('SELECT InitSpatialMetaData()')
+            sqlite.execute('select load_extension("%s")' % current_app.spatialite_modulename)
             sqlite.enable_load_extension(False)
 
     app.session_interface = sess

+ 8 - 8
ffdnispdb/constants.py

@@ -1,15 +1,15 @@
 # -*- coding: utf-8 -*-
 
-from flask_babel import lazy_gettext as _
+from flask.ext.babel import lazy_gettext as _
 
 STEPS = {
-    1: _('Project considered'),
-    2: _('Primary members found'),
-    3: _('Legal structure being created'),
-    4: _('Legal structure created'),
-    5: _('Base tools created (bank account, first members)'),
-    6: _('ISP partially functional (first subscribers, maybe in degraded mode)'),
-    7: _('ISP fully working')
+    1: _(u'Project considered'),
+    2: _(u'Primary members found'),
+    3: _(u'Legal structure being created'),
+    4: _(u'Legal structure created'),
+    5: _(u'Base tools created (bank account, first members)'),
+    6: _(u'ISP partially functional (first subscribers, maybe in degraded mode)'),
+    7: _(u'ISP fully working')
 }
 
 STEPS_LABELS = {

+ 22 - 22
ffdnispdb/crawler.py

@@ -28,7 +28,7 @@ class Crawler(object):
 
     MAX_JSON_SIZE = 1 * 1024 * 1024
 
-    escape = staticmethod(lambda x: str(str(x), 'utf8') if type(x) != str else x)
+    escape = staticmethod(lambda x: unicode(str(x), 'utf8') if type(x) != unicode else x)
 
     def __init__(self):
         self.success = False
@@ -39,18 +39,18 @@ class Crawler(object):
 
     def m(self, msg, evt=None):
         if not evt:
-            return '%s\n' % msg
+            return u'%s\n' % msg
         else:
-            return ''
+            return u''
 
     def err(self, msg, *args):
-        return self.m('! %s' % msg, *args)
+        return self.m(u'! %s' % msg, *args)
 
     def warn(self, msg):
-        return self.m('@ %s' % msg)
+        return self.m(u'@ %s' % msg)
 
     def info(self, msg):
-        return self.m('\u2013 %s' % msg)
+        return self.m(u'\u2013 %s' % msg)
 
     def abort(self, msg):
         raise NotImplemented
@@ -70,9 +70,9 @@ class Crawler(object):
     def format_validation_errors(self, errs):
         r = []
         for e in errs:
-            r.append('    %s: %s' % ('.'.join(list(e.schema_path)[1:]), e.message))
+            r.append(u'    %s: %s' % ('.'.join(list(e.schema_path)[1:]), e.message))
 
-        return '\n'.join(r) + '\n'
+        return u'\n'.join(r) + '\n'
 
     def pre_done_cb(self, *args):
         pass
@@ -126,7 +126,7 @@ class Crawler(object):
             # so that it's logged.
             tb = sys.exc_info()[2]
             yield self.abort('Unexpected request exception')
-            raise e.with_traceback(tb)
+            raise e, None, tb
 
         if r is None:
             yield self.abort('Connection could not be established, aborting')
@@ -154,7 +154,7 @@ class Crawler(object):
                 yield self.warn('Invalid max-age ' + esc(_maxage))
 
             yield self.info('Cache control: ' + self.bold(esc(
-                ', '.join([k + '=' + v if type(v) != bool else k for k, v in cachecontrol.items()]))
+                ', '.join([k + '=' + v if type(v) != bool else k for k, v in cachecontrol.iteritems()]))
             ))
 
         _expires = r.headers.get('expires')
@@ -318,36 +318,36 @@ class PrettyValidator(Crawler):
         super(PrettyValidator, self).__init__(*args, **kwargs)
         self.session = session
         self.sesskey = sesskey
-        self.escape = lambda x: escape(str(str(x), 'utf8') if type(x) != str else x)
+        self.escape = lambda x: escape(unicode(str(x), 'utf8') if type(x) != unicode else x)
 
     def m(self, msg, evt=None):
-        return '%sdata: %s\n\n' % ('event: %s\n' % evt if evt else '', msg)
+        return u'%sdata: %s\n\n' % (u'event: %s\n' % evt if evt else '', msg)
 
     def err(self, msg, *args):
-        return self.m('<strong style="color: crimson">!</strong> %s' % msg, *args)
+        return self.m(u'<strong style="color: crimson">!</strong> %s' % msg, *args)
 
     def warn(self, msg):
-        return self.m('<strong style="color: dodgerblue">@</strong> %s' % msg)
+        return self.m(u'<strong style="color: dodgerblue">@</strong> %s' % msg)
 
     def info(self, msg):
-        return self.m('&ndash; %s' % msg)
+        return self.m(u'&ndash; %s' % msg)
 
     def abort(self, msg):
-        return (self.m('<br />== <span style="color: crimson">%s</span>' % msg) +
+        return (self.m(u'<br />== <span style="color: crimson">%s</span>' % msg) +
                 self.m(json.dumps({'closed': 1}), 'control'))
 
     def bold(self, msg):
-        return '<strong>%s</strong>' % msg
+        return u'<strong>%s</strong>' % msg
 
     def italics(self, msg):
-        return '<em>%s</em>' % msg
+        return u'<em>%s</em>' % msg
 
     def color(self, color, msg):
-        return '<span style="color: %s">%s</span>' % (color, msg)
+        return u'<span style="color: %s">%s</span>' % (color, msg)
 
     def format_validation_errors(self, errs):
         lns = super(PrettyValidator, self).format_validation_errors(errs)
-        buf = ''
+        buf = u''
         for l in lns.split('\n'):
             buf += self.m(self.escape(l))
         return buf
@@ -380,6 +380,6 @@ class WebValidator(PrettyValidator):
 class TextValidator(Crawler):
 
     def abort(self, msg):
-        res = 'FATAL ERROR: %s\n' % msg
-        pad = '=' * (len(res) - 1) + '\n'
+        res = u'FATAL ERROR: %s\n' % msg
+        pad = u'=' * (len(res) - 1) + '\n'
         return self.m(pad + res + pad)

+ 22 - 18
ffdnispdb/cron_task.py

@@ -4,7 +4,7 @@
 import signal
 import traceback
 from datetime import datetime, timedelta
-from flask_mail import Message
+from flask.ext.mail import Message
 from flask import url_for
 import itsdangerous
 
@@ -65,7 +65,7 @@ def gen_reactivate_key(isp):
 
 
 def send_warning_email(isp, debug_msg):
-    msg = Message("Problem while updating your ISP's data", sender=app.config['EMAIL_SENDER'])
+    msg = Message(u"Problem while updating your ISP's data", sender=app.config['EMAIL_SENDER'])
     msg.body = """
 Hello,
 
@@ -89,7 +89,7 @@ https://db.ffdn.org
     """.strip() % (isp.complete_name, isp.json_url, debug_msg.strip(),
                  url_for('ispdb.reactivate_isp', projectid=isp.id), gen_reactivate_key(isp))
     msg.add_recipient(isp.tech_email)
-    print('    Sending notification email to %s' % (isp.tech_email))
+    print u'    Sending notification email to %s' % (isp.tech_email)
     mail.send(msg)
 
 
@@ -102,11 +102,11 @@ try:
                                 ISP.update_error_strike < 3)\
             .order_by(ISP.last_update_success):
         try:
-            print('%s: Attempting to update %s' % (datetime.now(), isp))
-            print('    last successful update=%s' % (utils.tosystemtz(isp.last_update_success)))
-            print('    last update attempt=%s' % (utils.tosystemtz(isp.last_update_attempt)))
-            print('    next update was scheduled %s ago' % (utils.utcnow() - isp.next_update))
-            print('    strike=%d' % (isp.update_error_strike))
+            print u'%s: Attempting to update %s' % (datetime.now(), isp)
+            print u'    last successful update=%s' % (utils.tosystemtz(isp.last_update_success))
+            print u'    last update attempt=%s' % (utils.tosystemtz(isp.last_update_attempt))
+            print u'    next update was scheduled %s ago' % (utils.utcnow() - isp.next_update)
+            print u'    strike=%d' % (isp.update_error_strike)
 
             isp.last_update_attempt = utils.utcnow()
             db.session.add(isp)
@@ -129,15 +129,15 @@ try:
                 isp.next_update = utils.utcnow() + timedelta(seconds=validator.jdict_max_age)
                 db.session.add(isp)
                 db.session.commit()
-                print('%s: Error while updating:' % (datetime.now()))
+                print u'%s: Error while updating:' % (datetime.now())
                 if isp.update_error_strike >= 3:
-                    print('    three strikes, you\'re out')
+                    print u'    three strikes, you\'re out'
                     send_warning_email(isp, log)
 
-                print(log.rstrip().encode('utf-8') + '\n')
+                print log.rstrip().encode('utf-8') + '\n'
                 if exc:
-                    print('Unexpected exception in the validator: %r' % exc)
-                    print(exc_trace)
+                    print u'Unexpected exception in the validator: %r' % exc
+                    print exc_trace
 
                 continue
 
@@ -150,10 +150,10 @@ try:
             db.session.add(isp)
             db.session.commit()
 
-            print('%s: Update successful !' % (datetime.now()))
-            print('    next update is scheduled for %s\n' % (isp.next_update))
+            print u'%s: Update successful !' % (datetime.now())
+            print u'    next update is scheduled for %s\n' % (isp.next_update)
         except Timeout:
-            print('%s: Timeout while updating:' % (datetime.now()))
+            print u'%s: Timeout while updating:' % (datetime.now())
             isp = ISP.query.get(isp.id)
             isp.update_error_strike += 1
             db.session.add(isp)
@@ -161,8 +161,12 @@ try:
             if isp.update_error_strike >= 3:
                 send_warning_email(isp, 'Your ISP took more then 18 seconds to process. '
                                         'Having problems with your webserver ?')
-                print('    three strikes, you\'re out')
-            print(traceback.format_exc())
+                print u'    three strikes, you\'re out'
+            print traceback.format_exc()
+        except:
+            print u'Unknown error, see call stack below.'
+            traceback.print_exc() # To help debugging the cause of the failure
+            db.session.rollback()
 
 except ScriptTimeout:
     pass

+ 2 - 2
ffdnispdb/default_settings.py

@@ -7,8 +7,8 @@ CRAWLER_DEFAULT_CACHE_TIME = 60 * 60 * 12  # 12 hours
 CRAWLER_CRON_INTERVAL = 60 * 20  # used to return valid cache info in the API
 SYSTEM_TIME_ZONE = 'Europe/Paris'
 LANGUAGES = {
-    'en': 'English',
-    'fr': 'Français',
+    'en': u'English',
+    'fr': u'Français',
 }
 ISP_FORM_GEOJSON_MAX_SIZE = 256 * 1024
 ISP_FORM_GEOJSON_MAX_SIZE_TOTAL = 1024 * 1024

+ 60 - 60
ffdnispdb/forms.py

@@ -1,10 +1,10 @@
 from functools import partial
 import itertools
-import urllib.parse
+import urlparse
 import json
 import collections
 from flask import current_app
-from flask_wtf import Form
+from flask.ext.wtf import Form
 from wtforms import Form as InsecureForm
 from wtforms import (TextField, DateField, DecimalField, IntegerField,
                      SelectField, SelectMultipleField, FieldList, FormField)
@@ -12,7 +12,7 @@ from wtforms.widgets import (TextInput, ListWidget, html_params, HTMLString,
                              CheckboxInput, Select, TextArea)
 from wtforms.validators import (DataRequired, Optional, URL, Email, Length,
                                 NumberRange, ValidationError, StopValidation)
-from flask_babel import lazy_gettext as _, gettext
+from flask.ext.babel import lazy_gettext as _, gettext
 from babel.support import LazyProxy
 from ispformat.validator import validate_geojson
 from .constants import STEPS
@@ -46,7 +46,7 @@ class MyFormField(FormField):
 
     @property
     def flattened_errors(self):
-        return list(itertools.chain.from_iterable(list(self.errors.values())))
+        return list(itertools.chain.from_iterable(self.errors.values()))
 
 
 class GeoJSONField(TextField):
@@ -56,12 +56,12 @@ class GeoJSONField(TextField):
         if valuelist and valuelist[0]:
             max_size = current_app.config['ISP_FORM_GEOJSON_MAX_SIZE']
             if len(valuelist[0]) > max_size:
-                raise ValueError(_('JSON value too big, must be less than %(max_size)s',
+                raise ValueError(_(u'JSON value too big, must be less than %(max_size)s',
                                    max_size=filesize_fmt(max_size)))
             try:
                 self.data = json.loads(valuelist[0], object_pairs_hook=collections.OrderedDict)
             except Exception as e:
-                raise ValueError(_('Not a valid JSON value'))
+                raise ValueError(_(u'Not a valid JSON value'))
         elif valuelist and valuelist[0].strip() == '':
             self.data = None  # if an empty string was passed, set data as None
 
@@ -76,10 +76,10 @@ class GeoJSONField(TextField):
     def pre_validate(self, form):
         if self.data is not None:
             if not validate_geojson(self.data):
-                raise StopValidation(_('Invalid GeoJSON, please check it'))
+                raise StopValidation(_(u'Invalid GeoJSON, please check it'))
             if not check_geojson_spatialite(self.data):
                 current_app.logger.error('Spatialite could not decode the following GeoJSON: %s', self.data)
-                raise StopValidation(_('Unable to store GeoJSON in database'))
+                raise StopValidation(_(u'Unable to store GeoJSON in database'))
 
 
 class Unique(object):
@@ -89,7 +89,7 @@ class Unique(object):
         self.model = model
         self.field = field
         if not message:
-            message = _('this element already exists')
+            message = _(u'this element already exists')
         self.message = message
 
     def __call__(self, form, field):
@@ -112,59 +112,59 @@ TECHNOLOGIES_CHOICES = (
 
 
 class CoveredArea(InsecureForm):
-    name = TextField(_('name'), widget=partial(TextInput(), class_='input-medium', placeholder=_('Area')))
-    technologies = SelectMultipleField(_('technologies'), choices=TECHNOLOGIES_CHOICES,
-                                       widget=partial(Select(True), **{'class': 'selectpicker', 'data-title': _('Technologies deployed')}))
+    name = TextField(_(u'name'), widget=partial(TextInput(), class_='input-medium', placeholder=_(u'Area')))
+    technologies = SelectMultipleField(_(u'technologies'), choices=TECHNOLOGIES_CHOICES,
+                                       widget=partial(Select(True), **{'class': 'selectpicker', 'data-title': _(u'Technologies deployed')}))
     area = GeoJSONField(_('area'), widget=partial(TextArea(), class_='geoinput'))
 
     def validate(self, *args, **kwargs):
         r = super(CoveredArea, self).validate(*args, **kwargs)
         if bool(self.name.data) != bool(self.technologies.data):
-            self._fields['name'].errors += [_('You must fill both fields')]
+            self._fields['name'].errors += [_(u'You must fill both fields')]
             r = False
         return r
 
 
 class OtherWebsites(InsecureForm):
-    name = TextField(_('name'), widget=partial(TextInput(), class_='input-small', placeholder=_('Name')))
-    url = TextField(_('url'), widget=partial(TextInput(), class_='input-medium', placeholder=_('URL')),
+    name = TextField(_(u'name'), widget=partial(TextInput(), class_='input-small', placeholder=_(u'Name')))
+    url = TextField(_(u'url'), widget=partial(TextInput(), class_='input-medium', placeholder=_(u'URL')),
                     validators=[Optional(), URL(require_tld=True)])
 
 
-STEP_CHOICES = [(k, LazyProxy(lambda k, s: '%u - %s' % (k, s), k, STEPS[k], enable_cache=False)) for k in STEPS]
+STEP_CHOICES = [(k, LazyProxy(lambda k, s: u'%u - %s' % (k, s), k, STEPS[k], enable_cache=False)) for k in STEPS]
 
 
 class ProjectForm(Form):
-    name = TextField(_('full name'), description=[_('E.g. French Data Network')],
+    name = TextField(_(u'full name'), description=[_(u'E.g. French Data Network')],
                      validators=[DataRequired(), Length(min=2), Unique(ISP, ISP.name)])
-    shortname = TextField(_('short name'), description=[_('E.g. FDN')],
+    shortname = TextField(_(u'short name'), description=[_(u'E.g. FDN')],
                           validators=[Optional(), Length(min=2, max=15), Unique(ISP, ISP.shortname)])
-    description = TextField(_('description'), description=[None, _('Short text describing the project')])
-    logo_url = TextField(_('logo url'), validators=[Optional(), URL(require_tld=True)])
-    website = TextField(_('website'), validators=[Optional(), URL(require_tld=True)])
+    description = TextField(_(u'description'), description=[None, _(u'Short text describing the project')])
+    logo_url = TextField(_(u'logo url'), validators=[Optional(), URL(require_tld=True)])
+    website = TextField(_(u'website'), validators=[Optional(), URL(require_tld=True)])
     other_websites = FieldList(MyFormField(OtherWebsites, widget=partial(InputListWidget(), class_='formfield')),
                                min_entries=1, widget=InputListWidget(),
-                               description=[None, _('Additional websites that you host (e.g. wiki, etherpad...)')])
-    contact_email = TextField(_('contact email'), validators=[Optional(), Email()],
-                              description=[None, _('General contact email address')])
-    main_ml = TextField(_('main mailing list'), validators=[Optional(), Email()],
-                        description=[None, _('Address of your main mailing list')])
-    creation_date = DateField(_('creation date'), validators=[Optional()], widget=partial(TextInput(), placeholder=_('YYYY-mm-dd')),
-                              description=[None, _('Date at which the legal structure for your project was created')])
-    chatrooms = FieldList(TextField(_('chatrooms')), min_entries=1, widget=InputListWidget(),
-                          description=[None, _('In URI form, e.g. <code>irc://irc.isp.net/#isp</code> or ' +
+                               description=[None, _(u'Additional websites that you host (e.g. wiki, etherpad...)')])
+    contact_email = TextField(_(u'contact email'), validators=[Optional(), Email()],
+                              description=[None, _(u'General contact email address')])
+    main_ml = TextField(_(u'main mailing list'), validators=[Optional(), Email()],
+                        description=[None, _(u'Address of your main mailing list')])
+    creation_date = DateField(_(u'creation date'), validators=[Optional()], widget=partial(TextInput(), placeholder=_(u'YYYY-mm-dd')),
+                              description=[None, _(u'Date at which the legal structure for your project was created')])
+    chatrooms = FieldList(TextField(_(u'chatrooms')), min_entries=1, widget=InputListWidget(),
+                          description=[None, _(u'In URI form, e.g. <code>irc://irc.isp.net/#isp</code> or ' +
                           '<code>xmpp:isp@chat.isp.net?join</code>')])
     covered_areas = FieldList(MyFormField(CoveredArea, _('Covered Areas'), widget=partial(InputListWidget(), class_='formfield')),
                               min_entries=1, widget=InputListWidget(),
-                              description=[None, _('Descriptive name of the covered areas and technologies deployed')])
-    latitude = DecimalField(_('latitude'), validators=[Optional(), NumberRange(min=-90, max=90)],
-                            description=[None, _('Coordinates of your registered office or usual meeting location. '
+                              description=[None, _(u'Descriptive name of the covered areas and technologies deployed')])
+    latitude = DecimalField(_(u'latitude'), validators=[Optional(), NumberRange(min=-90, max=90)],
+                            description=[None, _(u'Coordinates of your registered office or usual meeting location. '
                             '<strong>Required in order to appear on the map.</strong>')])
-    longitude = DecimalField(_('longitude'), validators=[Optional(), NumberRange(min=-180, max=180)])
-    step = SelectField(_('progress step'), choices=STEP_CHOICES, coerce=int)
-    member_count = IntegerField(_('members'), validators=[Optional(), NumberRange(min=0)],
+    longitude = DecimalField(_(u'longitude'), validators=[Optional(), NumberRange(min=-180, max=180)])
+    step = SelectField(_(u'progress step'), choices=STEP_CHOICES, coerce=int)
+    member_count = IntegerField(_(u'members'), validators=[Optional(), NumberRange(min=0)],
                                 description=[None, _('Number of members')])
-    subscriber_count = IntegerField(_('subscribers'), validators=[Optional(), NumberRange(min=0)],
+    subscriber_count = IntegerField(_(u'subscribers'), validators=[Optional(), NumberRange(min=0)],
                                     description=[None, _('Number of subscribers to an internet access')])
 
     tech_email = TextField(_('Email'), validators=[Email(), DataRequired()], description=[None,
@@ -173,20 +173,20 @@ class ProjectForm(Form):
     def validate(self, *args, **kwargs):
         r = super(ProjectForm, self).validate(*args, **kwargs)
         if (self.latitude.data is None) != (self.longitude.data is None):
-            self._fields['longitude'].errors += [_('You must fill both fields')]
+            self._fields['longitude'].errors += [_(u'You must fill both fields')]
             r = False
         return r
 
     def validate_covered_areas(self, field):
-        if len([e for e in field.data if e['name']]) == 0:
+        if len(filter(lambda e: e['name'], field.data)) == 0:
             # not printed, whatever..
-            raise ValidationError(_('You must specify at least one area'))
+            raise ValidationError(_(u'You must specify at least one area'))
 
         geojson_size = sum([len(ca.area.raw_data[0]) for ca in self.covered_areas if ca.area.raw_data])
         max_size = current_app.config['ISP_FORM_GEOJSON_MAX_SIZE_TOTAL']
         if geojson_size > max_size:
             # TODO: XXX This is not printed !
-            raise ValidationError(gettext('The size of all GeoJSON data combined must not exceed %(max_size)s',
+            raise ValidationError(gettext(u'The size of all GeoJSON data combined must not exceed %(max_size)s',
                                           max_size=filesize_fmt(max_size)))
 
     def to_json(self, json=None):
@@ -222,7 +222,7 @@ class ProjectForm(Form):
         optstr('progressStatus', self.step.data)
         optstr('memberCount', self.member_count.data)
         optstr('subscriberCount', self.subscriber_count.data)
-        optlist('chatrooms', list(filter(bool, self.chatrooms.data)))  # remove empty strings
+        optlist('chatrooms', filter(bool, self.chatrooms.data))  # remove empty strings
         optstr('coordinates', {'latitude': self.latitude.data, 'longitude': self.longitude.data}
               if self.latitude.data else {})
         optlist('coveredAreas', list(transform_covered_areas(self.covered_areas.data)))
@@ -256,7 +256,7 @@ class ProjectForm(Form):
             set_attr('latitude', d=json['coordinates'])
             set_attr('longitude', d=json['coordinates'])
         if 'otherWebsites' in json:
-            setattr(obj, 'other_websites', [{'name': n, 'url': w} for n, w in json['otherWebsites'].items()])
+            setattr(obj, 'other_websites', [{'name': n, 'url': w} for n, w in json['otherWebsites'].iteritems()])
         set_attr('covered_areas', 'coveredAreas')
         obj.tech_email = isp.tech_email
         return cls(obj=obj)
@@ -265,53 +265,53 @@ class ProjectForm(Form):
 class URLField(TextField):
 
     def _value(self):
-        if isinstance(self.data, str):
+        if isinstance(self.data, basestring):
             return self.data
         elif self.data is None:
             return ''
         else:
-            return urllib.parse.urlunsplit(self.data)
+            return urlparse.urlunsplit(self.data)
 
     def process_formdata(self, valuelist):
         if valuelist:
             try:
-                self.data = urllib.parse.urlsplit(valuelist[0])
+                self.data = urlparse.urlsplit(valuelist[0])
             except:
                 self.data = None
-                raise ValidationError(_('Invalid URL'))
+                raise ValidationError(_(u'Invalid URL'))
 
 
 def is_url_unique(url):
-    if isinstance(url, str):
-        url = urllib.parse.urlsplit(url)
+    if isinstance(url, basestring):
+        url = urlparse.urlsplit(url)
     t = list(url)
     t[2] = ''
-    u1 = urllib.parse.urlunsplit(t)
+    u1 = urlparse.urlunsplit(t)
     t[0] = 'http' if t[0] == 'https' else 'https'
-    u2 = urllib.parse.urlunsplit(t)
+    u2 = urlparse.urlunsplit(t)
     if ISP.query.filter(ISP.json_url.startswith(u1) | ISP.json_url.startswith(u2)).count() > 0:
         return False
     return True
 
 
 class ProjectJSONForm(Form):
-    json_url = URLField(_('base url'), description=[_('E.g. https://isp.com/'),
-                            _('A ressource implementing our JSON-Schema specification ' +
+    json_url = URLField(_(u'base url'), description=[_(u'E.g. https://isp.com/'),
+                            _(u'A ressource implementing our JSON-Schema specification ' +
                        'must exist at path /isp.json')])
-    tech_email = TextField(_('Email'), validators=[Email()], description=[None,
-                           _('Technical contact, in case of problems')])
+    tech_email = TextField(_(u'Email'), validators=[Email()], description=[None,
+                           _(u'Technical contact, in case of problems')])
 
     def validate_json_url(self, field):
         if not field.data.netloc:
-            raise ValidationError(_('Invalid URL'))
+            raise ValidationError(_(u'Invalid URL'))
 
         if field.data.scheme not in ('http', 'https'):
-            raise ValidationError(_('Invalid URL (must be HTTP(S))'))
+            raise ValidationError(_(u'Invalid URL (must be HTTP(S))'))
 
         if not field.object_data and not is_url_unique(field.data):
-            raise ValidationError(_('This URL is already in our database'))
+            raise ValidationError(_(u'This URL is already in our database'))
 
 
 class RequestEditToken(Form):
-    tech_email = TextField(_('Tech Email'), validators=[Email()], description=[None,
-                           _('The Technical contact you provided while registering')])
+    tech_email = TextField(_(u'Tech Email'), validators=[Email()], description=[None,
+                           _(u'The Technical contact you provided while registering')])

+ 32 - 35
ffdnispdb/models.py

@@ -13,7 +13,7 @@ import flask_sqlalchemy
 from sqlalchemy.types import TypeDecorator, VARCHAR, DateTime
 from sqlalchemy.ext.mutable import MutableDict
 from sqlalchemy import event
-import geoalchemy2 as geo
+import geoalchemy as geo
 import whoosh
 from whoosh import fields, index, qparser
 
@@ -90,12 +90,6 @@ class ISP(db.Model):
         self.json = {}
 
     def pre_save(self, *args):
-        if 'name' in self.json:
-            assert self.name == self.json['name']
-
-        if 'shortname' in self.json:
-            assert self.shortname == self.json['shortname']
-
         if db.inspect(self).attrs.json.history.has_changes():
             self._sync_covered_areas()
 
@@ -133,9 +127,9 @@ class ISP(db.Model):
     @property
     def complete_name(self):
         if 'shortname' in self.json:
-            return '%s (%s)' % (self.json['shortname'], self.json['name'])
+            return u'%s (%s)' % (self.json['shortname'], self.json['name'])
         else:
-            return '%s' % self.json['name']
+            return u'%s' % self.json['name']
 
     @staticmethod
     def str2date(_str):
@@ -160,7 +154,7 @@ class ISP(db.Model):
         return False
 
     def __repr__(self):
-        return '<ISP %r>' % (self.shortname if self.shortname else self.name,)
+        return u'<ISP %r>' % (self.shortname if self.shortname else self.name,)
 
 
 class CoveredArea(db.Model):
@@ -168,7 +162,7 @@ class CoveredArea(db.Model):
     id = db.Column(db.Integer, primary_key=True)
     isp_id = db.Column(db.Integer, db.ForeignKey('isp.id'))
     name = db.Column(db.String)
-    area = db.Column(geo.Geometry('MULTIPOLYGON', management=True))
+    area = geo.GeometryColumn(geo.MultiPolygon(2))
     area_geojson = db.column_property(db.func.AsGeoJSON(db.literal_column('area')), deferred=True)
 
     @classmethod
@@ -182,18 +176,18 @@ class CoveredArea(db.Model):
         )
 
     def __repr__(self):
-        return '<CoveredArea %r>' % (self.name,)
+        return u'<CoveredArea %r>' % (self.name,)
 
-# geo.GeometryDDL(CoveredArea.__table__)
+geo.GeometryDDL(CoveredArea.__table__)
 
 
 class RegisteredOffice(db.Model):
     __tablename__ = 'registered_offices'
     id = db.Column(db.Integer, primary_key=True)
     isp_id = db.Column(db.Integer, db.ForeignKey('isp.id'))
-    point = geo.Geometry('POINT')
+    point = geo.GeometryColumn(geo.Point(0))
 
-# geo.GeometryDDL(RegisteredOffice.__table__)
+geo.GeometryDDL(RegisteredOffice.__table__)
 
 
 @event.listens_for(db.metadata, 'before_create')
@@ -257,44 +251,47 @@ class ISPWhoosh(object):
             if not len(ranks):
                 return []
 
-            _res = ISP.query.filter(ISP.id.in_(list(ranks.keys())))
+            _res = ISP.query.filter(ISP.id.in_(ranks.keys()))
 
         return sorted(_res, key=lambda r: ranks[r.id])
 
     @classmethod
     def update_document(cls, writer, model):
         kw = {
-            'id': str(model.id),
+            'id': unicode(model.id),
             '_stored_id': model.id,
             'is_ffdn_member': model.is_ffdn_member,
             'is_disabled': model.is_disabled,
             'name': model.name,
             'shortname': model.shortname,
             'description': model.json.get('description'),
-            'covered_areas': ','.join(model.covered_areas_names()),
+            'covered_areas': unicode(','.join(model.covered_areas_names())),
             'step': model.json.get('progressStatus')
         }
         writer.update_document(**kw)
 
     @classmethod
     def _after_flush(cls, app, changes):
-        isp_changes = []
-        for change in changes:
-            if change[0].__class__ == ISP:
-                update = change[1] in ('update', 'insert')
-                isp_changes.append((update, change[0]))
-
-        if not len(changes):
-            return
-
-        idx = cls.get_index()
-        with idx.writer() as writer:
-            for update, model in isp_changes:
-                if update:
-                    cls.update_document(writer, model)
-                else:
-                    writer.delete_by_term(cls.primary_key, model.id)
+        try:
+            isp_changes = []
+            for change in changes:
+                if change[0].__class__ == ISP:
+                    update = change[1] in ('update', 'insert')
+                    isp_changes.append((update, change[0]))
+
+            if not len(changes):
+                return
+
+            idx = cls.get_index()
+            with idx.writer() as writer:
+                for update, model in isp_changes:
+                    if update:
+                        cls.update_document(writer, model)
+                    else:
+                        writer.delete_by_term(cls.primary_key, model.id)
+        except Exception as e:
+            print("Error while updating woosh db. Cause: {}".format(e))
 
 
 flask_sqlalchemy.models_committed.connect(ISPWhoosh._after_flush)
-event.listen(flask_sqlalchemy.SignallingSession, 'before_commit', pre_save_hook)
+event.listen(flask_sqlalchemy.Session, 'before_commit', pre_save_hook)

+ 4 - 4
ffdnispdb/sessions.py

@@ -9,7 +9,7 @@ from sqlalchemy import Table, Column, String, LargeBinary, DateTime,\
 from random import SystemRandom, randrange
 import string
 from datetime import datetime, timedelta
-import pickle
+import cPickle
 
 random = SystemRandom()
 
@@ -32,7 +32,7 @@ class SQLSession(CallbackDict, SessionMixin):
             self.db.execute(self.table.insert({
                 'session_id': self.sid,
                 'expire': datetime.utcnow() + timedelta(hours=1),
-                'value': pickle.dumps(dict(self), -1)
+                'value': cPickle.dumps(dict(self), -1)
             }))
             self.new = False
         else:
@@ -40,7 +40,7 @@ class SQLSession(CallbackDict, SessionMixin):
                 self.table.c.session_id == self.sid,
                 {
                     'expire': datetime.utcnow() + timedelta(hours=1),
-                    'value': pickle.dumps(dict(self), -1)
+                    'value': cPickle.dumps(dict(self), -1)
                 }
             ))
 
@@ -62,7 +62,7 @@ class MySessionInterface(SessionInterface):
             res = self.db.engine.execute(select([self.table.c.value], (self.table.c.session_id == sid) &
                                                 (self.table.c.expire > datetime.utcnow()))).first()
             if res:
-                return SQLSession(sid, self.db.engine, self.table, False, pickle.loads(res[0]))
+                return SQLSession(sid, self.db.engine, self.table, False, cPickle.loads(res[0]))
 
         while True:
             sid = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(32))

+ 1 - 1
ffdnispdb/templates/project_detail.html

@@ -42,7 +42,7 @@
         {%- endif %}
         {%- if project.otherWebsites %}
         {{ field(_("other websites")) }}
-          {% for n, w in project.otherWebsites.items() -%}
+          {% for n, w in project.otherWebsites.iteritems() -%}
           <dd>{{ n }}: <a href="{{ w }}">{{ w }}</a></dd>
           {%- endfor -%}
         {%- endif %}

+ 7 - 0
ffdnispdb/templates/project_form_generic.html

@@ -1,6 +1,13 @@
 {% extends "layout.html" %}
 {% import "form_macros.html" as fm %}
 {% block container %}
+
+{% if isp %}
+<ul class="nav nav-tabs">
+  <li role="presentation" class="active"><a href="#">{{ _("Manual updates") }}</a></li>
+  <li role="presentation"><a href="{{ url_for('.edit_project_auto_update', projectid=isp.id) }}">{{ _("Automatic updates (isp.json)") }}</a></li>
+</ul>
+{% endif %}
 <div class="row">
   <div class="span11 well">
     <form method="post" class="form-horizontal">

+ 8 - 1
ffdnispdb/templates/project_json_form_generic.html

@@ -1,14 +1,21 @@
 {% extends "layout.html" %}
 {% import "form_macros.html" as fm %}
 {% block container %}
+
+{% if isp %}
+<ul class="nav nav-tabs">
+  <li role="presentation"><a href="{{ url_for('.edit_project', projectid=isp.id) }}">{{ _("Manual updates") }}</a></li>
+  <li role="presentation" class="active"><a href="#">{{ _("Automatic updates (isp.json)") }}</a></li>
+</ul>
+{% endif %}
 <div class="row">
   <div class="span11 well">
     <form method="post" class="form-horizontal">
       {{ form.csrf_token }}
       <fieldset>
         <legend>{{ page_title }}</legend>
-        {{ fm.render_field(form.json_url) }}
         {{ fm.render_field(form.tech_email) }}
+        {{ fm.render_field(form.json_url) }}
         <div class="form-actions">
           <input type="submit" class="btn btn-primary" value="{{ _("Next") }}" />
           <input type="reset" class="btn" value="{{ _("Cancel") }}" />

BIN
ffdnispdb/translations/fr/LC_MESSAGES/messages.mo


+ 10 - 0
ffdnispdb/translations/fr/LC_MESSAGES/messages.po

@@ -382,6 +382,16 @@ msgstr ""
 msgid "That JSON thing is too cool for me"
 msgstr "Le JSON c'est trop fort pour moi"
 
+#: ffdnispdb/templates/project_form_generic.html:7
+#: ffdnispdb/templates/project_json_form_generic.html:7
+msgid "Manual updates"
+msgstr "Mises à jour manuelles"
+
+#: ffdnispdb/templates/project_form_generic.html:8
+#: ffdnispdb/templates/project_json_form_generic.html:8
+msgid "Automatic updates (isp.json)"
+msgstr "Mises à jour automatiques (isp.json)"
+
 #: ffdnispdb/templates/add_project_form.html:1
 msgid "Add a new project"
 msgstr "Ajouter un nouveau projet"

+ 3 - 1
ffdnispdb/utils.py

@@ -4,7 +4,7 @@ from flask import current_app
 from flask.globals import _request_ctx_stack
 from collections import OrderedDict
 from datetime import datetime
-from urllib.parse import urlunsplit
+from urlparse import urlunsplit
 import pytz
 import json
 import sys
@@ -59,6 +59,8 @@ def tosystemtz(d):
     """
     Convert the UTC datetime ``d`` to the system time zone defined in the settings
     """
+    if d is None:
+        return 'None'
     return d.astimezone(pytz.timezone(current_app.config['SYSTEM_TIME_ZONE']))
 
 

+ 61 - 36
ffdnispdb/views.py

@@ -3,8 +3,8 @@
 from flask import request, redirect, url_for, abort, \
     render_template, flash, json, session, Response, Markup, \
     current_app, Blueprint
-from flask_babel import gettext as _, get_locale
-from flask_mail import Message
+from flask.ext.babel import gettext as _, get_locale
+from flask.ext.mail import Message
 from sqlalchemy.sql import func, asc
 import itsdangerous
 import docutils.core
@@ -58,7 +58,7 @@ def isp_map_data():
     data = []
     for isp in isps:
         d = dict(isp.json)
-        for k in list(d.keys()):
+        for k in d.keys():
             if k not in ('name', 'shortname', 'coordinates'):
                 del d[k]
 
@@ -77,7 +77,7 @@ def isp_map_data_cube():
     data = []
     for isp in isps:
         d = dict(isp.json)
-        for k in list(d.keys()):
+        for k in d.keys():
             if k not in ('name', 'shortname', 'coordinates'):
                 del d[k]
 
@@ -178,32 +178,57 @@ def edit_project(projectid):
     elif (sess_token is None or (datetime.utcnow() - sess_token).total_seconds() > MAX_TOKEN_AGE):
         return redirect(url_for('.gen_edit_token', projectid=isp.id))
 
-    if isp.is_local:
-        form = forms.ProjectForm.edit_json(isp)
-        if form.validate_on_submit():
-            isp.name = form.name.data
-            isp.shortname = form.shortname.data or None
-            isp.json = form.to_json(isp.json)
-            isp.tech_email = form.tech_email.data
-
-            db.session.add(isp)
-            db.session.commit()
-            flash(_('Project modified'), 'info')
-            return redirect(url_for('.project', projectid=isp.id))
-        return render_template('edit_project_form.html', form=form)
-    else:
-        form = forms.ProjectJSONForm(obj=isp)
-        if form.validate_on_submit():
-            isp.tech_email = form.tech_email.data
-            url = utils.make_ispjson_url(form.json_url.data)
-            isp.json_url = url
+    form = forms.ProjectForm.edit_json(isp)
+    if form.validate_on_submit():
+        isp.name = form.name.data
+        isp.shortname = form.shortname.data or None
+        isp.json = form.to_json(isp.json)
+        isp.tech_email = form.tech_email.data
+        isp.json_url = None
+
+        db.session.add(isp)
+        db.session.commit()
+        flash(_(u'Project modified'), 'info')
+        return redirect(url_for('.project', projectid=isp.id))
+    return render_template('edit_project_form.html', form=form, isp=isp)
+
+
+@ispdb.route('/isp/<projectid>/edit_json_url', methods=['GET', 'POST'])
+def edit_project_auto_update(projectid):
+    MAX_TOKEN_AGE = 3600
+    isp = ISP.query.filter_by(id=projectid, is_disabled=False).first_or_404()
+    sess_token = session.get('edit_tokens', {}).get(isp.id)
+
+    if 'token' in request.args:
+        s = itsdangerous.URLSafeTimedSerializer(current_app.secret_key, salt='edit')
+        try:
+            r = s.loads(request.args['token'], max_age=MAX_TOKEN_AGE,
+                        return_timestamp=True)
+        except:
+            abort(403)
+
+        if r[0] != isp.id:
+            abort(403)
+
+        tokens = session.setdefault('edit_tokens', {})
+        session.modified = True  # ITS A TARP
+        tokens[r[0]] = r[1]
+        # refresh page, without the token in the url
+        return redirect(url_for('.edit_project', projectid=r[0]))
+    elif (sess_token is None or (datetime.utcnow() - sess_token).total_seconds() > MAX_TOKEN_AGE):
+        return redirect(url_for('.gen_edit_token', projectid=isp.id))
 
-            db.session.add(isp)
-            db.session.commit()
-            flash(_('Project modified'), 'info')
-            return redirect(url_for('.project', projectid=isp.id))
-        return render_template('edit_project_json_form.html', form=form)
+    form = forms.ProjectJSONForm(obj=isp)
+    if form.validate_on_submit():
+        isp.tech_email = form.tech_email.data
+        url = utils.make_ispjson_url(form.json_url.data)
+        isp.json_url = url
 
+        db.session.add(isp)
+        db.session.commit()
+        flash(_(u'Project modified'), 'info')
+        return redirect(url_for('.project', projectid=isp.id))
+    return render_template('edit_project_json_form.html', form=form, isp=isp)
 
 @ispdb.route('/isp/<projectid>/gen_edit_token', methods=['GET', 'POST'])
 def gen_edit_token(projectid):
@@ -235,7 +260,7 @@ https://db.ffdn.org
             mail.send(msg)
 
         # if the email provided is not the correct one, we still redirect
-        flash(_('If you provided the correct email adress, '
+        flash(_(u'If you provided the correct email adress, '
                 'you must will receive a message shortly (check your spam folder)'), 'info')
         return redirect(url_for('.project', projectid=isp.id))
 
@@ -259,7 +284,7 @@ def create_project_form():
 
         db.session.add(isp)
         db.session.commit()
-        flash(_('Project created'), 'info')
+        flash(_(u'Project created'), 'info')
         return redirect(url_for('.project', projectid=isp.id))
     return render_template('add_project_form.html', form=form)
 
@@ -315,7 +340,7 @@ def create_project_json_confirm():
 
         db.session.add(isp)
         db.session.commit()
-        flash(_('Project created'), 'info')
+        flash(_(u'Project created'), 'info')
         return redirect(url_for('.project', projectid=isp.id))
     else:
         return redirect(url_for('.create_project_json'))
@@ -382,7 +407,7 @@ def reactivate_isp(projectid):
         db.session.add(p)
         db.session.commit()
 
-        flash(_('Automatic updates activated'), 'info')
+        flash(_(u'Automatic updates activated'), 'info')
         return redirect(url_for('.project', projectid=p.id))
 
 
@@ -468,7 +493,7 @@ def locale_selector():
         return resp
 
     return render_template('locale_selector.html', locales=(
-        (code, LOCALES_FLAGS[code], name) for code, name in l.items()
+        (code, LOCALES_FLAGS[code], name) for code, name in l.iteritems()
     ))
 
 
@@ -478,9 +503,9 @@ def locale_selector():
 @ispdb.app_template_filter('step_to_label')
 def step_to_label(step):
     if step:
-        return "<a href='#' data-toggle='tooltip' data-placement='right' title='" + STEPS[step] + "'><span class='badge badge-" + STEPS_LABELS[step] + "'>" + str(step) + "</span></a>"
+        return u"<a href='#' data-toggle='tooltip' data-placement='right' title='" + STEPS[step] + "'><span class='badge badge-" + STEPS_LABELS[step] + "'>" + str(step) + "</span></a>"
     else:
-        return '-'
+        return u'-'
 
 
 @ispdb.app_template_filter('stepname')
@@ -490,7 +515,7 @@ def stepname(step):
 
 @ispdb.app_template_filter('js_str')
 def json_filter(v):
-    return Markup(json.dumps(str(v)))
+    return Markup(json.dumps(unicode(v)))
 
 
 @ispdb.app_template_filter('locale_flag')

+ 5 - 3
ffdnispdb/views_api.py

@@ -76,10 +76,10 @@ class RESTException(Exception):
         self.error_type = error_type
 
     def __iter__(self):
-        return iter({
+        return {
             'error_type': self.error_type,
             'message': self.message
-        }.items())
+        }.iteritems()
 
     def __json__(self):
         return {
@@ -112,6 +112,8 @@ class Resource(MethodView, REST):
 
     def dispatch_request(self, *args, **kwargs):
         meth = getattr(self, request.method.lower(), None)
+        if not meth:
+            return self.negociated_resp(None, 405, None) # 405 Method not allowed
         resp = meth(*args, **kwargs)
         if isinstance(resp, Response):
             return resp
@@ -130,7 +132,7 @@ class Resource(MethodView, REST):
         if not range_:
             return None
         try:
-            range_ = list(map(int, [_f for _f in range_.split(',', 1) if _f]))
+            range_ = map(int, filter(None, range_.split(',', 1)))
             return range_
         except ValueError:
             return None

+ 4 - 4
manage.py

@@ -8,7 +8,7 @@ from werkzeug.debug import DebuggedApplication
 
 import os; os.environ.setdefault('FFDNISPDB_SETTINGS', '../settings_dev.py')
 import sys
-from flask_script import (Shell, Server, Manager, Command, Option,
+from flask.ext.script import (Shell, Server, Manager, Command, Option,
                               prompt, prompt_bool, prompt_pass)
 import ffdnispdb
 
@@ -23,7 +23,7 @@ class CreateDB(Command):
     def run(self):
         ffdnispdb.db.create_all()
 
-database_manager = Manager(usage='Perform database operations')
+database_manager = Manager(usage=u'Perform database operations')
 database_manager.add_command("create", CreateDB)
 
 @database_manager.command
@@ -33,7 +33,7 @@ def drop():
         ffdnispdb.db.drop_all()
 
 
-index_manager = Manager(usage='Manage the Whoosh index')
+index_manager = Manager(usage=u'Manage the Whoosh index')
 
 @index_manager.command
 def rebuild():
@@ -56,7 +56,7 @@ class MyServer(Server):
     def handle(self, app, host, port, use_debugger, use_reloader,
                threaded, processes, passthrough_errors):
         if os.environ.get('WERKZEUG_RUN_MAIN') != 'true':
-            print(' * Running on http://%s:%d/'%(host, port), file=sys.stderr)
+            print >>sys.stderr, ' * Running on http://%s:%d/'%(host, port)
 
         if use_debugger:
             app=DebuggedApplication(app, evalex=True)

+ 22 - 22
requirements.txt

@@ -1,28 +1,28 @@
-SQLAlchemy>0.8.4
-Flask>0.10.1
-Flask-Babel>0.9
-Flask-SQLAlchemy>=1.0
-Flask-WTF>0.9.4
-Flask-Mail>0.9.0
-Flask-Caching>1.3
-wtforms>2.0
-Jinja2>2.7.1
-MarkupSafe>0.18
-Werkzeug>0.9.4
-argparse>1.2.1
-itsdangerous>0.23
-blinker>1.3
-GeoAlchemy2>=0.4.0
-gevent>=1.0.2,<1.2
-jsonschema>=2.3.0
-requests>=2.1.0
+SQLAlchemy==0.8.4
+Flask==0.10.1
+Flask-Babel==0.9
+Flask-SQLAlchemy==1.0
+Flask-WTF==0.9.4
+Flask-Mail==0.9.0
+Flask-Cache==0.12
+Jinja2==2.7.1
+MarkupSafe==0.18
+Werkzeug==0.9.4
+argparse==1.2.1
+itsdangerous==0.23
+wsgiref==0.1.2
+blinker==1.3
+GeoAlchemy==0.7.2
+gevent==1.0.2
+jsonschema==2.3.0
+requests==2.6.0
 # ndg & pyasn required for SNI
 -e git+https://code.ffdn.org/FFDN/ndg_httpsclient.git#egg=ndg-httpsclient
-pyasn1>0.1.7
-docutils>0.11
+pyasn1==0.1.7
+docutils==0.11
 # full-text search
-Whoosh>2.5.6
+Whoosh==2.5.6
 # manage.py
-Flask-Script>0.6.6
+Flask-Script==0.6.6
 # isp format
 -e git+https://code.ffdn.org/ffdn/isp-format.git#egg=isp-format

+ 0 - 1
settings_dev.py

@@ -5,4 +5,3 @@ EMAIL_SENDER='FFDN DB <no-reply@db.ffdn.org>'
 #SERVER_NAME = 'db.ffdn.org'
 DEBUG = True
 SECRET_KEY = '{J@uRKO,xO-PK7B,jF?>iHbxLasF9s#zjOoy=+:'
-SQLALCHEMY_TRACK_MODIFICATIONS = True

+ 0 - 1
settings_prod.py.dist

@@ -6,4 +6,3 @@ EMAIL_SENDER='FFDN DB <no-reply@db.ffdn.org>'
 DEBUG = False
 SECRET_KEY = None # Generate one
 ADMINS = ('your@email.com',)
-SQLALCHEMY_TRACK_MODIFICATIONS = True

+ 2 - 2
test_ffdnispdb.py

@@ -2,7 +2,7 @@
 from ffdnispdb import create_app, db, utils
 from ffdnispdb.models import ISP, CoveredArea
 from flask import Flask
-from flask_sqlalchemy import SQLAlchemy
+from flask.ext.sqlalchemy import SQLAlchemy
 import unittest
 import doctest
 import json
@@ -180,7 +180,7 @@ class TestAPI(TestCase):
         db_urls = [u[0] for u in db_urls]
         c = self.client.get('/api/v1/isp/export_urls/')
         self.assertStatus(c, 200)
-        api_urls = [x['json_url'] for x in json.loads(c.data)['isps']]
+        api_urls = map(lambda x: x['json_url'], json.loads(c.data)['isps'])
         self.assertEqual(len(api_urls), len(db_urls))
         for au in api_urls:
             self.assertIn(au, db_urls)