├── mapsme7_quest.tar ├── requirements.txt ├── .gitignore ├── www ├── templates │ ├── done.html │ ├── index.html │ ├── over.html │ ├── puzzle.html │ ├── layout.html │ └── task.html ├── __init__.py ├── static │ └── style.css ├── db.py └── mapsme7.py ├── mapsme7.wsgi ├── run.py ├── config.py └── kml └── mapbbcode2kmz.py /mapsme7_quest.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/mapsme7/master/mapsme7_quest.tar -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | peewee>=2.8.0 2 | flask>=0.11 3 | flask-Compress 4 | flask-OAuthlib 5 | PyYAML 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.sql 4 | *.db 5 | quest.yml 6 | tasks.txt 7 | venv/ 8 | config_local.py 9 | *.km* 10 | -------------------------------------------------------------------------------- /www/templates/done.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block content %} 3 | {% include 'puzzle.html' %} 4 | 5 |
Вы прошли весь свой квест и помогли собрать пазл на семилетие MAPS.ME! Спасибо!
6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /www/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block content %} 3 |

MAPS.ME семь лет!

4 | {% include 'puzzle.html' %} 5 | 6 |
Войдите, чтобы участвовать в квесте
7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /www/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__) 4 | app.config.from_object('config') 5 | 6 | try: 7 | from flask_compress import Compress 8 | Compress(app) 9 | except ImportError: 10 | pass 11 | 12 | import www.mapsme7 13 | -------------------------------------------------------------------------------- /mapsme7.wsgi: -------------------------------------------------------------------------------- 1 | import os, sys 2 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 3 | sys.path.insert(0, BASE_DIR) 4 | PYTHON = 'python2.7' 5 | VENV_DIR = os.path.join(BASE_DIR, 'venv', 'lib', PYTHON, 'site-packages') 6 | if os.path.exists(VENV_DIR): 7 | sys.path.insert(1, VENV_DIR) 8 | 9 | from www import app as application 10 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 5 | sys.path.insert(0, BASE_DIR) 6 | PYTHON = 'python2.7' 7 | VENV_DIR = os.path.join(BASE_DIR, 'venv', 'lib', PYTHON, 'site-packages') 8 | if os.path.exists(VENV_DIR): 9 | sys.path.insert(1, VENV_DIR) 10 | 11 | from www import app 12 | from www.db import migrate 13 | migrate() 14 | app.run(debug=True) 15 | -------------------------------------------------------------------------------- /www/templates/over.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block content %} 3 |

MAPS.ME исполнилось семь лет!

4 |
5 | 6 |
7 | {% if not participated %} 8 | Квест завершён. Пользуйтесь MAPS.ME и до встречи в следующем году! 9 | {% else %} 10 | Квест завершён. Спасибо, что помогали собрать пазл! Пользуйтесь MAPS.ME и до встречи в следующем году. 11 | {% endif %} 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 3 | 4 | DEBUG = False 5 | 6 | DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, 'mapsme7.db') 7 | # DATABASE_URI = 'postgresql://localhost/cf_audit' 8 | 9 | OVER = False 10 | ADMINS = set([290271]) # Zverik 11 | 12 | # Override these (and anything else) in config_local.py 13 | OAUTH_KEY = '' 14 | OAUTH_SECRET = '' 15 | SECRET_KEY = 'sdkjfhsfljhsadf' 16 | 17 | try: 18 | from config_local import * 19 | except ImportError: 20 | pass 21 | -------------------------------------------------------------------------------- /www/templates/puzzle.html: -------------------------------------------------------------------------------- 1 | 2 | {% for row in range(puzzle.rows) %} 3 | 4 | {% for col in range(puzzle.columns) %} 5 | {% set pp = row * puzzle.columns + col %} 6 | 13 | {% endfor %} 14 | 15 | {% endfor %} 16 |
7 | {% if pp in puzzle.pieces %} 8 | 9 | {% else %} 10 | 11 | {% endif %} 12 |
17 | -------------------------------------------------------------------------------- /www/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Семилетие MAPS.ME 5 | 6 | 7 | 8 | {% block header %}{% endblock %} 9 | 10 | 11 | {% block content %}{% endblock %} 12 | {% with messages = get_flashed_messages() %} 13 | {% for message in messages %} 14 |
{{ message }}
15 | {% endfor %} 16 | {% endwith %} 17 | 18 | 19 | -------------------------------------------------------------------------------- /www/templates/task.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block content %} 3 |

