├── .editorconfig ├── .eslintrc.yaml ├── .gitignore ├── .prettierrc.yaml ├── LICENSE ├── README.md ├── bin ├── README.md ├── requirements_linux.sh ├── requirements_osx_brew.sh ├── requirements_osx_port.sh └── test_hello_world.sh ├── bower.json ├── gulp ├── config.coffee ├── paths.coffee ├── tasks │ ├── build.coffee │ ├── clean.coffee │ ├── dep.coffee │ ├── ext.coffee │ ├── script.coffee │ ├── style.coffee │ └── watch.coffee └── util.coffee ├── gulpfile.coffee ├── gulpfile.js ├── magic.py ├── main ├── api │ ├── __init__.py │ ├── fields.py │ ├── helpers.py │ └── v1 │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── config.py │ │ ├── resource.py │ │ └── user.py ├── app.yaml ├── appengine_config.py ├── auth │ ├── __init__.py │ ├── auth.py │ ├── azure_ad.py │ ├── bitbucket.py │ ├── dropbox.py │ ├── facebook.py │ ├── gae.py │ ├── github.py │ ├── google.py │ ├── instagram.py │ ├── linkedin.py │ ├── mailru.py │ ├── microsoft.py │ ├── reddit.py │ ├── twitter.py │ ├── vk.py │ └── yahoo.py ├── cache.py ├── config.py ├── control │ ├── __init__.py │ ├── admin.py │ ├── error.py │ ├── feedback.py │ ├── profile.py │ ├── resource.py │ ├── serve.py │ ├── test.py │ ├── user.py │ └── welcome.py ├── main.py ├── model │ ├── __init__.py │ ├── base.py │ ├── config.py │ ├── config_auth.py │ ├── resource.py │ └── user.py ├── path_util.py ├── static │ ├── img │ │ └── favicon.ico │ ├── robots.txt │ └── src │ │ ├── script │ │ ├── common │ │ │ ├── api.coffee │ │ │ ├── upload.coffee │ │ │ └── util.coffee │ │ └── site │ │ │ ├── app.coffee │ │ │ ├── auth.coffee │ │ │ ├── pretty-file.coffee │ │ │ ├── resource.coffee │ │ │ └── user.coffee │ │ └── style │ │ ├── base.less │ │ ├── footer.less │ │ ├── mixins.less │ │ ├── resource.less │ │ ├── signin.less │ │ ├── style.less │ │ ├── test.less │ │ ├── user.less │ │ └── variables.less ├── task.py ├── templates │ ├── admin │ │ ├── admin.html │ │ ├── admin_auth.html │ │ ├── admin_base.html │ │ ├── admin_config.html │ │ ├── bit │ │ │ ├── azure_ad_oauth.html │ │ │ ├── bitbucket_oauth.html │ │ │ ├── dropbox_oauth.html │ │ │ ├── facebook_oauth.html │ │ │ ├── github_oauth.html │ │ │ ├── google_analytics_tracking_id.html │ │ │ ├── google_cloud_bucket_name.html │ │ │ ├── google_oauth.html │ │ │ ├── instagram_oauth.html │ │ │ ├── linkedin_oauth.html │ │ │ ├── mailru_oauth.html │ │ │ ├── microsoft_oauth.html │ │ │ ├── recaptcha.html │ │ │ ├── reddit_oauth.html │ │ │ ├── security.html │ │ │ ├── twitter_oauth.html │ │ │ ├── vk_oauth.html │ │ │ └── yahoo_oauth.html │ │ └── test │ │ │ ├── test.html │ │ │ ├── test_alert.html │ │ │ ├── test_badge.html │ │ │ ├── test_button.html │ │ │ ├── test_filter.html │ │ │ ├── test_font.html │ │ │ ├── test_form.html │ │ │ ├── test_grid.html │ │ │ ├── test_heading.html │ │ │ ├── test_label.html │ │ │ ├── test_one.html │ │ │ ├── test_pageres.html │ │ │ ├── test_pagination.html │ │ │ ├── test_paragraph.html │ │ │ ├── test_responsive.html │ │ │ ├── test_social.html │ │ │ └── test_table.html │ ├── auth │ │ ├── auth.html │ │ ├── signin_form.html │ │ └── signup_form.html │ ├── base.html │ ├── bit │ │ ├── analytics.html │ │ ├── announcement.html │ │ ├── contact_menu.html │ │ ├── footer.html │ │ ├── header.html │ │ ├── meta.html │ │ ├── notifications.html │ │ ├── script.html │ │ ├── style.html │ │ └── user_menu.html │ ├── error.html │ ├── error_static.html │ ├── feedback.html │ ├── macro │ │ ├── forms.html │ │ └── utils.html │ ├── profile │ │ ├── profile.html │ │ ├── profile_base.html │ │ ├── profile_password.html │ │ └── profile_update.html │ ├── resource │ │ ├── resource_list.html │ │ ├── resource_update.html │ │ ├── resource_upload.html │ │ └── resource_view.html │ ├── sitemap.xml │ ├── user │ │ ├── user_activate.html │ │ ├── user_email_field.html │ │ ├── user_forgot.html │ │ ├── user_list.html │ │ ├── user_merge.html │ │ ├── user_reset.html │ │ └── user_update.html │ └── welcome.html └── util.py ├── package-lock.json ├── package.json ├── requirements.txt └── run.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | node: true 4 | 5 | plugins: 6 | - prettier 7 | 8 | extends: 9 | - prettier 10 | 11 | parserOptions: 12 | ecmaVersion: 8 13 | sourceType: module 14 | ecmaFeatures: 15 | jsx: true 16 | 17 | rules: 18 | curly: 2 19 | dot-notation: 2 20 | id-length: 2 21 | no-const-assign: 2 22 | no-dupe-class-members: 2 23 | no-else-return: 2 24 | no-inner-declarations: 2 25 | no-lonely-if: 2 26 | no-magic-numbers: [2, {ignore: [-1, 0, 1]}] 27 | no-shadow: 2 28 | no-unneeded-ternary: 2 29 | no-unused-expressions: 2 30 | no-unused-vars: [2, {args: none}] 31 | no-useless-return: 2 32 | no-var: 2 33 | one-var: [2, never] 34 | prefer-arrow-callback: 2 35 | prefer-const: 2 36 | prefer-promise-reject-errors: 2 37 | prettier/prettier: 2 38 | sort-imports: 2 39 | sort-keys: [2, asc, {caseSensitive: true, natural: true}] 40 | sort-vars: 2 41 | strict: [2, global] 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | .DS_Store 4 | .git* 5 | .hg* 6 | .idea 7 | bower_components 8 | main/index.yaml 9 | main/lib 10 | main/lib.zip 11 | main/static/dev 12 | main/static/ext 13 | main/static/min 14 | node_modules 15 | temp/ 16 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | bracketSpacing: false 2 | jsxBracketSameLine: false 3 | printWidth: 80 4 | proseWrap: never 5 | requirePragma: false 6 | semi: true 7 | singleQuote: true 8 | tabWidth: 2 9 | trailingComma: all 10 | useTabs: false 11 | 12 | overrides: 13 | - files: "*.json" 14 | options: 15 | printWidth: 200 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Panayiotis Lipiridis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gae-init-upload 2 | 3 | [![Slack Status](https://gae-init-slack.herokuapp.com/badge.svg)](https://gae-init-slack.herokuapp.com) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 4 | 5 | > **gae-init-upload** is the easiest boilerplate to kick start new applications on Google App Engine using Python, Flask, RESTful, Bootstrap, Google Cloud Storage and tons of other cool features. 6 | 7 | Read the [documentation][], where you can find a complete [feature list][], a detailed [tutorial][], the [how to][] section and more.. 8 | 9 | The latest version is always accessible from [http://upload.gae-init.appspot.com](http://upload.gae-init.appspot.com) 10 | 11 | ## Requirements 12 | 13 | * [Google App Engine SDK for Python][] 14 | * [Node.js][], [pip][], [virtualenv][] 15 | * [macOS][] or [Linux][] or [Windows][] 16 | 17 | Make sure you have all of the above or refer to the docs on how to [install the requirements](http://docs.gae-init.appspot.com/requirement/). 18 | 19 | ## Running the Development Environment 20 | 21 | ```bash 22 | $ cd /path/to/project-name 23 | $ gulp 24 | ``` 25 | 26 | To test it visit `http://localhost:3000` in your browser. 27 | 28 | --- 29 | 30 | For a complete list of commands: 31 | 32 | ```bash 33 | $ gulp help 34 | ``` 35 | 36 | ## Initializing or Resetting the project 37 | 38 | ```bash 39 | $ cd /path/to/project-name 40 | $ npm install 41 | $ gulp 42 | ``` 43 | 44 | If something goes wrong you can always do: 45 | 46 | ```bash 47 | $ gulp reset 48 | $ npm install 49 | $ gulp 50 | ``` 51 | 52 | --- 53 | 54 | To install [Gulp][] as a global package: 55 | 56 | ```bash 57 | $ npm install -g gulp 58 | ``` 59 | 60 | ## Deploying on Google App Engine 61 | 62 | ```bash 63 | $ gulp deploy 64 | $ gulp deploy --project=foo 65 | $ gulp deploy --project=foo --version=bar 66 | $ gulp deploy --project=foo --version=bar --no-promote 67 | ``` 68 | 69 | ## Tech Stack 70 | 71 | * [Google App Engine][], [NDB][] 72 | * [Jinja2][], [Flask][], [Flask-RESTful][], [Flask-WTF][] 73 | * [CoffeeScript][], [Less][] 74 | * [Bootstrap][], [Font Awesome][], [Social Buttons][] 75 | * [jQuery][], [Moment.js][] 76 | * [OpenID][] sign in (Google, Facebook, Twitter and more) 77 | * [Python 2.7][], [pip][], [virtualenv][] 78 | * [Gulp][], [Bower][] 79 | 80 | [bootstrap]: http://getbootstrap.com/ 81 | [bower]: http://bower.io/ 82 | [coffeescript]: http://coffeescript.org/ 83 | [documentation]: http://docs.gae-init.appspot.com 84 | [feature list]: http://docs.gae-init.appspot.com/features/ 85 | [flask-restful]: https://flask-restful.readthedocs.org 86 | [flask-wtf]: https://flask-wtf.readthedocs.org 87 | [flask]: http://flask.pocoo.org/ 88 | [font awesome]: http://fortawesome.github.com/Font-Awesome/ 89 | [google app engine sdk for python]: https://developers.google.com/appengine/downloads 90 | [google app engine]: https://developers.google.com/appengine/ 91 | [gulp]: http://gulpjs.com 92 | [how to]: http://docs.gae-init.appspot.com/howto/ 93 | [jinja2]: http://jinja.pocoo.org/docs/ 94 | [jquery]: https://jquery.com/ 95 | [less]: http://lesscss.org/ 96 | [linux]: http://www.ubuntu.com 97 | [macos]: http://www.apple.com/macos/ 98 | [moment.js]: http://momentjs.com/ 99 | [ndb]: https://developers.google.com/appengine/docs/python/ndb/ 100 | [node.js]: http://nodejs.org/ 101 | [openid]: http://en.wikipedia.org/wiki/OpenID 102 | [pip]: http://www.pip-installer.org/ 103 | [python 2.7]: https://developers.google.com/appengine/docs/python/python27/using27 104 | [social buttons]: http://lipis.github.io/bootstrap-social/ 105 | [tutorial]: http://docs.gae-init.appspot.com/tutorial/ 106 | [virtualenv]: http://www.virtualenv.org/ 107 | [windows]: http://windows.microsoft.com/ 108 | -------------------------------------------------------------------------------- /bin/README.md: -------------------------------------------------------------------------------- 1 | # Command Line Scripts 2 | 3 | ## Requirements 4 | 5 | You can install the requirements easily by executing the following scripts using `curl` & `bash`: 6 | 7 | ### [Cloud Shell](https://cloud.google.com/shell/) 8 | 9 | All requirements are met, out of the box. 10 | 11 | ### macOS — [Homebrew](http://brew.sh/) 12 | 13 | ```bash 14 | curl https://raw.githubusercontent.com/gae-init/gae-init/master/bin/requirements_osx_brew.sh | bash 15 | ``` 16 | 17 | ### macOS — [MacPorts](https://www.macports.org/) 18 | 19 | ```bash 20 | curl https://raw.githubusercontent.com/gae-init/gae-init/master/bin/requirements_osx_port.sh | bash 21 | ``` 22 | 23 | ### Linux 24 | 25 | ```bash 26 | curl https://raw.githubusercontent.com/gae-init/gae-init/master/bin/requirements_linux.sh | bash 27 | ``` 28 | -------------------------------------------------------------------------------- /bin/requirements_linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Cloud SDK and App Engine 4 | curl https://sdk.cloud.google.com | bash 5 | gcloud components install app-engine-python 6 | 7 | # Node.js 8 | sudo apt-get install nodejs 9 | 10 | # Gulp.js 11 | sudo npm install -g gulp 12 | 13 | # Python related 14 | curl https://bootstrap.pypa.io/get-pip.py | sudo python 15 | sudo pip install virtualenv 16 | 17 | # Git 18 | sudo apt-get install git 19 | -------------------------------------------------------------------------------- /bin/requirements_osx_brew.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Cloud SDK and App Engine 4 | curl https://sdk.cloud.google.com | bash 5 | gcloud components install app-engine-python 6 | 7 | # Node.js 8 | brew install node 9 | 10 | # Gulp.js 11 | npm install -g gulp 12 | 13 | # Python related 14 | curl https://bootstrap.pypa.io/get-pip.py | python 15 | pip install virtualenv 16 | 17 | # Git 18 | brew install git 19 | -------------------------------------------------------------------------------- /bin/requirements_osx_port.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Cloud SDK and App Engine 4 | curl https://sdk.cloud.google.com | bash 5 | gcloud components install app-engine-python 6 | 7 | # Node.js 8 | sudo port install nodejs 9 | 10 | # Gulp.js 11 | npm install -g gulp 12 | 13 | # Python related 14 | curl https://bootstrap.pypa.io/get-pip.py | python 15 | pip install virtualenv 16 | 17 | # Git 18 | sudo port install git-core 19 | -------------------------------------------------------------------------------- /bin/test_hello_world.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Hello, World! 4 | cd ~ 5 | git clone https://github.com/gae-init/gae-init.git hello-world 6 | cd hello-world 7 | npm install 8 | gulp 9 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "bootstrap": "3.3.7", 4 | "font-awesome": "4.7.0", 5 | "jquery": "3.3.1", 6 | "moment": "2.20.1" 7 | }, 8 | "name": "gae-init", 9 | "overrides": { 10 | "bootstrap": { 11 | "main": ["less/**", "fonts/*", "js/*"] 12 | }, 13 | "font-awesome": { 14 | "main": ["less/*", "fonts/*"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gulp/config.coffee: -------------------------------------------------------------------------------- 1 | paths = require './paths' 2 | 3 | config = 4 | host: "127.0.0.1" 5 | port: "8080" 6 | ext: [ 7 | "#{paths.static.ext}/jquery/dist/jquery.js" 8 | "#{paths.static.ext}/moment/moment.js" 9 | "#{paths.static.ext}/bootstrap/js/alert.js" 10 | "#{paths.static.ext}/bootstrap/js/button.js" 11 | "#{paths.static.ext}/bootstrap/js/transition.js" 12 | "#{paths.static.ext}/bootstrap/js/collapse.js" 13 | "#{paths.static.ext}/bootstrap/js/dropdown.js" 14 | "#{paths.static.ext}/bootstrap/js/tooltip.js" 15 | ] 16 | style: [ 17 | "#{paths.src.style}/style.less" 18 | ] 19 | script: [ 20 | "#{paths.src.script}/**/*.coffee" 21 | "#{paths.src.script}/**/*.js" 22 | ] 23 | 24 | 25 | module.exports = config 26 | -------------------------------------------------------------------------------- /gulp/paths.coffee: -------------------------------------------------------------------------------- 1 | paths = 2 | main: 'main' 3 | 4 | dep: {} 5 | py: {} 6 | static: {} 7 | src: {} 8 | temp: {} 9 | 10 | paths.temp.root = 'temp' 11 | paths.temp.storage = "#{paths.temp.root}/storage" 12 | paths.temp.venv = "#{paths.temp.root}/venv" 13 | 14 | paths.dep.bower_components = 'bower_components' 15 | paths.dep.node_modules = 'node_modules' 16 | paths.dep.py = "#{paths.temp.root}/venv" 17 | paths.dep.py_guard = "#{paths.temp.root}/pip.guard" 18 | 19 | paths.py.lib = "#{paths.main}/lib" 20 | paths.py.lib_file = "#{paths.py.lib}.zip" 21 | 22 | paths.static.root = "#{paths.main}/static" 23 | paths.static.dev = "#{paths.static.root}/dev" 24 | paths.static.ext = "#{paths.static.root}/ext" 25 | paths.static.min = "#{paths.static.root}/min" 26 | 27 | paths.src.root = "#{paths.static.root}/src" 28 | paths.src.script = "#{paths.src.root}/script" 29 | paths.src.style = "#{paths.src.root}/style" 30 | 31 | 32 | module.exports = paths 33 | -------------------------------------------------------------------------------- /gulp/tasks/build.coffee: -------------------------------------------------------------------------------- 1 | gulp = require('gulp-help') require 'gulp' 2 | yargs = require 'yargs-parser' 3 | $ = require('gulp-load-plugins')() 4 | config = require '../config' 5 | paths = require '../paths' 6 | 7 | 8 | gulp.task 'build', 9 | "Build project to prepare it for a deployment. Minify CSS & JS files and pack 10 | Python dependencies into #{paths.py.lib_file}.", 11 | $.sequence 'clean:min', 'init', 'ext', ['script', 'style', 'zip'] 12 | 13 | 14 | gulp.task 'rebuild', 15 | 'Re-build project from scratch. Equivalent to "reset" and "build" tasks.', 16 | $.sequence 'reset', 'build' 17 | 18 | 19 | gulp.task 'deploy', 'Deploy project to Google App Engine.', ['build'], -> 20 | options = yargs process.argv, configuration: 21 | 'boolean-negation': false 22 | 'camel-case-expansion': false 23 | delete options['_'] 24 | options_str = '' 25 | for k of options 26 | if options[k] == true 27 | options[k] = '' 28 | options_str += " #{if k.length > 1 then '-' else ''}-#{k} #{options[k]}" 29 | 30 | gulp.src('run.py').pipe $.start [{ 31 | match: /run.py$/ 32 | cmd: "gcloud app deploy main/*.yaml#{options_str}" 33 | }] 34 | 35 | 36 | gulp.task 'run', 37 | 'Start the local server. Available options:\n 38 | -o HOST - the host to start the dev_appserver.py\n 39 | -p PORT - the port to start the dev_appserver.py\n 40 | -a="..." - all following args are passed to dev_appserver.py\n', -> 41 | $.sequence('init', ['ext:dev', 'script:dev', 'style:dev']) -> 42 | argv = process.argv.slice 2 43 | 44 | known_options = 45 | default: 46 | p: '' 47 | o: '' 48 | a: '' 49 | 50 | options = yargs(argv) 51 | options_str = '-s' 52 | for k of known_options.default 53 | if options[k] 54 | if k == 'a' 55 | options_str += " --appserver-args \"#{options[k]}\"" 56 | else 57 | options_str += " -#{k} #{options[k]}" 58 | 59 | if options['p'] 60 | config.port = options['p'] 61 | if options['o'] 62 | config.host = options['o'] 63 | 64 | gulp.start('browser-sync') 65 | gulp.src('run.py').pipe $.start [{ 66 | match: /run.py$/ 67 | cmd: "python run.py #{options_str}" 68 | }] 69 | -------------------------------------------------------------------------------- /gulp/tasks/clean.coffee: -------------------------------------------------------------------------------- 1 | del = require 'del' 2 | gulp = require('gulp-help') require 'gulp' 3 | paths = require '../paths' 4 | 5 | 6 | gulp.task 'clean', 7 | 'Clean project from temporary files, generated CSS & JS and compiled Python 8 | files.', -> 9 | del './**/*.pyc' 10 | del './**/*.pyo' 11 | del './**/*.~' 12 | 13 | 14 | gulp.task 'clean:dev', false, -> 15 | del paths.static.ext 16 | del paths.static.dev 17 | 18 | 19 | gulp.task 'clean:min', false, -> 20 | del paths.static.ext 21 | del paths.static.min 22 | 23 | 24 | gulp.task 'clean:venv', false, -> 25 | del paths.py.lib 26 | del paths.py.lib_file 27 | del paths.dep.py 28 | del paths.dep.py_guard 29 | 30 | 31 | gulp.task 'reset', 32 | 'Complete reset of project. Run "npm install" after this procedure.', 33 | ['clean', 'clean:dev', 'clean:min', 'clean:venv'], -> 34 | del paths.dep.bower_components 35 | del paths.dep.node_modules 36 | 37 | 38 | gulp.task 'flush', 'Clear local datastore, blobstore, etc.', -> 39 | del paths.temp.storage 40 | -------------------------------------------------------------------------------- /gulp/tasks/dep.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | gulp = require('gulp-help') require 'gulp' 3 | main_bower_files = require 'main-bower-files' 4 | $ = require('gulp-load-plugins')() 5 | paths = require '../paths' 6 | 7 | 8 | gulp.task 'npm', false, -> 9 | gulp.src 'package.json' 10 | .pipe $.plumber() 11 | .pipe $.start() 12 | 13 | 14 | gulp.task 'bower', false, -> 15 | cmd = 'node_modules/.bin/bower install' 16 | if /^win/.test process.platform 17 | cmd = cmd.replace /\//g, '\\' 18 | start_map = [{match: /bower.json$/, cmd: cmd}] 19 | gulp.src 'bower.json' 20 | .pipe $.plumber() 21 | .pipe $.start start_map 22 | 23 | 24 | gulp.task 'copy_bower_files', false, ['bower'], -> 25 | gulp.src main_bower_files(), base: paths.dep.bower_components 26 | .pipe gulp.dest paths.static.ext 27 | 28 | 29 | gulp.task 'pip', false, -> 30 | gulp.src('run.py').pipe $.start [{match: /run.py$/, cmd: 'python run.py -d'}] 31 | 32 | 33 | gulp.task 'zip', false, -> 34 | fs.exists paths.py.lib_file, (exists) -> 35 | if not exists 36 | fs.exists paths.py.lib, (exists) -> 37 | if exists 38 | gulp.src "#{paths.py.lib}/**" 39 | .pipe $.plumber() 40 | .pipe $.zip 'lib.zip' 41 | .pipe gulp.dest paths.main 42 | 43 | 44 | gulp.task 'init', false, $.sequence 'pip', 'copy_bower_files' 45 | -------------------------------------------------------------------------------- /gulp/tasks/ext.coffee: -------------------------------------------------------------------------------- 1 | gulp = require('gulp-help') require 'gulp' 2 | $ = require('gulp-load-plugins')() 3 | uglify = require('gulp-uglify-es').default 4 | config = require '../config' 5 | paths = require '../paths' 6 | util = require '../util' 7 | 8 | 9 | gulp.task 'ext', false, -> 10 | gulp.src config.ext 11 | .pipe $.plumber errorHandler: util.onError 12 | .pipe $.concat 'ext.js' 13 | .pipe uglify() 14 | .pipe $.size {title: 'Minified ext libs'} 15 | .pipe gulp.dest "#{paths.static.min}/script" 16 | 17 | 18 | gulp.task 'ext:dev', false, -> 19 | gulp.src config.ext 20 | .pipe $.plumber errorHandler: util.onError 21 | .pipe $.sourcemaps.init() 22 | .pipe $.concat 'ext.js' 23 | .pipe $.sourcemaps.write() 24 | .pipe gulp.dest "#{paths.static.dev}/script" 25 | -------------------------------------------------------------------------------- /gulp/tasks/script.coffee: -------------------------------------------------------------------------------- 1 | gulp = require('gulp-help') require 'gulp' 2 | $ = require('gulp-load-plugins')() 3 | uglify = require('gulp-uglify-es').default 4 | config = require '../config' 5 | paths = require '../paths' 6 | util = require '../util' 7 | 8 | 9 | is_coffee = (file) -> 10 | return file.path.indexOf('.coffee') > 0 11 | 12 | 13 | gulp.task 'script', false, -> 14 | gulp.src config.script 15 | .pipe $.plumber errorHandler: util.onError 16 | .pipe $.if is_coffee, $.coffee() 17 | .pipe $.concat 'script.js' 18 | .pipe $.babel presets: ['es2015'] 19 | .pipe uglify() 20 | .pipe $.size {title: 'Minified scripts'} 21 | .pipe gulp.dest "#{paths.static.min}/script" 22 | 23 | 24 | gulp.task 'script:dev', false, -> 25 | gulp.src config.script 26 | .pipe $.plumber errorHandler: util.onError 27 | .pipe $.sourcemaps.init() 28 | .pipe $.if is_coffee, $.coffee() 29 | .pipe $.concat 'script.js' 30 | .pipe $.sourcemaps.write() 31 | .pipe gulp.dest "#{paths.static.dev}/script" 32 | -------------------------------------------------------------------------------- /gulp/tasks/style.coffee: -------------------------------------------------------------------------------- 1 | gulp = require('gulp-help') require 'gulp' 2 | $ = require('gulp-load-plugins')() 3 | config = require '../config' 4 | paths = require '../paths' 5 | util = require '../util' 6 | 7 | 8 | gulp.task 'style', false, -> 9 | gulp.src config.style 10 | .pipe $.plumber errorHandler: util.onError 11 | .pipe $.less() 12 | .pipe $.cssnano 13 | discardComments: removeAll: true 14 | zindex: false 15 | .pipe $.size {title: 'Minified styles'} 16 | .pipe gulp.dest "#{paths.static.min}/style" 17 | 18 | 19 | gulp.task 'style:dev', false, -> 20 | gulp.src config.style 21 | .pipe $.plumber errorHandler: util.onError 22 | .pipe $.sourcemaps.init() 23 | .pipe $.less() 24 | .pipe $.autoprefixer {map: true} 25 | .pipe $.sourcemaps.write() 26 | .pipe gulp.dest "#{paths.static.dev}/style" 27 | -------------------------------------------------------------------------------- /gulp/tasks/watch.coffee: -------------------------------------------------------------------------------- 1 | gulp = require('gulp-help') require 'gulp' 2 | browserSync = require('browser-sync') 3 | $ = require('gulp-load-plugins')() 4 | config = require '../config' 5 | paths = require '../paths' 6 | 7 | 8 | gulp.task 'browser-sync', false, -> 9 | browserSync.init 10 | proxy: "#{config.host}:#{config.port}" 11 | notify: false 12 | $.watch [ 13 | "#{paths.static.dev}/**/*.{css,js}" 14 | "#{paths.main}/**/*.{html,py}" 15 | ], events: ['change'], (file) -> 16 | browserSync.reload() 17 | 18 | 19 | gulp.task 'ext_watch_rebuild', false, (callback) -> 20 | $.sequence('clean:dev', 'copy_bower_files', 'ext:dev', 'style:dev') callback 21 | 22 | 23 | gulp.task 'watch', false, -> 24 | $.watch 'requirements.txt', -> 25 | $.sequence('pip')() 26 | $.watch 'package.json', -> 27 | $.sequence('npm')() 28 | $.watch 'bower.json', -> 29 | $.sequence('ext_watch_rebuild')() 30 | $.watch 'gulp/config.coffee', -> 31 | $.sequence('ext:dev', ['style:dev', 'script:dev'])() 32 | $.watch paths.static.ext, -> 33 | $.sequence('ext:dev')() 34 | $.watch "#{paths.src.script}/**/*.{coffee,js}", -> 35 | $.sequence('script:dev')() 36 | $.watch "#{paths.src.style}/**/*.less", -> 37 | $.sequence('style:dev')() 38 | -------------------------------------------------------------------------------- /gulp/util.coffee: -------------------------------------------------------------------------------- 1 | $ = require('gulp-load-plugins')() 2 | 3 | 4 | onError = (err) -> 5 | $.util.beep() 6 | console.log err 7 | this.emit 'end' 8 | 9 | 10 | module.exports = {onError} 11 | -------------------------------------------------------------------------------- /gulpfile.coffee: -------------------------------------------------------------------------------- 1 | gulp = require('gulp-help') require 'gulp' 2 | requireDir = require('require-dir') './gulp/tasks' 3 | $ = require('gulp-load-plugins')() 4 | 5 | 6 | gulp.task 'default', 7 | 'Start the local server, watch for changes and reload browser automatically. 8 | For available options refer to "run" task.', 9 | $.sequence 'run', ['watch'] 10 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | require('coffee-script/register'); 2 | require('./gulpfile.coffee'); 3 | -------------------------------------------------------------------------------- /main/api/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | -------------------------------------------------------------------------------- /main/api/fields.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import urllib 4 | 5 | from flask_restful import fields 6 | from flask_restful.fields import * 7 | 8 | 9 | class BlobKey(fields.Raw): 10 | def format(self, value): 11 | return urllib.quote(str(value)) 12 | 13 | 14 | class Blob(fields.Raw): 15 | def format(self, value): 16 | return repr(value) 17 | 18 | 19 | class DateTime(fields.DateTime): 20 | def format(self, value): 21 | return value.isoformat() 22 | 23 | 24 | class GeoPt(fields.Raw): 25 | def format(self, value): 26 | return '%s,%s' % (value.lat, value.lon) 27 | 28 | 29 | class Id(fields.Raw): 30 | def output(self, key, obj): 31 | try: 32 | value = getattr(obj, 'key', None).id() 33 | return super(Id, self).output(key, {'id': value}) 34 | except AttributeError: 35 | return None 36 | 37 | 38 | class Integer(fields.Integer): 39 | def format(self, value): 40 | if value > 9007199254740992 or value < -9007199254740992: 41 | return str(value) 42 | return value 43 | 44 | 45 | class Key(fields.Raw): 46 | def format(self, value): 47 | return value.urlsafe() 48 | -------------------------------------------------------------------------------- /main/api/helpers.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from datetime import datetime 4 | import logging 5 | 6 | from werkzeug import exceptions 7 | import flask 8 | import flask_restful 9 | 10 | import util 11 | 12 | 13 | class Api(flask_restful.Api): 14 | def unauthorized(self, response): 15 | flask.abort(401) 16 | 17 | def handle_error(self, e): 18 | return handle_error(e) 19 | 20 | 21 | def handle_error(e): 22 | logging.exception(e) 23 | try: 24 | e.code 25 | except AttributeError: 26 | e.code = 500 27 | e.name = e.description = 'Internal Server Error' 28 | return util.jsonpify({ 29 | 'status': 'error', 30 | 'error_code': e.code, 31 | 'error_name': util.slugify(e.name), 32 | 'error_message': e.name, 33 | 'error_class': e.__class__.__name__, 34 | 'description': e.description, 35 | }), e.code 36 | 37 | 38 | def make_response(data, marshal_table, cursors=None): 39 | if util.is_iterable(data): 40 | response = { 41 | 'status': 'success', 42 | 'count': len(data), 43 | 'now': datetime.utcnow().isoformat(), 44 | 'result': [flask_restful.marshal(d, marshal_table) for d in data], 45 | } 46 | if cursors: 47 | if isinstance(cursors, dict): 48 | if cursors.get('next'): 49 | response['next_cursor'] = cursors['next'] 50 | response['next_url'] = util.generate_next_url(cursors['next']) 51 | if cursors.get('prev'): 52 | response['prev_cursor'] = cursors['prev'] 53 | response['prev_url'] = util.generate_next_url(cursors['prev']) 54 | else: 55 | response['next_cursor'] = cursors 56 | response['next_url'] = util.generate_next_url(cursors) 57 | return util.jsonpify(response) 58 | return util.jsonpify({ 59 | 'status': 'success', 60 | 'now': datetime.utcnow().isoformat(), 61 | 'result': flask_restful.marshal(data, marshal_table), 62 | }) 63 | 64 | 65 | def make_not_found_exception(description): 66 | exception = exceptions.NotFound() 67 | exception.description = description 68 | raise exception 69 | -------------------------------------------------------------------------------- /main/api/v1/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from .auth import * 4 | from .config import * 5 | from .user import * 6 | from .resource import * 7 | -------------------------------------------------------------------------------- /main/api/v1/auth.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from webargs.flaskparser import parser 6 | from webargs import fields as wf 7 | import flask 8 | import flask_restful 9 | 10 | from api import helpers 11 | import auth 12 | import model 13 | import util 14 | 15 | from main import api_v1 16 | 17 | 18 | @api_v1.resource('/auth/signin/', endpoint='api.auth.signin') 19 | class AuthAPI(flask_restful.Resource): 20 | def post(self): 21 | args = parser.parse({ 22 | 'username': wf.Str(missing=None), 23 | 'email': wf.Str(missing=None), 24 | 'password': wf.Str(missing=None), 25 | }) 26 | handler = args['username'] or args['email'] 27 | password = args['password'] 28 | if not handler or not password: 29 | return flask.abort(400) 30 | 31 | user_db = model.User.get_by( 32 | 'email' if '@' in handler else 'username', handler.lower() 33 | ) 34 | 35 | if user_db and user_db.password_hash == util.password_hash(user_db, password): 36 | auth.signin_user_db(user_db) 37 | return helpers.make_response(user_db, model.User.FIELDS) 38 | return flask.abort(401) 39 | -------------------------------------------------------------------------------- /main/api/v1/config.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import flask_restful 6 | 7 | from api import helpers 8 | import auth 9 | import config 10 | import model 11 | 12 | from main import api_v1 13 | 14 | 15 | @api_v1.resource('/admin/config/', endpoint='api.admin.config') 16 | class ConfigAPI(flask_restful.Resource): 17 | @auth.admin_required 18 | def get(self): 19 | return helpers.make_response(config.CONFIG_DB, model.Config.FIELDS) 20 | -------------------------------------------------------------------------------- /main/api/v1/resource.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from google.appengine.api import images 6 | from google.appengine.ext import blobstore 7 | from google.appengine.ext import ndb 8 | import flask 9 | import flask_restful 10 | import werkzeug 11 | 12 | from api import helpers 13 | import auth 14 | import config 15 | import model 16 | import util 17 | 18 | from main import api_v1 19 | 20 | 21 | ############################################################################### 22 | # Endpoints 23 | ############################################################################### 24 | @api_v1.resource('/resource/', endpoint='api.resource.list') 25 | class ResourceListAPI(flask_restful.Resource): 26 | @auth.admin_required 27 | def get(self): 28 | resource_keys = util.param('resource_keys', list) 29 | if resource_keys: 30 | resource_db_keys = [ndb.Key(urlsafe=k) for k in resource_keys] 31 | resource_dbs = ndb.get_multi(resource_db_keys) 32 | return helpers.make_response(resource_dbs, model.Resource.FIELDS) 33 | 34 | resource_dbs, next_cursor = model.Resource.get_dbs() 35 | return helpers.make_response( 36 | resource_dbs, model.Resource.FIELDS, next_cursor, 37 | ) 38 | 39 | @auth.admin_required 40 | def delete(self): 41 | resource_keys = util.param('resource_keys', list) 42 | if not resource_keys: 43 | helpers.make_not_found_exception( 44 | 'Resource(s) %s not found' % resource_keys 45 | ) 46 | resource_db_keys = [ndb.Key(urlsafe=k) for k in resource_keys] 47 | delete_resource_dbs(resource_db_keys) 48 | return flask.jsonify({ 49 | 'result': resource_keys, 50 | 'status': 'success', 51 | }) 52 | 53 | 54 | @api_v1.resource('/resource//', endpoint='api.resource') 55 | class ResourceAPI(flask_restful.Resource): 56 | @auth.login_required 57 | def get(self, key): 58 | resource_db = ndb.Key(urlsafe=key).get() 59 | if not resource_db and resource_db.user_key != auth.current_user_key(): 60 | helpers.make_not_found_exception('Resource %s not found' % key) 61 | return helpers.make_response(resource_db, model.Resource.FIELDS) 62 | 63 | @auth.login_required 64 | def delete(self, key): 65 | resource_db = ndb.Key(urlsafe=key).get() 66 | if not resource_db or resource_db.user_key != auth.current_user_key(): 67 | helpers.make_not_found_exception('Resource %s not found' % key) 68 | delete_resource_key(resource_db.key) 69 | return helpers.make_response(resource_db, model.Resource.FIELDS) 70 | 71 | 72 | @api_v1.resource('/resource/upload/', endpoint='api.resource.upload') 73 | class ResourceUploadAPI(flask_restful.Resource): 74 | @auth.login_required 75 | def get(self): 76 | count = util.param('count', int) or 1 77 | urls = [] 78 | for i in range(count): 79 | urls.append({'upload_url': blobstore.create_upload_url( 80 | flask.request.path, 81 | gs_bucket_name=config.CONFIG_DB.bucket_name or None, 82 | )}) 83 | return flask.jsonify({ 84 | 'status': 'success', 85 | 'count': count, 86 | 'result': urls, 87 | }) 88 | 89 | @auth.login_required 90 | def post(self): 91 | resource_db = resource_db_from_upload() 92 | if resource_db: 93 | return helpers.make_response(resource_db, model.Resource.FIELDS) 94 | flask.abort(500) 95 | 96 | 97 | ############################################################################### 98 | # Helpers 99 | ############################################################################### 100 | def delete_resource_dbs(resource_db_keys): 101 | ndb.delete_multi(resource_db_keys) 102 | 103 | 104 | def resource_db_from_upload(): 105 | try: 106 | uploaded_file = flask.request.files['file'] 107 | except: 108 | return None 109 | headers = uploaded_file.headers['Content-Type'] 110 | blob_info_key = werkzeug.parse_options_header(headers)[1]['blob-key'] 111 | blob_info = blobstore.BlobInfo.get(blob_info_key) 112 | 113 | image_url = None 114 | if blob_info.content_type.startswith('image'): 115 | try: 116 | image_url = images.get_serving_url(blob_info.key()) 117 | except: 118 | pass 119 | 120 | resource_db = model.Resource( 121 | user_key=auth.current_user_key(), 122 | blob_key=blob_info.key(), 123 | name=blob_info.filename, 124 | content_type=blob_info.content_type, 125 | size=blob_info.size, 126 | image_url=image_url, 127 | bucket_name=config.CONFIG_DB.bucket_name or None, 128 | ) 129 | resource_db.put() 130 | return resource_db 131 | -------------------------------------------------------------------------------- /main/api/v1/user.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import logging 6 | 7 | from google.appengine.ext import blobstore 8 | from google.appengine.ext import deferred 9 | from google.appengine.ext import ndb 10 | import flask 11 | import flask_restful 12 | 13 | from api import helpers 14 | import auth 15 | import model 16 | import util 17 | 18 | from main import api_v1 19 | 20 | 21 | @api_v1.resource('/admin/user/', endpoint='api.admin.user.list') 22 | class AdminUserListAPI(flask_restful.Resource): 23 | @auth.admin_required 24 | def get(self): 25 | user_keys = util.param('user_keys', list) 26 | if user_keys: 27 | user_db_keys = [ndb.Key(urlsafe=k) for k in user_keys] 28 | user_dbs = ndb.get_multi(user_db_keys) 29 | return helpers.make_response(user_dbs, model.User.FIELDS) 30 | 31 | user_dbs, cursors = model.User.get_dbs(prev_cursor=True) 32 | return helpers.make_response(user_dbs, model.User.FIELDS, cursors) 33 | 34 | @auth.admin_required 35 | def delete(self): 36 | user_keys = util.param('user_keys', list) 37 | if not user_keys: 38 | helpers.make_not_found_exception('User(s) %s not found' % user_keys) 39 | user_db_keys = [ndb.Key(urlsafe=k) for k in user_keys] 40 | delete_user_dbs(user_db_keys) 41 | return flask.jsonify({ 42 | 'result': user_keys, 43 | 'status': 'success', 44 | }) 45 | 46 | 47 | @api_v1.resource('/admin/user//', endpoint='api.admin.user') 48 | class AdminUserAPI(flask_restful.Resource): 49 | @auth.admin_required 50 | def get(self, user_key): 51 | user_db = ndb.Key(urlsafe=user_key).get() 52 | if not user_db: 53 | helpers.make_not_found_exception('User %s not found' % user_key) 54 | return helpers.make_response(user_db, model.User.FIELDS) 55 | 56 | @auth.admin_required 57 | def delete(self, user_key): 58 | user_db = ndb.Key(urlsafe=user_key).get() 59 | if not user_db: 60 | helpers.make_not_found_exception('User %s not found' % user_key) 61 | delete_user_dbs([user_db.key]) 62 | return helpers.make_response(user_db, model.User.FIELDS) 63 | 64 | 65 | ############################################################################### 66 | # Helpers 67 | ############################################################################### 68 | def delete_user_dbs(user_db_keys): 69 | ndb.delete_multi(user_db_keys) 70 | -------------------------------------------------------------------------------- /main/app.yaml: -------------------------------------------------------------------------------- 1 | service: default 2 | instance_class: F1 3 | runtime: python27 4 | api_version: 1 5 | threadsafe: true 6 | 7 | builtins: 8 | - appstats: on 9 | - deferred: on 10 | - remote_api: on 11 | 12 | inbound_services: 13 | - warmup 14 | 15 | libraries: 16 | - name: jinja2 17 | version: latest 18 | - name: PIL 19 | version: latest 20 | 21 | error_handlers: 22 | - file: templates/error_static.html 23 | 24 | handlers: 25 | - url: /favicon.ico 26 | static_files: static/img/favicon.ico 27 | upload: static/img/favicon.ico 28 | 29 | - url: /robots.txt 30 | static_files: static/robots.txt 31 | upload: static/robots.txt 32 | 33 | - url: /p/(.*\.ttf) 34 | static_files: static/\1 35 | upload: static/(.*\.ttf) 36 | mime_type: font/ttf 37 | expiration: "365d" 38 | 39 | - url: /p/(.*\.woff2) 40 | static_files: static/\1 41 | upload: static/(.*\.woff2) 42 | mime_type: font/woff2 43 | expiration: "365d" 44 | 45 | - url: /p/ 46 | static_dir: static/ 47 | expiration: "365d" 48 | 49 | - url: /serve/.* 50 | script: control.serve.app 51 | 52 | - url: /.* 53 | script: main.app 54 | secure: always 55 | redirect_http_response_code: 301 56 | 57 | skip_files: 58 | - ^(.*/)?#.*# 59 | - ^(.*/)?.*/RCS/.* 60 | - ^(.*/)?.*\.bak$ 61 | - ^(.*/)?.*\.py[co] 62 | - ^(.*/)?.*~ 63 | - ^(.*/)?Icon\r 64 | - ^(.*/)?\..* 65 | - ^(.*/)?app\.yaml 66 | - ^(.*/)?app\.yml 67 | - ^(.*/)?index\.yaml 68 | - ^(.*/)?index\.yml 69 | - ^lib/.* 70 | - ^static/dev/.* 71 | - ^static/ext/.*\.coffee 72 | - ^static/ext/.*\.css 73 | - ^static/ext/.*\.js 74 | - ^static/ext/.*\.less 75 | - ^static/ext/.*\.json 76 | - ^static/src/.* 77 | -------------------------------------------------------------------------------- /main/appengine_config.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | import sys 5 | import tempfile 6 | 7 | from path_util import sys_path_insert 8 | 9 | tempfile.SpooledTemporaryFile = tempfile.TemporaryFile 10 | 11 | if os.environ.get('SERVER_SOFTWARE', '').startswith('Google App Engine'): 12 | sys_path_insert('lib.zip') 13 | else: 14 | if os.name == 'nt': 15 | os.name = None 16 | sys.platform = '' 17 | 18 | import re 19 | from google.appengine.tools.devappserver2.python import runtime 20 | 21 | re_ = runtime.stubs.FakeFile._skip_files.pattern.replace('|^lib/.*', '') 22 | re_ = re.compile(re_) 23 | runtime.stubs.FakeFile._skip_files = re_ 24 | sys_path_insert('lib') 25 | 26 | sys_path_insert('libx') 27 | 28 | 29 | def webapp_add_wsgi_middleware(app): 30 | from google.appengine.ext.appstats import recording 31 | app = recording.appstats_wsgi_middleware(app) 32 | return app 33 | -------------------------------------------------------------------------------- /main/auth/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from .auth import * 4 | from .azure_ad import * 5 | from .bitbucket import * 6 | from .dropbox import * 7 | from .facebook import * 8 | from .github import * 9 | from .gae import * 10 | from .google import * 11 | from .instagram import * 12 | from .linkedin import * 13 | from .mailru import * 14 | from .microsoft import * 15 | from .reddit import * 16 | from .twitter import * 17 | from .vk import * 18 | from .yahoo import * 19 | -------------------------------------------------------------------------------- /main/auth/azure_ad.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import flask 6 | import jwt 7 | 8 | import auth 9 | import config 10 | import util 11 | 12 | from main import app 13 | 14 | 15 | azure_ad_config = dict( 16 | access_token_method='POST', 17 | access_token_url='https://login.microsoftonline.com/common/oauth2/token', 18 | authorize_url='https://login.microsoftonline.com/common/oauth2/authorize', 19 | base_url='', 20 | consumer_key=config.CONFIG_DB.azure_ad_client_id, 21 | consumer_secret=config.CONFIG_DB.azure_ad_client_secret, 22 | request_token_params={ 23 | 'scope': 'openid profile user_impersonation', 24 | }, 25 | ) 26 | 27 | azure_ad = auth.create_oauth_app(azure_ad_config, 'azure_ad') 28 | 29 | 30 | @app.route('/api/auth/callback/azure_ad/') 31 | def azure_ad_authorized(): 32 | response = azure_ad.authorized_response() 33 | print response 34 | if response is None: 35 | flask.flash('You denied the request to sign in.') 36 | return flask.redirect(util.get_next_url) 37 | id_token = response['id_token'] 38 | flask.session['oauth_token'] = (id_token, '') 39 | try: 40 | decoded_id_token = jwt.decode(id_token, verify=False) 41 | except (jwt.DecodeError, jwt.ExpiredSignature): 42 | flask.flash('You denied the request to sign in.') 43 | return flask.redirect(util.get_next_url) 44 | user_db = retrieve_user_from_azure_ad(decoded_id_token) 45 | return auth.signin_user_db(user_db) 46 | 47 | 48 | @azure_ad.tokengetter 49 | def get_azure_ad_oauth_token(): 50 | return flask.session.get('oauth_token') 51 | 52 | 53 | @app.route('/signin/azure_ad/') 54 | def signin_azure_ad(): 55 | return auth.signin_oauth(azure_ad) 56 | 57 | 58 | def retrieve_user_from_azure_ad(response): 59 | auth_id = 'azure_ad_%s' % response['oid'] 60 | email = response.get('upn', '') 61 | first_name = response.get('given_name', '') 62 | last_name = response.get('family_name', '') 63 | username = ' '.join((first_name, last_name)).strip() 64 | return auth.create_user_db( 65 | auth_id=auth_id, 66 | name='%s %s' % (first_name, last_name), 67 | username=email or username, 68 | email=email, 69 | verified=bool(email), 70 | ) 71 | -------------------------------------------------------------------------------- /main/auth/bitbucket.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import flask 6 | 7 | import auth 8 | import config 9 | import model 10 | import util 11 | 12 | from main import app 13 | 14 | bitbucket_config = dict( 15 | access_token_method='POST', 16 | access_token_url='https://bitbucket.org/site/oauth2/access_token', 17 | authorize_url='https://bitbucket.org/site/oauth2/authorize', 18 | base_url='https://api.bitbucket.org/2.0/', 19 | consumer_key=config.CONFIG_DB.bitbucket_key, 20 | consumer_secret=config.CONFIG_DB.bitbucket_secret, 21 | ) 22 | 23 | bitbucket = auth.create_oauth_app(bitbucket_config, 'bitbucket') 24 | 25 | 26 | @app.route('/api/auth/callback/bitbucket/') 27 | def bitbucket_authorized(): 28 | response = bitbucket.authorized_response() 29 | if response is None: 30 | flask.flash('You denied the request to sign in.') 31 | return flask.redirect(util.get_next_url()) 32 | 33 | flask.session['oauth_token'] = (response['access_token'], '') 34 | me = bitbucket.get('user') 35 | user_db = retrieve_user_from_bitbucket(me.data) 36 | return auth.signin_user_db(user_db) 37 | 38 | 39 | @bitbucket.tokengetter 40 | def get_bitbucket_oauth_token(): 41 | return flask.session.get('oauth_token') 42 | 43 | 44 | @app.route('/signin/bitbucket/') 45 | def signin_bitbucket(): 46 | return auth.signin_oauth(bitbucket) 47 | 48 | 49 | def retrieve_user_from_bitbucket(response): 50 | auth_id = 'bitbucket_%s' % response['username'] 51 | user_db = model.User.get_by('auth_ids', auth_id) 52 | if user_db: 53 | return user_db 54 | emails = bitbucket.get('user/emails').data['values'] 55 | email = ''.join([e['email'] for e in emails if e['is_primary']][0:1]) 56 | return auth.create_user_db( 57 | auth_id=auth_id, 58 | name=response['display_name'], 59 | username=response['username'], 60 | email=email, 61 | verified=bool(email), 62 | ) 63 | -------------------------------------------------------------------------------- /main/auth/dropbox.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import flask 6 | 7 | import auth 8 | import config 9 | import model 10 | import util 11 | 12 | from main import app 13 | 14 | dropbox_config = dict( 15 | access_token_method='POST', 16 | access_token_url='https://api.dropbox.com/1/oauth2/token', 17 | authorize_url='https://www.dropbox.com/1/oauth2/authorize', 18 | base_url='https://www.dropbox.com/1/', 19 | consumer_key=config.CONFIG_DB.dropbox_app_key, 20 | consumer_secret=config.CONFIG_DB.dropbox_app_secret, 21 | ) 22 | 23 | dropbox = auth.create_oauth_app(dropbox_config, 'dropbox') 24 | 25 | 26 | @app.route('/api/auth/callback/dropbox/') 27 | def dropbox_authorized(): 28 | response = dropbox.authorized_response() 29 | if response is None: 30 | flask.flash('You denied the request to sign in.') 31 | return flask.redirect(util.get_next_url()) 32 | flask.session['oauth_token'] = (response['access_token'], '') 33 | me = dropbox.get('account/info') 34 | user_db = retrieve_user_from_dropbox(me.data) 35 | return auth.signin_user_db(user_db) 36 | 37 | 38 | @dropbox.tokengetter 39 | def get_dropbox_oauth_token(): 40 | return flask.session.get('oauth_token') 41 | 42 | 43 | @app.route('/signin/dropbox/') 44 | def signin_dropbox(): 45 | return auth.signin_oauth(dropbox, 'https') 46 | 47 | 48 | def retrieve_user_from_dropbox(response): 49 | auth_id = 'dropbox_%s' % response['uid'] 50 | user_db = model.User.get_by('auth_ids', auth_id) 51 | if user_db: 52 | return user_db 53 | 54 | return auth.create_user_db( 55 | auth_id=auth_id, 56 | name=response['display_name'], 57 | username=response['display_name'], 58 | ) 59 | -------------------------------------------------------------------------------- /main/auth/facebook.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import flask 6 | 7 | import auth 8 | import config 9 | import model 10 | import util 11 | 12 | from main import app 13 | 14 | facebook_config = dict( 15 | access_token_url='/oauth/access_token', 16 | authorize_url='/oauth/authorize', 17 | base_url='https://graph.facebook.com/', 18 | consumer_key=config.CONFIG_DB.facebook_app_id, 19 | consumer_secret=config.CONFIG_DB.facebook_app_secret, 20 | request_token_params={'scope': 'email'}, 21 | ) 22 | 23 | facebook = auth.create_oauth_app(facebook_config, 'facebook') 24 | 25 | 26 | @app.route('/api/auth/callback/facebook/') 27 | def facebook_authorized(): 28 | response = facebook.authorized_response() 29 | if response is None: 30 | flask.flash('You denied the request to sign in.') 31 | return flask.redirect(util.get_next_url()) 32 | 33 | flask.session['oauth_token'] = (response['access_token'], '') 34 | me = facebook.get('/me?fields=name,email') 35 | user_db = retrieve_user_from_facebook(me.data) 36 | return auth.signin_user_db(user_db) 37 | 38 | 39 | @facebook.tokengetter 40 | def get_facebook_oauth_token(): 41 | return flask.session.get('oauth_token') 42 | 43 | 44 | @app.route('/signin/facebook/') 45 | def signin_facebook(): 46 | return auth.signin_oauth(facebook) 47 | 48 | 49 | def retrieve_user_from_facebook(response): 50 | auth_id = 'facebook_%s' % response['id'] 51 | user_db = model.User.get_by('auth_ids', auth_id) 52 | return user_db or auth.create_user_db( 53 | auth_id=auth_id, 54 | name=response['name'], 55 | username=response.get('username', response['name']), 56 | email=response.get('email', ''), 57 | verified=bool(response.get('email', '')), 58 | ) 59 | -------------------------------------------------------------------------------- /main/auth/gae.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from google.appengine.api import users 6 | import flask 7 | 8 | import auth 9 | import model 10 | import util 11 | 12 | from main import app 13 | 14 | 15 | @app.route('/signin/gae/') 16 | def signin_gae(): 17 | auth.save_request_params() 18 | gae_url = users.create_login_url(flask.url_for('gae_authorized')) 19 | return flask.redirect(gae_url) 20 | 21 | 22 | @app.route('/api/auth/callback/gae/') 23 | def gae_authorized(): 24 | gae_user = users.get_current_user() 25 | if gae_user is None: 26 | flask.flash('You denied the request to sign in.') 27 | return flask.redirect(util.get_next_url()) 28 | 29 | user_db = retrieve_user_from_gae(gae_user) 30 | return auth.signin_user_db(user_db) 31 | 32 | 33 | def retrieve_user_from_gae(gae_user): 34 | auth_id = 'federated_%s' % gae_user.user_id() 35 | user_db = model.User.get_by('auth_ids', auth_id) 36 | if user_db: 37 | if not user_db.admin and users.is_current_user_admin(): 38 | user_db.admin = True 39 | user_db.put() 40 | return user_db 41 | 42 | return auth.create_user_db( 43 | auth_id=auth_id, 44 | name=util.create_name_from_email(gae_user.email()), 45 | username=gae_user.email(), 46 | email=gae_user.email(), 47 | verified=True, 48 | admin=users.is_current_user_admin(), 49 | ) 50 | -------------------------------------------------------------------------------- /main/auth/github.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import flask 6 | 7 | import auth 8 | import config 9 | import model 10 | import util 11 | 12 | from main import app 13 | 14 | github_config = dict( 15 | access_token_method='POST', 16 | access_token_url='https://github.com/login/oauth/access_token', 17 | authorize_url='https://github.com/login/oauth/authorize', 18 | base_url='https://api.github.com/', 19 | consumer_key=config.CONFIG_DB.github_client_id, 20 | consumer_secret=config.CONFIG_DB.github_client_secret, 21 | request_token_params={'scope': 'user:email'}, 22 | ) 23 | 24 | github = auth.create_oauth_app(github_config, 'github') 25 | 26 | 27 | @app.route('/api/auth/callback/github/') 28 | def github_authorized(): 29 | response = github.authorized_response() 30 | if response is None: 31 | flask.flash('You denied the request to sign in.') 32 | return flask.redirect(util.get_next_url()) 33 | flask.session['oauth_token'] = (response['access_token'], '') 34 | me = github.get('user') 35 | user_db = retrieve_user_from_github(me.data) 36 | return auth.signin_user_db(user_db) 37 | 38 | 39 | @github.tokengetter 40 | def get_github_oauth_token(): 41 | return flask.session.get('oauth_token') 42 | 43 | 44 | @app.route('/signin/github/') 45 | def signin_github(): 46 | return auth.signin_oauth(github) 47 | 48 | 49 | def retrieve_user_from_github(response): 50 | auth_id = 'github_%s' % str(response['id']) 51 | user_db = model.User.get_by('auth_ids', auth_id) 52 | return user_db or auth.create_user_db( 53 | auth_id=auth_id, 54 | name=response['name'] or response['login'], 55 | username=response['login'], 56 | email=response.get('email', ''), 57 | verified=bool(response.get('email', '')), 58 | ) 59 | -------------------------------------------------------------------------------- /main/auth/google.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import flask 6 | 7 | import auth 8 | import config 9 | import model 10 | import util 11 | 12 | from main import app 13 | 14 | google_config = dict( 15 | access_token_method='POST', 16 | access_token_url='https://accounts.google.com/o/oauth2/token', 17 | authorize_url='https://accounts.google.com/o/oauth2/auth', 18 | base_url='https://www.googleapis.com/plus/v1/people/', 19 | consumer_key=config.CONFIG_DB.google_client_id, 20 | consumer_secret=config.CONFIG_DB.google_client_secret, 21 | request_token_params={'scope': 'email profile'}, 22 | ) 23 | 24 | google = auth.create_oauth_app(google_config, 'google') 25 | 26 | 27 | @app.route('/api/auth/callback/google/') 28 | def google_authorized(): 29 | response = google.authorized_response() 30 | if response is None: 31 | flask.flash('You denied the request to sign in.') 32 | return flask.redirect(util.get_next_url()) 33 | 34 | flask.session['oauth_token'] = (response['access_token'], '') 35 | me = google.get('me', data={'access_token': response['access_token']}) 36 | user_db = retrieve_user_from_google(me.data) 37 | return auth.signin_user_db(user_db) 38 | 39 | 40 | @google.tokengetter 41 | def get_google_oauth_token(): 42 | return flask.session.get('oauth_token') 43 | 44 | 45 | @app.route('/signin/google/') 46 | def signin_google(): 47 | return auth.signin_oauth(google) 48 | 49 | 50 | def retrieve_user_from_google(response): 51 | auth_id = 'google_%s' % response['id'] 52 | user_db = model.User.get_by('auth_ids', auth_id) 53 | if user_db: 54 | return user_db 55 | 56 | if 'email' in response: 57 | email = response['email'] 58 | elif 'emails' in response: 59 | email = response['emails'][0]['value'] 60 | else: 61 | email = '' 62 | 63 | if 'displayName' in response: 64 | name = response['displayName'] 65 | elif 'name' in response: 66 | names = response['name'] 67 | given_name = names.get('givenName', '') 68 | family_name = names.get('familyName', '') 69 | name = ' '.join([given_name, family_name]).strip() 70 | else: 71 | name = 'google_user_%s' % id 72 | 73 | return auth.create_user_db( 74 | auth_id=auth_id, 75 | name=name, 76 | username=email or name, 77 | email=email, 78 | verified=bool(email), 79 | ) 80 | -------------------------------------------------------------------------------- /main/auth/instagram.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import flask 6 | 7 | import auth 8 | import model 9 | import util 10 | 11 | from main import app 12 | 13 | instagram_config = dict( 14 | access_token_method='POST', 15 | access_token_url='https://api.instagram.com/oauth/access_token', 16 | authorize_url='https://instagram.com/oauth/authorize/', 17 | base_url='https://api.instagram.com/v1', 18 | consumer_key=model.Config.get_master_db().instagram_client_id, 19 | consumer_secret=model.Config.get_master_db().instagram_client_secret, 20 | ) 21 | 22 | instagram = auth.create_oauth_app(instagram_config, 'instagram') 23 | 24 | 25 | @app.route('/api/auth/callback/instagram/') 26 | def instagram_authorized(): 27 | response = instagram.authorized_response() 28 | if response is None: 29 | flask.flash('You denied the request to sign in.') 30 | return flask.redirect(util.get_next_url()) 31 | 32 | flask.session['oauth_token'] = (response['access_token'], '') 33 | user_db = retrieve_user_from_instagram(response['user']) 34 | return auth.signin_user_db(user_db) 35 | 36 | 37 | @instagram.tokengetter 38 | def get_instagram_oauth_token(): 39 | return flask.session.get('oauth_token') 40 | 41 | 42 | @app.route('/signin/instagram/') 43 | def signin_instagram(): 44 | return auth.signin_oauth(instagram) 45 | 46 | 47 | def retrieve_user_from_instagram(response): 48 | auth_id = 'instagram_%s' % response['id'] 49 | user_db = model.User.get_by('auth_ids', auth_id) 50 | if user_db: 51 | return user_db 52 | 53 | return auth.create_user_db( 54 | auth_id=auth_id, 55 | name=response.get('full_name', '').strip() or response.get('username'), 56 | username=response.get('username'), 57 | ) 58 | -------------------------------------------------------------------------------- /main/auth/linkedin.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import flask 6 | 7 | import auth 8 | import config 9 | import model 10 | import util 11 | 12 | from main import app 13 | 14 | linkedin_config = dict( 15 | access_token_method='POST', 16 | access_token_url='https://www.linkedin.com/uas/oauth2/accessToken', 17 | authorize_url='https://www.linkedin.com/uas/oauth2/authorization', 18 | base_url='https://api.linkedin.com/v1/', 19 | consumer_key=config.CONFIG_DB.linkedin_api_key, 20 | consumer_secret=config.CONFIG_DB.linkedin_secret_key, 21 | request_token_params={ 22 | 'scope': 'r_basicprofile r_emailaddress', 23 | 'state': util.uuid(), 24 | }, 25 | ) 26 | 27 | linkedin = auth.create_oauth_app(linkedin_config, 'linkedin') 28 | 29 | 30 | def change_linkedin_query(uri, headers, body): 31 | headers['x-li-format'] = 'json' 32 | return uri, headers, body 33 | 34 | 35 | linkedin.pre_request = change_linkedin_query 36 | 37 | 38 | @app.route('/api/auth/callback/linkedin/') 39 | def linkedin_authorized(): 40 | response = linkedin.authorized_response() 41 | if response is None: 42 | flask.flash('You denied the request to sign in.') 43 | return flask.redirect(util.get_next_url()) 44 | 45 | flask.session['access_token'] = (response['access_token'], '') 46 | me = linkedin.get('people/~:(id,first-name,last-name,email-address)') 47 | user_db = retrieve_user_from_linkedin(me.data) 48 | return auth.signin_user_db(user_db) 49 | 50 | 51 | @linkedin.tokengetter 52 | def get_linkedin_oauth_token(): 53 | return flask.session.get('access_token') 54 | 55 | 56 | @app.route('/signin/linkedin/') 57 | def signin_linkedin(): 58 | return auth.signin_oauth(linkedin) 59 | 60 | 61 | def retrieve_user_from_linkedin(response): 62 | auth_id = 'linkedin_%s' % response['id'] 63 | user_db = model.User.get_by('auth_ids', auth_id) 64 | if user_db: 65 | return user_db 66 | 67 | names = [response.get('firstName', ''), response.get('lastName', '')] 68 | name = ' '.join(names).strip() 69 | email = response.get('emailAddress', '') 70 | return auth.create_user_db( 71 | auth_id=auth_id, 72 | name=name, 73 | username=email or name, 74 | email=email, 75 | verified=bool(email), 76 | ) 77 | -------------------------------------------------------------------------------- /main/auth/mailru.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import hashlib 6 | 7 | import flask 8 | 9 | import auth 10 | import config 11 | import model 12 | import util 13 | 14 | from main import app 15 | 16 | mailru_config = dict( 17 | access_token_url='https://connect.mail.ru/oauth/token', 18 | authorize_url='https://connect.mail.ru/oauth/authorize', 19 | base_url='https://www.appsmail.ru/', 20 | consumer_key=config.CONFIG_DB.mailru_app_id, 21 | consumer_secret=config.CONFIG_DB.mailru_app_secret, 22 | ) 23 | 24 | mailru = auth.create_oauth_app(mailru_config, 'mailru') 25 | 26 | 27 | def mailru_sig(data): 28 | param_list = sorted(['%s=%s' % (item, data[item]) for item in data]) 29 | return hashlib.md5(''.join(param_list) + mailru.consumer_secret).hexdigest() 30 | 31 | 32 | @app.route('/api/auth/callback/mailru/') 33 | def mailru_authorized(): 34 | response = mailru.authorized_response() 35 | if response is None: 36 | flask.flash(u'You denied the request to sign in.') 37 | return flask.redirect(util.get_next_url()) 38 | 39 | access_token = response['access_token'] 40 | flask.session['oauth_token'] = (access_token, '') 41 | data = { 42 | 'method': 'users.getInfo', 43 | 'app_id': mailru.consumer_key, 44 | 'session_key': access_token, 45 | 'secure': '1', 46 | } 47 | data['sig'] = mailru_sig(data) 48 | me = mailru.get('/platform/api', data=data) 49 | user_db = retrieve_user_from_mailru(me.data[0]) 50 | return auth.signin_user_db(user_db) 51 | 52 | 53 | @mailru.tokengetter 54 | def get_mailru_oauth_token(): 55 | return flask.session.get('oauth_token') 56 | 57 | 58 | @app.route('/signin/mailru/') 59 | def signin_mailru(): 60 | return auth.signin_oauth(mailru) 61 | 62 | 63 | def retrieve_user_from_mailru(response): 64 | auth_id = 'mailru_%s' % response['uid'] 65 | user_db = model.User.get_by('auth_ids', auth_id) 66 | if user_db: 67 | return user_db 68 | name = u' '.join([ 69 | response.get('first_name', u''), 70 | response.get('last_name', u'') 71 | ]).strip() 72 | email = response.get('email', '') 73 | return auth.create_user_db( 74 | auth_id=auth_id, 75 | name=name or email, 76 | username=email or name, 77 | email=email, 78 | verified=bool(email), 79 | ) 80 | -------------------------------------------------------------------------------- /main/auth/microsoft.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import flask 6 | 7 | import auth 8 | import config 9 | import model 10 | import util 11 | 12 | from main import app 13 | 14 | microsoft_config = dict( 15 | access_token_method='POST', 16 | access_token_url='https://login.live.com/oauth20_token.srf', 17 | authorize_url='https://login.live.com/oauth20_authorize.srf', 18 | base_url='https://apis.live.net/v5.0/', 19 | consumer_key=config.CONFIG_DB.microsoft_client_id, 20 | consumer_secret=config.CONFIG_DB.microsoft_client_secret, 21 | request_token_params={'scope': 'wl.emails'}, 22 | ) 23 | 24 | microsoft = auth.create_oauth_app(microsoft_config, 'microsoft') 25 | 26 | 27 | @app.route('/api/auth/callback/microsoft/') 28 | def microsoft_authorized(): 29 | response = microsoft.authorized_response() 30 | if response is None: 31 | flask.flash('You denied the request to sign in.') 32 | return flask.redirect(util.get_next_url()) 33 | flask.session['oauth_token'] = (response['access_token'], '') 34 | me = microsoft.get('me') 35 | if me.data.get('error', {}): 36 | return 'Unknown error: error:%s error_description:%s' % ( 37 | me['error']['code'], 38 | me['error']['message'], 39 | ) 40 | user_db = retrieve_user_from_microsoft(me.data) 41 | return auth.signin_user_db(user_db) 42 | 43 | 44 | @microsoft.tokengetter 45 | def get_microsoft_oauth_token(): 46 | return flask.session.get('oauth_token') 47 | 48 | 49 | @app.route('/signin/microsoft/') 50 | def signin_microsoft(): 51 | return auth.signin_oauth(microsoft) 52 | 53 | 54 | def retrieve_user_from_microsoft(response): 55 | auth_id = 'microsoft_%s' % response['id'] 56 | user_db = model.User.get_by('auth_ids', auth_id) 57 | if user_db: 58 | return user_db 59 | email = response['emails']['preferred'] or response['emails']['account'] 60 | return auth.create_user_db( 61 | auth_id=auth_id, 62 | name=response.get('name', ''), 63 | username=email, 64 | email=email, 65 | verified=bool(email), 66 | ) 67 | -------------------------------------------------------------------------------- /main/auth/reddit.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import base64 6 | 7 | from flask_oauthlib import client 8 | from werkzeug import urls 9 | import flask 10 | 11 | import auth 12 | import config 13 | import model 14 | import util 15 | 16 | from main import app 17 | 18 | reddit_config = dict( 19 | access_token_method='POST', 20 | access_token_params={'grant_type': 'authorization_code'}, 21 | access_token_url='https://ssl.reddit.com/api/v1/access_token', 22 | authorize_url='https://ssl.reddit.com/api/v1/authorize', 23 | base_url='https://oauth.reddit.com/api/v1/', 24 | consumer_key=model.Config.get_master_db().reddit_client_id, 25 | consumer_secret=model.Config.get_master_db().reddit_client_secret, 26 | request_token_params={'scope': 'identity', 'state': util.uuid()}, 27 | ) 28 | 29 | reddit = auth.create_oauth_app(reddit_config, 'reddit') 30 | 31 | 32 | def reddit_handle_oauth2_response(): 33 | access_args = { 34 | 'code': flask.request.args.get('code'), 35 | 'client_id': reddit.consumer_key, 36 | 'redirect_uri': flask.session.get('%s_oauthredir' % reddit.name), 37 | } 38 | access_args.update(reddit.access_token_params) 39 | auth_header = 'Basic %s' % base64.b64encode( 40 | ('%s:%s' % (reddit.consumer_key, reddit.consumer_secret)).encode('latin1') 41 | ).strip().decode('latin1') 42 | response, content = reddit.http_request( 43 | reddit.expand_url(reddit.access_token_url), 44 | method=reddit.access_token_method, 45 | data=urls.url_encode(access_args), 46 | headers={ 47 | 'Authorization': auth_header, 48 | 'User-Agent': config.USER_AGENT, 49 | }, 50 | ) 51 | data = client.parse_response(response, content) 52 | if response.code not in (200, 201): 53 | raise client.OAuthException( 54 | 'Invalid response from %s' % reddit.name, 55 | type='invalid_response', data=data, 56 | ) 57 | return data 58 | 59 | 60 | reddit.handle_oauth2_response = reddit_handle_oauth2_response 61 | 62 | 63 | @app.route('/api/auth/callback/reddit/') 64 | def reddit_authorized(): 65 | response = reddit.authorized_response() 66 | if response is None or flask.request.args.get('error'): 67 | flask.flash('You denied the request to sign in.') 68 | return flask.redirect(util.get_next_url()) 69 | 70 | flask.session['oauth_token'] = (response['access_token'], '') 71 | me = reddit.request('me') 72 | user_db = retrieve_user_from_reddit(me.data) 73 | return auth.signin_user_db(user_db) 74 | 75 | 76 | @reddit.tokengetter 77 | def get_reddit_oauth_token(): 78 | return flask.session.get('oauth_token') 79 | 80 | 81 | @app.route('/signin/reddit/') 82 | def signin_reddit(): 83 | return auth.signin_oauth(reddit) 84 | 85 | 86 | def retrieve_user_from_reddit(response): 87 | auth_id = 'reddit_%s' % response['id'] 88 | user_db = model.User.get_by('auth_ids', auth_id) 89 | if user_db: 90 | return user_db 91 | 92 | return auth.create_user_db( 93 | auth_id=auth_id, 94 | name=response['name'], 95 | username=response['name'], 96 | ) 97 | -------------------------------------------------------------------------------- /main/auth/twitter.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import flask 6 | 7 | import auth 8 | import config 9 | import model 10 | import util 11 | 12 | from main import app 13 | 14 | twitter_config = dict( 15 | access_token_url='https://api.twitter.com/oauth/access_token', 16 | authorize_url='https://api.twitter.com/oauth/authorize', 17 | base_url='https://api.twitter.com/1.1/', 18 | consumer_key=config.CONFIG_DB.twitter_consumer_key, 19 | consumer_secret=config.CONFIG_DB.twitter_consumer_secret, 20 | request_token_url='https://api.twitter.com/oauth/request_token', 21 | ) 22 | 23 | twitter = auth.create_oauth_app(twitter_config, 'twitter') 24 | 25 | 26 | @app.route('/api/auth/callback/twitter/') 27 | def twitter_authorized(): 28 | response = twitter.authorized_response() 29 | if response is None: 30 | flask.flash('You denied the request to sign in.') 31 | return flask.redirect(util.get_next_url()) 32 | 33 | flask.session['oauth_token'] = ( 34 | response['oauth_token'], 35 | response['oauth_token_secret'], 36 | ) 37 | user_db = retrieve_user_from_twitter(response) 38 | return auth.signin_user_db(user_db) 39 | 40 | 41 | @twitter.tokengetter 42 | def get_twitter_token(): 43 | return flask.session.get('oauth_token') 44 | 45 | 46 | @app.route('/signin/twitter/') 47 | def signin_twitter(): 48 | return auth.signin_oauth(twitter) 49 | 50 | 51 | def retrieve_user_from_twitter(response): 52 | auth_id = 'twitter_%s' % response['user_id'] 53 | user_db = model.User.get_by('auth_ids', auth_id) 54 | return user_db or auth.create_user_db( 55 | auth_id=auth_id, 56 | name=response['screen_name'], 57 | username=response['screen_name'], 58 | ) 59 | -------------------------------------------------------------------------------- /main/auth/vk.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import flask 6 | 7 | import auth 8 | import config 9 | import model 10 | import util 11 | 12 | from main import app 13 | 14 | vk_config = dict( 15 | access_token_url='https://oauth.vk.com/access_token', 16 | authorize_url='https://oauth.vk.com/authorize', 17 | base_url='https://api.vk.com/', 18 | consumer_key=config.CONFIG_DB.vk_app_id, 19 | consumer_secret=config.CONFIG_DB.vk_app_secret, 20 | ) 21 | 22 | vk = auth.create_oauth_app(vk_config, 'vk') 23 | 24 | 25 | @app.route('/api/auth/callback/vk/') 26 | def vk_authorized(): 27 | response = vk.authorized_response() 28 | if response is None: 29 | flask.flash(u'You denied the request to sign in.') 30 | return flask.redirect(util.get_next_url()) 31 | 32 | access_token = response['access_token'] 33 | flask.session['oauth_token'] = (access_token, '') 34 | me = vk.get( 35 | '/method/users.get', 36 | data={ 37 | 'access_token': access_token, 38 | 'format': 'json', 39 | }, 40 | ) 41 | user_db = retrieve_user_from_vk(me.data['response'][0]) 42 | return auth.signin_user_db(user_db) 43 | 44 | 45 | @vk.tokengetter 46 | def get_vk_oauth_token(): 47 | return flask.session.get('oauth_token') 48 | 49 | 50 | @app.route('/signin/vk/') 51 | def signin_vk(): 52 | return auth.signin_oauth(vk) 53 | 54 | 55 | def retrieve_user_from_vk(response): 56 | auth_id = 'vk_%s' % response['uid'] 57 | user_db = model.User.get_by('auth_ids', auth_id) 58 | if user_db: 59 | return user_db 60 | 61 | name = ' '.join((response['first_name'], response['last_name'])).strip() 62 | return auth.create_user_db( 63 | auth_id=auth_id, 64 | name=name, 65 | username=name, 66 | ) 67 | -------------------------------------------------------------------------------- /main/auth/yahoo.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import flask 6 | 7 | import auth 8 | import model 9 | import util 10 | 11 | from main import app 12 | 13 | yahoo_config = dict( 14 | access_token_url='https://api.login.yahoo.com/oauth/v2/get_token', 15 | authorize_url='https://api.login.yahoo.com/oauth/v2/request_auth', 16 | base_url='https://query.yahooapis.com/', 17 | consumer_key=model.Config.get_master_db().yahoo_consumer_key, 18 | consumer_secret=model.Config.get_master_db().yahoo_consumer_secret, 19 | request_token_url='https://api.login.yahoo.com/oauth/v2/get_request_token', 20 | ) 21 | 22 | yahoo = auth.create_oauth_app(yahoo_config, 'yahoo') 23 | 24 | 25 | @app.route('/api/auth/callback/yahoo/') 26 | def yahoo_authorized(): 27 | response = yahoo.authorized_response() 28 | if response is None: 29 | flask.flash('You denied the request to sign in.') 30 | return flask.redirect(util.get_next_url()) 31 | 32 | flask.session['oauth_token'] = ( 33 | response['oauth_token'], 34 | response['oauth_token_secret'], 35 | ) 36 | 37 | fields = 'guid, emails, familyName, givenName, nickname' 38 | me = yahoo.get( 39 | '/v1/yql', 40 | data={ 41 | 'format': 'json', 42 | 'q': 'select %s from social.profile where guid = me;' % fields, 43 | 'realm': 'yahooapis.com', 44 | }, 45 | ) 46 | user_db = retrieve_user_from_yahoo(me.data['query']['results']['profile']) 47 | return auth.signin_user_db(user_db) 48 | 49 | 50 | @yahoo.tokengetter 51 | def get_yahoo_oauth_token(): 52 | return flask.session.get('oauth_token') 53 | 54 | 55 | @app.route('/signin/yahoo/') 56 | def signin_yahoo(): 57 | return auth.signin_oauth(yahoo) 58 | 59 | 60 | def retrieve_user_from_yahoo(response): 61 | auth_id = 'yahoo_%s' % response['guid'] 62 | user_db = model.User.get_by('auth_ids', auth_id) 63 | if user_db: 64 | return user_db 65 | 66 | names = [response.get('givenName', ''), response.get('familyName', '')] 67 | emails = response.get('emails', {}) 68 | if not isinstance(emails, list): 69 | emails = [emails] 70 | emails = [e for e in emails if 'handle' in e] 71 | emails.sort(key=lambda e: e.get('primary', False)) 72 | email = emails[0]['handle'] if emails else '' 73 | return auth.create_user_db( 74 | auth_id=auth_id, 75 | name=' '.join(names).strip() or response['nickname'], 76 | username=response['nickname'], 77 | email=email, 78 | verified=bool(email), 79 | ) 80 | -------------------------------------------------------------------------------- /main/cache.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from google.appengine.api import memcache 4 | import flask 5 | 6 | import config 7 | 8 | 9 | ############################################################################### 10 | # Helpers 11 | ############################################################################### 12 | def bump_counter(key, time=3600, limit=4): 13 | client = memcache.Client() 14 | for _ in range(limit): 15 | counter = client.gets(key) 16 | if counter is None: 17 | client.set(key, 0, time=time) 18 | counter = 0 19 | if client.cas(key, counter + 1): 20 | break 21 | 22 | 23 | ############################################################################### 24 | # Auth Attempts stuff 25 | ############################################################################### 26 | def get_auth_attempt_key(): 27 | return 'auth_attempt_%s' % flask.request.remote_addr 28 | 29 | 30 | def reset_auth_attempt(): 31 | client = memcache.Client() 32 | client.set(get_auth_attempt_key(), 0, time=3600) 33 | 34 | 35 | def get_auth_attempt(): 36 | client = memcache.Client() 37 | return client.get(get_auth_attempt_key()) or 0 38 | 39 | 40 | def bump_auth_attempt(): 41 | bump_counter(get_auth_attempt_key(), limit=config.SIGNIN_RETRY_LIMIT) 42 | -------------------------------------------------------------------------------- /main/config.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | 5 | PRODUCTION = os.environ.get('SERVER_SOFTWARE', '').startswith('Google App Eng') 6 | DEBUG = DEVELOPMENT = not PRODUCTION 7 | 8 | try: 9 | # This part is surrounded in try/except because the config.py file is 10 | # also used in the run.py script which is used to compile/minify the client 11 | # side files (*.less, *.coffee, *.js) and is not aware of the GAE 12 | from google.appengine.api import app_identity 13 | 14 | APPLICATION_ID = app_identity.get_application_id() 15 | DEFAULT_GCS_BUCKET_NAME = app_identity.get_default_gcs_bucket_name() 16 | except (ImportError, AttributeError): 17 | pass 18 | else: 19 | from datetime import datetime 20 | 21 | CURRENT_VERSION_ID = os.environ.get('CURRENT_VERSION_ID') 22 | CURRENT_VERSION_NAME = CURRENT_VERSION_ID.split('.')[0] 23 | CURRENT_VERSION_TIMESTAMP = long(CURRENT_VERSION_ID.split('.')[1]) >> 28 24 | if DEVELOPMENT: 25 | import calendar 26 | 27 | CURRENT_VERSION_TIMESTAMP = calendar.timegm(datetime.utcnow().timetuple()) 28 | CURRENT_VERSION_DATE = datetime.utcfromtimestamp(CURRENT_VERSION_TIMESTAMP) 29 | USER_AGENT = '%s/%s' % (APPLICATION_ID, CURRENT_VERSION_ID) 30 | 31 | import model 32 | 33 | CONFIG_DB = model.Config.get_master_db() 34 | SECRET_KEY = CONFIG_DB.flask_secret_key.encode('ascii') 35 | RECAPTCHA_PUBLIC_KEY = CONFIG_DB.recaptcha_public_key 36 | RECAPTCHA_PRIVATE_KEY = CONFIG_DB.recaptcha_private_key 37 | RECAPTCHA_LIMIT = 8 38 | TRUSTED_HOSTS = CONFIG_DB.trusted_hosts 39 | 40 | DEFAULT_DB_LIMIT = 64 41 | SIGNIN_RETRY_LIMIT = 4 42 | TAG_SEPARATOR = ' ' 43 | -------------------------------------------------------------------------------- /main/control/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from .user import * 4 | 5 | from .admin import * 6 | from .error import * 7 | from .feedback import * 8 | from .profile import * 9 | from .test import * 10 | from .welcome import * 11 | from .resource import * 12 | -------------------------------------------------------------------------------- /main/control/error.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import logging 4 | 5 | import flask 6 | 7 | from api import helpers 8 | import config 9 | 10 | from main import app 11 | 12 | 13 | @app.errorhandler(400) # Bad Request 14 | @app.errorhandler(401) # Unauthorized 15 | @app.errorhandler(403) # Forbidden 16 | @app.errorhandler(404) # Not Found 17 | @app.errorhandler(405) # Method Not Allowed 18 | @app.errorhandler(410) # Gone 19 | @app.errorhandler(418) # I'm a Teapot 20 | @app.errorhandler(422) # Unprocessable Entity 21 | @app.errorhandler(500) # Internal Server Error 22 | def error_handler(e): 23 | try: 24 | e.code 25 | except AttributeError: 26 | e.code = 500 27 | e.name = 'Internal Server Error' 28 | 29 | logging.error('%d - %s: %s', e.code, e.name, flask.request.url) 30 | if e.code != 404: 31 | logging.exception(e) 32 | 33 | if flask.request.path.startswith('/api/'): 34 | return helpers.handle_error(e) 35 | 36 | return flask.render_template( 37 | 'error.html', 38 | title='Error %d (%s)!!1' % (e.code, e.name), 39 | html_class='error-page', 40 | error=e, 41 | ), e.code 42 | 43 | 44 | if config.PRODUCTION: 45 | @app.errorhandler(Exception) 46 | def production_error_handler(e): 47 | return error_handler(e) 48 | -------------------------------------------------------------------------------- /main/control/feedback.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | import flask_wtf 5 | import wtforms 6 | 7 | import auth 8 | import config 9 | import task 10 | import util 11 | 12 | from main import app 13 | 14 | 15 | class FeedbackForm(flask_wtf.FlaskForm): 16 | message = wtforms.TextAreaField( 17 | 'Message', 18 | [wtforms.validators.required()], filters=[util.strip_filter], 19 | ) 20 | email = wtforms.StringField( 21 | 'Your email', 22 | [wtforms.validators.optional(), wtforms.validators.email()], 23 | filters=[util.email_filter], 24 | ) 25 | recaptcha = flask_wtf.RecaptchaField() 26 | 27 | 28 | @app.route('/feedback/', methods=['GET', 'POST']) 29 | def feedback(): 30 | if not config.CONFIG_DB.feedback_email: 31 | return flask.abort(418) 32 | 33 | form = FeedbackForm(obj=auth.current_user_db()) 34 | if not config.CONFIG_DB.has_anonymous_recaptcha or auth.is_logged_in(): 35 | del form.recaptcha 36 | if form.validate_on_submit(): 37 | body = '%s\n\n%s' % (form.message.data, form.email.data) 38 | kwargs = {'reply_to': form.email.data} if form.email.data else {} 39 | task.send_mail_notification('%s...' % body[:48].strip(), body, **kwargs) 40 | flask.flash('Thank you for your feedback!', category='success') 41 | return flask.redirect(flask.url_for('welcome')) 42 | 43 | return flask.render_template( 44 | 'feedback.html', 45 | title='Feedback', 46 | html_class='feedback', 47 | form=form, 48 | ) 49 | -------------------------------------------------------------------------------- /main/control/profile.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | import flask_wtf 5 | import wtforms 6 | 7 | import auth 8 | import config 9 | import model 10 | import util 11 | import task 12 | 13 | from main import app 14 | 15 | 16 | ############################################################################### 17 | # Profile View 18 | ############################################################################### 19 | @app.route('/profile/') 20 | @auth.login_required 21 | def profile(): 22 | user_db = auth.current_user_db() 23 | 24 | return flask.render_template( 25 | 'profile/profile.html', 26 | title=user_db.name, 27 | html_class='profile-view', 28 | user_db=user_db, 29 | ) 30 | 31 | 32 | ############################################################################### 33 | # Profile Update 34 | ############################################################################### 35 | class ProfileUpdateForm(flask_wtf.FlaskForm): 36 | name = wtforms.StringField( 37 | model.User.name._verbose_name, 38 | [wtforms.validators.required()], filters=[util.strip_filter], 39 | ) 40 | email = wtforms.StringField( 41 | model.User.email._verbose_name, 42 | [wtforms.validators.optional(), wtforms.validators.email()], 43 | filters=[util.email_filter], 44 | ) 45 | 46 | 47 | @app.route('/profile/update/', methods=['GET', 'POST']) 48 | @auth.login_required 49 | def profile_update(): 50 | user_db = auth.current_user_db() 51 | form = ProfileUpdateForm(obj=user_db) 52 | 53 | if form.validate_on_submit(): 54 | email = form.email.data 55 | if email and not user_db.is_email_available(email, user_db.key): 56 | form.email.errors.append('This email is already taken.') 57 | 58 | if not form.errors: 59 | send_verification = not user_db.token or user_db.email != email 60 | form.populate_obj(user_db) 61 | if send_verification: 62 | user_db.verified = False 63 | task.verify_email_notification(user_db) 64 | user_db.put() 65 | return flask.redirect(flask.url_for('profile')) 66 | 67 | return flask.render_template( 68 | 'profile/profile_update.html', 69 | title=user_db.name, 70 | html_class='profile-update', 71 | form=form, 72 | user_db=user_db, 73 | ) 74 | 75 | 76 | ############################################################################### 77 | # Profile Password 78 | ############################################################################### 79 | class ProfilePasswordForm(flask_wtf.FlaskForm): 80 | old_password = wtforms.StringField( 81 | 'Old Password', [wtforms.validators.required()], 82 | ) 83 | new_password = wtforms.StringField( 84 | 'New Password', 85 | [wtforms.validators.required(), wtforms.validators.length(min=6)] 86 | ) 87 | 88 | 89 | @app.route('/profile/password/', methods=['GET', 'POST']) 90 | @auth.login_required 91 | def profile_password(): 92 | if not config.CONFIG_DB.has_email_authentication: 93 | flask.abort(418) 94 | user_db = auth.current_user_db() 95 | form = ProfilePasswordForm(obj=user_db) 96 | 97 | if not user_db.password_hash: 98 | del form.old_password 99 | 100 | if form.validate_on_submit(): 101 | errors = False 102 | old_password = form.old_password.data if form.old_password else None 103 | new_password = form.new_password.data 104 | if new_password or old_password: 105 | if user_db.password_hash: 106 | if util.password_hash(user_db, old_password) != user_db.password_hash: 107 | form.old_password.errors.append('Invalid current password') 108 | errors = True 109 | 110 | if not (form.errors or errors): 111 | user_db.password_hash = util.password_hash(user_db, new_password) 112 | flask.flash('Your password has been changed.', category='success') 113 | 114 | if not (form.errors or errors): 115 | user_db.put() 116 | return flask.redirect(flask.url_for('profile')) 117 | 118 | return flask.render_template( 119 | 'profile/profile_password.html', 120 | title=user_db.name, 121 | html_class='profile-password', 122 | form=form, 123 | user_db=user_db, 124 | ) 125 | -------------------------------------------------------------------------------- /main/control/resource.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import urllib 4 | 5 | from google.appengine.ext import blobstore 6 | import flask 7 | import flask_wtf 8 | import wtforms 9 | 10 | import auth 11 | import config 12 | import model 13 | import util 14 | 15 | from main import app 16 | 17 | 18 | ############################################################################### 19 | # Upload 20 | ############################################################################### 21 | @app.route('/resource/upload/') 22 | @auth.login_required 23 | def resource_upload(): 24 | return flask.render_template( 25 | 'resource/resource_upload.html', 26 | title='Resource Upload', 27 | html_class='resource-upload', 28 | get_upload_url=flask.url_for('api.resource.upload'), 29 | has_json=True, 30 | upload_url=blobstore.create_upload_url( 31 | flask.request.path, 32 | gs_bucket_name=config.CONFIG_DB.bucket_name or None, 33 | ), 34 | ) 35 | 36 | 37 | ############################################################################### 38 | # List 39 | ############################################################################### 40 | @app.route('/resource/', endpoint='resource_list') 41 | @auth.login_required 42 | def resource_list(): 43 | resource_dbs, resource_cursor = auth.current_user_db().get_resource_dbs() 44 | 45 | return flask.render_template( 46 | 'resource/resource_list.html', 47 | html_class='resource-list', 48 | title='Resource List', 49 | resource_dbs=resource_dbs, 50 | next_url=util.generate_next_url(resource_cursor), 51 | api_url=flask.url_for('api.resource.list'), 52 | ) 53 | 54 | 55 | ############################################################################### 56 | # View 57 | ############################################################################### 58 | @app.route('/resource//', endpoint='resource_view') 59 | @auth.login_required 60 | def resource_view(resource_id): 61 | resource_db = model.Resource.get_by_id(resource_id) 62 | 63 | if not resource_db or resource_db.user_key != auth.current_user_key(): 64 | return flask.abort(404) 65 | 66 | return flask.render_template( 67 | 'resource/resource_view.html', 68 | html_class='resource-view', 69 | title='%s' % (resource_db.name), 70 | resource_db=resource_db, 71 | api_url=flask.url_for('api.resource', key=resource_db.key.urlsafe()), 72 | ) 73 | 74 | 75 | ############################################################################### 76 | # Update 77 | ############################################################################### 78 | class ResourceUpdateForm(flask_wtf.FlaskForm): 79 | name = wtforms.TextField('Name', [wtforms.validators.required()]) 80 | 81 | 82 | @app.route('/resource//update/', methods=['GET', 'POST'], endpoint='resource_update') 83 | @auth.login_required 84 | def resource_update(resource_id): 85 | resource_db = model.Resource.get_by_id(resource_id) 86 | 87 | if not resource_db or resource_db.user_key != auth.current_user_key(): 88 | return flask.abort(404) 89 | 90 | form = ResourceUpdateForm(obj=resource_db) 91 | 92 | if form.validate_on_submit(): 93 | form.populate_obj(resource_db) 94 | resource_db.put() 95 | return flask.redirect(flask.url_for( 96 | 'resource_view', resource_id=resource_db.key.id(), 97 | )) 98 | 99 | return flask.render_template( 100 | 'resource/resource_update.html', 101 | html_class='resource-update', 102 | title='%s' % (resource_db.name), 103 | resource_db=resource_db, 104 | form=form, 105 | api_url=flask.url_for('api.resource', key=resource_db.key.urlsafe()), 106 | ) 107 | 108 | 109 | ############################################################################### 110 | # Download 111 | ############################################################################### 112 | @app.route('/resource//download/') 113 | @auth.login_required 114 | def resource_download(resource_id): 115 | resource_db = model.Resource.get_by_id(resource_id) 116 | if not resource_db or resource_db.user_key != auth.current_user_key(): 117 | return flask.abort(404) 118 | name = urllib.quote(resource_db.name.encode('utf-8')) 119 | url = '/serve/%s?save_as=%s' % (resource_db.blob_key, name) 120 | return flask.redirect(url) 121 | -------------------------------------------------------------------------------- /main/control/serve.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import urllib 4 | 5 | import webapp2 6 | from google.appengine.ext.webapp import blobstore_handlers 7 | from google.appengine.ext import blobstore 8 | 9 | 10 | class ServeHandler(blobstore_handlers.BlobstoreDownloadHandler): 11 | def get(self, blob_key): 12 | blob_key = str(urllib.unquote(blob_key)) 13 | blob_info = blobstore.BlobInfo.get(blob_key) 14 | save_as = self.request.get('save_as', None) 15 | if save_as: 16 | save_as = urllib.quote(save_as.encode('utf-8'), safe='%!()[]=') 17 | self.send_blob(blob_info, save_as=save_as) 18 | 19 | 20 | app = webapp2.WSGIApplication([ 21 | ('/serve/([^/]+)?', ServeHandler), 22 | ], debug=True) 23 | -------------------------------------------------------------------------------- /main/control/test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | import flask_wtf 5 | import wtforms 6 | 7 | import auth 8 | import util 9 | 10 | from main import app 11 | 12 | TESTS = [ 13 | 'alert', 14 | 'badge', 15 | 'button', 16 | 'filter', 17 | 'font', 18 | 'form', 19 | 'grid', 20 | 'heading', 21 | 'label', 22 | 'pageres', 23 | 'pagination', 24 | 'paragraph', 25 | 'responsive', 26 | 'social', 27 | 'table', 28 | ] 29 | 30 | 31 | class TestForm(flask_wtf.FlaskForm): 32 | name = wtforms.StringField( 33 | 'Text', 34 | [wtforms.validators.required()], filters=[util.strip_filter], 35 | description='This is a very important field', 36 | ) 37 | number = wtforms.IntegerField('Integer', [wtforms.validators.optional()]) 38 | email = wtforms.StringField( 39 | 'Email', 40 | [wtforms.validators.optional(), wtforms.validators.email()], 41 | filters=[util.email_filter], 42 | ) 43 | date = wtforms.DateField('Date', [wtforms.validators.optional()]) 44 | textarea = wtforms.TextAreaField('Textarea') 45 | boolean = wtforms.BooleanField( 46 | 'Render it as Markdown', 47 | [wtforms.validators.optional()], 48 | ) 49 | password = wtforms.PasswordField( 50 | 'Password', 51 | [wtforms.validators.optional(), wtforms.validators.length(min=6)], 52 | ) 53 | password_visible = wtforms.StringField( 54 | 'Password visible', 55 | [wtforms.validators.optional(), wtforms.validators.length(min=6)], 56 | description='Visible passwords for the win!' 57 | ) 58 | prefix = wtforms.StringField('Prefix', [wtforms.validators.optional()]) 59 | suffix = wtforms.StringField('Suffix', [wtforms.validators.required()]) 60 | both = wtforms.IntegerField('Both', [wtforms.validators.required()]) 61 | select = wtforms.SelectField( 62 | 'Language', 63 | [wtforms.validators.optional()], 64 | choices=[(s, s.title()) for s in ['english', 'greek', 'spanish']] 65 | ) 66 | checkboxes = wtforms.SelectMultipleField( 67 | 'User permissions', 68 | [wtforms.validators.required()], 69 | choices=[(c, c.title()) for c in ['admin', 'moderator', 'slave']] 70 | ) 71 | radios = wtforms.SelectField( 72 | 'Choose your weapon', 73 | [wtforms.validators.optional()], 74 | choices=[(r, r.title()) for r in ['gun', 'knife', 'chainsaw', 'sword']] 75 | ) 76 | public = wtforms.StringField('Public Key', [wtforms.validators.optional()]) 77 | private = wtforms.StringField('Private Key', [wtforms.validators.optional()]) 78 | recaptcha = flask_wtf.RecaptchaField() 79 | 80 | 81 | @app.route('/admin/test//', methods=['GET', 'POST']) 82 | @app.route('/admin/test/', methods=['GET', 'POST']) 83 | @auth.admin_required 84 | def admin_test(test=None): 85 | if test and test not in TESTS: 86 | flask.abort(404) 87 | form = TestForm() 88 | if form.validate_on_submit(): 89 | pass 90 | 91 | return flask.render_template( 92 | 'admin/test/test_one.html' if test else 'admin/test/test.html', 93 | title='Test: %s' % test.title() if test else 'Test', 94 | html_class='test', 95 | form=form, 96 | test=test, 97 | tests=TESTS, 98 | back_url_for='admin_test' if test else None, 99 | ) 100 | -------------------------------------------------------------------------------- /main/control/welcome.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | 5 | import config 6 | 7 | from main import app 8 | 9 | 10 | ############################################################################### 11 | # Welcome 12 | ############################################################################### 13 | @app.route('/') 14 | def welcome(): 15 | return flask.render_template('welcome.html', html_class='welcome') 16 | 17 | 18 | ############################################################################### 19 | # Sitemap stuff 20 | ############################################################################### 21 | @app.route('/sitemap.xml') 22 | def sitemap(): 23 | response = flask.make_response(flask.render_template( 24 | 'sitemap.xml', 25 | lastmod=config.CURRENT_VERSION_DATE.strftime('%Y-%m-%d'), 26 | )) 27 | response.headers['Content-Type'] = 'application/xml' 28 | return response 29 | 30 | 31 | ############################################################################### 32 | # Warmup request 33 | ############################################################################### 34 | @app.route('/_ah/warmup') 35 | def warmup(): 36 | # TODO: put your warmup code here 37 | return 'success' 38 | -------------------------------------------------------------------------------- /main/main.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | 5 | import config 6 | import util 7 | 8 | 9 | class GaeRequest(flask.Request): 10 | trusted_hosts = config.TRUSTED_HOSTS 11 | 12 | 13 | app = flask.Flask(__name__) 14 | app.config.from_object(config) 15 | app.request_class = GaeRequest if config.TRUSTED_HOSTS else flask.Request 16 | 17 | app.jinja_env.line_statement_prefix = '#' 18 | app.jinja_env.line_comment_prefix = '##' 19 | app.jinja_env.globals.update( 20 | check_form_fields=util.check_form_fields, 21 | is_iterable=util.is_iterable, 22 | slugify=util.slugify, 23 | update_query_argument=util.update_query_argument, 24 | ) 25 | 26 | import auth 27 | import control 28 | import model 29 | import task 30 | 31 | from api import helpers 32 | 33 | api_v1 = helpers.Api(app, prefix='/api/v1') 34 | 35 | import api.v1 36 | 37 | if config.DEVELOPMENT: 38 | from werkzeug import debug 39 | try: 40 | app.wsgi_app = debug.DebuggedApplication( 41 | app.wsgi_app, evalex=True, pin_security=False, 42 | ) 43 | except TypeError: 44 | app.wsgi_app = debug.DebuggedApplication(app.wsgi_app, evalex=True) 45 | app.testing = False 46 | -------------------------------------------------------------------------------- /main/model/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from .base import Base 4 | from .config_auth import ConfigAuth 5 | from .config import Config 6 | from .user import User 7 | from .resource import Resource 8 | -------------------------------------------------------------------------------- /main/model/base.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from google.appengine.ext import ndb 6 | from marshmallow import validate 7 | from webargs.flaskparser import parser 8 | from webargs import fields as wf 9 | 10 | from api import fields 11 | import config 12 | import util 13 | 14 | 15 | class Base(ndb.Model): 16 | created = ndb.DateTimeProperty(auto_now_add=True) 17 | modified = ndb.DateTimeProperty(auto_now=True) 18 | version = ndb.IntegerProperty(default=config.CURRENT_VERSION_TIMESTAMP) 19 | 20 | @classmethod 21 | def get_by(cls, name, value): 22 | return cls.query(getattr(cls, name) == value).get() 23 | 24 | @classmethod 25 | def get_dbs(cls, query=None, ancestor=None, order=None, limit=None, cursor=None, **kwargs): 26 | args = parser.parse({ 27 | 'cursor': wf.Str(missing=None), 28 | 'limit': wf.Int(missing=None, validate=validate.Range(min=-1)), 29 | 'order': wf.Str(missing=None), 30 | }) 31 | return util.get_dbs( 32 | query or cls.query(ancestor=ancestor), 33 | limit=limit or args['limit'], 34 | cursor=cursor or args['cursor'], 35 | order=order or args['order'], 36 | **kwargs 37 | ) 38 | 39 | FIELDS = { 40 | 'created': fields.DateTime, 41 | 'id': fields.Id, 42 | 'key': fields.Key, 43 | 'modified': fields.DateTime, 44 | 'version': fields.Integer, 45 | } 46 | -------------------------------------------------------------------------------- /main/model/config.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from google.appengine.ext import ndb 6 | 7 | from api import fields 8 | import config 9 | import model 10 | import util 11 | 12 | 13 | class Config(model.Base, model.ConfigAuth): 14 | analytics_id = ndb.StringProperty(default='', verbose_name='Tracking ID') 15 | announcement_html = ndb.TextProperty(default='', verbose_name='Announcement HTML') 16 | announcement_type = ndb.StringProperty(default='info', choices=['info', 'warning', 'success', 'danger']) 17 | anonymous_recaptcha = ndb.BooleanProperty(default=False, verbose_name='Use reCAPTCHA in forms for unauthorized users') 18 | brand_name = ndb.StringProperty(default=config.APPLICATION_ID) 19 | bucket_name = ndb.StringProperty(default=config.DEFAULT_GCS_BUCKET_NAME) 20 | check_unique_email = ndb.BooleanProperty(default=True, verbose_name='Check for uniqueness of the verified emails') 21 | email_authentication = ndb.BooleanProperty(default=False, verbose_name='Email authentication for sign in/sign up') 22 | feedback_email = ndb.StringProperty(default='') 23 | flask_secret_key = ndb.StringProperty(default=util.uuid()) 24 | notify_on_new_user = ndb.BooleanProperty(default=True, verbose_name='Send an email notification when a user signs up') 25 | recaptcha_private_key = ndb.StringProperty(default='', verbose_name='Private Key') 26 | recaptcha_public_key = ndb.StringProperty(default='', verbose_name='Public Key') 27 | salt = ndb.StringProperty(default=util.uuid()) 28 | trusted_hosts = ndb.StringProperty(repeated=True, verbose_name='Trusted Hosts') 29 | verify_email = ndb.BooleanProperty(default=True, verbose_name='Verify user emails') 30 | 31 | @property 32 | def has_anonymous_recaptcha(self): 33 | return bool(self.anonymous_recaptcha and self.has_recaptcha) 34 | 35 | @property 36 | def has_email_authentication(self): 37 | return bool(self.email_authentication and self.feedback_email and self.verify_email) 38 | 39 | @property 40 | def has_recaptcha(self): 41 | return bool(self.recaptcha_private_key and self.recaptcha_public_key) 42 | 43 | @classmethod 44 | def get_master_db(cls): 45 | return cls.get_or_insert('master') 46 | 47 | FIELDS = { 48 | 'analytics_id': fields.String, 49 | 'announcement_html': fields.String, 50 | 'announcement_type': fields.String, 51 | 'anonymous_recaptcha': fields.Boolean, 52 | 'brand_name': fields.String, 53 | 'bucket_name': fields.String, 54 | 'check_unique_email': fields.Boolean, 55 | 'email_authentication': fields.Boolean, 56 | 'feedback_email': fields.String, 57 | 'flask_secret_key': fields.String, 58 | 'notify_on_new_user': fields.Boolean, 59 | 'recaptcha_private_key': fields.String, 60 | 'recaptcha_public_key': fields.String, 61 | 'salt': fields.String, 62 | 'trusted_hosts': fields.List(fields.String), 63 | 'verify_email': fields.Boolean, 64 | } 65 | 66 | FIELDS.update(model.Base.FIELDS) 67 | FIELDS.update(model.ConfigAuth.FIELDS) 68 | -------------------------------------------------------------------------------- /main/model/config_auth.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from google.appengine.ext import ndb 6 | 7 | from api import fields 8 | import model 9 | 10 | 11 | class ConfigAuth(object): 12 | azure_ad_client_id = ndb.StringProperty(default='', verbose_name='Client ID') 13 | azure_ad_client_secret = ndb.StringProperty(default='', verbose_name='Client Secret') 14 | bitbucket_key = ndb.StringProperty(default='', verbose_name='Key') 15 | bitbucket_secret = ndb.StringProperty(default='', verbose_name='Secret') 16 | dropbox_app_key = ndb.StringProperty(default='', verbose_name='App Key') 17 | dropbox_app_secret = ndb.StringProperty(default='', verbose_name='App Secret') 18 | facebook_app_id = ndb.StringProperty(default='', verbose_name='App ID') 19 | facebook_app_secret = ndb.StringProperty(default='', verbose_name='App Secret') 20 | github_client_id = ndb.StringProperty(default='', verbose_name='Client ID') 21 | github_client_secret = ndb.StringProperty(default='', verbose_name='Client Secret') 22 | google_client_id = ndb.StringProperty(default='', verbose_name='Client ID') 23 | google_client_secret = ndb.StringProperty(default='', verbose_name='Client Secret') 24 | instagram_client_id = ndb.StringProperty(default='', verbose_name='Client ID') 25 | instagram_client_secret = ndb.StringProperty(default='', verbose_name='Client Secret') 26 | linkedin_api_key = ndb.StringProperty(default='', verbose_name='API Key') 27 | linkedin_secret_key = ndb.StringProperty(default='', verbose_name='Secret Key') 28 | mailru_app_id = ndb.StringProperty(default='', verbose_name='App ID') 29 | mailru_app_secret = ndb.StringProperty(default='', verbose_name='App Secret') 30 | microsoft_client_id = ndb.StringProperty(default='', verbose_name='Client ID') 31 | microsoft_client_secret = ndb.StringProperty(default='', verbose_name='Client Secret') 32 | reddit_client_id = ndb.StringProperty(default='', verbose_name='Client ID') 33 | reddit_client_secret = ndb.StringProperty(default='', verbose_name='Client Secret') 34 | twitter_consumer_key = ndb.StringProperty(default='', verbose_name='Consumer Key') 35 | twitter_consumer_secret = ndb.StringProperty(default='', verbose_name='Consumer Secret') 36 | vk_app_id = ndb.StringProperty(default='', verbose_name='App ID') 37 | vk_app_secret = ndb.StringProperty(default='', verbose_name='App Secret') 38 | yahoo_consumer_key = ndb.StringProperty(default='', verbose_name='Consumer Key') 39 | yahoo_consumer_secret = ndb.StringProperty(default='', verbose_name='Consumer Secret') 40 | 41 | @property 42 | def has_azure_ad(self): 43 | return bool(self.azure_ad_client_id and self.azure_ad_client_secret) 44 | 45 | @property 46 | def has_bitbucket(self): 47 | return bool(self.bitbucket_key and self.bitbucket_secret) 48 | 49 | @property 50 | def has_dropbox(self): 51 | return bool(self.dropbox_app_key and self.dropbox_app_secret) 52 | 53 | @property 54 | def has_facebook(self): 55 | return bool(self.facebook_app_id and self.facebook_app_secret) 56 | 57 | @property 58 | def has_google(self): 59 | return bool(self.google_client_id and self.google_client_secret) 60 | 61 | @property 62 | def has_github(self): 63 | return bool(self.github_client_id and self.github_client_secret) 64 | 65 | @property 66 | def has_instagram(self): 67 | return bool(self.instagram_client_id and self.instagram_client_secret) 68 | 69 | @property 70 | def has_linkedin(self): 71 | return bool(self.linkedin_api_key and self.linkedin_secret_key) 72 | 73 | @property 74 | def has_mailru(self): 75 | return bool(self.mailru_app_id and self.mailru_app_secret) 76 | 77 | @property 78 | def has_microsoft(self): 79 | return bool(self.microsoft_client_id and self.microsoft_client_secret) 80 | 81 | @property 82 | def has_reddit(self): 83 | return bool(self.reddit_client_id and self.reddit_client_secret) 84 | 85 | @property 86 | def has_twitter(self): 87 | return bool(self.twitter_consumer_key and self.twitter_consumer_secret) 88 | 89 | @property 90 | def has_vk(self): 91 | return bool(self.vk_app_id and self.vk_app_secret) 92 | 93 | @property 94 | def has_yahoo(self): 95 | return bool(self.yahoo_consumer_key and self.yahoo_consumer_secret) 96 | 97 | FIELDS = { 98 | 'azure_ad_client_id': fields.String, 99 | 'azure_ad_client_secret': fields.String, 100 | 'bitbucket_key': fields.String, 101 | 'bitbucket_secret': fields.String, 102 | 'dropbox_app_key': fields.String, 103 | 'dropbox_app_secret': fields.String, 104 | 'facebook_app_id': fields.String, 105 | 'facebook_app_secret': fields.String, 106 | 'github_client_id': fields.String, 107 | 'github_client_secret': fields.String, 108 | 'google_client_id': fields.String, 109 | 'google_client_secret': fields.String, 110 | 'instagram_client_id': fields.String, 111 | 'instagram_client_secret': fields.String, 112 | 'linkedin_api_key': fields.String, 113 | 'linkedin_secret_key': fields.String, 114 | 'mailru_client_id': fields.String, 115 | 'mailru_client_secret': fields.String, 116 | 'microsoft_client_id': fields.String, 117 | 'microsoft_client_secret': fields.String, 118 | 'reddit_client_id': fields.String, 119 | 'reddit_client_secret': fields.String, 120 | 'twitter_consumer_key': fields.String, 121 | 'twitter_consumer_secret': fields.String, 122 | 'vk_app_id': fields.String, 123 | 'vk_app_secret': fields.String, 124 | 'yahoo_consumer_key': fields.String, 125 | 'yahoo_consumer_secret': fields.String, 126 | } 127 | 128 | FIELDS.update(model.Base.FIELDS) 129 | -------------------------------------------------------------------------------- /main/model/resource.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import logging 6 | 7 | from google.appengine.ext import blobstore 8 | from google.appengine.ext import ndb 9 | import flask 10 | 11 | from api import fields 12 | import model 13 | import util 14 | 15 | 16 | class Resource(model.Base): 17 | user_key = ndb.KeyProperty(kind=model.User, required=True) 18 | blob_key = ndb.BlobKeyProperty(required=True) 19 | name = ndb.StringProperty(required=True) 20 | bucket_name = ndb.StringProperty() 21 | image_url = ndb.StringProperty(default='') 22 | content_type = ndb.StringProperty(default='') 23 | size = ndb.IntegerProperty(default=0) 24 | 25 | @ndb.ComputedProperty 26 | def size_human(self): 27 | return util.size_human(self.size or 0) 28 | 29 | @property 30 | def download_url(self): 31 | if self.key: 32 | return flask.url_for( 33 | 'resource_download', resource_id=self.key.id(), _external=True 34 | ) 35 | return None 36 | 37 | @property 38 | def view_url(self): 39 | if self.key: 40 | return flask.url_for( 41 | 'resource_view', resource_id=self.key.id(), _external=True, 42 | ) 43 | return None 44 | 45 | @property 46 | def serve_url(self): 47 | return '%s/serve/%s' % (flask.request.url_root[:-1], self.blob_key) 48 | 49 | @classmethod 50 | def _pre_delete_hook(cls, key): 51 | resource_db = key.get() 52 | try: 53 | blobstore.BlobInfo.get(resource_db.blob_key).delete() 54 | except AttributeError: 55 | logging.error('Blob %s not found during delete (resource_key: %s)' % ( 56 | resource_db.blob_key, resource_db.key().urlsafe(), 57 | )) 58 | 59 | FIELDS = { 60 | 'bucket_name': fields.String, 61 | 'content_type': fields.String, 62 | 'download_url': fields.String, 63 | 'image_url': fields.String, 64 | 'name': fields.String, 65 | 'serve_url': fields.String, 66 | 'size': fields.Integer, 67 | 'size_human': fields.String, 68 | 'view_url': fields.String, 69 | } 70 | 71 | FIELDS.update(model.Base.FIELDS) 72 | -------------------------------------------------------------------------------- /main/model/user.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import hashlib 6 | 7 | from google.appengine.ext import ndb 8 | from webargs.flaskparser import parser 9 | from webargs import fields as wf 10 | 11 | from api import fields 12 | import model 13 | import util 14 | import config 15 | 16 | 17 | class User(model.Base): 18 | name = ndb.StringProperty(required=True) 19 | username = ndb.StringProperty(required=True) 20 | email = ndb.StringProperty(default='') 21 | auth_ids = ndb.StringProperty(repeated=True) 22 | active = ndb.BooleanProperty(default=True) 23 | admin = ndb.BooleanProperty(default=False) 24 | permissions = ndb.StringProperty(repeated=True) 25 | verified = ndb.BooleanProperty(default=False) 26 | token = ndb.StringProperty(default='') 27 | password_hash = ndb.StringProperty(default='') 28 | 29 | def has_permission(self, perm): 30 | return self.admin or perm in self.permissions 31 | 32 | def has_facebook(self): 33 | for auth_id in self.auth_ids: 34 | if auth_id.startswith('facebook'): 35 | return auth_id 36 | return None 37 | 38 | def avatar_url_size(self, size=None): 39 | facebook_id = self.has_facebook() 40 | if facebook_id: 41 | return '//graph.facebook.com/%(id)s/picture%(size)s' % { 42 | 'id': facebook_id.split('_')[1], 43 | 'size': '?width=%s&height=%s' % (size, size) if size else '', 44 | } 45 | 46 | return '//gravatar.com/avatar/%(hash)s?d=identicon&r=x%(size)s' % { 47 | 'hash': hashlib.md5( 48 | (self.email or self.username).encode('utf-8')).hexdigest(), 49 | 'size': '&s=%d' % size if size > 0 else '', 50 | } 51 | 52 | avatar_url = property(avatar_url_size) 53 | 54 | @classmethod 55 | def get_dbs( 56 | cls, admin=None, active=None, verified=None, permissions=None, **kwargs 57 | ): 58 | args = parser.parse({ 59 | 'admin': wf.Bool(missing=None), 60 | 'active': wf.Bool(missing=None), 61 | 'verified': wf.Bool(missing=None), 62 | 'permissions': wf.DelimitedList(wf.Str(), delimiter=',', missing=[]), 63 | }) 64 | return super(User, cls).get_dbs( 65 | admin=admin or args['admin'], 66 | active=active or args['active'], 67 | verified=verified or args['verified'], 68 | permissions=permissions or args['permissions'], 69 | **kwargs 70 | ) 71 | 72 | @classmethod 73 | def is_username_available(cls, username, self_key=None): 74 | if self_key is None: 75 | return cls.get_by('username', username) is None 76 | user_keys, _ = util.get_keys(cls.query(), username=username, limit=2) 77 | return not user_keys or self_key in user_keys and not user_keys[1:] 78 | 79 | @classmethod 80 | def is_email_available(cls, email, self_key=None): 81 | if not config.CONFIG_DB.check_unique_email: 82 | return True 83 | user_keys, _ = util.get_keys( 84 | cls.query(), email=email, verified=True, limit=2, 85 | ) 86 | return not user_keys or self_key in user_keys and not user_keys[1:] 87 | 88 | def get_resource_dbs(self, **kwargs): 89 | return model.Resource.get_dbs(user_key=self.key, **kwargs) 90 | 91 | @classmethod 92 | def _pre_delete_hook(cls, key): 93 | user_db = key.get() 94 | resource_keys = user_db.get_resource_dbs(keys_only=True, limit=-1)[0] 95 | ndb.delete_multi(resource_keys) 96 | 97 | FIELDS = { 98 | 'active': fields.Boolean, 99 | 'admin': fields.Boolean, 100 | 'auth_ids': fields.List(fields.String), 101 | 'avatar_url': fields.String, 102 | 'email': fields.String, 103 | 'name': fields.String, 104 | 'permissions': fields.List(fields.String), 105 | 'username': fields.String, 106 | 'verified': fields.Boolean, 107 | } 108 | 109 | FIELDS.update(model.Base.FIELDS) 110 | -------------------------------------------------------------------------------- /main/path_util.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | import pkgutil 5 | import sys 6 | 7 | 8 | def path_package_path(deps_path, shadow_pkgs): 9 | sys.path.insert(0, deps_path) 10 | for _, pkg, ispkg in pkgutil.iter_modules(): 11 | if ispkg and pkg in shadow_pkgs: 12 | global_pkg = __import__(pkg) 13 | global_pkg.__path__.insert(0, '%s/%s' % (deps_path, pkg)) 14 | 15 | 16 | def is_shadowing(package_name): 17 | try: 18 | __import__(os.path.splitext(package_name)[0]) 19 | return True 20 | except ImportError: 21 | pass 22 | return False 23 | 24 | 25 | def get_shadows_zip(filename): 26 | import zipfile 27 | 28 | shadow_pkgs = set() 29 | with zipfile.ZipFile(filename) as lib_zip: 30 | already_test = [] 31 | for fname in lib_zip.namelist(): 32 | pname, fname = os.path.split(fname) 33 | if fname or (pname and fname): 34 | continue 35 | if pname not in already_test and '/' not in pname: 36 | already_test.append(pname) 37 | if is_shadowing(pname): 38 | shadow_pkgs.add(pname) 39 | return shadow_pkgs 40 | 41 | 42 | def get_shadows_dir(dirname): 43 | shadow_pkgs = set() 44 | if not os.path.exists(dirname): 45 | return shadow_pkgs 46 | for pkg in os.listdir(dirname): 47 | if not pkg == '__init__.py' and os.path.isfile(pkg) and is_shadowing(pkg): 48 | shadow_pkgs.add(pkg) 49 | return shadow_pkgs 50 | 51 | 52 | def sys_path_insert(dirname): 53 | if dirname.endswith('.zip'): 54 | path_package_path(dirname, get_shadows_zip(dirname)) 55 | else: 56 | path_package_path(dirname, get_shadows_dir(dirname)) 57 | -------------------------------------------------------------------------------- /main/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gae-init/gae-init-upload/314c151b438a02724f7925f5be1af3b5ec93630d/main/static/img/favicon.ico -------------------------------------------------------------------------------- /main/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /signout/ 3 | Disallow: /signin/*/ 4 | Disallow: /api/ 5 | Disallow: /_ah/ 6 | -------------------------------------------------------------------------------- /main/static/src/script/common/api.coffee: -------------------------------------------------------------------------------- 1 | window.api_call = (method, url, params, data, callback) -> 2 | callback = callback || data || params 3 | data = data || params 4 | if arguments.length == 4 5 | data = undefined 6 | if arguments.length == 3 7 | params = undefined 8 | data = undefined 9 | params = params || {} 10 | for k, v of params 11 | delete params[k] if not v? 12 | separator = if url.search('\\?') >= 0 then '&' else '?' 13 | $.ajax 14 | type: method 15 | url: "#{url}#{separator}#{$.param params}" 16 | contentType: 'application/json' 17 | accepts: 'application/json' 18 | dataType: 'json' 19 | data: if data then JSON.stringify(data) else undefined 20 | success: (data) -> 21 | if data.status == 'success' 22 | more = undefined 23 | if data.next_url 24 | more = (callback) -> api_call(method, data.next_url, {}, callback) 25 | callback? undefined, data.result, more 26 | else 27 | callback? data 28 | error: (jqXHR, textStatus, errorThrown) -> 29 | error = 30 | error_code: 'ajax_error' 31 | text_status: textStatus 32 | error_thrown: errorThrown 33 | jqXHR: jqXHR 34 | try 35 | error = $.parseJSON(jqXHR.responseText) if jqXHR.responseText 36 | catch e 37 | error = error 38 | LOG 'api_call error', error 39 | callback? error 40 | -------------------------------------------------------------------------------- /main/static/src/script/common/upload.coffee: -------------------------------------------------------------------------------- 1 | (-> 2 | class window.FileUploader 3 | constructor: (@options) -> 4 | @upload_handler = @options.upload_handler 5 | @selector = @options.selector 6 | @drop_area = @options.drop_area 7 | @upload_url = @options.upload_url or "/api/v1#{window.location.pathname}" 8 | @confirm_message = @options.confirm_message or 'Files are still being uploaded.' 9 | @allowed_types = @options.allowed_types 10 | @max_size = @options.max_size 11 | 12 | @active_files = 0 13 | 14 | @selector?.bind 'change', (e) => 15 | @file_select_handler(e) 16 | 17 | xhr = new XMLHttpRequest() 18 | if @drop_area? and xhr.upload 19 | @drop_area.on 'dragover', @file_drag_hover 20 | @drop_area.on 'dragleave', @file_drag_hover 21 | @drop_area.on 'drop', (e) => 22 | @file_select_handler e 23 | @drop_area.show() 24 | 25 | window.onbeforeunload = => 26 | if @confirm_message? and @active_files > 0 27 | return @confirm_message 28 | 29 | file_drag_hover: (e) => 30 | if not @drop_area? 31 | return 32 | e.stopPropagation() 33 | e.preventDefault() 34 | if e.type is 'dragover' 35 | @drop_area.addClass 'drag-hover' 36 | else 37 | @drop_area.removeClass 'drag-hover' 38 | 39 | file_select_handler: (e) => 40 | @file_drag_hover(e) 41 | files = e.originalEvent.dataTransfer?.files or e.target?.files or e.dataTransfer?.files 42 | if files?.length > 0 43 | @upload_files(files) 44 | 45 | upload_files: (files) => 46 | @get_upload_urls files.length, (error, urls) => 47 | if error 48 | console.log 'Error getting URLs', error 49 | return 50 | @process_files files, urls, 0 51 | 52 | get_upload_urls: (n, callback) => 53 | return if n <= 0 54 | api_call 'GET', @upload_url, {count: n}, (error, result) -> 55 | if error 56 | callback error 57 | throw error 58 | callback undefined, result 59 | 60 | process_files: (files, urls, i) => 61 | return if i >= files.length 62 | @upload_file files[i], urls[i].upload_url, @upload_handler?.preview(files[i]), () => 63 | @process_files files, urls, i + 1, @upload_handler? 64 | 65 | upload_file: (file, url, progress, callback) => 66 | xhr = new XMLHttpRequest() 67 | if @allowed_types?.length > 0 68 | if file.type not in @allowed_types 69 | progress 0, undefined, 'wrong_type' 70 | callback() 71 | return 72 | 73 | if @max_size? 74 | if file.size > @max_size 75 | progress 0, undefined, 'too_big' 76 | callback() 77 | return 78 | 79 | @active_files += 1 80 | 81 | xhr.upload.addEventListener 'progress', (event) -> 82 | progress parseInt event.loaded / event.total * 100.0 83 | 84 | xhr.onreadystatechange = (event) => 85 | if xhr.readyState == 4 86 | if xhr.status == 200 87 | response = JSON.parse(xhr.responseText) 88 | progress 100.0, response.result 89 | @active_files -= 1 90 | else 91 | progress 0, undefined, 'error' 92 | @active_files -= 1 93 | 94 | xhr.open 'POST', url, true 95 | data = new FormData() 96 | data.append 'file', file 97 | xhr.send data 98 | callback() 99 | )() 100 | -------------------------------------------------------------------------------- /main/static/src/script/common/util.coffee: -------------------------------------------------------------------------------- 1 | window.LOG = -> 2 | console?.log? arguments... 3 | 4 | 5 | window.init_common = -> 6 | init_loading_button() 7 | init_confirm_button() 8 | init_password_show_button() 9 | init_time() 10 | init_announcement() 11 | init_row_link() 12 | 13 | 14 | window.init_loading_button = -> 15 | $('body').on 'click', '.btn-loading', -> 16 | $(this).button 'loading' 17 | 18 | 19 | window.init_confirm_button = -> 20 | $('body').on 'click', '.btn-confirm', -> 21 | if not confirm $(this).data('message') or 'Are you sure?' 22 | event.preventDefault() 23 | 24 | 25 | window.init_password_show_button = -> 26 | $('body').on 'click', '.btn-password-show', -> 27 | $target = $($(this).data 'target') 28 | $target.focus() 29 | if $(this).hasClass 'active' 30 | $target.attr 'type', 'password' 31 | else 32 | $target.attr 'type', 'text' 33 | 34 | 35 | window.init_time = -> 36 | if $('time').length > 0 37 | recalculate = -> 38 | $('time[datetime]').each -> 39 | date = moment.utc $(this).attr 'datetime' 40 | diff = moment().diff date , 'days' 41 | if diff > 25 42 | $(this).text date.local().format 'YYYY-MM-DD' 43 | else 44 | $(this).text date.fromNow() 45 | $(this).attr 'title', date.local().format 'dddd, MMMM Do YYYY, HH:mm:ss Z' 46 | setTimeout recalculate, 1000 * 45 47 | recalculate() 48 | 49 | 50 | window.init_announcement = -> 51 | $('.alert-announcement button.close').click -> 52 | sessionStorage?.setItem 'closedAnnouncement', $('.alert-announcement').html() 53 | 54 | if sessionStorage?.getItem('closedAnnouncement') != $('.alert-announcement').html() 55 | $('.alert-announcement').show() 56 | 57 | 58 | window.init_row_link = -> 59 | $('body').on 'click', '.row-link', -> 60 | window.location.href = $(this).data 'href' 61 | 62 | $('body').on 'click', '.not-link', (e) -> 63 | e.stopPropagation() 64 | 65 | 66 | window.clear_notifications = -> 67 | $('#notifications').empty() 68 | 69 | 70 | window.show_notification = (message, category='warning') -> 71 | clear_notifications() 72 | return if not message 73 | 74 | $('#notifications').append """ 75 |
76 | 77 | #{message} 78 |
79 | """ 80 | 81 | 82 | window.size_human = (nbytes) -> 83 | for suffix in ['B', 'KB', 'MB', 'GB', 'TB'] 84 | if nbytes < 1000 85 | if suffix == 'B' 86 | return "#{nbytes} #{suffix}" 87 | return "#{parseInt(nbytes * 10) / 10} #{suffix}" 88 | nbytes /= 1024.0 89 | 90 | -------------------------------------------------------------------------------- /main/static/src/script/site/app.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | init_common() 3 | 4 | $ -> $('html.auth').each -> 5 | init_auth() 6 | 7 | $ -> $('html.user-list').each -> 8 | init_user_list() 9 | 10 | $ -> $('html.user-merge').each -> 11 | init_user_merge() 12 | 13 | $ -> $('html.resource-list').each -> 14 | init_resource_list() 15 | 16 | $ -> $('html.resource-view').each -> 17 | init_resource_view() 18 | 19 | $ -> $('html.resource-upload').each -> 20 | init_resource_upload() 21 | -------------------------------------------------------------------------------- /main/static/src/script/site/auth.coffee: -------------------------------------------------------------------------------- 1 | window.init_auth = -> 2 | $('.remember').change -> 3 | buttons = $('.btn-social').toArray().concat $('.btn-social-icon').toArray() 4 | for button in buttons 5 | href = $(button).prop 'href' 6 | if $('.remember input').is ':checked' 7 | $(button).prop 'href', "#{href}&remember=true" 8 | $('#remember').prop 'checked', true 9 | else 10 | $(button).prop 'href', href.replace '&remember=true', '' 11 | $('#remember').prop 'checked', false 12 | 13 | $('.remember').change() 14 | -------------------------------------------------------------------------------- /main/static/src/script/site/pretty-file.coffee: -------------------------------------------------------------------------------- 1 | # http://blog.anorgan.com/2012/09/30/pretty-multi-file-upload-bootstrap-jquery-twig-silex/ 2 | if $(".pretty-file").length 3 | $(".pretty-file").each () -> 4 | pretty_file = $(this) 5 | file_input = pretty_file.find('input[type="file"]') 6 | file_input.hide() 7 | file_input.change () -> 8 | files = file_input[0].files 9 | info = "" 10 | if files.length > 1 11 | info = "#{files.length} files selected" 12 | else 13 | path = file_input.val().split("\\") 14 | info = path[path.length - 1] 15 | pretty_file.find(".input-group input").val(info) 16 | pretty_file.find(".input-group").click (e) -> 17 | e.preventDefault() 18 | file_input.click() 19 | $(this).blur() 20 | -------------------------------------------------------------------------------- /main/static/src/script/site/resource.coffee: -------------------------------------------------------------------------------- 1 | window.init_resource_list = () -> 2 | init_delete_resource_button() 3 | 4 | window.init_resource_view = () -> 5 | init_delete_resource_button() 6 | 7 | window.init_resource_upload = () -> 8 | if window.File and window.FileList and window.FileReader 9 | window.file_uploader = new FileUploader 10 | upload_handler: upload_handler 11 | selector: $('.file') 12 | drop_area: $('.drop-area') 13 | confirm_message: 'Files are still being uploaded.' 14 | upload_url: $('.file').data('get-upload-url') 15 | allowed_types: [] 16 | max_size: 1024 * 1024 * 1024 17 | 18 | upload_handler = 19 | preview: (file) -> 20 | $resource = $ """ 21 |
22 |
23 |
24 |
#{file.name}
25 |
26 |
27 |
28 |
29 |
30 |
31 | """ 32 | $preview = $('.preview', $resource) 33 | 34 | if file_uploader.active_files < 16 and file.type.indexOf("image") is 0 35 | reader = new FileReader() 36 | reader.onload = (e) => 37 | $preview.css('background-image', "url(#{e.target.result})") 38 | reader.readAsDataURL(file) 39 | else 40 | $preview.text(file.type or 'application/octet-stream') 41 | 42 | $('.resource-uploads').prepend($resource) 43 | 44 | (progress, resource, error) => 45 | if error 46 | $('.progress-bar', $resource).css('width', '100%') 47 | $('.progress-bar', $resource).addClass('progress-bar-danger') 48 | if error == 'too_big' 49 | $('.progress-text', $resource).text("Failed! Too big, max: #{size_human(file_uploader.max_size)}.") 50 | else if error == 'wrong_type' 51 | $('.progress-text', $resource).text("Failed! Wrong file type.") 52 | else 53 | $('.progress-text', $resource).text('Failed!') 54 | return 55 | 56 | if progress == 100.0 and resource 57 | $('.progress-bar', $resource).addClass('progress-bar-success') 58 | $('.progress-text', $resource).text("Success #{size_human(file.size)}") 59 | if resource.image_url and $preview.text().length > 0 60 | $preview.css('background-image', "url(#{resource.image_url})") 61 | $preview.text('') 62 | else if progress == 100.0 63 | $('.progress-bar', $resource).css('width', '100%') 64 | $('.progress-text', $resource).text("100% - Processing..") 65 | else 66 | $('.progress-bar', $resource).css('width', "#{progress}%") 67 | $('.progress-text', $resource).text("#{progress}% of #{size_human(file.size)}") 68 | 69 | 70 | window.init_delete_resource_button = () -> 71 | $('body').on 'click', '.btn-delete', (e) -> 72 | e.preventDefault() 73 | if confirm('Press OK to delete the resource') 74 | $(this).attr('disabled', 'disabled') 75 | api_call 'DELETE', $(this).data('api-url'), (err, result) => 76 | if err 77 | $(this).removeAttr('disabled') 78 | LOG 'Something went terribly wrong during delete!', err 79 | return 80 | target = $(this).data('target') 81 | redirect_url = $(this).data('redirect-url') 82 | if target 83 | $("#{target}").remove() 84 | if redirect_url 85 | window.location.href = redirect_url 86 | -------------------------------------------------------------------------------- /main/static/src/script/site/user.coffee: -------------------------------------------------------------------------------- 1 | window.init_user_list = -> 2 | init_user_selections() 3 | init_user_delete_btn() 4 | init_user_merge_btn() 5 | 6 | 7 | init_user_selections = -> 8 | $('input[name=user_db]').each -> 9 | user_select_row $(this) 10 | 11 | $('#select-all').change -> 12 | $('input[name=user_db]').prop 'checked', $(this).is ':checked' 13 | $('input[name=user_db]').each -> 14 | user_select_row $(this) 15 | 16 | $('input[name=user_db]').change -> 17 | user_select_row $(this) 18 | 19 | 20 | user_select_row = ($element) -> 21 | update_user_selections() 22 | $('input[name=user_db]').each -> 23 | id = $element.val() 24 | $("##{id}").toggleClass 'warning', $element.is ':checked' 25 | 26 | 27 | update_user_selections = -> 28 | selected = $('input[name=user_db]:checked').length 29 | $('#user-actions').toggleClass 'hidden', selected == 0 30 | $('#user-merge').toggleClass 'hidden', selected < 2 31 | if selected is 0 32 | $('#select-all').prop 'indeterminate', false 33 | $('#select-all').prop 'checked', false 34 | else if $('input[name=user_db]:not(:checked)').length is 0 35 | $('#select-all').prop 'indeterminate', false 36 | $('#select-all').prop 'checked', true 37 | else 38 | $('#select-all').prop 'indeterminate', true 39 | 40 | 41 | ############################################################################### 42 | # Delete Users Stuff 43 | ############################################################################### 44 | init_user_delete_btn = -> 45 | $('#user-delete').click (e) -> 46 | clear_notifications() 47 | e.preventDefault() 48 | confirm_message = ($(this).data 'confirm').replace '{users}', $('input[name=user_db]:checked').length 49 | if confirm confirm_message 50 | user_keys = [] 51 | $('input[name=user_db]:checked').each -> 52 | $(this).attr 'disabled', true 53 | user_keys.push $(this).val() 54 | delete_url = $(this).data 'api-url' 55 | success_message = $(this).data 'success' 56 | error_message = $(this).data 'error' 57 | api_call 'DELETE', delete_url, {user_keys: user_keys.join(',')}, (err, result) -> 58 | if err 59 | $('input[name=user_db]:disabled').removeAttr 'disabled' 60 | show_notification error_message.replace('{users}', user_keys.length), 'danger' 61 | return 62 | $("##{result.join(', #')}").fadeOut -> 63 | $(this).remove() 64 | update_user_selections() 65 | show_notification success_message.replace('{users}', user_keys.length), 'success' 66 | 67 | 68 | ############################################################################### 69 | # Merge Users Stuff 70 | ############################################################################### 71 | window.init_user_merge = -> 72 | user_keys = $('#user_keys').val() 73 | api_url = $('.api-url').data 'api-url' 74 | api_call 'GET', api_url, {user_keys: user_keys}, (error, result) -> 75 | if error 76 | LOG 'Something went terribly wrong' 77 | return 78 | window.user_dbs = result 79 | $('input[name=user_db]').removeAttr 'disabled' 80 | 81 | $('input[name=user_db]').change (event) -> 82 | user_key = $(event.currentTarget).val() 83 | select_default_user user_key 84 | 85 | 86 | select_default_user = (user_key) -> 87 | $('.user-row').removeClass('success').addClass 'danger' 88 | $("##{user_key}").removeClass('danger').addClass 'success' 89 | 90 | for user_db in user_dbs 91 | if user_key == user_db.key 92 | $('input[name=user_key]').val user_db.key 93 | $('input[name=username]').val user_db.username 94 | $('input[name=name]').val user_db.name 95 | $('input[name=email]').val user_db.email 96 | break 97 | 98 | 99 | init_user_merge_btn = -> 100 | $('#user-merge').click (e) -> 101 | e.preventDefault() 102 | user_keys = [] 103 | $('input[name=user_db]:checked').each -> 104 | user_keys.push $(this).val() 105 | user_merge_url = $(this).data 'user-merge-url' 106 | window.location.href = "#{user_merge_url}?user_keys=#{user_keys.join(',')}" 107 | -------------------------------------------------------------------------------- /main/static/src/style/base.less: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: @navbar-height; 3 | > .container { 4 | padding-top: floor(@line-height-computed / 2); 5 | padding-bottom: @line-height-computed * 2; 6 | } 7 | } 8 | 9 | .alert-announcement { 10 | display: none; 11 | &.container { 12 | border-radius: 0; 13 | margin-bottom: 0; 14 | padding: @alert-padding; 15 | } 16 | > .close { 17 | right: 0; 18 | } 19 | } 20 | 21 | .img-error { 22 | max-width: 100%; 23 | margin: auto; 24 | display: block; 25 | } 26 | 27 | .anchor { 28 | display: block; 29 | position: relative; 30 | top: -@navbar-height; 31 | visibility: hidden; 32 | } 33 | 34 | .recaptcha { 35 | min-height: 78px; 36 | } 37 | -------------------------------------------------------------------------------- /main/static/src/style/footer.less: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | } 5 | 6 | body { 7 | margin-bottom: @footer-height; 8 | } 9 | 10 | .footer { 11 | position: absolute; 12 | bottom: 0; 13 | display: table; 14 | width: 100%; 15 | height: @footer-height; 16 | border-top: 1px solid @hr-border; 17 | color: @text-muted; 18 | background-color: @well-bg; 19 | text-align: center; 20 | & > .container { 21 | display: table-cell; 22 | vertical-align: middle; 23 | p:last-child { 24 | margin-bottom: 0; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /main/static/src/style/mixins.less: -------------------------------------------------------------------------------- 1 | .ellipsis { 2 | white-space: nowrap; 3 | overflow: hidden; 4 | text-overflow: ellipsis; 5 | line-height: normal; 6 | } 7 | 8 | td { 9 | &.ellipsis { 10 | max-width: 0; 11 | } 12 | } 13 | 14 | .row-link { 15 | cursor: pointer; 16 | .not-link { 17 | cursor: default; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /main/static/src/style/resource.less: -------------------------------------------------------------------------------- 1 | .drop-area { 2 | .well; 3 | display: none; 4 | text-align: center; 5 | cursor: default; 6 | &.drag-hover { 7 | color: @brand-success; 8 | border-style: dashed; 9 | border-color: @brand-success; 10 | .box-shadow(inset 0 3px 4px @gray-light); 11 | } 12 | } 13 | 14 | body { 15 | &.drag-hover { 16 | .opacity(50); 17 | } 18 | } 19 | 20 | .ellipsis { 21 | white-space: nowrap; 22 | overflow: hidden; 23 | text-overflow: ellipsis; 24 | } 25 | 26 | td { 27 | &.ellipsis { 28 | max-width: 0; 29 | } 30 | } 31 | 32 | .resource-uploads { 33 | .thumbnail { 34 | margin-bottom: (@grid-gutter-width / 2); 35 | .preview { 36 | white-space: nowrap; 37 | overflow: hidden; 38 | text-overflow: ellipsis; 39 | height: 80px; 40 | line-height: 80px; 41 | text-align: center; 42 | border-radius: 3px; 43 | border: 1px solid @thumbnail-border; 44 | background-repeat: no-repeat; 45 | background-position: 50% 50%; 46 | background-size: cover; 47 | background-color: @well-bg; 48 | } 49 | h5 { 50 | font-size: @font-size-small; 51 | .ellipsis; 52 | } 53 | .progress { 54 | margin-bottom: 0px; 55 | position: relative; 56 | background-color: #aaa; 57 | .progress-text { 58 | color: white; 59 | position: absolute; 60 | left: 0; 61 | right: 0; 62 | font-size: @font-size-small; 63 | text-align: center; 64 | white-space: nowrap; 65 | overflow: hidden; 66 | text-overflow: ellipsis; 67 | text-shadow: 0 0 1px black, 0 0 1px black; 68 | line-height: @line-height-computed; 69 | cursor: default; 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /main/static/src/style/signin.less: -------------------------------------------------------------------------------- 1 | @height-base: @line-height-computed + @padding-base-vertical * 2; 2 | @height-lg: floor(@font-size-large * @line-height-base) + 3 | @padding-large-vertical * 2; 4 | @height-sm: floor(@font-size-small * 1.5) + @padding-small-vertical * 2; 5 | @height-xs: floor(@font-size-small * 1.2) + @padding-small-vertical + 1; 6 | 7 | .btn-social { 8 | position: relative; 9 | padding-left: @height-base + @padding-base-horizontal; 10 | text-align: left; 11 | white-space: nowrap; 12 | overflow: hidden; 13 | text-overflow: ellipsis; 14 | :first-child { 15 | position: absolute; 16 | left: 0; 17 | top: 0; 18 | bottom: 0; 19 | width: @height-base; 20 | line-height: @height-base + 2; 21 | font-size: 1.6em; 22 | text-align: center; 23 | border-right: 1px solid rgba(0, 0, 0, 0.2); 24 | } 25 | &.btn-lg { 26 | padding-left: @height-lg + @padding-large-horizontal; 27 | :first-child { 28 | line-height: @height-lg; 29 | width: @height-lg; 30 | font-size: 1.8em; 31 | } 32 | } 33 | &.btn-sm { 34 | padding-left: @height-sm + @padding-small-horizontal; 35 | :first-child { 36 | line-height: @height-sm; 37 | width: @height-sm; 38 | font-size: 1.4em; 39 | } 40 | } 41 | &.btn-xs { 42 | padding-left: @height-xs + @padding-small-horizontal; 43 | :first-child { 44 | line-height: @height-xs; 45 | width: @height-xs; 46 | font-size: 1.2em; 47 | } 48 | } 49 | } 50 | 51 | .btn-social-icon { 52 | .btn-social; 53 | height: @height-base + 2; 54 | width: @height-base + 2; 55 | padding-left: 0; 56 | padding-right: 0; 57 | :first-child { 58 | border: none; 59 | text-align: center; 60 | width: 100% !important; 61 | } 62 | &.btn-lg { 63 | height: @height-lg; 64 | width: @height-lg; 65 | padding-left: 0; 66 | padding-right: 0; 67 | } 68 | &.btn-sm { 69 | height: @height-sm + 2; 70 | width: @height-sm + 2; 71 | padding-left: 0; 72 | padding-right: 0; 73 | } 74 | &.btn-xs { 75 | height: @height-xs + 2; 76 | width: @height-xs + 2; 77 | padding-left: 0; 78 | padding-right: 0; 79 | } 80 | } 81 | 82 | .btn-social(@color-bg, @color: #fff) { 83 | background-color: @color-bg; 84 | .button-variant(@color, @color-bg, rgba(0,0,0,0.2)); 85 | } 86 | 87 | .auth { 88 | .btn-social-icon { 89 | margin-top: 2px; 90 | margin-bottom: 2px; 91 | } 92 | } 93 | 94 | .remember { 95 | .text-center; 96 | label { 97 | display: inline-block; 98 | } 99 | } 100 | 101 | .btn-azure_ad { 102 | .btn-social(#307ea7); 103 | } 104 | .btn-bitbucket { 105 | .btn-social(#205081); 106 | } 107 | .btn-dropbox { 108 | .btn-social(#007ee5); 109 | } 110 | .btn-facebook { 111 | .btn-social(#3b5998); 112 | } 113 | .btn-github { 114 | .btn-social(#444444); 115 | } 116 | .btn-google { 117 | .btn-social(#dd4b39); 118 | } 119 | .btn-instagram { 120 | .btn-social(#3f729b); 121 | } 122 | .btn-linkedin { 123 | .btn-social(#007bb6); 124 | } 125 | .btn-mailru { 126 | .btn-social(#168de2); 127 | } 128 | .btn-microsoft { 129 | .btn-social(#2672ec); 130 | } 131 | .btn-reddit { 132 | .btn-social(#eff7ff, #000); 133 | } 134 | .btn-twitter { 135 | .btn-social(#55acee); 136 | } 137 | .btn-vk { 138 | .btn-social(#587ea3); 139 | } 140 | .btn-yahoo { 141 | .btn-social(#720e9e); 142 | } 143 | -------------------------------------------------------------------------------- /main/static/src/style/style.less: -------------------------------------------------------------------------------- 1 | @import '../../ext/font-awesome/less/font-awesome'; 2 | @import '../../ext/bootstrap/less/bootstrap'; 3 | 4 | @import 'base'; 5 | @import 'variables'; 6 | @import 'test'; 7 | @import 'mixins'; 8 | @import 'footer'; 9 | @import 'signin'; 10 | @import 'user'; 11 | @import 'resource'; 12 | -------------------------------------------------------------------------------- /main/static/src/style/test.less: -------------------------------------------------------------------------------- 1 | .test { 2 | section { 3 | padding: @line-height-computed 0; 4 | > h2 { 5 | margin-bottom: @line-height-computed; 6 | border-bottom: 1px solid @hr-border; 7 | a { 8 | color: @headings-color; 9 | text-decoration: none; 10 | } 11 | 12 | .on-hover { 13 | display: none; 14 | } 15 | &:hover { 16 | .on-hover { 17 | display: inline-block; 18 | } 19 | } 20 | } 21 | } 22 | 23 | .test-button, 24 | .test-social { 25 | .btn { 26 | margin-bottom: @line-height-computed / 2; 27 | } 28 | } 29 | 30 | .test-grid { 31 | .cell { 32 | .text-center; 33 | .bg-info; 34 | .text-info; 35 | .small; 36 | border-radius: @border-radius-base; 37 | border: 1px solid darken(@brand-info, 20%); 38 | overflow: hidden; 39 | margin-bottom: 8px; 40 | padding: @padding-base-vertical 0; 41 | } 42 | } 43 | 44 | .test-font { 45 | p::first-line { 46 | font-size: @font-size-large; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /main/static/src/style/user.less: -------------------------------------------------------------------------------- 1 | .user-list { 2 | .name { 3 | .h5; 4 | label { 5 | margin-bottom: 0; 6 | img { 7 | border: 1px solid @hr-border; 8 | padding: 2px; 9 | border-radius: 4px; 10 | .square(40px); 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /main/static/src/style/variables.less: -------------------------------------------------------------------------------- 1 | @fa-font-path: '/p/ext/font-awesome/fonts'; 2 | @icon-font-path: '/p/ext/bootstrap/fonts/'; 3 | 4 | @footer-height: @line-height-computed * 5; 5 | -------------------------------------------------------------------------------- /main/task.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import logging 4 | 5 | import flask 6 | from google.appengine.api import mail 7 | from google.appengine.ext import deferred 8 | 9 | import config 10 | import util 11 | 12 | 13 | ############################################################################### 14 | # Helpers 15 | ############################################################################### 16 | def send_mail_notification(subject, body, to=None, **kwargs): 17 | if not config.CONFIG_DB.feedback_email: 18 | return 19 | brand_name = config.CONFIG_DB.brand_name 20 | sender = '%s <%s>' % (brand_name, config.CONFIG_DB.feedback_email) 21 | subject = '[%s] %s' % (brand_name, subject) 22 | if config.DEVELOPMENT: 23 | logging.info( 24 | '\n' 25 | '######### Deferring to send this email: #############################' 26 | '\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n' 27 | '#####################################################################', 28 | sender, to or sender, subject, body 29 | ) 30 | deferred.defer(mail.send_mail, sender, to or sender, subject, body, **kwargs) 31 | 32 | 33 | ############################################################################### 34 | # Admin Notifications 35 | ############################################################################### 36 | def new_user_notification(user_db): 37 | if not config.CONFIG_DB.notify_on_new_user: 38 | return 39 | body = 'name: %s\nusername: %s\nemail: %s\n%s\n%s' % ( 40 | user_db.name, 41 | user_db.username, 42 | user_db.email, 43 | ''.join([': '.join(('%s\n' % a).split('_')) for a in user_db.auth_ids]), 44 | flask.url_for('user_update', user_id=user_db.key.id(), _external=True), 45 | ) 46 | send_mail_notification('New user: %s' % user_db.name, body) 47 | 48 | 49 | ############################################################################### 50 | # User Related 51 | ############################################################################### 52 | def verify_email_notification(user_db): 53 | if not (config.CONFIG_DB.verify_email and user_db.email) or user_db.verified: 54 | return 55 | user_db.token = util.uuid() 56 | user_db.put() 57 | 58 | to = '%s <%s>' % (user_db.name, user_db.email) 59 | body = '''Hello %(name)s, 60 | 61 | it seems someone (hopefully you) tried to verify your email with %(brand)s. 62 | 63 | In case it was you, please verify it by following this link: 64 | 65 | %(link)s 66 | 67 | If it wasn't you, we apologize. You can either ignore this email or reply to it 68 | so we can take a look. 69 | 70 | Best regards, 71 | %(brand)s 72 | ''' % { 73 | 'name': user_db.name, 74 | 'link': flask.url_for('user_verify', token=user_db.token, _external=True), 75 | 'brand': config.CONFIG_DB.brand_name, 76 | } 77 | 78 | flask.flash( 79 | 'A verification link has been sent to your email address.', 80 | category='success', 81 | ) 82 | send_mail_notification('Verify your email.', body, to) 83 | 84 | 85 | def reset_password_notification(user_db): 86 | if not user_db.email: 87 | return 88 | user_db.token = util.uuid() 89 | user_db.put() 90 | 91 | to = '%s <%s>' % (user_db.name, user_db.email) 92 | body = '''Hello %(name)s, 93 | 94 | it seems someone (hopefully you) tried to reset your password with %(brand)s. 95 | 96 | In case it was you, please reset it by following this link: 97 | 98 | %(link)s 99 | 100 | If it wasn't you, we apologize. You can either ignore this email or reply to it 101 | so we can take a look. 102 | 103 | Best regards, 104 | %(brand)s 105 | ''' % { 106 | 'name': user_db.name, 107 | 'link': flask.url_for('user_reset', token=user_db.token, _external=True), 108 | 'brand': config.CONFIG_DB.brand_name, 109 | } 110 | 111 | flask.flash( 112 | 'A reset link has been sent to your email address.', 113 | category='success', 114 | ) 115 | send_mail_notification('Reset your password', body, to) 116 | 117 | 118 | def activate_user_notification(user_db): 119 | if not user_db.email: 120 | return 121 | user_db.token = util.uuid() 122 | user_db.put() 123 | 124 | to = user_db.email 125 | body = '''Welcome to %(brand)s. 126 | 127 | Follow the link below to confirm your email address and activate your account: 128 | 129 | %(link)s 130 | 131 | If it wasn't you, we apologize. You can either ignore this email or reply to it 132 | so we can take a look. 133 | 134 | Best regards, 135 | %(brand)s 136 | ''' % { 137 | 'link': flask.url_for('user_activate', token=user_db.token, _external=True), 138 | 'brand': config.CONFIG_DB.brand_name, 139 | } 140 | 141 | flask.flash( 142 | 'An activation link has been sent to your email address.', 143 | category='success', 144 | ) 145 | send_mail_notification('Activate your account', body, to) 146 | 147 | 148 | ############################################################################### 149 | # Admin Related 150 | ############################################################################### 151 | def email_conflict_notification(email): 152 | body = 'There is a conflict with %s\n\n%s' % ( 153 | email, 154 | flask.url_for('user_list', email=email, _external=True), 155 | ) 156 | send_mail_notification('Conflict with: %s' % email, body) 157 | -------------------------------------------------------------------------------- /main/templates/admin/admin.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | 3 | # block content 4 | 7 | 8 |
9 | {{admin_link('App Config', 'cog', url_for('admin_config'))}} 10 | {{admin_link('Auth Config', 'lock', url_for('admin_auth'))}} 11 | {{admin_link('Test', 'sliders', url_for('admin_test'))}} 12 |
13 | 14 | 17 | 18 |
19 | {{admin_link('User List', 'group', url_for('user_list', order='-modified', active=True))}} 20 |
21 | 22 | 28 | 29 |
30 | # if localhost 31 | {{admin_link('Localhost', 'home', localhost, 'gae')}} 32 | # endif 33 | {{admin_link('Dashboard', 'tachometer', 'https://console.cloud.google.com/home/dashboard?project=%s' % config.APPLICATION_ID, 'gae')}} 34 | {{admin_link('Datastore', 'database', 'https://console.cloud.google.com/datastore/query?project=%s' % config.APPLICATION_ID, 'gae')}} 35 | {{admin_link('Instances', 'bolt', 'https://console.cloud.google.com/appengine/instances?project=%s' % config.APPLICATION_ID, 'gae')}} 36 | {{admin_link('Versions', 'history', 'https://console.cloud.google.com/appengine/versions?project=%s' % config.APPLICATION_ID, 'gae')}} 37 | {{admin_link('Logs', 'bullhorn', 'https://console.cloud.google.com/logs?project=%s&versionId=%s' % (config.APPLICATION_ID, config.CURRENT_VERSION_NAME), 'gae')}} 38 | {{admin_link('APIs', 'wrench', 'https://console.developers.google.com/apis/library?project=%s' % config.APPLICATION_ID, 'gae')}} 39 | {{admin_link('Settings', 'cogs', 'https://console.cloud.google.com/appengine/settings?project=%s' % config.APPLICATION_ID, 'gae')}} 40 | {{admin_link('Billing', 'credit-card', 'https://console.cloud.google.com/billing', 'gae')}} 41 |
42 | 43 | # endblock 44 | 45 | 46 | # macro admin_link(title, icon, url, target='_self') 47 | 53 | # endmacro 54 | -------------------------------------------------------------------------------- /main/templates/admin/admin_auth.html: -------------------------------------------------------------------------------- 1 | # extends 'admin/admin_base.html' 2 | 3 | # block admin_content 4 |
5 |
6 |
7 | {{form.csrf_token}} 8 |
9 | # import 'macro/forms.html' as forms 10 | # include 'admin/bit/azure_ad_oauth.html' 11 | # include 'admin/bit/bitbucket_oauth.html' 12 | # include 'admin/bit/dropbox_oauth.html' 13 | # include 'admin/bit/facebook_oauth.html' 14 | # include 'admin/bit/github_oauth.html' 15 | # include 'admin/bit/google_oauth.html' 16 | # include 'admin/bit/instagram_oauth.html' 17 | # include 'admin/bit/linkedin_oauth.html' 18 | # include 'admin/bit/mailru_oauth.html' 19 | # include 'admin/bit/microsoft_oauth.html' 20 | # include 'admin/bit/reddit_oauth.html' 21 | # include 'admin/bit/twitter_oauth.html' 22 | # include 'admin/bit/vk_oauth.html' 23 | # include 'admin/bit/yahoo_oauth.html' 24 |
25 | 28 |
29 |
30 |
31 | # endblock 32 | -------------------------------------------------------------------------------- /main/templates/admin/admin_base.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block content 5 | 30 | 31 | # block admin_content 32 | # endblock 33 | # endblock 34 | -------------------------------------------------------------------------------- /main/templates/admin/admin_config.html: -------------------------------------------------------------------------------- 1 | # extends 'admin/admin_base.html' 2 | # import 'macro/forms.html' as forms 3 | 4 | # block admin_content 5 |
6 |
7 |
8 | {{form.csrf_token}} 9 | {{forms.text_field(form.brand_name, autofocus=True)}} 10 | {{forms.email_field(form.feedback_email)}} 11 | {{forms.textarea_field(form.announcement_html)}} 12 | {{forms.select_field(form.announcement_type)}} 13 |
14 |
15 | {{forms.checkbox_field(form.notify_on_new_user)}} 16 | {{forms.checkbox_field(form.verify_email)}} 17 | {{forms.checkbox_field(form.check_unique_email)}} 18 | {{forms.checkbox_field(form.email_authentication)}} 19 | {{forms.checkbox_field(form.anonymous_recaptcha)}} 20 |
21 | # include 'admin/bit/security.html' 22 | # include 'admin/bit/google_analytics_tracking_id.html' 23 | # include 'admin/bit/google_cloud_bucket_name.html' 24 | # include 'admin/bit/recaptcha.html' 25 |
26 |
27 |
28 |
29 |
30 |
31 | 34 |
35 |
36 |
37 | # endblock 38 | -------------------------------------------------------------------------------- /main/templates/admin/bit/azure_ad_oauth.html: -------------------------------------------------------------------------------- 1 | 2 | {{ 3 | forms.panel_fields( 4 | 'Azure AD', 5 | (form.azure_ad_client_id, form.azure_ad_client_secret), 6 | ''' 7 | Callback URL for Azure AD application: 8 | http://%s 9 | ''' % request.host 10 | ) 11 | }} 12 | -------------------------------------------------------------------------------- /main/templates/admin/bit/bitbucket_oauth.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'Bitbucket', 4 | (form.bitbucket_key, form.bitbucket_secret), 5 | ''' 6 | Callback URL for Bitbucket: %s (needs Email and Read permissions for Account) 7 | ''' % url_for('bitbucket_authorized', _external=True) 8 | ) 9 | }} 10 | -------------------------------------------------------------------------------- /main/templates/admin/bit/dropbox_oauth.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'Dropbox', 4 | (form.dropbox_app_key, form.dropbox_app_secret), 5 | ''' 6 | OAuth redirect URI for Dropbox: 7 | https://%s%s 8 | ''' % (request.host, url_for('dropbox_authorized')) 9 | ) 10 | }} 11 | -------------------------------------------------------------------------------- /main/templates/admin/bit/facebook_oauth.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'Facebook', 4 | (form.facebook_app_id, form.facebook_app_secret), 5 | ''' 6 | Site URL for Facebook application: 7 | %s 8 | ''' % url_for('facebook_authorized', _external=True) 9 | ) 10 | }} 11 | -------------------------------------------------------------------------------- /main/templates/admin/bit/github_oauth.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'GitHub', 4 | (form.github_client_id, form.github_client_secret), 5 | ''' 6 | Callback URL for GitHub application: 7 | %s 8 | ''' % url_for('github_authorized', _external=True) 9 | ) 10 | }} 11 | -------------------------------------------------------------------------------- /main/templates/admin/bit/google_analytics_tracking_id.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'Google Analytics', 4 | form.analytics_id, 5 | ''' 6 | Get it from 7 | Google Analytics 8 | ''' 9 | ) 10 | }} 11 | -------------------------------------------------------------------------------- /main/templates/admin/bit/google_cloud_bucket_name.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'Google Cloud', 4 | (form.bucket_name,), 5 | form.bucket_name.data, 6 | ) 7 | }} 8 | -------------------------------------------------------------------------------- /main/templates/admin/bit/google_oauth.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'Google', 4 | (form.google_client_id, form.google_client_secret), 5 | ''' 6 | Redirect URI for Google application: 7 | %s
8 | Do not forget to enable "Google+ API" in your console. 9 | ''' % (config.APPLICATION_ID, url_for('google_authorized', _external=True), config.APPLICATION_ID) 10 | ) 11 | }} 12 | -------------------------------------------------------------------------------- /main/templates/admin/bit/instagram_oauth.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'Instagram', 4 | (form.instagram_client_id, form.instagram_client_secret), 5 | ''' 6 | Redirect URI for Instagram application: 7 | %s 8 | ''' % url_for('instagram_authorized', _external=True) 9 | ) 10 | }} 11 | -------------------------------------------------------------------------------- /main/templates/admin/bit/linkedin_oauth.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'LinkedIn', 4 | (form.linkedin_api_key, form.linkedin_secret_key), 5 | ''' 6 | OAuth 2.0 Redirect URL for LinkedIn Application: 7 | %s 8 | ''' % url_for('linkedin_authorized', _external=True) 9 | ) 10 | }} 11 | -------------------------------------------------------------------------------- /main/templates/admin/bit/mailru_oauth.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'Mail.Ru', 4 | (form.mailru_app_id, form.mailru_app_secret), 5 | ''' 6 | Base domain for Mail.Ru application: 7 | %s 8 | ''' % request.host 9 | ) 10 | }} 11 | -------------------------------------------------------------------------------- /main/templates/admin/bit/microsoft_oauth.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'Microsoft', 4 | (form.microsoft_client_id, form.microsoft_client_secret), 5 | ''' 6 | Redirect domain for Microsoft Application Registration: 7 | %s 8 | ''' % url_for('microsoft_authorized', _external=True) 9 | ) 10 | }} 11 | -------------------------------------------------------------------------------- /main/templates/admin/bit/recaptcha.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'reCAPTCHA', 4 | (form.recaptcha_public_key, form.recaptcha_private_key), 5 | ''' 6 | Domain name for reCAPTCHA: 7 | %s 8 | ''' % request.host 9 | ) 10 | }} 11 | -------------------------------------------------------------------------------- /main/templates/admin/bit/reddit_oauth.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'Reddit', 4 | (form.reddit_client_id, form.reddit_client_secret), 5 | ''' 6 | Redirect URL for Reddit application: 7 | %s 8 | ''' % url_for('reddit_authorized', _external=True) 9 | ) 10 | }} 11 | -------------------------------------------------------------------------------- /main/templates/admin/bit/security.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'Security', 4 | (form.trusted_hosts, form.flask_secret_key, form.salt), 5 | ''' 6 | Read more about 7 | Flask secret key 8 | and 9 | salt in cryptography. 10 | ''' 11 | ) 12 | }} 13 | -------------------------------------------------------------------------------- /main/templates/admin/bit/twitter_oauth.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'Twitter', 4 | (form.twitter_consumer_key, form.twitter_consumer_secret), 5 | ''' 6 | Callback URL for Twitter application: 7 | %s 8 | ''' % url_for('twitter_authorized', _external=True) 9 | ) 10 | }} 11 | -------------------------------------------------------------------------------- /main/templates/admin/bit/vk_oauth.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'VK', 4 | (form.vk_app_id, form.vk_app_secret), 5 | ''' 6 | Base domain for VK application: 7 | %s 8 | ''' % request.host 9 | ) 10 | }} 11 | -------------------------------------------------------------------------------- /main/templates/admin/bit/yahoo_oauth.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'Yahoo!', 4 | (form.yahoo_consumer_key, form.yahoo_consumer_secret), 5 | ''' 6 | Yahoo! Projects 7 | ''' 8 | ) 9 | }} 10 | -------------------------------------------------------------------------------- /main/templates/admin/test/test.html: -------------------------------------------------------------------------------- 1 | # extends 'admin/admin_base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | 5 | # block admin_header 6 |
7 | # for test in tests 8 | {{test.title()}} 9 | # endfor 10 |
11 | 16 | # endblock 17 | 18 | 19 | # block admin_content 20 | # for test in tests 21 |
22 |
23 |

24 | 25 | {{test.title()}} 26 | 27 | 28 | 29 |

30 | # include 'admin/test/test_%s.html' % test 31 |
32 | # endfor 33 | # endblock 34 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_alert.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Well done! You successfully read this important alert message. 4 |
5 | 6 |
7 | 8 | Heads up! This alert needs your attention, but it's not super important. 9 |
10 | 11 |
12 | 13 | Warning! Better check yourself, you're not looking too good. 14 |
15 | 16 |
17 | 18 | Oh snap! Change a few things up and try submitting again. 19 |
20 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_badge.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_filter.html: -------------------------------------------------------------------------------- 1 | # import 'macro/utils.html' as utils 2 | 3 |
4 |
5 |
6 | 7 | {{utils.filter_by_link('limit', 16, hash=test)}} 8 | {{utils.filter_by_link('limit', 64, hash=test)}} 9 | {{utils.filter_by_link('limit', 128, hash=test)}} 10 | {{utils.filter_by_link('limit', 512, hash=test)}} 11 | {{utils.filter_by_link('limit', -1, hash=test, label='∞')}} 12 |
13 | 14 |
15 | 16 | {{utils.filter_by_link('admin', True, 'thumbs-o-up', hash=test)}} 17 | {{utils.filter_by_link('admin', False, 'thumbs-o-down', hash=test)}} 18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_font.html: -------------------------------------------------------------------------------- 1 | # set lorem = ''' 2 | The quick brown fox jumps over the lazy dog. 3 | %s lorem ipsum dolor sit amet, consectetur adipiscing elit, feugiat nec metus. 4 | ''' % config.CONFIG_DB.brand_name 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | # for w in range(1, 10): 17 | # set weight = w * 100 18 | 19 | 20 | 23 | 26 | 27 | # endfor 28 | 29 |
WeightNormalItalic
{{weight}} 21 |

{{lorem}}

22 |
24 |

{{lorem}}

25 |
30 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_form.html: -------------------------------------------------------------------------------- 1 | # import 'macro/forms.html' as forms 2 | 3 |
4 | {{form.csrf_token}} 5 | 6 |
7 |
8 | {{forms.text_field(form.name)}} 9 | {{forms.number_field(form.number)}} 10 | {{forms.email_field(form.email, placeholder='steve@apple.com')}} 11 | {{forms.date_field(form.date)}} 12 | {{forms.textarea_field(form.textarea, rows=3)}} 13 | {{forms.checkbox_field(form.boolean)}} 14 |
15 |
16 | {{forms.password_field(form.password)}} 17 | {{forms.password_visible_field(form.password_visible)}} 18 | {{forms.text_field(form.prefix, prefix='@')}} 19 | {{forms.text_field(form.suffix, suffix='@example.com')}} 20 | {{forms.number_field(form.both, prefix='$', suffix='.00')}} 21 | {{forms.select_field(form.select)}} 22 |
23 |
24 | {{forms.multiple_checkbox_field(form.checkboxes)}} 25 | {{forms.radio_field(form.radios)}} 26 | {{ 27 | forms.panel_fields( 28 | 'Secret Keys', 29 | (form.public, form.private), 30 | 'You can see how they are used in app config.' % url_for('admin_config') 31 | ) 32 | }} 33 | # if config.CONFIG_DB.has_recaptcha 34 | {{forms.recaptcha_field(form.recaptcha)}} 35 | # endif 36 |
37 |
38 |
39 |
40 |
41 | 44 |
45 |
46 |
47 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_grid.html: -------------------------------------------------------------------------------- 1 |
2 | # for col in range(12) 3 |
4 |
1
5 |
6 | # endfor 7 |
8 | 9 |
10 | # for col in range(6) 11 |
12 |
2
13 |
14 | # endfor 15 |
16 | 17 |
18 | # for col in range(4) 19 |
20 |
3
21 |
22 | # endfor 23 |
24 | 25 |
26 | # for col in range(3) 27 |
28 |
4
29 |
30 | # endfor 31 |
32 | 33 |
34 | # for col in range(2) 35 |
36 |
6
37 |
38 | # endfor 39 |
40 | 41 |
42 |
43 |
8
44 |
45 |
46 |
4
47 |
48 |
49 | 50 |
51 |
52 |
10
53 |
54 |
55 |
2
56 |
57 |
58 | 59 |
60 |
61 |
12
62 |
63 |
64 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_heading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

h1. {{config.CONFIG_DB.brand_name}} Heading

h2. {{config.CONFIG_DB.brand_name}} Heading

h3. {{config.CONFIG_DB.brand_name}} Heading

h4. {{config.CONFIG_DB.brand_name}} Heading

h5. {{config.CONFIG_DB.brand_name}} Heading
h6. {{config.CONFIG_DB.brand_name}} Heading
23 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_label.html: -------------------------------------------------------------------------------- 1 | Default 2 | Primary 3 | Success 4 | Warning 5 | Danger 6 | Info 7 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_one.html: -------------------------------------------------------------------------------- 1 | # extends 'admin/admin_base.html' 2 | 3 | 4 | # block admin_content 5 |
6 | # include 'admin/test/test_%s.html' % test 7 |
8 | # endblock 9 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_pageres.html: -------------------------------------------------------------------------------- 1 |

Requirements

2 | 3 |
 4 |   npm install -g pageres-cli
 5 | 
6 | 7 | 8 |

Run the followings in your terminal

9 | 10 | # set resolutions = [ 11 | '360x640', 12 | '375x667', 13 | '414x736', 14 | '435x773', 15 | '800x600', 16 | '1024x768', 17 | '1280x1024', 18 | ] 19 | 20 | # set routes = [ 21 | 'welcome', 22 | 'feedback', 23 | 'signin', 24 | ] 25 | 26 |
27 | # for route in routes
28 | pageres {{url_for(route, _external=True)}} {{' '.join(resolutions)}} --filename='{{route}} <%= size %> - <%= url %>'
29 | # endfor
30 | 
31 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_pagination.html: -------------------------------------------------------------------------------- 1 | # import 'macro/utils.html' as utils 2 | 3 |
4 |
5 |

6 | {{utils.back_link('Back to pagination', 'admin_test', test='pagination')}} 7 |

8 |
9 |
10 | {{utils.next_link('#%s' % test)}} 11 |
12 |
13 | {{utils.next_link('#%s' % test, '#%s' % test)}} 14 |
15 |
16 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_paragraph.html: -------------------------------------------------------------------------------- 1 |

2 | Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor 3 | auctor. Duis mollis, est non commodo luctus. 4 |

5 |

6 | Nullam quis risus eget urna mollis ornare vel eu leo. Cum sociis 7 | natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. 8 | Nullam id dolor id nibh ultricies vehicula. 9 |

10 |

11 | Cum sociis natoque penatibus et magnis dis parturient 12 | montes, nascetur ridiculus mus. Donec ullamcorper nulla non metus auctor 13 | fringilla. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, 14 | eget lacinia odio sem nec elit. Donec ullamcorper nulla non 15 | metus auctor fringilla. 16 |

17 |

18 | Maecenas sed diam eget risus varius blandit sit amet non magna. Donec id 19 | elit non mi porta gravida at eget metus. Duis mollis, est non commodo 20 | luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. 21 |

22 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_responsive.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
✔ Visible on extra small devices
Phones (<768px)
5 |
6 |
7 | 8 |
✔ Visible on small devices
Tablets (≥768px)
9 |
10 |
11 |
12 |
Medium devices
Desktops (≥992px)
13 |
✔ Visible on medium devices
Desktops (≥992px)
14 |
15 |
16 | 17 |
✔ Visible on large devices
Desktops (≥1200px)
18 |
19 |
20 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_social.html: -------------------------------------------------------------------------------- 1 | # import 'macro/utils.html' as utils 2 | 3 |
4 |
5 | {{utils.signin_button('Google', 'btn-google', 'fa-google-plus', 'javascript:')}} 6 |
7 |
8 | {{utils.signin_button('Facebook', 'btn-facebook', 'fa-facebook', 'javascript:')}} 9 |
10 |
11 | {{utils.signin_button('Twitter', 'btn-twitter', 'fa-twitter', 'javascript:')}} 12 |
13 |
14 | {{utils.signin_button('Google', 'btn-google', 'fa-google-plus', 'javascript:', True)}} 15 | {{utils.signin_button('Facebook', 'btn-facebook', 'fa-facebook', 'javascript:', True)}} 16 | {{utils.signin_button('Twitter', 'btn-twitter', 'fa-twitter', 'javascript:', True)}} 17 |
18 |
19 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_table.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
#Column headingColumn headingColumn heading
1Column contentColumn contentColumn content
2Column contentColumn contentColumn content
3Column contentColumn contentColumn content
4Column contentColumn contentColumn content
5Column contentColumn contentColumn content
6Column contentColumn contentColumn content
7Column contentColumn contentColumn content
8Column contentColumn contentColumn content
9Column contentColumn contentColumn content
67 | -------------------------------------------------------------------------------- /main/templates/auth/auth.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block content 5 | 8 | 9 | # if current_user.id == 0 10 |
11 |
12 | # if config.CONFIG_DB.has_email_authentication 13 | # if form_type == 'signin' 14 | # include 'auth/signin_form.html' 15 | # else 16 | # include 'auth/signup_form.html' 17 | # endif 18 | or 19 | # endif 20 | 21 | # set is_icon = config.CONFIG_DB.has_email_authentication 22 |
23 | {{utils.signin_button('GAE' if config.CONFIG_DB.has_google else 'Google', 'btn-google', 'fa-google', gae_signin_url, is_icon)}} 24 | {{utils.signin_button('Facebook', 'btn-facebook', 'fa-facebook', facebook_signin_url, is_icon) if config.CONFIG_DB.has_facebook}} 25 | {{utils.signin_button('Twitter', 'btn-twitter', 'fa-twitter', twitter_signin_url, is_icon) if config.CONFIG_DB.has_twitter}} 26 | {{utils.signin_button('Azure AD', 'btn-azure_ad', 'fa-windows', azure_ad_signin_url, is_icon) if config.CONFIG_DB.has_azure_ad}} 27 | {{utils.signin_button('Bitbucket', 'btn-bitbucket', 'fa-bitbucket', bitbucket_signin_url, is_icon) if config.CONFIG_DB.has_bitbucket}} 28 | {{utils.signin_button('Dropbox', 'btn-dropbox', 'fa-dropbox', dropbox_signin_url, is_icon) if config.CONFIG_DB.has_dropbox}} 29 | {{utils.signin_button('GitHub', 'btn-github', 'fa-github', github_signin_url, is_icon) if config.CONFIG_DB.has_github}} 30 | {{utils.signin_button('Google', 'btn-google', 'fa-google-plus', google_signin_url, is_icon) if config.CONFIG_DB.has_google}} 31 | {{utils.signin_button('Instagram', 'btn-instagram', 'fa-instagram', instagram_signin_url, is_icon) if config.CONFIG_DB.has_instagram}} 32 | {{utils.signin_button('LinkedIn', 'btn-linkedin', 'fa-linkedin', linkedin_signin_url, is_icon) if config.CONFIG_DB.has_linkedin}} 33 | {{utils.signin_button('Mail.Ru', 'btn-mailru', 'fa-at', mailru_signin_url, is_icon) if config.CONFIG_DB.has_mailru}} 34 | {{utils.signin_button('Microsoft', 'btn-microsoft', 'fa-windows', microsoft_signin_url, is_icon) if config.CONFIG_DB.has_microsoft}} 35 | {{utils.signin_button('Reddit', 'btn-reddit', 'fa-reddit', reddit_signin_url, is_icon) if config.CONFIG_DB.has_reddit}} 36 | {{utils.signin_button('VK', 'btn-vk', 'fa-vk', vk_signin_url, is_icon) if config.CONFIG_DB.has_vk}} 37 | {{utils.signin_button('Yahoo!', 'btn-yahoo', 'fa-yahoo', yahoo_signin_url, is_icon) if config.CONFIG_DB.has_yahoo}} 38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 | # else 46 |
47 |

You are already signed in as {{current_user.user_db.name}}!

48 | Please sign out 49 | first if you want to sign in with a different account. 50 |
51 | # endif 52 | # endblock 53 | -------------------------------------------------------------------------------- /main/templates/auth/signin_form.html: -------------------------------------------------------------------------------- 1 | # import 'macro/forms.html' as forms 2 | 3 |
4 | {{form.csrf_token}} 5 | {{forms.hidden_field(form.next_url)}} 6 | {{form.remember(class='hide')}} 7 | {{forms.email_field(form.email, autofocus=True, class='form-control input-lg')}} 8 | {{forms.password_visible_field(form.password, size='lg')}} 9 | {{forms.recaptcha_field(form.recaptcha)}} 10 |
11 | 14 |
15 |
16 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /main/templates/auth/signup_form.html: -------------------------------------------------------------------------------- 1 | # import 'macro/forms.html' as forms 2 | 3 |
4 | {{form.csrf_token}} 5 | {{forms.email_field(form.email, autofocus=True, class='form-control input-lg')}} 6 | {{forms.recaptcha_field(form.recaptcha)}} 7 |
8 | 11 |
12 |
13 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /main/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # include 'bit/meta.html' 5 | 6 | # block title 7 | {{title + ' |' if title}} 8 | # endblock 9 | {{config.CONFIG_DB.brand_name}} 10 | 11 | # include 'bit/style.html' 12 | # block head 13 | # endblock 14 | # include 'bit/analytics.html' 15 | 16 | 17 | 18 | # include 'bit/header.html' 19 | # include 'bit/announcement.html' 20 | # block header 21 | # endblock 22 |
23 | # include 'bit/notifications.html' 24 | # block content 25 | # endblock 26 |
27 | # include 'bit/footer.html' 28 | # include 'bit/script.html' 29 | # block scripts 30 | # endblock 31 | 32 | 33 | -------------------------------------------------------------------------------- /main/templates/bit/analytics.html: -------------------------------------------------------------------------------- 1 | # if not current_user.admin and config.CONFIG_DB.analytics_id 2 | 10 | # endif 11 | -------------------------------------------------------------------------------- /main/templates/bit/announcement.html: -------------------------------------------------------------------------------- 1 | # if config.CONFIG_DB.announcement_html 2 |
3 |
4 | 5 | {{config.CONFIG_DB.announcement_html|safe}} 6 |
7 |
8 | # endif 9 | -------------------------------------------------------------------------------- /main/templates/bit/contact_menu.html: -------------------------------------------------------------------------------- 1 | # if config.CONFIG_DB.feedback_email 2 |
  • 3 | Feedback 4 |
  • 5 | # endif 6 | -------------------------------------------------------------------------------- /main/templates/bit/footer.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | # if current_user.admin 4 |

    5 | Version: {{config.CURRENT_VERSION_NAME}} 6 | () 7 | # if api_url 8 | | View in JSON 9 | # endif 10 |

    11 | # endif 12 |

    © 2018 - {{config.CONFIG_DB.brand_name}}

    13 |
    14 |
    15 | -------------------------------------------------------------------------------- /main/templates/bit/header.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /main/templates/bit/meta.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /main/templates/bit/notifications.html: -------------------------------------------------------------------------------- 1 |
    2 | # for message in get_flashed_messages(with_categories=True) 3 |
    4 | 5 | {{message[1]}} 6 |
    7 | # endfor 8 |
    9 | -------------------------------------------------------------------------------- /main/templates/bit/script.html: -------------------------------------------------------------------------------- 1 | # if config.DEVELOPMENT 2 | 3 | 4 | # else 5 | 6 | 7 | # endif 8 | -------------------------------------------------------------------------------- /main/templates/bit/style.html: -------------------------------------------------------------------------------- 1 | # if config.DEVELOPMENT 2 | 3 | # else 4 | 5 | # endif 6 | -------------------------------------------------------------------------------- /main/templates/bit/user_menu.html: -------------------------------------------------------------------------------- 1 | # if current_user.id > 0 2 | 18 | # else 19 |
  • 20 | Sign in 21 |
  • 22 | # endif 23 | -------------------------------------------------------------------------------- /main/templates/error.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | 3 | # block header 4 |
    5 |
    6 |

    7 | {{error.code}} 8 | Oh no...! {{error.name}} 9 |

    10 |

    Maybe it's our fault.. maybe it's yours.. but either case, this page is unavailable!

    11 |
    12 |
    13 | # endblock 14 | 15 | # block content 16 | xkcd: Wisdom of the Ancients 17 | # endblock 18 | -------------------------------------------------------------------------------- /main/templates/error_static.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error!!1 7 | 25 | 26 | 27 |
    28 |

    Oh no...! Something went wrong :(

    29 |

    Please check back in a bit.

    30 |
    31 | 32 | 33 | -------------------------------------------------------------------------------- /main/templates/feedback.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/forms.html' as forms 3 | 4 | # block content 5 | 8 | 9 |
    10 |
    11 |
    12 | {{form.csrf_token}} 13 | 14 | {{forms.textarea_field(form.message, rows=8, autofocus=True)}} 15 | {{forms.email_field(form.email)}} 16 | {{forms.recaptcha_field(form.recaptcha)}} 17 | 18 | 21 |
    22 |
    23 |
    24 | # endblock 25 | -------------------------------------------------------------------------------- /main/templates/macro/utils.html: -------------------------------------------------------------------------------- 1 | # macro order_by_link(property, title, ignore='cursor', hash=None) 2 | # if request.args.get('order') == property 3 | {{title}} 4 | 5 | # elif request.args.get('order') == '-' + property 6 | {{title}} 7 | 8 | #else 9 | {{title}} 10 | #endif 11 | # endmacro 12 | 13 | 14 | # macro filter_by_link(property, value, icon=None, ignore='cursor', is_list=False, hash=None, label=None) 15 | # set value = '%s' % value 16 | 18 | # if icon 19 | 20 | # elif label 21 | {{label|safe}} 22 | # else 23 | {{value}} 24 | # endif 25 | 26 | # endmacro 27 | 28 | 29 | # macro back_link(title, route) 30 | 31 | 32 | 33 | # endmacro 34 | 35 | 36 | # macro next_link(next_url, prev_url=None, next_caption='', prev_caption='') 37 | # if next_url or prev_url 38 | 46 | # endif 47 | # endmacro 48 | 49 | 50 | # macro prefetch_link(url) 51 | # if url 52 | 53 | 54 | # endif 55 | # endmacro 56 | 57 | 58 | # macro signin_button(brand, class_btn, class_icon, url, is_icon=False) 59 | # set caption = 'Sign in with %s' % brand 60 | 61 | 62 | {{caption if not is_icon}} 63 | 64 | # endmacro 65 | 66 | 67 | # macro auth_icon(auth_id) 68 | # if auth_id == 'email_auth' 69 | 70 | # elif auth_id.startswith('azure_ad') 71 | 72 | # elif auth_id.startswith('bitbucket') 73 | 74 | # elif auth_id.startswith('dropbox') 75 | 76 | # elif auth_id.startswith('facebook') 77 | 78 | # elif auth_id.startswith('github') 79 | 80 | # elif auth_id.startswith('google') 81 | 82 | # elif auth_id.startswith('federated') 83 | 84 | # elif auth_id.startswith('instagram') 85 | 86 | # elif auth_id.startswith('linkedin') 87 | 88 | # elif auth_id.startswith('mailru') 89 | 90 | # elif auth_id.startswith('microsoft') 91 | 92 | # elif auth_id.startswith('reddit') 93 | 94 | # elif auth_id.startswith('twitter') 95 | 96 | # elif auth_id.startswith('yahoo') 97 | 98 | # else 99 | 100 | # endif 101 | # endmacro 102 | 103 | 104 | # macro auth_icons(user_db, max=0) 105 | # set count = user_db.auth_ids|length 106 | # set max = 3 if max > 0 and max < 3 else max 107 | # if user_db.password_hash 108 | # set max = max - 1 if max else max 109 | {{auth_icon('email_auth')}} 110 | # endif 111 | # set max = max - 1 if max and count > max else max 112 | # set more = count - max if max else 0 113 | # for auth_id in user_db.auth_ids 114 | # if not max or loop.index0 < max 115 | {{auth_icon(auth_id)}} 116 | # elif max and loop.index0 == max 117 | 118 | # endif 119 | # endfor 120 | # endmacro 121 | 122 | 123 | # macro html_element(name, content) 124 | <{{name}} 125 | #- for arg in kwargs 126 | {{arg}}="{{kwargs[arg]}}" 127 | #- endfor 128 | > 129 | #- if content 130 | {{content}} 131 | #- endif 132 | # endmacro 133 | -------------------------------------------------------------------------------- /main/templates/profile/profile.html: -------------------------------------------------------------------------------- 1 | # extends 'profile/profile_base.html' 2 | # import 'macro/forms.html' as forms 3 | # import 'macro/utils.html' as utils 4 | 5 | # block profile_content 6 |

    {{user_db.username}}

    7 |

    8 | {{user_db.email}} 9 | # if config.CONFIG_DB.verify_email and user_db.email and user_db.verified 10 | 11 | # elif config.CONFIG_DB.verify_email and user_db.email 12 | 13 | # endif 14 |

    15 |
    16 |

    17 | 18 | {{utils.auth_icons(user_db)}} 19 |

    20 | # endblock 21 | -------------------------------------------------------------------------------- /main/templates/profile/profile_base.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/forms.html' as forms 3 | # import 'macro/utils.html' as utils 4 | 5 | # block content 6 | 34 | 35 |
    36 |
    37 | Avatar of {{user_db.name}} 38 | # if html_class == 'profile-update' 39 |

    40 | # if user_db.has_facebook() 41 | Change on Facebook 42 | # else 43 | Change on Gravatar 44 | # endif 45 |

    46 | # endif 47 |
    48 | 49 |
    50 | # block profile_content 51 | # endblock 52 |
    53 |
    54 | # endblock 55 | -------------------------------------------------------------------------------- /main/templates/profile/profile_password.html: -------------------------------------------------------------------------------- 1 | # extends 'profile/profile_base.html' 2 | # import 'macro/forms.html' as forms 3 | 4 | # block profile_content 5 |
    6 | {{form.csrf_token}} 7 | {{forms.password_visible_field(form.new_password, autofocus=True)}} 8 | # if user_db.password_hash 9 | {{forms.password_visible_field(form.old_password)}} 10 | # if form.old_password.errors 11 |
    12 | Forgot password? 13 |
    14 | # endif 15 | # endif 16 |
    17 | 24 |
    25 |
    26 | # endblock 27 | -------------------------------------------------------------------------------- /main/templates/profile/profile_update.html: -------------------------------------------------------------------------------- 1 | # extends 'profile/profile_base.html' 2 | # import 'macro/forms.html' as forms 3 | 4 | # block profile_content 5 |
    6 | {{form.csrf_token}} 7 | {{forms.text_field(form.name, autofocus=True)}} 8 | # include 'user/user_email_field.html' 9 | 10 |
    11 | 14 |
    15 |
    16 | # endblock 17 | -------------------------------------------------------------------------------- /main/templates/resource/resource_list.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block content 5 | 15 | 16 |
    17 | There are no resources 18 |
    19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | # for resource_db in resource_dbs 32 | 33 | 34 | 35 | 40 | 41 | 52 | 53 | # endfor 54 | 55 |
    {{utils.order_by_link('name', 'Name')}}{{utils.order_by_link('size', 'Size')}}Actions
    56 | 57 | {{utils.next_link(next_url)}} 58 | # endblock 59 | -------------------------------------------------------------------------------- /main/templates/resource/resource_update.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/forms.html' as forms 3 | # import 'macro/utils.html' as utils 4 | 5 | # block content 6 | 12 | 13 |
    14 |
    15 |
    16 |
    17 | {{form.csrf_token}} 18 | {{forms.text_field(form.name)}} 19 | 20 | 23 |
    24 |
    25 |
    26 | # if resource_db.image_url 27 |
    28 |
    29 | 30 |
    31 |
    32 | # endif 33 |
    34 | # endblock 35 | -------------------------------------------------------------------------------- /main/templates/resource/resource_upload.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block content 5 | 11 | 12 |
    13 |
    14 |
    15 |
    16 | 17 |
    18 | Choose Files 19 | 20 | 21 | 22 | 23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    29 |
    30 | drop files here 31 |
    32 |
    33 |
    34 |
    35 |
    36 |
    Example preview!
    37 |
    38 |
    39 |
    Hola amigo
    40 |
    41 |
    42 |
    43 |
    44 | # endblock 45 | -------------------------------------------------------------------------------- /main/templates/resource/resource_view.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block content 5 | 29 | 30 |
    31 |
    32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 53 | 54 | 55 | 56 | 61 | 62 | 63 |
    Name{{resource_db.name}}
    Content Type{{resource_db.content_type}}
    Size{{resource_db.size_human}}
    Created 49 | 52 |
    Modifed 57 | 60 |
    64 |
    65 | # if resource_db.image_url 66 |
    67 |
    68 | 69 |
    70 |
    71 | # endif 72 |
    73 | # endblock 74 | -------------------------------------------------------------------------------- /main/templates/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{url_for('welcome', _external=True)}} 6 | {{lastmod}} 7 | weekly 8 | 0.8 9 | 10 | 11 | {{url_for('feedback', _external=True)}} 12 | {{lastmod}} 13 | weekly 14 | 0.8 15 | 16 | 17 | {{url_for('signin', _external=True)}} 18 | {{lastmod}} 19 | weekly 20 | 0.5 21 | 22 | 23 | -------------------------------------------------------------------------------- /main/templates/user/user_activate.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/forms.html' as forms 3 | # import 'macro/utils.html' as utils 4 | 5 | # block content 6 | 14 | 15 |
    16 |
    17 |
    18 | {{form.csrf_token}} 19 | {{forms.text_field(form.name, autofocus=True, class='form-control input-lg')}} 20 | {{forms.password_visible_field(form.password, autofocus=True, size='lg')}} 21 |
    22 | 25 |
    26 |
    27 |
    28 |
    29 | # endblock 30 | -------------------------------------------------------------------------------- /main/templates/user/user_email_field.html: -------------------------------------------------------------------------------- 1 | # import 'macro/forms.html' as forms 2 | 3 | # if config.CONFIG_DB.verify_email 4 |
    5 | {{form.email.label(class='control-label')}} 6 | {{forms.field_optional(form.email)}} 7 | {{form.email(class='form-control')}} 8 | # if request.method == 'GET' and user_db.email 9 | # if user_db.verified 10 | 11 | # else 12 | 13 | # endif 14 | # endif 15 | {{forms.field_errors(form.email)}} 16 |
    17 | # else 18 | {{forms.email_field(form.email)}} 19 | # endif 20 | -------------------------------------------------------------------------------- /main/templates/user/user_forgot.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/forms.html' as forms 3 | # import 'macro/utils.html' as utils 4 | 5 | # block content 6 | 12 | 13 |
    14 |
    15 |
    16 | {{form.csrf_token}} 17 | {{forms.email_field(form.email, autofocus=True, class='form-control input-lg', autocomplete='off')}} 18 | {{forms.recaptcha_field(form.recaptcha)}} 19 |
    20 | 23 | 24 | I think I know it! 25 | 26 |
    27 |
    28 |
    29 |
    30 | # endblock 31 | -------------------------------------------------------------------------------- /main/templates/user/user_merge.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/forms.html' as forms 3 | # import 'macro/utils.html' as utils 4 | 5 | 6 | # block content 7 | 13 |
    14 |
    15 |
    16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | # for user_db in user_dbs 29 | 30 | 38 | 39 | 40 | 45 | 56 | 57 | 58 | # endfor 59 | 60 |
    NameUsernameEmailCreated Permissions
    31 | 32 | Avatar of {{user_db.name}} 33 | {{user_db.name}} 34 | # if current_user.id == user_db.key.id() 35 | 36 | # endif 37 | {{user_db.username}}{{user_db.email}} 41 | 44 | 46 | # if user_db.admin 47 | admin 48 | # endif 49 | # if not user_db.active 50 | inactive 51 | # endif 52 | # for permission in user_db.permissions 53 | {{permission}} 54 | # endfor 55 | {{utils.auth_icons(user_db)}}
    61 |
    62 |
    63 |
    64 |
    65 |
    66 |
    67 |
    68 | {{form.csrf_token}} 69 | {{forms.hidden_field(form.user_keys)}} 70 | {{forms.hidden_field(form.user_key)}} 71 | {{forms.hidden_field(form.username)}} 72 | 73 | {{forms.text_field(form.username, disabled=True)}} 74 | {{forms.text_field(form.name)}} 75 | {{forms.email_field(form.email)}} 76 |
    77 |
    78 |
    79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | # for auth_id in auth_ids 89 | 90 | 91 | 92 | 93 | # endfor 94 | 95 |
    Auth ID
    {{utils.auth_icon(auth_id)}}{{auth_id}}
    96 |
    97 |
    98 |
    99 |
    100 |
      101 |
    • Select the user's entity that you want to keep (the other entities will be deactivated)
    • 102 |
    • Before merging make sure the entities with references to the user are being taking care of
    • 103 |
    • For deactivated users the 3rd party associated accounts will be cleared
    • 104 |
    105 |
    106 |
    107 |
    108 |
    109 | 112 |
    113 |
    114 |
    115 | # endblock 116 | -------------------------------------------------------------------------------- /main/templates/user/user_reset.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/forms.html' as forms 3 | # import 'macro/utils.html' as utils 4 | 5 | # block content 6 | 11 | 12 |
    13 |
    14 |

    15 | Avatar of {{user_db.name}} 16 | {{user_db.name}}
    17 | {{user_db.email or user_db.username}} 18 |

    19 |
    20 | {{form.csrf_token}} 21 | {{forms.password_visible_field(form.new_password, autofocus=True, size='lg')}} 22 |
    23 | 26 |
    27 |
    28 |
    29 |
    30 | # endblock 31 | -------------------------------------------------------------------------------- /main/templates/user/user_update.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/forms.html' as forms 3 | # import 'macro/utils.html' as utils 4 | 5 | # block content 6 | 15 |
    16 |
    17 |
    18 | {{form.csrf_token}} 19 | {{forms.text_field(form.name, autofocus=True)}} 20 | {{forms.text_field(form.username, autocomplete='off')}} 21 | # include 'user/user_email_field.html' 22 | {{forms.checkbox_field(form.verified)}} 23 | # if current_user.user_db.key != user_db.key 24 | {{forms.checkbox_field(form.admin)}} 25 | {{forms.checkbox_field(form.active)}} 26 | # else 27 | {{forms.checkbox_field(form.admin, disabled=True, checked=user_db.admin)}} 28 | {{forms.checkbox_field(form.active, disabled=True, checked=user_db.active)}} 29 | # endif 30 | # if form.permissions.choices 31 | {{forms.multiple_checkbox_field(form.permissions)}} 32 | # endif 33 |
    34 |
    35 |
    36 | 37 |
    38 | Avatar of {{user_db.name}} 39 |
    40 |
    41 |
    42 |
    43 |
    44 | 45 |
    46 | No associated accounts 47 |
    48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | # if user_db.password_hash 57 | 58 | 59 | 60 | 61 | # endif 62 | # for auth_id in user_db.auth_ids 63 | 64 | 65 | 66 | 67 | # endfor 68 | 69 |
    Auth ID
    {{utils.auth_icon('email_auth')}}{{'Email Authentication'}}
    {{utils.auth_icon(auth_id)}}{{auth_id}}
    70 |
    71 |
    72 |
    73 |
    74 |
    75 |
    76 | 83 |
    84 |
    85 |
    86 | # endblock 87 | -------------------------------------------------------------------------------- /main/templates/welcome.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | 3 | # block header 4 |
    5 |
    6 |

    {{config.CONFIG_DB.brand_name}}

    7 |

    Hello, world!

    8 |
    9 |
    10 | # endblock 11 | 12 | # block content 13 |
    14 |
    15 |

    Heading

    16 |

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

    17 |
    18 |
    19 |

    Heading

    20 |

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

    21 |
    22 |
    23 |

    Heading

    24 |

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

    25 |
    26 |
    27 |

    Heading

    28 |

    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

    29 |
    30 |
    31 | # endblock 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Panayiotis Lipiridis ", 3 | "devDependencies": { 4 | "babel-core": "6.26.0", 5 | "babel-preset-es2015": "6.24.1", 6 | "bower": "1.8.2", 7 | "browser-sync": "2.23.6", 8 | "coffee-script": "1.12.7", 9 | "del": "3.0.0", 10 | "eslint": "4.19.0", 11 | "eslint-config-prettier": "2.9.0", 12 | "eslint-plugin-prettier": "2.6.0", 13 | "gulp": "3.9.1", 14 | "gulp-autoprefixer": "5.0.0", 15 | "gulp-babel": "7.0.1", 16 | "gulp-bower": "0.0.14", 17 | "gulp-coffee": "3.0.2", 18 | "gulp-concat": "2.6.1", 19 | "gulp-cssnano": "2.1.2", 20 | "gulp-help": "1.6.1", 21 | "gulp-if": "2.0.2", 22 | "gulp-less": "4.0.0", 23 | "gulp-load-plugins": "1.5.0", 24 | "gulp-plumber": "1.2.0", 25 | "gulp-sequence": "1.0.0", 26 | "gulp-size": "3.0.0", 27 | "gulp-sourcemaps": "2.6.4", 28 | "gulp-start": "1.0.1", 29 | "gulp-uglify-es": "1.0.1", 30 | "gulp-util": "3.0.8", 31 | "gulp-watch": "5.0.0", 32 | "gulp-zip": "4.1.0", 33 | "husky": "0.14.3", 34 | "less": "3.0.1", 35 | "lint-staged": "7.0.0", 36 | "main-bower-files": "2.13.1", 37 | "prettier": "1.11.1", 38 | "require-dir": "1.0.0", 39 | "uglify-es": "3.3.9", 40 | "yargs-parser": "9.0.2" 41 | }, 42 | "license": "MIT", 43 | "name": "gae-init", 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/gae-init" 47 | }, 48 | "lint-staged": { 49 | "*.js": ["eslint --fix", "git add"], 50 | "*.{json,less,md}": ["prettier --write", "git add"] 51 | }, 52 | "scripts": { 53 | "build": "gulp build", 54 | "fix:assets": "npm run prettier -- --write", 55 | "fix:code": "npm run test:code -- --fix", 56 | "fix": "npm run fix:assets && npm run fix:code", 57 | "install": "gulp init", 58 | "prettier": "prettier --ignore-path .gitignore \"**/*.{json,less,md}\"", 59 | "postinstall": "echo 'Run `gulp` to start or `gulp help` for more.'", 60 | "precommit": "lint-staged", 61 | "start": "gulp", 62 | "test:assets": "npm run prettier -- --list-different", 63 | "test:code": "eslint --ignore-path .gitignore \"**/*.js\"", 64 | "test": "npm run test:assets && npm run test:code" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | blinker==1.4 2 | flask-login==0.4.1 3 | flask-oauthlib==0.9.4 4 | flask-restful==0.3.6 5 | flask-wtf==0.14.2 6 | flask==0.12.2 7 | pyjwt==1.6.1 8 | unidecode==1.0.22 9 | webargs==2.0.0 10 | --------------------------------------------------------------------------------