plint

French poetry validator
git clone https://a3nm.net/git/plint/
Log | Files | Refs | README

plint_web.py (6518B)


      1 #!/usr/bin/python3 -Ou
      2 #encoding: utf8
      3 
      4 import localization
      5 import re
      6 import template
      7 import error
      8 import diaeresis
      9 from bottle import run, Bottle, request, static_file, redirect, response
     10 from jinja2 import Environment, PackageLoader
     11 from json import dumps
     12 import time
     13 
     14 env = Environment(loader=PackageLoader('plint_web', 'views'))
     15 
     16 # force HTTPS usage
     17 # http://bottlepy.org/docs/dev/faq.html#problems-with-reverse-proxies
     18 # because bottle makes absolute redirects
     19 # https://github.com/bottlepy/bottle/blob/9fe68c89e465004a5e6babed0955bc1eeba88002/bottle.py#L2637
     20 # even though relative Location: is now allowed
     21 # http://stackoverflow.com/a/25643550
     22 def fix_https(app):
     23   def fixed_app(environ, start_response):
     24     environ['wsgi.url_scheme'] = 'https'
     25     return app(environ, start_response)
     26   return fixed_app
     27 app = Bottle()
     28 app.wsgi = fix_https(app.wsgi)
     29 
     30 THROTTLE_DELAY = 2
     31 throttle = set()
     32 
     33 def best_match(matches, header):
     34   # inspired by http://www.xml.com/pub/a/2005/06/08/restful.html
     35 
     36   def parse_one(t):
     37     parts = t.split(";")
     38     d = {}
     39     for param in parts[1:]:
     40         spl = param.split("=")
     41         if (len(spl) != 2):
     42             # this should be formatted as key=value
     43             # so ignore it
     44             continue
     45         k, v = spl
     46         d[k.strip().lower()] = v.strip()
     47     if 'q' not in d.keys():
     48       d['q'] = "1"
     49     return (parts[0], d)
     50 
     51   parts = []
     52   for p in header.split(","):
     53     parsed = parse_one(p)
     54     try:
     55         value = float(parsed[1]['q'])
     56     except ValueError:
     57         # q value should be a float; set it to 0
     58         value = 0
     59     parts.append((value, parsed[0].split("-")))
     60   for lang in [x[1] for x in sorted(parts, reverse=True)]:
     61     for match in matches:
     62       if match in lang:
     63         return match
     64   return matches[0]
     65 
     66 def get_locale():
     67   header = request.headers.get('Accept-Language')
     68   print(header)
     69   try:
     70     return best_match(['fr', 'en'], header)
     71   except AttributeError:
     72     return 'en'
     73 
     74 def get_title(lang):
     75   if lang == 'fr':
     76     return "plint -- vérification formelle de poèmes"
     77   else:
     78     return "plint -- French poetry checker"
     79 
     80 @app.route('/static/tpl/<filename>')
     81 def server_static(filename):
     82   return static_file(filename, root="./static/tpl", mimetype="text/plain")
     83 
     84 @app.route('/<lang>/static/img/<filename>')
     85 def server_static(filename, lang=None):
     86   return static_file(filename, root="./static/img")
     87 
     88 @app.route('/<lang>/static/tpl/<filename>')
     89 def server_static(filename, lang=None):
     90   return static_file(filename, root="./static/tpl", mimetype="text/plain")
     91 
     92 @app.route('/static/<filename>')
     93 def server_static(filename):
     94   return static_file(filename, root="./static")
     95 
     96 @app.route('/<lang>/static/<filename>')
     97 def server_static(filename, lang=None):
     98   return static_file(filename, root="./static")
     99 
    100 @app.route('/')
    101 def root():
    102   redirect('/' + get_locale() + '/')
    103 
    104 @app.route('/<page>')
    105 def paged(page):
    106   redirect('/' + get_locale() + '/' + page)
    107 
    108 @app.route('/<lang>/')
    109 def root(lang):
    110   if lang not in ['fr', 'en']:
    111     return paged(lang)
    112   return env.get_template('index.html').render(title=get_title(lang),
    113       lang=lang, path="")
    114 
    115 @app.route('/<lang>/about')
    116 def about(lang):
    117   return env.get_template('about.html').render(title=get_title(lang),
    118       lang=lang, path="about")
    119 
    120 MAX_POEM_LEN = 8192
    121 MAX_LINE_LEN = 512
    122 
    123 class TooBigException(Exception):
    124     pass
    125 
    126 class TooLongLinesException(Exception):
    127     pass
    128 
    129 def check(poem):
    130   if len(poem) > MAX_POEM_LEN:
    131     raise TooBigException
    132   s = poem.split("\n")
    133   for x in range(len(s)):
    134     if len(s[x]) > MAX_LINE_LEN:
    135       raise TooLongLinesException
    136     s[x] = s[x].strip()
    137   return s
    138 
    139 @app.route('/<lang>/checkjs', method='POST')
    140 def q(lang):
    141   global throttle
    142   # necessary when serving with lighttpd proxy-core
    143   ip = request.environ.get('HTTP_X_FORWARDED_FOR')
    144   if not ip:
    145     # fallback; this is 127.0.0.1 with proxy-core
    146     ip = request.environ.get('REMOTE_ADDR')
    147   t = time.time()
    148   print("== %s %s ==" % (ip, t))
    149   response.content_type = 'application/json'
    150   localization.init_locale(lang)
    151   throttle = set(x for x in throttle if t - x[1] < THROTTLE_DELAY)
    152   if ip in (x[0] for x in throttle):
    153     if lang == 'fr':
    154       msg = (("Trop de requêtes pour vérifier le poème,"
    155         + " veuillez réessayer dans %d secondes") %
    156           THROTTLE_DELAY)
    157     else:
    158       msg = (("Too many requests to check poem,"
    159         + " please try again in %d seconds") %
    160           THROTTLE_DELAY)
    161     return dumps({'error': msg})
    162   throttle.add((ip, t))
    163   poem = re.sub(r'<>&', '', request.forms.get('poem'))
    164   print(poem)
    165 
    166   # default message
    167   if lang == 'fr':
    168     msg = "Le poème est vide"
    169   else:
    170     msg = "Poem is empty"
    171   
    172   try:
    173       poem = check(poem)
    174   except TooBigException:
    175       poem = None
    176       if lang == 'fr':
    177           msg = "Le poème est trop long (maximum %d caractères)" % MAX_POEM_LEN
    178       else:
    179           msg = "Poem is too long (maximum %d characters)" % MAX_POEM_LEN
    180   except TooLongLinesException:
    181       poem = None
    182       if lang == 'fr':
    183           msg = "Certaines lignes du poème sont trop longues (maximum %d caractères)" % MAX_LINE_LEN
    184       else:
    185           msg = "Some lines of the poem are too long (maximum %d characters)" % MAX_LINE_LEN
    186   if not poem or len(poem) == 0 or (len(poem) == 1 and len(poem[0]) == 0):
    187       return dumps({'error': msg})
    188   templateName = re.sub(r'[^a-z_]', '', request.forms.get('template'))
    189   print(templateName)
    190   if templateName == 'custom':
    191     x = request.forms.get('custom_template')
    192   else:
    193     try:
    194       f = open("static/tpl/" + templateName + ".tpl")
    195       x = f.read()
    196       f.close()
    197     except IOError:
    198       if lang == 'fr':
    199         msg = "Modèle inexistant"
    200       else:
    201         msg = "No such template"
    202       return dumps({'error': msg})
    203   print(x)
    204   try:
    205     templ = template.Template(x)
    206   except error.TemplateLoadError as e:
    207     if lang == 'fr':
    208       msg = "Erreur à la lecture du modèle : " + e.msg
    209     else:
    210       msg = "Error when reading template: " + e.msg
    211     return dumps({'error': msg})
    212   poem.append(None)
    213   r = []
    214   i = 0
    215   d = {}
    216   for line in poem:
    217     i += 1
    218     last = False
    219     if line == None:
    220       line = ""
    221       last = True
    222     errors = templ.check(line, last=last)
    223     if errors:
    224       r.append({
    225         'line': line,
    226         'num': i,
    227         'errors': sum(errors.lines(short=True), [])
    228         })
    229   d['result'] = r
    230   return dumps(d)
    231 
    232 if __name__ == '__main__':
    233   diaeresis.load_diaeresis('diaeresis.json')
    234   run(app, port='5000', server="cherrypy", host="::")
    235