diff --git a/messages/en.json b/messages/en.json index 6aae01f..cae3278 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1,47 +1,56 @@ { "@metadata": { "authors": [ "Martin Urbanec" ] }, "prague": "Prague", "default-value": "default value", "enter-number-of-item": "Wikidata item number. Objects will be searched around this item.", "enter-coordinates": "Search for objects with coordinates", "enter-name-of-article": "Search for objects around coordinates of this article", "enter-radius": "Set a radius for searching (km)", "select-displayed": "Choose objects that should be displayed on a map", "search": "Search", "photographed": "Photographed", "unphotographed": "Unphotographed", "report-problem": "Report problem", "source-code": "Source code", "your-email": "Your email", "problem-summary": "Summary of the problem", "problem-body": "Tell us more details about your problem", "submit": "Submit", "stats-before": "This tool has generated", "stats-after": "maps already", "no-coordinates": "We were not able to find coordinates from info you submitted.", "change-language": "Change language", "admin-link": "Admin", "admin-interface": "Admin interface", "admin-description": "Please choose action: ", "admin-variables": "List of variables", "and": "and", "admin-description-latlon": "Contains coordinates, only used when searching via coordinates", "admin-description-item": "Contains item ID (Q is included), only used when searching via article name or item number", "admin-description-radius": "Contains radius, always used", "admin-success": "Your request to change queries was successfully completed.", "admin-edit-layers": "Edit layers", + "admin-edit-layer": "Edit layer", + "admin-add-layer": "Add layer", "admin-layers": "Layers", + "admin-edit-users": "Edit users", + "admin-edit-user": "Edit user", + "admin-users": "Users", + "administrator": "administrator", + "inactive": "inactive", + "admin-user-isactive": "Is active?", + "admin-user-isadmin": "Is admin?", "welcome": "Welcome", "login": "Login", "logout": "Logout", "permission-denied": "Permission denied. If you believe this happened in error, contact an administrator.", "new": "new", "admin-layer-name": "Name", "admin-layer-color": "Color", "admin-layer-definition": "Definition", "processing": "Processing..." } diff --git a/src/app.py b/src/app.py index 7401678..f840a4c 100644 --- a/src/app.py +++ b/src/app.py @@ -1,306 +1,326 @@ # -*- coding: utf-8 -*- # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . import flask import os import yaml import simplejson as json from flask import redirect, request, jsonify, make_response, render_template, session, url_for from flask import Flask import requests import urllib.parse import toolforge import pymysql import mwoauth from flask_jsonlocale import Locales from SPARQLWrapper import SPARQLWrapper, JSON app = Flask(__name__) # Load configuration from YAML file __dir__ = os.path.dirname(__file__) app.config.update( yaml.safe_load(open(os.path.join(__dir__, 'config.yaml')))) locales = Locales(app) stats_filename = app.config.get('STATS_COUNTER_FILE', '/tmp/wikinity-stats.txt') QUERY_TYPES = [ "coordinate", "item", "photographed", "unphotographed", "all", "end", ] @app.before_request def force_https(): if request.headers.get('X-Forwarded-Proto') == 'http': return redirect( 'https://' + request.headers['Host'] + request.headers['X-Original-URI'], code=301 ) @app.before_request def db_check_language_permissions(): if logged(): conn = connect() with conn.cursor() as cur: cur.execute('SELECT id, is_active, language FROM users WHERE username=%s', getusername()) data = cur.fetchall() if len(data) == 0: with conn.cursor() as cur: cur.execute('INSERT INTO users(username, language) VALUES (%s, %s)', (getusername(), locales.get_locale())) conn.commit() else: if data[0][1] == 1: locales.set_locale(data[0][2]) else: return render_template('permission_denied.html') @app.context_processor def inject_base_variables(): return { "logged": logged(), "username": getusername(), "admin": isadmin() } def connect(): if app.config.get('DB_CONF'): return pymysql.connect( database=app.config.get('DB_NAME'), host=app.config.get('DB_HOST'), read_default_file=app.config.get('DB_CONF') ) else: return pymysql.connect( database=app.config.get('DB_NAME'), host=app.config.get('DB_HOST'), user=app.config.get('DB_USER'), password=app.config.get('DB_PASS') ) def logged(): return flask.session.get('username') != None def getusername(): return flask.session.get('username') def isadmin(): if logged(): conn = connect() with conn.cursor() as cur: cur.execute('SELECT username FROM users WHERE is_active=1 AND is_admin=1 AND username=%s', getusername()) return len(cur.fetchall()) == 1 else: return False @app.route('/') def index(): return render_template('index.html') @app.route('/change_language', methods=['GET', 'POST']) def change_language(): if request.method == 'GET': return render_template('change_language.html', locales=locales.get_locales(), permanent_locale=locales.get_permanent_locale()) else: if logged(): conn = connect() with conn.cursor() as cur: cur.execute('UPDATE users SET language=%s WHERE username=%s', (request.form.get('locale', 'en'), getusername())) conn.commit() locales.set_locale(request.form.get('locale')) return redirect(url_for('index')) @app.route('/stats') def stats(): if os.path.isfile(stats_filename): return open(stats_filename).read() return '0' @app.route('/map') def map(): try: stats_num = str(int(open(stats_filename).read())+1) open(stats_filename, 'w').write(stats_num) except: open(stats_filename, 'w').write('1') typ = request.args.get('type', 'item') subtype = request.args.get('subtype', 'unphotographed') radius = int(request.args.get('radius') or 5) if typ == "coordinate": lat = request.args.get('lat') or '50.0385383' lon = request.args.get('lon') or '15.7802056' else: if typ == "article": typ = "item" article = request.args.get('article') or 'Praha' project = request.args.get('project') or 'cswiki' r = requests.get('https://www.wikidata.org/w/api.php', params={ "action": "wbgetentities", "format": "json", "sites": project, "titles": article }) item = list(r.json()['entities'].keys())[0] if item == '-1': item = 'Q1085' else: item = request.args.get('item') or 'Q1085' r = requests.get('https://www.wikidata.org/w/api.php', params={ "action": "wbgetclaims", "format": "json", "entity": item }) data = r.json() coor = data["claims"]["P625"][0]["mainsnak"]["datavalue"]["value"] lat = coor["latitude"] lon = coor["longitude"] query = "\n".join((open('../queries/start-%s.txt' % typ).read(), get_layers_query(), open('../queries/where-%s.txt' % subtype).read(), open('../queries/end.txt').read())) if typ == "coor": query = query.replace('@@LAT@@', lat).replace('@@LON@@', lon).replace('@@RADIUS@@', str(radius)) else: query = query.replace('@@ITEM@@', item).replace('@@RADIUS@@', str(radius)) if 'onlyquery' in request.args: return query sparql = SPARQLWrapper("https://query.wikidata.org/sparql") sparql.setQuery(query) sparql.setReturnFormat(JSON) res = { "lat": lat, "lon": lon, "wikidata": sparql.query().convert() } return jsonify(res) def get_layers(): conn = connect() with conn.cursor() as cur: cur.execute('SELECT * FROM layers') return cur.fetchall() def get_layer(id): conn = connect() with conn.cursor() as cur: cur.execute('SELECT * FROM layers WHERE id=%s', id) return cur.fetchall()[0] def get_layers_query(): layers = get_layers() res = "" for layer in layers: res += """ OPTIONAL { %s BIND("%s" AS ?layer) BIND("%s" as ?rgb) } """ % (layer[2], layer[3], layer[1]) return res @app.route('/admin') def admin(): return render_template('admin/index.html') +@app.route('/admin/users') +def admin_users(): + conn = connect() + with conn.cursor() as cur: + cur.execute('SELECT * FROM users') + users = cur.fetchall() + return render_template('admin/users.html', users=users) + +@app.route('/admin/user/', methods=['GET', 'POST']) +def admin_user(id): + conn = connect() + if request.method == 'POST': + with conn.cursor() as cur: + cur.execute('UPDATE users SET is_active=%s, is_admin=%s WHERE id=%s', (int(request.form.get('isactive', 0)), int(request.form.get('isadmin', 0)), id)) + conn.commit() + with conn.cursor() as cur: + cur.execute('SELECT * FROM users WHERE id=%s', id) + user = cur.fetchall()[0] + return render_template('admin/user.html', user=user) + @app.route('/admin/layers') def admin_layers(): return render_template('admin/layers.html', layers=get_layers()) @app.route('/admin/layer/new', methods=['GET', 'POST']) def admin_layer_new(): if request.method == 'GET': return render_template('admin/layer.html') else: conn = connect() with conn.cursor() as cur: cur.execute('INSERT INTO layers(color, definition, name) VALUES(%s, %s, %s)', (request.form['color'], request.form['definition'], request.form['name'])) conn.commit() with conn.cursor() as cur: cur.execute('SELECT id FROM layers ORDER BY id DESC') return redirect(url_for('admin_layer', id=cur.fetchall()[0][0])) @app.route('/admin/layer/', methods=['GET', 'POST']) def admin_layer(id): if request.method == 'GET': return render_template('admin/layer.html', layer=get_layer(id)) else: conn = connect() with conn.cursor() as cur: cur.execute('UPDATE layers SET color=%s, definition=%s, name=%s WHERE id=%s', (request.form['color'], request.form['definition'], request.form['name'], id)) conn.commit() return render_template('admin/layer.html', layer=get_layer(id), success=True) @app.route('/login') def login(): """Initiate an OAuth login. Call the MediaWiki server to get request secrets and then redirect the user to the MediaWiki server to sign the request. """ consumer_token = mwoauth.ConsumerToken( app.config['CONSUMER_KEY'], app.config['CONSUMER_SECRET']) try: redirect, request_token = mwoauth.initiate( app.config['OAUTH_MWURI'], consumer_token) except Exception: app.logger.exception('mwoauth.initiate failed') return flask.redirect(flask.url_for('index')) else: flask.session['request_token'] = dict(zip( request_token._fields, request_token)) return flask.redirect(redirect) @app.route('/oauth-callback') def oauth_callback(): """OAuth handshake callback.""" if 'request_token' not in flask.session: flask.flash(u'OAuth callback failed. Are cookies disabled?') return flask.redirect(flask.url_for('index')) consumer_token = mwoauth.ConsumerToken(app.config['CONSUMER_KEY'], app.config['CONSUMER_SECRET']) try: access_token = mwoauth.complete( app.config['OAUTH_MWURI'], consumer_token, mwoauth.RequestToken(**flask.session['request_token']), flask.request.query_string) identity = mwoauth.identify(app.config['OAUTH_MWURI'], consumer_token, access_token) except Exception: app.logger.exception('OAuth authentication failed') else: flask.session['request_token_secret'] = dict(zip(access_token._fields, access_token))['secret'] flask.session['request_token_key'] = dict(zip(access_token._fields, access_token))['key'] flask.session['username'] = identity['username'] return flask.redirect(flask.url_for('index')) @app.route('/logout') def logout(): """Log the user out by clearing their session.""" flask.session.clear() return flask.redirect(flask.url_for('index')) if __name__ == "__main__": app.run(debug=True, threaded=True) diff --git a/src/templates/admin/index.html b/src/templates/admin/index.html index aa0235c..1542004 100644 --- a/src/templates/admin/index.html +++ b/src/templates/admin/index.html @@ -1,10 +1,11 @@ {% extends 'base.html' %} {% block content %}

