diff --git a/Problems/dicerolls.c4 b/Problems/dicerolls.c4 new file mode 100644 index 0000000..3e6d51d Binary files /dev/null and b/Problems/dicerolls.c4 differ diff --git a/Problems/double.c4 b/Problems/double.c4 new file mode 100644 index 0000000..5636fd4 Binary files /dev/null and b/Problems/double.c4 differ diff --git a/Problems/end.c4 b/Problems/end.c4 new file mode 100644 index 0000000..ca5f972 Binary files /dev/null and b/Problems/end.c4 differ diff --git a/Problems/img.c4 b/Problems/img.c4 new file mode 100644 index 0000000..5f6c3bd Binary files /dev/null and b/Problems/img.c4 differ diff --git a/Problems/img_loading_test.c4 b/Problems/img_loading_test.c4 new file mode 100644 index 0000000..f801e22 Binary files /dev/null and b/Problems/img_loading_test.c4 differ diff --git a/Problems/imgtest.c4 b/Problems/imgtest.c4 new file mode 100644 index 0000000..32b4642 Binary files /dev/null and b/Problems/imgtest.c4 differ diff --git a/Problems/newtest.c4 b/Problems/newtest.c4 new file mode 100644 index 0000000..820a8d9 Binary files /dev/null and b/Problems/newtest.c4 differ diff --git a/Problems/problem.c4 b/Problems/problem.c4 new file mode 100644 index 0000000..958d36d Binary files /dev/null and b/Problems/problem.c4 differ diff --git a/Problems/secret.c4 b/Problems/secret.c4 new file mode 100644 index 0000000..9c72dd4 Binary files /dev/null and b/Problems/secret.c4 differ diff --git a/Problems/utility.c4 b/Problems/utility.c4 new file mode 100644 index 0000000..d79f622 Binary files /dev/null and b/Problems/utility.c4 differ diff --git a/Problems/yoyo.c4 b/Problems/yoyo.c4 new file mode 100644 index 0000000..12fbe8e Binary files /dev/null and b/Problems/yoyo.c4 differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..41c0551 --- /dev/null +++ b/main.py @@ -0,0 +1,283 @@ +import gzip, json, base64, flask, threading, os, time, functools +from io import BytesIO +import pydrive.auth, pydrive.drive, pydrive.files +import bcrypt +import flask_limiter, flask_limiter.util + +gauth = pydrive.auth.GoogleAuth() +scope = ['https://www.googleapis.com/auth/drive'] +gauth.credentials = pydrive.auth.ServiceAccountCredentials.from_json_keyfile_dict(json.loads(base64.b64decode(os.getenv('AUTH'))), scope) +drive = pydrive.drive.GoogleDrive(gauth) + + +for i in os.listdir('Problems/'): + os.remove('Problems/' + i) +for i in drive.ListFile().GetList(): + i.GetContentFile('Problems/' + i['title']) + +app = flask.Flask('') +app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 * 5 +app.config['SECRET_KEY'] = base64.b64decode(os.getenv('KEY')) +app.config['AUTH_TIMEOUT'] = 15 # seconds + +limiter = flask_limiter.Limiter(app, key_func=flask_limiter.util.get_remote_address) +rate_limit = limiter.shared_limit("1/second;30/minute", scope="gdrive") + +@app.before_request +def run_auth(): + flask.g.auth = None + is_auth() + +def is_auth(): + if flask.g.auth: + return flask.g.auth + login = False + timeout = False + if 'auth' not in flask.session: + flask.session['auth'] = None + if flask.session['auth'] is not None: + timeout = time.time() - flask.session['auth'] > app.config['AUTH_TIMEOUT'] + if timeout: print(flask.request.endpoint) + login = not timeout + flask.session['auth'] = time.time() if login else None + flask.g.auth = {'ok': login, 'message': 'Sucessfully authorized.' if login else 'Your session has expired. Refresh the page to log in again.' if timeout else 'You must log in to perform this action.'} + return flask.g.auth + +app.jinja_env.globals.update(is_auth=is_auth) + +def needs_auth(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + authed = is_auth() + if not authed['ok']: + if flask.request.method == "GET": + return flask.redirect(flask.url_for('route_login', next=flask.request.full_path)) + else: + return authed, 403 + return f(*args, **kwargs) + return wrapper + +@app.errorhandler(429) +@app.errorhandler(pydrive.files.ApiRequestError) +def rate_limited(e): + return {'ok': False, 'message': 'You are performing this action too quickly. You must wait before trying again.'}, 429 + +@app.route('/favicon.ico') +def route_favicon(): + return flask.send_from_directory('static', filename='img/favicon.ico') + + + +@app.route('/') +def route_index(): + return flask.render_template('index.html') + + + +@app.route('/problems', methods=['GET', 'POST']) +def route_problems(): + return {'GET': route_problems_get, 'POST': route_problems_post}[flask.request.method]() + +@needs_auth +def route_problems_get(): + return flask.render_template('problems.html', problems=os.listdir('Problems')) + +def route_problems_post(): + out = {} + for filename in os.listdir('Problems'): + with open('Problems/' + filename, 'rb') as f: + f.seek(-12, os.SEEK_END) + out[filename] = base64.b64encode(f.read()).decode('utf8') + return json.dumps(out) + + + +@app.route('/create') +@needs_auth +def route_create(): + filename = flask.request.args.get('edit') + problem = None if filename is None else unproblem(filename) + if problem is not None: + problem['tests'] = [[int(i['visible']), i['input'], i['output']] for i in problem['tests']] + problem['images'] = list(problem['images'].keys()) + return flask.render_template('create.html', filename=os.path.splitext(filename or '')[0], problem=problem) + + + +@app.route('/problems/', methods=['GET', 'POST', 'DELETE']) +def route_problem(name): + return {'GET': route_problem_get, 'POST': route_problem_post, 'DELETE': route_problem_delete}[flask.request.method](name) + +def route_problem_get(name): + return flask.send_file('Problems/' + name, as_attachment=True) + +@needs_auth +@rate_limit +def route_problem_post(oldname): + old = unproblem(oldname) + + name = flask.request.form.get('name') + filename = flask.request.form.get('filename') + text = flask.request.form.get('text') + + if any(i is None or len(i.strip()) == 0 for i in [name, filename, text]): + return {'ok': False, 'message': f'missing name/filename/text\n{name}\n{filename}\n{text}'} + + tests = [] + for i in range(1, 26): + test = {'visible': flask.request.form.get('visible_' + str(i)) == '1', 'input': flask.request.form.get('input_' + str(i)), 'output': flask.request.form.get('output_' + str(i))} + if all(test[i] is not None and len(str(test[i]).strip()) > 0 for i in test): + tests.append(test) + + + images = {} + for i in range(1, 11): + imgname = flask.request.form.get('filename_img_' + str(i)) + img = flask.request.files.get('img_' + str(i)) + if imgname is not None and img is not None: + img = img.read() + imgname = filenamify(imgname, False) + if len(img) == 0: + if old and imgname in old['images']: + oldimg = old['images'][imgname] + if oldimg: + images[imgname] = oldimg + else: + images[imgname] = b64(img) + + problem(filenamify(filename, True), oldname=oldname, name=namify(name), text=text[:1024*1024], tests=tests, images=images) + return {'ok': True, 'message': 'made problem'} + +@needs_auth +@rate_limit +def route_problem_delete(name): + gfiles = drive.ListFile({'q': f" title='{name}' "}).GetList() + if not gfiles: + return {'ok': False, 'message': f'No problem "{name}" exists.'}, 404 + gfiles[0].Delete() + if os.path.exists('Problems/' + name): + os.remove('Problems/' + name) + return {'ok': True, 'message': f'Successfully deleted problem "{name}".'} + + + +@app.route('/login', methods=['GET', 'POST']) +def route_login(): + if flask.request.method == 'GET': + return flask.redirect(flask.request.args.get('next') or flask.url_for('route_index')) if is_auth()['ok'] else flask.render_template('login.html') + elif flask.request.method == 'POST': + flask.session['auth'] = None + json = flask.request.json + password = json.get('password') if json else None + out = {'ok': False, 'message': 'You must supply a password.'} + if password: + out['ok'] = bcrypt.checkpw(str(password).encode('utf-8'), os.getenv('PW').encode('utf-8')) + if out['ok']: + flask.session['auth'] = time.time() + out['message'] = 'Successfully logged in.' + else: + out['message'] = 'Incorrect password.' + return out + + + +@app.route('/logout') +def route_logout(): + flask.session['auth'] = None + return flask.render_template('logout.html') + + + +def run(): + app.run(host='0.0.0.0', port=8080) +threading.Thread(target=run).start() + + + + + + +def compress(bytes): + if type(bytes) is str: + bytes = bytearray(bytes, encoding='utf8') + return gzip.compress(bytes) + +def decompress(bytes): + return gzip.decompress(bytes).decode("utf8") + +def checksum(string): + #print('---') + #print(list(string)) + #print('---') + mask = (1 << 96) - 1 + out = mask + with BytesIO(string) as f: + while True: + nbytes = f.read(12) + if len(nbytes) == 0: + break + out ^= int.from_bytes(nbytes, 'little') + out <<= 1 + out |= out >> 96 + out &= mask + #print(list(out.to_bytes(12, byteorder='big')[::-1])) + return out.to_bytes(12, byteorder='big')[::-1] + +def problem(filename, oldname=None, **kwargs): + out = {} + for k, v in kwargs.items(): + out[k] = v + #print(json.dumps(out)) + #print(compress(json.dumps(out))) + #print(checksum(compress(json.dumps(out)))) + #print(json.dumps(out)) + #print(len(json.dumps(out))) + compressed = compress(json.dumps(out)) + #print(len(compressed)) + with open('Problems/' + filename, 'wb') as file: + #print('-----' + str(list(compressed))) + file.write(compressed) + check = checksum(compressed) + #print(check) + file.write(check) + if oldname and oldname != filename and os.path.exists('Problems/' + oldname): + os.remove('Problems/' + oldname) + gfiles = drive.ListFile({'q': f" title='{oldname or filename}' "}).GetList() + file = drive.CreateFile({'id': gfiles[0]['id']} if gfiles else {'title': filename}) + file['title'] = filename + file.Upload() + file.SetContentFile('Problems/' + filename) + file.Upload() + +def unproblem(filename): + try: + with open('Problems/' + filename, 'rb') as file: + file.seek(-12, os.SEEK_END) + length = file.tell() + check = file.read() + file.seek(0) + data = file.read(length) + if check != checksum(data): + print(f'CHECKSUM: {check} - {checksum(data)}') + return None + decompressed = json.loads(decompress(data)) + return decompressed + except Exception as e: + print('ERROR (unproblem): ' + str(e)) + return None + +def b64(str): + return base64.b64encode(str).decode('utf8') + +def namify(name): + return ' '.join(name.split())[:32] + +def filenamify(name, extension): + allowed = 'abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-' + disallowed = 'CON PRN AUX CLOCK NUL COM1 COM2 COM3 COM4 COM5 COM6 COM7 COM8 COM9 LPT1 LPT2 LPT3 LPT4 LPT5 LPT6 LPT7 LPT8 LPT9'.split(' ') + name = ''.join(i.lower() for i in name if i in allowed)[:32] + if not name or name.upper() in disallowed: + name += '_' + if extension: + name += '.c4' + return name \ No newline at end of file diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..0303a65 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,642 @@ +[[package]] +category = "main" +description = "Modern password hashing for your software and your servers" +name = "bcrypt" +optional = false +python-versions = ">=3.6" +version = "3.2.0" + +[package.dependencies] +cffi = ">=1.1" +six = ">=1.4.1" + +[package.extras] +tests = ["pytest (>=3.2.1,<3.3.0 || >3.3.0)"] +typecheck = ["mypy"] + +[[package]] +category = "main" +description = "Extensible memoizing collections and decorators" +name = "cachetools" +optional = false +python-versions = "~=3.5" +version = "4.1.1" + +[[package]] +category = "main" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2020.6.20" + +[[package]] +category = "main" +description = "Foreign Function Interface for Python calling C code." +name = "cffi" +optional = false +python-versions = "*" +version = "1.14.3" + +[package.dependencies] +pycparser = "*" + +[[package]] +category = "main" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "main" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.2" + +[[package]] +category = "main" +description = "A simple framework for building complex web applications." +name = "flask" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "1.1.2" + +[package.dependencies] +Jinja2 = ">=2.10.1" +Werkzeug = ">=0.15" +click = ">=5.1" +itsdangerous = ">=0.24" + +[package.extras] +dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] +docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] +dotenv = ["python-dotenv"] + +[[package]] +category = "main" +description = "Rate limiting for flask applications" +name = "flask-limiter" +optional = false +python-versions = "*" +version = "1.4" + +[package.dependencies] +Flask = ">=0.8" +limits = "*" +six = ">=1.4.1" + +[[package]] +category = "main" +description = "Google API client core library" +name = "google-api-core" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.22.4" + +[package.dependencies] +google-auth = ">=1.21.1,<2.0dev" +googleapis-common-protos = ">=1.6.0,<2.0dev" +protobuf = ">=3.12.0" +pytz = "*" +requests = ">=2.18.0,<3.0.0dev" +setuptools = ">=34.0.0" +six = ">=1.13.0" + +[package.extras] +grpc = ["grpcio (>=1.29.0,<2.0dev)"] +grpcgcp = ["grpcio-gcp (>=0.2.2)"] +grpcio-gcp = ["grpcio-gcp (>=0.2.2)"] + +[[package]] +category = "main" +description = "Google API Client Library for Python" +name = "google-api-python-client" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.12.3" + +[package.dependencies] +google-api-core = ">=1.21.0,<2dev" +google-auth = ">=1.16.0" +google-auth-httplib2 = ">=0.0.3" +httplib2 = ">=0.15.0,<1dev" +six = ">=1.13.0,<2dev" +uritemplate = ">=3.0.0,<4dev" + +[[package]] +category = "main" +description = "Google Authentication Library" +name = "google-auth" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.22.1" + +[package.dependencies] +cachetools = ">=2.0.0,<5.0" +pyasn1-modules = ">=0.2.1" +setuptools = ">=40.3.0" +six = ">=1.9.0" + +[package.dependencies.rsa] +python = ">=3.5" +version = ">=3.1.4,<5" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)"] + +[[package]] +category = "main" +description = "Google Authentication Library: httplib2 transport" +name = "google-auth-httplib2" +optional = false +python-versions = "*" +version = "0.0.4" + +[package.dependencies] +google-auth = "*" +httplib2 = ">=0.9.1" +six = "*" + +[[package]] +category = "main" +description = "Common protobufs used in Google APIs" +name = "googleapis-common-protos" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.52.0" + +[package.dependencies] +protobuf = ">=3.6.0" + +[package.extras] +grpc = ["grpcio (>=1.0.0)"] + +[[package]] +category = "main" +description = "A comprehensive HTTP client library." +name = "httplib2" +optional = false +python-versions = "*" +version = "0.18.1" + +[[package]] +category = "main" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.10" + +[[package]] +category = "main" +description = "Various helpers to pass data to untrusted environments and back." +name = "itsdangerous" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.1.0" + +[[package]] +category = "main" +description = "A very fast and expressive template engine." +name = "jinja2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.11.2" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + +[[package]] +category = "main" +description = "Rate limiting utilities" +name = "limits" +optional = false +python-versions = "*" +version = "1.5.1" + +[package.dependencies] +six = ">=1.4.1" + +[[package]] +category = "main" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.1.1" + +[[package]] +category = "main" +description = "OAuth 2.0 client library" +name = "oauth2client" +optional = false +python-versions = "*" +version = "4.1.3" + +[package.dependencies] +httplib2 = ">=0.9.1" +pyasn1 = ">=0.1.7" +pyasn1-modules = ">=0.0.5" +rsa = ">=3.1.4" +six = ">=1.6.1" + +[[package]] +category = "main" +description = "Protocol Buffers" +name = "protobuf" +optional = false +python-versions = "*" +version = "3.13.0" + +[package.dependencies] +setuptools = "*" +six = ">=1.9" + +[[package]] +category = "main" +description = "ASN.1 types and codecs" +name = "pyasn1" +optional = false +python-versions = "*" +version = "0.4.8" + +[[package]] +category = "main" +description = "A collection of ASN.1-based protocols modules." +name = "pyasn1-modules" +optional = false +python-versions = "*" +version = "0.2.8" + +[package.dependencies] +pyasn1 = ">=0.4.6,<0.5.0" + +[[package]] +category = "main" +description = "C parser in Python" +name = "pycparser" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.20" + +[[package]] +category = "main" +description = "Google Drive API made easy." +name = "pydrive" +optional = false +python-versions = "*" +version = "1.3.1" + +[package.dependencies] +PyYAML = ">=3.0" +google-api-python-client = ">=1.2" +oauth2client = ">=4.0.0" + +[[package]] +category = "main" +description = "World timezone definitions, modern and historical" +name = "pytz" +optional = false +python-versions = "*" +version = "2020.1" + +[[package]] +category = "main" +description = "YAML parser and emitter for Python" +name = "pyyaml" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "5.3.1" + +[[package]] +category = "main" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.24.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[[package]] +category = "main" +description = "Pure-Python RSA implementation" +name = "rsa" +optional = false +python-versions = ">=3.5, <4" +version = "4.6" + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" + +[[package]] +category = "main" +description = "URI templates" +name = "uritemplate" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.0.1" + +[[package]] +category = "main" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "1.25.10" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + +[[package]] +category = "main" +description = "The comprehensive WSGI web application library." +name = "werkzeug" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "1.0.1" + +[package.extras] +dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] +watchdog = ["watchdog"] + +[metadata] +content-hash = "91e566f2e543ec44640dfc2f2a6f29c7699d16fd32177619633389d7cd58f56b" +python-versions = "^3.8" + +[metadata.files] +bcrypt = [ + {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, + {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, + {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, + {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, +] +cachetools = [ + {file = "cachetools-4.1.1-py3-none-any.whl", hash = "sha256:513d4ff98dd27f85743a8dc0e92f55ddb1b49e060c2d5961512855cda2c01a98"}, + {file = "cachetools-4.1.1.tar.gz", hash = "sha256:bbaa39c3dede00175df2dc2b03d0cf18dd2d32a7de7beb68072d13043c9edb20"}, +] +certifi = [ + {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, + {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, +] +cffi = [ + {file = "cffi-1.14.3-2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc"}, + {file = "cffi-1.14.3-2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768"}, + {file = "cffi-1.14.3-2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d"}, + {file = "cffi-1.14.3-2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1"}, + {file = "cffi-1.14.3-2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca"}, + {file = "cffi-1.14.3-2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a"}, + {file = "cffi-1.14.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c"}, + {file = "cffi-1.14.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730"}, + {file = "cffi-1.14.3-cp27-cp27m-win32.whl", hash = "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d"}, + {file = "cffi-1.14.3-cp27-cp27m-win_amd64.whl", hash = "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05"}, + {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b"}, + {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171"}, + {file = "cffi-1.14.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f"}, + {file = "cffi-1.14.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4"}, + {file = "cffi-1.14.3-cp35-cp35m-win32.whl", hash = "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d"}, + {file = "cffi-1.14.3-cp35-cp35m-win_amd64.whl", hash = "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d"}, + {file = "cffi-1.14.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3"}, + {file = "cffi-1.14.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808"}, + {file = "cffi-1.14.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537"}, + {file = "cffi-1.14.3-cp36-cp36m-win32.whl", hash = "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0"}, + {file = "cffi-1.14.3-cp36-cp36m-win_amd64.whl", hash = "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e"}, + {file = "cffi-1.14.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1"}, + {file = "cffi-1.14.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579"}, + {file = "cffi-1.14.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394"}, + {file = "cffi-1.14.3-cp37-cp37m-win32.whl", hash = "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc"}, + {file = "cffi-1.14.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869"}, + {file = "cffi-1.14.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e"}, + {file = "cffi-1.14.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828"}, + {file = "cffi-1.14.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9"}, + {file = "cffi-1.14.3-cp38-cp38-win32.whl", hash = "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522"}, + {file = "cffi-1.14.3-cp38-cp38-win_amd64.whl", hash = "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15"}, + {file = "cffi-1.14.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d"}, + {file = "cffi-1.14.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c"}, + {file = "cffi-1.14.3-cp39-cp39-win32.whl", hash = "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b"}, + {file = "cffi-1.14.3-cp39-cp39-win_amd64.whl", hash = "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3"}, + {file = "cffi-1.14.3.tar.gz", hash = "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +flask = [ + {file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"}, + {file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"}, +] +flask-limiter = [ + {file = "Flask-Limiter-1.4.tar.gz", hash = "sha256:021279c905a1e24f181377ab3be711be7541734b494f4e6db2b8edeba7601e48"}, + {file = "Flask_Limiter-1.4-py3-none-any.whl", hash = "sha256:f8a65a7874f48ff8df2ea5e86d5b85b48fcbae065ebeb5271b317fe68fcfa979"}, + {file = "Flask_Limiter-1.4-py3.7.egg", hash = "sha256:055a388a89f4d5768c64025443f1f41e3babcbbbf315c728413c27b4975af239"}, +] +google-api-core = [ + {file = "google-api-core-1.22.4.tar.gz", hash = "sha256:4a9d7ac2527a9e298eebb580a5e24e7e41d6afd97010848dd0f306cae198ec1a"}, + {file = "google_api_core-1.22.4-py2.py3-none-any.whl", hash = "sha256:15e00ceb7e6dc44159e2a41a222830744e9ebcb3a553c580b61cb5a66572f2f0"}, +] +google-api-python-client = [ + {file = "google-api-python-client-1.12.3.tar.gz", hash = "sha256:844ef76bda585ea0ea2d5e7f8f9a0eb10d6e2eba66c4fea0210ec7843941cb1a"}, + {file = "google_api_python_client-1.12.3-py2.py3-none-any.whl", hash = "sha256:5b3f4908c041f3109135841cf7e49fc4477f26f7d02b6db72753a17f8b338d01"}, +] +google-auth = [ + {file = "google-auth-1.22.1.tar.gz", hash = "sha256:9c0f71789438d703f77b94aad4ea545afaec9a65f10e6cc1bc8b89ce242244bb"}, + {file = "google_auth-1.22.1-py2.py3-none-any.whl", hash = "sha256:712dd7d140a9a1ea218e5688c7fcb04af71b431a29ec9ce433e384c60e387b98"}, +] +google-auth-httplib2 = [ + {file = "google-auth-httplib2-0.0.4.tar.gz", hash = "sha256:8d092cc60fb16517b12057ec0bba9185a96e3b7169d86ae12eae98e645b7bc39"}, + {file = "google_auth_httplib2-0.0.4-py2.py3-none-any.whl", hash = "sha256:aeaff501738b289717fac1980db9711d77908a6c227f60e4aa1923410b43e2ee"}, +] +googleapis-common-protos = [ + {file = "googleapis-common-protos-1.52.0.tar.gz", hash = "sha256:560716c807117394da12cecb0a54da5a451b5cf9866f1d37e9a5e2329a665351"}, + {file = "googleapis_common_protos-1.52.0-py2.py3-none-any.whl", hash = "sha256:c8961760f5aad9a711d37b675be103e0cc4e9a39327e0d6d857872f698403e24"}, +] +httplib2 = [ + {file = "httplib2-0.18.1-py3-none-any.whl", hash = "sha256:ca2914b015b6247791c4866782fa6042f495b94401a0f0bd3e1d6e0ba2236782"}, + {file = "httplib2-0.18.1.tar.gz", hash = "sha256:8af66c1c52c7ffe1aa5dc4bcd7c769885254b0756e6e69f953c7f0ab49a70ba3"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +itsdangerous = [ + {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, + {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, +] +jinja2 = [ + {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, + {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, +] +limits = [ + {file = "limits-1.5.1-py2-none-any.whl", hash = "sha256:0e5f8b10f18dd809eb2342f5046eb9aa5e4e69a0258567b5f4aa270647d438b3"}, + {file = "limits-1.5.1.tar.gz", hash = "sha256:f0c3319f032c4bfad68438ed1325c0fac86dac64582c7c25cddc87a0b658fa20"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +oauth2client = [ + {file = "oauth2client-4.1.3-py2.py3-none-any.whl", hash = "sha256:b8a81cc5d60e2d364f0b1b98f958dbd472887acaf1a5b05e21c28c31a2d6d3ac"}, + {file = "oauth2client-4.1.3.tar.gz", hash = "sha256:d486741e451287f69568a4d26d70d9acd73a2bbfa275746c535b4209891cccc6"}, +] +protobuf = [ + {file = "protobuf-3.13.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9c2e63c1743cba12737169c447374fab3dfeb18111a460a8c1a000e35836b18c"}, + {file = "protobuf-3.13.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1e834076dfef9e585815757a2c7e4560c7ccc5962b9d09f831214c693a91b463"}, + {file = "protobuf-3.13.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:df3932e1834a64b46ebc262e951cd82c3cf0fa936a154f0a42231140d8237060"}, + {file = "protobuf-3.13.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8c35bcbed1c0d29b127c886790e9d37e845ffc2725cc1db4bd06d70f4e8359f4"}, + {file = "protobuf-3.13.0-cp35-cp35m-win32.whl", hash = "sha256:339c3a003e3c797bc84499fa32e0aac83c768e67b3de4a5d7a5a9aa3b0da634c"}, + {file = "protobuf-3.13.0-cp35-cp35m-win_amd64.whl", hash = "sha256:361acd76f0ad38c6e38f14d08775514fbd241316cce08deb2ce914c7dfa1184a"}, + {file = "protobuf-3.13.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9edfdc679a3669988ec55a989ff62449f670dfa7018df6ad7f04e8dbacb10630"}, + {file = "protobuf-3.13.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5db9d3e12b6ede5e601b8d8684a7f9d90581882925c96acf8495957b4f1b204b"}, + {file = "protobuf-3.13.0-cp36-cp36m-win32.whl", hash = "sha256:c8abd7605185836f6f11f97b21200f8a864f9cb078a193fe3c9e235711d3ff1e"}, + {file = "protobuf-3.13.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4d1174c9ed303070ad59553f435846a2f877598f59f9afc1b89757bdf846f2a7"}, + {file = "protobuf-3.13.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0bba42f439bf45c0f600c3c5993666fcb88e8441d011fad80a11df6f324eef33"}, + {file = "protobuf-3.13.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c0c5ab9c4b1eac0a9b838f1e46038c3175a95b0f2d944385884af72876bd6bc7"}, + {file = "protobuf-3.13.0-cp37-cp37m-win32.whl", hash = "sha256:f68eb9d03c7d84bd01c790948320b768de8559761897763731294e3bc316decb"}, + {file = "protobuf-3.13.0-cp37-cp37m-win_amd64.whl", hash = "sha256:91c2d897da84c62816e2f473ece60ebfeab024a16c1751aaf31100127ccd93ec"}, + {file = "protobuf-3.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3dee442884a18c16d023e52e32dd34a8930a889e511af493f6dc7d4d9bf12e4f"}, + {file = "protobuf-3.13.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e7662437ca1e0c51b93cadb988f9b353fa6b8013c0385d63a70c8a77d84da5f9"}, + {file = "protobuf-3.13.0-py2.py3-none-any.whl", hash = "sha256:d69697acac76d9f250ab745b46c725edf3e98ac24763990b24d58c16c642947a"}, + {file = "protobuf-3.13.0.tar.gz", hash = "sha256:6a82e0c8bb2bf58f606040cc5814e07715b2094caeba281e2e7d0b0e2e397db5"}, +] +pyasn1 = [ + {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, + {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, + {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, + {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, + {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, + {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, + {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, + {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, + {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, + {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, + {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, + {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, +] +pyasn1-modules = [ + {file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"}, + {file = "pyasn1_modules-0.2.8-py2.4.egg", hash = "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199"}, + {file = "pyasn1_modules-0.2.8-py2.5.egg", hash = "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"}, + {file = "pyasn1_modules-0.2.8-py2.6.egg", hash = "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb"}, + {file = "pyasn1_modules-0.2.8-py2.7.egg", hash = "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8"}, + {file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"}, + {file = "pyasn1_modules-0.2.8-py3.1.egg", hash = "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d"}, + {file = "pyasn1_modules-0.2.8-py3.2.egg", hash = "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45"}, + {file = "pyasn1_modules-0.2.8-py3.3.egg", hash = "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"}, + {file = "pyasn1_modules-0.2.8-py3.4.egg", hash = "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811"}, + {file = "pyasn1_modules-0.2.8-py3.5.egg", hash = "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed"}, + {file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"}, + {file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"}, +] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] +pydrive = [ + {file = "PyDrive-1.3.1-py2-none-any.whl", hash = "sha256:5b94e971430722eb5c40a090f21df46b32e51399d747c1511796f63f902d1095"}, + {file = "PyDrive-1.3.1.tar.gz", hash = "sha256:83890dcc2278081c6e3f6a8da1f8083e25de0bcc8eb7c91374908c5549a20787"}, +] +pytz = [ + {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, + {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, +] +pyyaml = [ + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, +] +requests = [ + {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, + {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, +] +rsa = [ + {file = "rsa-4.6-py3-none-any.whl", hash = "sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233"}, + {file = "rsa-4.6.tar.gz", hash = "sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +uritemplate = [ + {file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"}, + {file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"}, +] +urllib3 = [ + {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, + {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, +] +werkzeug = [ + {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, + {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b1d802a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[tool.poetry] +name = "repl_python3_C-4" +version = "0.1.0" +description = "" +authors = ["Your Name "] + +[tool.poetry.dependencies] +python = "^3.8" +flask = "^1.1.2" +pydrive = "^1.3.1" +bcrypt = "^3.2.0" +flask-limiter = "^1.4" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/static/C-4.exe b/static/C-4.exe new file mode 100644 index 0000000..ae04570 Binary files /dev/null and b/static/C-4.exe differ diff --git a/static/css/base.css b/static/css/base.css new file mode 100644 index 0000000..974763b --- /dev/null +++ b/static/css/base.css @@ -0,0 +1,68 @@ +body { + margin-top: 56px; + font-family: "Arial", sans-serif; +} + +textarea { + resize: none; +} + +legend, label { + text-align: left; +} + +.flex { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; +} + +.flex_grow { + flex-basis: 100%; +} + +div.load_div { + display: flex; + height: 32px; + align-items: center; +} + +img.load_img { + visibility: hidden; + height: 75%; + margin-left: 8px; +} + +#header { + display: flex; + justify-content: space-between; + align-items: center; + position: absolute; + left: 0px; + top: 0px; + box-sizing: border-box; + padding: 4px; + height: 48px; + width: 100%; + background-color: gray; +} + +#header a { + height: 100%; + border: 1px solid black; + border-radius: 8px; + transition: background-color 0.5s ease-out; +} + +#header a:hover { + background-color: #4c4c4c; +} + +#header a:active { + transition: background-color 0.25s ease-out; + background-color: #2c2c2c; +} + +#header img { + height: 100%; +} \ No newline at end of file diff --git a/static/css/create.css b/static/css/create.css new file mode 100644 index 0000000..6d78ba5 --- /dev/null +++ b/static/css/create.css @@ -0,0 +1,40 @@ +#create_back { + min-width: 400px; + text-align: center; +} + +#create_back > button { + width: 150px; +} + +fieldset.create_field { + margin: 16px 0px 16px 0px; + min-width: 400px; + box-sizing: border-box; +} + +fieldset.create_field > fieldset { + margin: 16px 0px 16px 0px; + flex-basis: 100%; +} + +fieldset.create_field > input { + flex-grow: 1; + margin: 4px; +} + +fieldset.create_field textarea { + flex-grow: 1; + margin: 4px; +} + +fieldset.create_field > fieldset > label { + flex-basis: 100%; + margin-bottom: 4px; +} + +#create_div_test > button, #create_div_image > button { + white-space: nowrap; + margin: 0px 4px 4px 4px; + width: 25px; +} diff --git a/static/css/problems.css b/static/css/problems.css new file mode 100644 index 0000000..e81f767 --- /dev/null +++ b/static/css/problems.css @@ -0,0 +1,13 @@ +#problems_form { + min-width: 500px; + text-align: center; +} + +#problems_form > button { + width: 150px; +} + +button.problems_btn { + margin: 4px; + width: 75px; +} \ No newline at end of file diff --git a/static/img/favicon.ico b/static/img/favicon.ico new file mode 100644 index 0000000..c5a267d Binary files /dev/null and b/static/img/favicon.ico differ diff --git a/static/img/key.png b/static/img/key.png new file mode 100644 index 0000000..d441129 Binary files /dev/null and b/static/img/key.png differ diff --git a/static/img/load.gif b/static/img/load.gif new file mode 100644 index 0000000..a2e45f5 Binary files /dev/null and b/static/img/load.gif differ diff --git a/static/img/lock.png b/static/img/lock.png new file mode 100644 index 0000000..1f3842a Binary files /dev/null and b/static/img/lock.png differ diff --git a/static/js/create.js b/static/js/create.js new file mode 100644 index 0000000..ae6f5da --- /dev/null +++ b/static/js/create.js @@ -0,0 +1,161 @@ +function add_field(type, count=1, values=[]) +{ + if(typeof this.counts === 'undefined') + { + this.counts = {}; + } + const types = { + 'test': +` + Test # + + + +`, + 'image': +` + Image # + + +`}; + const maxes = {'test': 25, 'image': 10}; + const sender = document.getElementById('create_div_' + type); + if(!this.counts.hasOwnProperty(type)) + { + this.counts[type] = 0; + } + for(var i = 0; i < count; i++) + { + if(this.counts[type] >= maxes[type]) + { + break; + } + var field = document.createElement('fieldset'); + field.className = 'flex' + field.id = `field_${type}_${++this.counts[type]}`; + var inner = types[type].replace(/#/g, this.counts[type]); + if(i < values.length) + { + var val = values[i]; + if(!Array.isArray(val)) + { + val = [val]; + } + inner = inner.split('$'); + inner = inner.slice(1).reduce((out, current, index) => out + (index < val.length ? escape(val[index]) : '') + current, inner[0]) + } + else + { + inner = inner.replace(/\$/g, ''); + } + field.innerHTML = inner; + sender.parentNode.insertBefore(field, sender); + } + for(var i = 0; i < -count; i++) + { + if(this.counts[type] <= 0) + { + break; + } + var del = document.getElementById(`field_${type}_${this.counts[type]--}`); + del.parentNode.removeChild(del); + } + setup(); +} + +function setup() +{ + [...document.querySelectorAll('input[name^=filename]')].forEach(elem => { + elem.oninput = checkFilename; + elem.pattern = '[a-z0-9-_]+'; + elem.value = filenamify(elem.value, false); + }); + [...document.querySelectorAll('input[type=file]')].forEach(elem => { + elem.onchange = checkFile; + }); + [...document.querySelectorAll('input[type=checkbox]')].forEach(elem => { + elem.checked = elem.previousSibling.value == '1'; + elem.onclick = toggle; + }); + document.querySelector('#create_form').onsubmit = upload; +} + +function checkFilename(e) +{ + e = e || window.event; + var sender = e.srcElement || e.target; + var pos = sender.selectionStart; + var substring = sender.value.substring(0, sender.selectionStart); + sender.value = filenamify(sender.value, false); + sender.selectionEnd = pos - substring.length + filenamify(substring, false).length; +} + +function checkFile(e) +{ + e = e || window.event; + var sender = e.srcElement || e.target; + var target = sender.previousElementSibling.firstElementChild + if(sender.files[0].size > 1024 * 1024 * 5) + { + alert('Image size cannot exceed 5MB.') + sender.value = ''; + } + target.value = filenamify(sender.files[0].name, true); +} + +function toggle(e) +{ + e = e || window.event; + var sender = e.srcElement || e.target; + sender.previousSibling.value = 1 - sender.previousSibling.value; +} + +function filenamify(str, extension) +{ + const remove = extension ? /\.[^.]*$|[^a-z0-9-_]/g : /[^a-z0-9-_]/g; + const disallowed = 'CON PRN AUX CLOCK NUL COM1 COM2 COM3 COM4 COM5 COM6 COM7 COM8 COM9 LPT1 LPT2 LPT3 LPT4 LPT5 LPT6 LPT7 LPT8 LPT9'.split(' '); + str = str.toLowerCase().replace(/\s/g, '_').replace(remove, ''); + if(disallowed.includes(str.toUpperCase())) + { + str += '_'; + } + if(str.length > 32) + { + str = str.substring(0, 32); + } + return str; +} + +function upload(e) +{ + e = e || window.event; + var sender = e.srcElement || e.target; + if(sender.checkValidity()) + { + e.preventDefault(); + + var form = new FormData(sender); + sender.querySelector('input[type=submit]').disabled = true; + sender.querySelector('img.load_img').style.visibility = 'visible'; + fetch('/problems/' + (new URLSearchParams(window.location.search).get('edit') || form.get('filename')), {method: 'POST', body: form}).then(x => x.json()).then(x => { + if(x['ok']) + { + window.location.href = '/problems'; + } + else + { + sender.querySelector('img.load_img').style.visibility = 'hidden'; + alert(x['message']); + sender.querySelector('input[type=submit]').disabled = false; + } + }); + } +} + +function escape(string) { + return String(string).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); +} + +window.onload = setup; \ No newline at end of file diff --git a/static/js/login.js b/static/js/login.js new file mode 100644 index 0000000..6175748 --- /dev/null +++ b/static/js/login.js @@ -0,0 +1,27 @@ +function login(e) +{ + e = e || window.event; + var sender = e.srcElement || e.target; + e.preventDefault(); + sender.querySelector('input[type=submit]').disabled = true; + sender.querySelector('img.load_img').style.visibility = 'visible'; + fetch('/login', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({'password': document.getElementById('password').value})}).then(x => x.json()).then(x => { + if(x['ok']) + { + window.location.href = new URLSearchParams(window.location.search).get('next') || '/'; + } + else + { + sender.querySelector('img.load_img').style.visibility = 'hidden'; + alert(x['message']); + sender.querySelector('input[type=submit]').disabled = false; + } + }); +} + +function setup() +{ + document.querySelector('#login_form').onsubmit = login; +} + +window.onload = setup; \ No newline at end of file diff --git a/static/js/problems.js b/static/js/problems.js new file mode 100644 index 0000000..9e8384a --- /dev/null +++ b/static/js/problems.js @@ -0,0 +1,45 @@ +function deleteProblem(problem) +{ + return function(e) + { + e = e || window.event; + var sender = e.srcElement || e.target; + if(confirm(`Are you sure you want to delete the problem "${problem}"?`)) + { + var del = false; + sender.disabled = true; + sender.parentNode.querySelector('img.load_img').style.visibility = 'visible'; + fetch('/problems/' + problem, {method: 'DELETE'}).then(x => { + del = x.status == 200 || x.status == 404; + return x.json(); + }).then(x => { + if(!x['ok']) + { + alert(x['message']); + } + if(del) + { + var elem = document.getElementById('problems_field_' + problem) + if(elem) + { + elem.parentNode.removeChild(elem); + } + } + else + { + sender.parentNode.querySelector('img.load_img').style.visibility = 'hidden'; + sender.disabled = false; + } + }); + } + }; +} + +function setup() +{ + [...document.querySelectorAll('button.problems_btn[name=delete]')].forEach(elem => { + elem.onclick = deleteProblem(elem.value); + }); +} + +window.onload = setup \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..13de56b --- /dev/null +++ b/templates/base.html @@ -0,0 +1,23 @@ + + + + Carver Coding Club Computer + +{%- if self.css() -%} + {%- for file in self.css().split('/') %} + + {%- endfor -%} +{%- endif -%}{%- if self.js() -%} + {%- for file in self.js().split('/') %} + + {%- endfor -%} +{%- endif %} + + + + {% block body %}{% endblock %} + + \ No newline at end of file diff --git a/templates/create.html b/templates/create.html new file mode 100644 index 0000000..3e18316 --- /dev/null +++ b/templates/create.html @@ -0,0 +1,39 @@ +{% extends 'base.html' %} +{% block css %}create.css{% endblock %} +{% block js %}create.js{% endblock %} +{% block body %} +{% set edit = problem is not none -%} +
+ +
+
+
+ Metadata + + + +
+
+ Tests +
+ +
+
+
+ Images +
+ +
+
+
+ + Loading... +
+ {%- if edit %} + + {%- endif %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..0bb4b9b --- /dev/null +++ b/templates/index.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} +{% block css %}{% endblock %} +{% block js %}{% endblock %} +{% block body %} +