Задание {{ step }} из {{ total_steps }}

4 |
{{ task | safe }}
5 | {% if step == 4 %} 6 |
7 | {% elif image %} 8 |
9 | {% endif %} 10 | {% if desc %} 11 |
{{ desc }}
12 | {% endif %} 13 |
14 | Код: 15 | 16 |
17 | 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /www/static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "PT Sans", Helvetica, Verdana, sans-serif; 3 | font-size: 18px; 4 | margin: 1em auto; 5 | max-width: 640px; 6 | } 7 | 8 | #puzzle td { 9 | vertical-align: top; 10 | } 11 | 12 | #puzzle img { 13 | width: 160px; 14 | display: block; 15 | } 16 | 17 | .flash { 18 | font-style: italic; 19 | color: darkred; 20 | } 21 | 22 | h1 { 23 | margin: 1em 0; 24 | text-align: center; 25 | font-size: 28px; 26 | } 27 | 28 | #login { 29 | margin-top: 2em; 30 | text-align: center; 31 | } 32 | 33 | #login a { 34 | background-color: green; 35 | color: white; 36 | padding: 4px 1em; 37 | } 38 | 39 | #image { 40 | margin: 1em 0; 41 | text-align: center; 42 | } 43 | 44 | #image img { 45 | max-width: 100%; 46 | } 47 | 48 | #desc { 49 | text-align: center; 50 | } 51 | 52 | form { 53 | margin: 2em 0 1em; 54 | text-align: center; 55 | } 56 | 57 | form, input { 58 | font-size: 24px; 59 | } 60 | -------------------------------------------------------------------------------- /www/db.py: -------------------------------------------------------------------------------- 1 | from peewee import ( 2 | fn, Model, IntegerField, CharField, DateTimeField 3 | ) 4 | from playhouse.migrate import ( 5 | migrate as peewee_migrate, 6 | SqliteMigrator, 7 | MySQLMigrator, 8 | PostgresqlMigrator 9 | ) 10 | from playhouse.db_url import connect 11 | import config 12 | import logging 13 | import datetime 14 | 15 | database = connect(config.DATABASE_URI) 16 | if 'mysql' in config.DATABASE_URI: 17 | fn_Random = fn.Rand 18 | else: 19 | fn_Random = fn.Random 20 | 21 | 22 | class BaseModel(Model): 23 | class Meta: 24 | database = database 25 | 26 | 27 | class User(BaseModel): 28 | uid = IntegerField(primary_key=True) 29 | name = CharField(max_length=250) 30 | path = IntegerField() 31 | step = IntegerField(default=1) 32 | updated = DateTimeField(default=datetime.datetime.now) 33 | 34 | 35 | # ------------------------------ MIGRATION ------------------------------ 36 | 37 | 38 | LAST_VERSION = 1 39 | 40 | 41 | class Version(BaseModel): 42 | version = IntegerField() 43 | 44 | 45 | @database.atomic() 46 | def migrate(): 47 | database.create_tables([Version], safe=True) 48 | try: 49 | v = Version.select().get() 50 | except Version.DoesNotExist: 51 | database.create_tables([User]) 52 | v = Version(version=LAST_VERSION) 53 | v.save() 54 | 55 | if v.version >= LAST_VERSION: 56 | return 57 | 58 | if 'mysql' in config.DATABASE_URI: 59 | migrator = MySQLMigrator(database) 60 | elif 'sqlite' in config.DATABASE_URI: 61 | migrator = SqliteMigrator(database) 62 | else: 63 | migrator = PostgresqlMigrator(database) 64 | 65 | # No migrations yet 66 | 67 | logging.info('Migrated the database to version %s', v.version) 68 | if v.version != LAST_VERSION: 69 | raise ValueError('LAST_VERSION in db.py should be {}'.format(v.version)) 70 | -------------------------------------------------------------------------------- /kml/mapbbcode2kmz.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import json 3 | import sys 4 | import os 5 | import zipfile 6 | import random 7 | import codecs 8 | from urllib.request import urlopen 9 | 10 | 11 | def esc(s): 12 | return s.replace('&', '&').replace('<', '<').replace('>', '>') 13 | 14 | tasks = {} 15 | rnd = [] 16 | with open('tasks.txt', 'r') as f: 17 | mapbbcode = next(f).strip() 18 | for line in f: 19 | p = line.find(' ') 20 | if p > 0: 21 | rnd.append(line[p+1:].strip()) 22 | if p > 3: 23 | tasks[line[:p]] = line[p+1:].strip() 24 | 25 | with urlopen('http://share.mapbbcode.org/{}?format=geojson'.format(mapbbcode)) as resp: 26 | if resp.getcode() != 200: 27 | print('Error requesting a geojson: {}'.format(resp.getcode())) 28 | sys.exit(2) 29 | data = json.load(codecs.getreader('utf-8')(resp))['features'] 30 | 31 | images = {x[:x.find('.')]: x for x in os.listdir('.') if x.endswith('.jpg')} 32 | 33 | seen = set() 34 | marks = {} 35 | for f in data: 36 | coords = f['geometry']['coordinates'] 37 | title = f['properties'].get('title') 38 | if not title: 39 | code = str(random.randint(1000, 9999)) 40 | while code in seen: 41 | code = str(random.randint(1000, 9999)) 42 | elif title[0] == 's': 43 | code = title[1:] 44 | else: 45 | code = title 46 | seen.add(code) 47 | 48 | if code in tasks: 49 | desc = tasks[code] 50 | else: 51 | desc = random.choice(rnd) 52 | 53 | if code in images: 54 | img = images[code] 55 | else: 56 | img = random.choice(list(images.values())) 57 | desc += '

