You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
398 lines
12 KiB
398 lines
12 KiB
# -*- coding: utf-8 -*- |
|
from flask import ( |
|
Flask, make_response, request, current_app, abort, session, flash, |
|
redirect, url_for, render_template |
|
) |
|
from simplejson import dumps |
|
from pymongo import MongoClient, DESCENDING # ASCENDING |
|
import datetime |
|
import dateutil.parser |
|
import bson |
|
from settings import mongo_config, app_password, app_user, app_secret |
|
from datetime import timedelta |
|
from functools import update_wrapper |
|
from auth import requires_auth, csrf_token_generator, generate_auth_token |
|
|
|
|
|
def crossdomain(origin=None, methods=None, headers=None, |
|
max_age=21600, attach_to_all=True, |
|
automatic_options=True): |
|
if methods is not None: |
|
methods = ', '.join(sorted(x.upper() for x in methods)) |
|
if headers is not None and not isinstance(headers, basestring): |
|
headers = ', '.join(x.upper() for x in headers) |
|
if not isinstance(origin, basestring): |
|
origin = ', '.join(origin) |
|
if isinstance(max_age, timedelta): |
|
max_age = max_age.total_seconds() |
|
|
|
def get_methods(): |
|
if methods is not None: |
|
return methods |
|
|
|
options_resp = current_app.make_default_options_response() |
|
return options_resp.headers['allow'] |
|
|
|
def decorator(f): |
|
def wrapped_function(*args, **kwargs): |
|
if automatic_options and request.method == 'OPTIONS': |
|
resp = current_app.make_default_options_response() |
|
else: |
|
resp = make_response(f(*args, **kwargs)) |
|
if not attach_to_all and request.method != 'OPTIONS': |
|
return resp |
|
|
|
h = resp.headers |
|
|
|
h['Access-Control-Allow-Origin'] = origin |
|
h['Access-Control-Allow-Methods'] = get_methods() |
|
h['Access-Control-Max-Age'] = str(max_age) |
|
if headers is not None: |
|
h['Access-Control-Allow-Headers'] = headers |
|
return resp |
|
|
|
f.provide_automatic_options = False |
|
return update_wrapper(wrapped_function, f) |
|
return decorator |
|
|
|
|
|
def generate_csrf_token(): |
|
if '_csrf_token' not in session: |
|
session['_csrf_token'] = csrf_token_generator() |
|
return session['_csrf_token'] |
|
|
|
|
|
app = Flask(__name__) |
|
# Load default config and override config from an environment variable |
|
app.config.update(dict( |
|
DEBUG=True, |
|
SECRET_KEY=app_secret, |
|
USERNAME=app_user, |
|
PASSWORD=app_password, |
|
)) |
|
app.config.from_envvar('FLASKR_SETTINGS', silent=True) |
|
app.jinja_env.globals['csrf_token'] = generate_csrf_token |
|
client = MongoClient(mongo_config) |
|
db = client.showtimes |
|
|
|
miscObjHandler = lambda obj: ( |
|
obj.isoformat() if isinstance(obj, datetime.datetime) |
|
or isinstance(obj, datetime.date) |
|
else str(obj) if isinstance(obj, bson.objectid.ObjectId) else None) |
|
|
|
|
|
# @app.before_request |
|
# def csrf_protect(): |
|
# ''' |
|
# Skip CSRF-token for RESTful service |
|
# ref: http://flask.pocoo.org/snippets/3/ |
|
# ''' |
|
# if request.method == "POST" and not request.json: |
|
# token = session.pop('_csrf_token', None) |
|
# if not token or token != request.form.get('_csrf_token'): |
|
# abort(403) |
|
|
|
|
|
@app.route('/') |
|
@app.route('/flask/') |
|
@crossdomain(origin='*') |
|
def hello_world(): |
|
# return 'This comes from Flask ^_^' |
|
return render_template('layout.html') |
|
|
|
|
|
@app.route('/movies/', methods=['GET']) |
|
@app.route('/movies/<option>/', methods=['GET']) |
|
@crossdomain(origin='*') |
|
def movie_list(option=''): |
|
_opt = ('nowshowing', 'comingsoon', 'older') |
|
option = option if option in _opt else 'nowshowing' |
|
query = {} |
|
now = datetime.datetime.now() |
|
today = datetime.datetime(now.year, now.month, now.day) |
|
if option == 'nowshowing': |
|
query['status'] = 'nowshowing' |
|
elif option == 'comingsoon': |
|
query['release_date'] = {'$gte': today} |
|
else: |
|
query['release_date'] = {'$lt': today} |
|
result = db.movies.find(query).sort('release_date', DESCENDING) |
|
movies = [] |
|
for i in result: |
|
if 'original' in i['title']: |
|
i['original_title'] = i['title']['original'] |
|
## disable some heavy overload data |
|
for j in ('tmdb', 'videos'): |
|
if j in i: |
|
del i[j] |
|
# i['title'] = i['title'][lang] |
|
# i['cast'] = i['cast'][lang] |
|
# i['tagline'] = i['tagline'][lang] |
|
# i['storyline'] = i['storyline'][lang] |
|
# i['director'] = i['director'][lang] |
|
movies.append(i) |
|
|
|
json_dict = { |
|
'meta': { |
|
'total_count': len(movies) |
|
}, |
|
'objects': movies |
|
} |
|
r = make_response(dumps(json_dict, default=miscObjHandler)) |
|
r.mimetype = 'application/json' |
|
return r |
|
|
|
|
|
@app.route('/movie/<mid>/<lang>/', methods=['GET']) |
|
@crossdomain(origin='*') |
|
def movie_item(mid, lang): |
|
''' |
|
Compatibility with #ShowtimesTH on Android as of v1.4 |
|
''' |
|
lang = lang if lang in ('en', 'th') else 'en' |
|
try: |
|
movie = db.movies.find_one({'tmdb_id': int(mid)}) |
|
except ValueError: |
|
movie = db.movies.find_one({'_id': bson.objectid.ObjectId(mid)}) |
|
|
|
if not movie: |
|
movie = {} |
|
else: |
|
for k in ('title', 'tagline', 'director', 'cast', 'storyline'): |
|
if movie[k][lang]: |
|
movie[k] = movie[k][lang] |
|
else: |
|
movie[k] = movie[k]['th' if lang == 'en' else lang] |
|
try: |
|
del movie['indexes'] |
|
except KeyError: |
|
pass |
|
r = make_response(dumps(movie, default=miscObjHandler)) |
|
r.mimetype = 'application/json' |
|
return r |
|
|
|
|
|
@app.route('/movie/<mid>/', methods=['GET', 'POST']) |
|
@requires_auth |
|
@crossdomain(origin='*') |
|
def raw_movie_item(mid): |
|
if request.method == 'GET': |
|
try: |
|
movie = db.movies.find_one({'tmdb_id': int(mid)}) |
|
except ValueError: |
|
movie = db.movies.find_one({'_id': bson.objectid.ObjectId(mid)}) |
|
|
|
r = make_response(dumps(movie, default=miscObjHandler)) |
|
r.mimetype = 'application/json' |
|
return r |
|
|
|
## This is POST -- updating with information provided |
|
if not request.json: |
|
abort(400) |
|
try: |
|
key = {'_id': bson.objectid.ObjectId(mid)} |
|
prev = db.movies.find_one(key) |
|
if not prev: |
|
abort(500) |
|
is_allowed = True |
|
## simple check if dict is smaller, then we don't update that |
|
posting_keys = request.json.keys() |
|
prev_keys = prev.keys() |
|
missing_fields = set(prev_keys) - set(posting_keys) |
|
is_allowed = False if missing_fields else True |
|
if is_allowed: |
|
result = db.movies.update(key, {'$set': request.json}, False) |
|
else: |
|
result = { # mimicking mongo update result for consistency |
|
'updatedExisting': False, 'ok': 0, 'nModified': 0, |
|
'error': "missing_fields:%s" % (','.join(missing_fields)), |
|
} |
|
r = make_response(dumps(result, default=miscObjHandler)) |
|
r.mimetype = 'application/json' |
|
return r |
|
except bson.errors.InvalidId: |
|
abort(500) |
|
|
|
|
|
@app.route('/groups/', methods=['GET']) |
|
@crossdomain(origin='*') |
|
def groups(): |
|
result = db.groups.find() |
|
known_groups = [] # ['sf', 'major', 'etc'] |
|
if result.count() == 0: |
|
for i in ('sf', 'major', 'etc'): |
|
db.groups.insert({'name': str(i)}) |
|
known_groups.append({'name': str(i)}) |
|
for i in result: |
|
known_groups.append(i) |
|
|
|
json_dict = { |
|
'meta': { |
|
'total_count': len(known_groups) |
|
}, |
|
'objects': known_groups |
|
} |
|
r = make_response(dumps(json_dict, default=miscObjHandler)) |
|
r.mimetype = 'application/json' |
|
return r |
|
|
|
|
|
@app.route('/group/<string:group_id>/', methods=['GET', 'POST']) |
|
@requires_auth |
|
@crossdomain(origin='*') |
|
def group(group_id): |
|
if request.method == 'GET': |
|
result = db.groups.find_one({'_id': bson.objectid.ObjectId(group_id)}) |
|
r = make_response(dumps(result, default=miscObjHandler)) |
|
r.mimetype = 'application/json' |
|
return r |
|
|
|
## This is POST -- updating with information provided |
|
if not request.json or not 'name' in request.json: |
|
abort(400) |
|
try: |
|
key = {'_id': bson.objectid.ObjectId(group_id)} |
|
result = db.groups.update(key, {'$set': request.json}, False) |
|
r = make_response(dumps(result, default=miscObjHandler)) |
|
r.mimetype = 'application/json' |
|
return r |
|
except bson.errors.InvalidId: |
|
abort(500) |
|
|
|
|
|
@app.route('/theaters/', methods=['GET']) |
|
@app.route('/theaters/<group>/', methods=['GET']) |
|
@crossdomain(origin='*') |
|
def list_theaters(group=None): |
|
if not group: |
|
result = db.theater.find() |
|
else: |
|
result = db.theater.find({'group': group}) |
|
items = [i for i in result] |
|
json_dict = { |
|
'meta': { |
|
'total_count': len(items) |
|
}, |
|
'objects': items |
|
} |
|
r = make_response(dumps(json_dict, default=miscObjHandler)) |
|
r.mimetype = 'application/json' |
|
return r |
|
|
|
|
|
@app.route('/theater/<string:theater_id>/', methods=['GET', 'POST']) |
|
@crossdomain(origin='*') |
|
@requires_auth |
|
def get_theater(theater_id): |
|
''' |
|
TODO: cannot add theater yet |
|
''' |
|
if request.method == 'GET': |
|
result = db.theater.find_one({'_id': bson.objectid.ObjectId(theater_id)}) |
|
r = make_response(dumps(result, default=miscObjHandler)) |
|
r.mimetype = 'application/json' |
|
return r |
|
|
|
## This is POST -- updating with information provided |
|
if not request.json or \ |
|
not 'name' in request.json or \ |
|
not 'group' in request.json or not 'code' in request.json: |
|
abort(400) |
|
try: |
|
key = {'_id': bson.objectid.ObjectId(theater_id)} |
|
prev = db.theater.find_one(key) |
|
if not prev: |
|
abort(500) |
|
is_allowed = True |
|
for k in ['code', 'group', 'name']: |
|
if request.json[k] != prev[k]: |
|
is_allowed = False |
|
if is_allowed: |
|
result = db.theater.update(key, {'$set': request.json}, False) |
|
else: |
|
result = { # mimicking mongo update result for consistency |
|
'updatedExisting': False, 'ok': 0, 'nModified': 0, |
|
'error': "cannot alter mandatory fields", |
|
} |
|
r = make_response(dumps(result, default=miscObjHandler)) |
|
r.mimetype = 'application/json' |
|
return r |
|
except bson.errors.InvalidId: |
|
abort(500) |
|
|
|
|
|
@app.route('/showtimes/<group>/', methods=['GET']) |
|
@app.route('/showtimes/<group>/<code>/', methods=['GET']) |
|
@crossdomain(origin='*') |
|
def list_showtimes(group=None, code=None): |
|
day = request.args.get('d', '') |
|
q = {} |
|
if day: |
|
q['date'] = dateutil.parser.parse(day) |
|
if group: |
|
q['group'] = group |
|
if code: |
|
q['theater'] = code |
|
|
|
result = db.showtimes.find(q).sort('date', DESCENDING) |
|
items = [i for i in result[:300]] |
|
json_dict = { |
|
'meta': { |
|
'total_count': len(items) |
|
}, |
|
'objects': items |
|
} |
|
r = make_response(dumps(json_dict, default=miscObjHandler)) |
|
r.mimetype = 'application/json' |
|
return r |
|
|
|
|
|
def check_basic_auth(user, passwd): |
|
if user != app.config['USERNAME'] or \ |
|
passwd != app.config['PASSWORD']: |
|
return False |
|
else: |
|
return True |
|
|
|
|
|
@app.route('/api/token', methods=['GET']) |
|
@crossdomain(origin='*') |
|
def get_token(): |
|
auth = request.authorization |
|
if not check_basic_auth(auth.username, auth.password): |
|
abort(401) |
|
token = generate_auth_token(app_user) |
|
r = make_response( |
|
dumps({'token': token.decode('ascii')}, default=miscObjHandler)) |
|
r.mimetype = 'application/json' |
|
return r |
|
|
|
|
|
@app.route('/login', methods=['GET', 'POST']) |
|
@crossdomain(origin='*') |
|
def login(): |
|
error = None |
|
if request.method == 'POST': |
|
if request.form['username'] != app.config['USERNAME']: |
|
error = 'Invalid username' |
|
elif request.form['password'] != app.config['PASSWORD']: |
|
error = 'Invalid password' |
|
else: |
|
session['logged_in'] = True |
|
flash('You were logged in') |
|
return redirect(url_for('hello_world')) |
|
if session.get('logged_in'): |
|
flash('New entry was successfully posted') |
|
return redirect(url_for('hello_world')) |
|
return render_template('login.html', error=error) |
|
|
|
|
|
@app.route('/logout') |
|
def logout(): |
|
session.pop('logged_in', None) |
|
flash('You were logged out') |
|
return redirect(url_for('hello_world')) |
|
|
|
|
|
if __name__ == '__main__': |
|
app.run(debug=True)
|
|
|