+ The Carver Coding Club Computer, or C-4, is an application designed to facilitate the creation and distribution of competitive programming problems. You can download it here. +

+{%- if is_auth()['ok'] %} +

+ Click here to manage the problem list and here to upload a new problem. +

+{%- endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..ccf30df --- /dev/null +++ b/templates/login.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} +{% block css %}{% endblock %} +{% block js %}login.js{% endblock %} +{% block body %} +
+ Accessing this page requires authorization: + +
+ + Loading... +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/logout.html b/templates/logout.html new file mode 100644 index 0000000..9f8a3d4 --- /dev/null +++ b/templates/logout.html @@ -0,0 +1,8 @@ +{% extends 'base.html' %} +{% block css %}{% endblock %} +{% block js %}{% endblock %} +{% block body %} +
+ You have successfully been logged out. +
+{% endblock %} \ No newline at end of file diff --git a/templates/problems.html b/templates/problems.html new file mode 100644 index 0000000..8a5d41b --- /dev/null +++ b/templates/problems.html @@ -0,0 +1,21 @@ +{% extends 'base.html' %} +{% block css %}problems.css{% endblock %} +{% block js %}problems.js{% endblock %} +{% block body %} +
+ +
+ Problems + {%for problem in problems%} +
+ {{problem}} +
+ + + Loading... +
+
+ {%endfor%} +
+
+{% endblock %} \ No newline at end of file