'.format(img) 58 | 59 | marks[code] = ''' 60 | 61 | {code} 62 | #placemark-purple 63 | {desc} 64 | {lon},{lat} 65 | 66 | '''.format(code=code, desc=esc(desc), lon=coords[0], lat=coords[1]) 67 | 68 | kml = ''' 69 | 70 | 71 | 78 | Семилетие MAPS.ME 79 | 1 80 | {} 81 | 82 | 83 | '''.format('\n'.join([marks[code] for code in sorted(marks.keys(), reverse=True)])) 84 | 85 | with open('mapsme7.kml', 'w') as f: 86 | f.write(kml) 87 | # with zipfile.ZipFile('mapsme7.kmz', 'w', zipfile.ZIP_DEFLATED) as z: 88 | # with zipfile.ZipFile('mapsme7.kmz', 'w') as z: 89 | # with z.open('mapsme7.kml', 'U') as f: 90 | # f.write(kml.encode('utf-8')) 91 | -------------------------------------------------------------------------------- /www/mapsme7.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from www import app 3 | from .db import database, User 4 | from peewee import fn 5 | from flask import session, url_for, redirect, request, render_template, flash, g 6 | from flask_oauthlib.client import OAuth 7 | import config 8 | import random 9 | import yaml 10 | import os 11 | import codecs 12 | import datetime 13 | 14 | oauth = OAuth() 15 | openstreetmap = oauth.remote_app( 16 | 'OpenStreetMap', 17 | base_url='https://api.openstreetmap.org/api/0.6/', 18 | request_token_url='https://www.openstreetmap.org/oauth/request_token', 19 | access_token_url='https://www.openstreetmap.org/oauth/access_token', 20 | authorize_url='https://www.openstreetmap.org/oauth/authorize', 21 | consumer_key=app.config['OAUTH_KEY'] or '123', 22 | consumer_secret=app.config['OAUTH_SECRET'] or '123' 23 | ) 24 | 25 | 26 | @app.before_request 27 | def before_request(): 28 | database.connect() 29 | load_quest() 30 | 31 | 32 | @app.teardown_request 33 | def teardown(exception): 34 | if not database.is_closed(): 35 | database.close() 36 | 37 | 38 | def choose_path(): 39 | # Select max step for each path, order by steps 40 | query = (User 41 | .select(User.path, User.step) 42 | .group_by(User.path) 43 | .having(User.step == fn.MAX(User.step)) 44 | .tuples()) 45 | paths = {p: 0 for p in range(len(g.quest['paths']))} 46 | paths.update({t[0]: t[1] for t in query}) 47 | smin = min(paths.values()) 48 | pmin = [p for p in paths if paths[p] == smin] 49 | path = random.choice(pmin) 50 | return path 51 | 52 | 53 | def get_user(): 54 | if 'osm_uid' in session: 55 | try: 56 | return User.get(User.uid == session['osm_uid']) 57 | except User.DoesNotExist: 58 | # Logging user out 59 | if 'osm_token' in session: 60 | del session['osm_token'] 61 | if 'osm_uid' in session: 62 | del session['osm_uid'] 63 | return None 64 | 65 | 66 | def is_admin(user): 67 | if not user: 68 | return False 69 | if user.uid in config.ADMINS: 70 | return True 71 | return False 72 | 73 | 74 | def load_quest(): 75 | with codecs.open(os.path.join(config.BASE_DIR, 'quest.yml'), 'r', 'utf-8') as f: 76 | g.quest = yaml.load(f) 77 | 78 | 79 | @app.route('/') 80 | def front(): 81 | user = get_user() 82 | if config.OVER: 83 | return render_template('over.html', participated=user and user.step >= 2) 84 | 85 | pquery = User.select(User.path).where(User.step == len(g.quest['steps'])+1).tuples() 86 | puzzle = { 87 | 'rows': 3, 88 | 'columns': 4, 89 | 'pieces': set([q[0] for q in pquery]), 90 | } 91 | if user: 92 | if user.step == len(g.quest['steps'])+1: 93 | return render_template('done.html', piece=user.path, puzzle=puzzle, 94 | admin=is_admin(user)) 95 | task = g.quest['paths'][user.path][user.step-1] 96 | img = None if len(task) <= 1 else task[1] 97 | desc = None if len(task) <= 2 else task[2] 98 | return render_template('task.html', step=user.step, admin=is_admin(user), 99 | task=g.quest['steps'][user.step-1], image=img, desc=desc, 100 | path=user.path, 101 | total_steps=len(g.quest['steps']), puzzle=puzzle) 102 | return render_template('index.html', puzzle=puzzle, admin=is_admin(user)) 103 | 104 | 105 | @app.route('/submit', methods=['POST']) 106 | def submit(): 107 | user = get_user() 108 | if not user: 109 | return redirect(url_for('login')) 110 | code = request.form['code'] 111 | if code.isdigit() and int(code) == g.quest['paths'][user.path][user.step-1][0]: 112 | user.step += 1 113 | user.updated = datetime.datetime.now() 114 | user.save() 115 | else: 116 | flash(u'Не угадали, извините.') 117 | return redirect(url_for('front')) 118 | 119 | 120 | @app.route('/robots.txt') 121 | def robots(): 122 | return app.response_class('User-agent: *\nDisallow: /', mimetype='text/plain') 123 | 124 | 125 | @app.route('/login') 126 | def login(): 127 | if 'osm_token' not in session: 128 | session['objects'] = request.args.get('objects') 129 | if request.args.get('next'): 130 | session['next'] = request.args.get('next') 131 | return openstreetmap.authorize(callback=url_for('oauth')) 132 | return redirect(url_for('front')) 133 | 134 | 135 | @app.route('/oauth') 136 | def oauth(): 137 | resp = openstreetmap.authorized_response() 138 | if resp is None: 139 | return 'Denied. Try again.' 140 | session['osm_token'] = ( 141 | resp['oauth_token'], 142 | resp['oauth_token_secret'] 143 | ) 144 | user_details = openstreetmap.get('user/details').data 145 | uid = int(user_details[0].get('id')) 146 | name = user_details[0].get('display_name') 147 | session['osm_uid'] = uid 148 | try: 149 | User.get(User.uid == uid) 150 | except User.DoesNotExist: 151 | User.create(uid=uid, name=name, path=choose_path(), step=1) 152 | 153 | if session.get('next'): 154 | redir = session['next'] 155 | del session['next'] 156 | else: 157 | redir = url_for('front') 158 | return redirect(redir) 159 | 160 | 161 | @openstreetmap.tokengetter 162 | def get_token(token='user'): 163 | if token == 'user' and 'osm_token' in session: 164 | return session['osm_token'] 165 | return None 166 | 167 | 168 | @app.route('/logout') 169 | def logout(): 170 | if 'osm_token' in session: 171 | del session['osm_token'] 172 | if 'osm_uid' in session: 173 | del session['osm_uid'] 174 | return redirect(url_for('front')) 175 | --------------------------------------------------------------------------------