├── 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 |
7 | {% if pp in puzzle.pieces %}
8 |
9 | {% else %}
10 |
11 | {% endif %}
12 |
13 | {% endfor %}
14 |
15 | {% endfor %}
16 |
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 |
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 |
--------------------------------------------------------------------------------