{{locale['admin-interface']}}

{{locale['admin-description']}}

{% endblock %} \ No newline at end of file diff --git a/src/templates/admin/layers.html b/src/templates/admin/layers.html index c80f013..9f253b0 100644 --- a/src/templates/admin/layers.html +++ b/src/templates/admin/layers.html @@ -1,13 +1,13 @@ {% extends 'base.html' %} {% block content %}
-

Layers

+

{{ locale['admin-layers'] }}

- Add layer + {{ locale['admin-add-layer'] }}
{% endblock %} \ No newline at end of file diff --git a/src/templates/admin/user.html b/src/templates/admin/user.html new file mode 100644 index 0000000..7220ad4 --- /dev/null +++ b/src/templates/admin/user.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} +{% block content %} +
+

{{ locale['admin-edit-user'] }}

+
+ +
+ +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/src/templates/admin/users.html b/src/templates/admin/users.html new file mode 100644 index 0000000..e8b817b --- /dev/null +++ b/src/templates/admin/users.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% block content %} +
+

{{locale["admin-users"]}}

+
    + {% for user in users %} +
  • + {{user.1}} + {% if user.2 == 0 %} + ({{ locale['inactive'] }}) + {% else %} + {% if user.3 == 1 %} + ({{ locale['administrator'] }}) + {% endif %} + {% endif %} +
  • + {% endfor %} +
+
+{% endblock %} \ No newline at end of file