├── .editorconfig ├── .gitignore ├── 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 │ │ ├── comment.py │ │ ├── config.py │ │ ├── language.py │ │ ├── post.py │ │ ├── user.py │ │ └── vote.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 │ ├── comment.py │ ├── error.py │ ├── extra.py │ ├── feedback.py │ ├── language.py │ ├── letsencrypt.py │ ├── post.py │ ├── profile.py │ ├── test.py │ ├── user.py │ ├── vote.py │ └── welcome.py ├── main.py ├── model │ ├── __init__.py │ ├── base.py │ ├── comment.py │ ├── config.py │ ├── config_auth.py │ ├── language.py │ ├── post.py │ ├── user.py │ └── vote.py ├── static │ ├── img │ │ ├── favicon.ico │ │ ├── fight-640.png │ │ └── fight.png │ ├── robots.txt │ └── src │ │ ├── script │ │ ├── common │ │ │ ├── api.coffee │ │ │ └── util.coffee │ │ ├── highlight.pack.js │ │ ├── post.js │ │ └── site │ │ │ ├── app.coffee │ │ │ ├── auth.coffee │ │ │ └── user.coffee │ │ └── style │ │ ├── base.less │ │ ├── footer.less │ │ ├── highlight │ │ ├── default.css │ │ └── line-numbers.less │ │ ├── mixins.less │ │ ├── post.less │ │ ├── signin.less │ │ ├── style.less │ │ ├── test.less │ │ ├── user.less │ │ ├── variables.less │ │ ├── vote.less │ │ └── welcome.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_oauth.html │ │ │ ├── instagram_oauth.html │ │ │ ├── letsencrypt.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 │ │ ├── footer.html │ │ ├── graph.html │ │ ├── header.html │ │ ├── meta.html │ │ ├── notifications.html │ │ ├── script.html │ │ ├── style.html │ │ └── user_menu.html │ ├── comment │ │ ├── admin_comment_list.html │ │ ├── admin_comment_update.html │ │ ├── comment_list.html │ │ ├── comment_update.html │ │ └── comment_view.html │ ├── error.html │ ├── error_static.html │ ├── feedback.html │ ├── language │ │ ├── admin_language_list.html │ │ ├── admin_language_update.html │ │ ├── language_list.html │ │ └── language_view.html │ ├── macro │ │ ├── forms.html │ │ └── utils.html │ ├── post │ │ ├── admin_post_list.html │ │ ├── admin_post_update.html │ │ ├── post_list.html │ │ ├── post_update.html │ │ └── post_view.html │ ├── profile │ │ ├── profile.html │ │ ├── profile_base.html │ │ ├── profile_password.html │ │ └── profile_update.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 │ ├── vote │ │ ├── admin_vote_list.html │ │ └── admin_vote_update.html │ └── welcome.html └── util.py ├── package.json ├── requirements.txt ├── run.py └── yarn.lock /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | # vote4code 2 | 3 | The vote4code website is place to upload two different versions of programming solutions to let the community vote for the best one. It's based on [**gae-init**](https://gae-init.appspot.com). 4 | 5 | ## Requirements 6 | 7 | ### Python 8 | - [Python 2.7](https://www.python.org/downloads/) 9 | - [pip](http://docs.gae-init.appspot.com/requirement/#pip) 10 | - [virtualenv](http://docs.gae-init.appspot.com/requirement/#virtualenv) 11 | - [Node.js](https://nodejs.org/en/) 12 | - [Gulp](http://gulpjs.com/) 13 | - [Yarn](https://yarnpkg.com/en/) 14 | 15 | ### Google Cloud Platform 16 | - [Google Cloud SDK with App Engine](http://docs.gae-init.appspot.com/requirement/#gcloud) 17 | - [Google App Engine SDK 1.9.53 for Python](https://cloud.google.com/appengine/docs/standard/python/download) 18 | 19 | ## Development 20 | 21 | ### First time 22 | 23 | Execute `yarn` to install all the dependencies and then follow the next step 24 | 25 | ### Running the Development Environment 26 | 27 | 1. Run `gulp` 28 | 2. Visit http://localhost:3000 in your browser 29 | 30 | ### Deploying on Google App Engine 31 | 32 | ```bash 33 | gloud login 34 | gulp deploy 35 | 36 | # Alternatives 37 | gulp deploy --project=vote4code 38 | gulp deploy --project=vote4code --version=bar 39 | gulp deploy --project=vote4code --version=bar --no-promote 40 | ``` 41 | 42 | ### Help 43 | 44 | For more options execute: 45 | 46 | ```bash 47 | gulp help 48 | ``` 49 | -------------------------------------------------------------------------------- /bin/README.md: -------------------------------------------------------------------------------- 1 | Command Line Scripts 2 | ==================== 3 | 4 | Requirements 5 | ------------ 6 | 7 | You can install the requirements easily by executing the following scripts using 8 | `curl` & `bash`: 9 | 10 | ### [Cloud Shell](https://cloud.google.com/shell/) 11 | 12 | All requirements are met, out of the box. 13 | 14 | ### macOS — [Homebrew](http://brew.sh/) 15 | 16 | ```bash 17 | curl https://raw.githubusercontent.com/gae-init/gae-init/master/bin/requirements_osx_brew.sh | bash 18 | ``` 19 | 20 | ### macOS — [MacPorts](https://www.macports.org/) 21 | 22 | ```bash 23 | curl https://raw.githubusercontent.com/gae-init/gae-init/master/bin/requirements_osx_port.sh | bash 24 | ``` 25 | 26 | ### Linux 27 | 28 | ```bash 29 | curl https://raw.githubusercontent.com/gae-init/gae-init/master/bin/requirements_linux.sh | bash 30 | ``` 31 | -------------------------------------------------------------------------------- /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 | "name": "gae-init", 3 | "dependencies": { 4 | "bootstrap": "3.3.7", 5 | "bootswatch": "3.3.7", 6 | "font-awesome": "4.7.0", 7 | "highlightjs-line-numbers.js": "1.1.0", 8 | "jquery": "3.2.1", 9 | "moment": "2.18.1" 10 | }, 11 | "overrides": { 12 | "bootstrap": { 13 | "main": [ 14 | "less/**", 15 | "fonts/*", 16 | "js/*" 17 | ] 18 | }, 19 | "bootswatch": { 20 | "main": [ 21 | "**/*.less" 22 | ] 23 | }, 24 | "font-awesome": { 25 | "main": [ 26 | "less/*", 27 | "fonts/*" 28 | ] 29 | }, 30 | "highlightjs-line-numbers.js": { 31 | "main": [ 32 | "src/highlightjs-line-numbers.js" 33 | ] 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /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 | "#{paths.static.ext}/highlightjs-line-numbers.js/src/highlightjs-line-numbers.js" 23 | ] 24 | 25 | 26 | module.exports = config 27 | -------------------------------------------------------------------------------- /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/app.yaml main/index.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 | config = require '../config' 4 | paths = require '../paths' 5 | util = require '../util' 6 | 7 | 8 | gulp.task 'ext', false, -> 9 | gulp.src config.ext 10 | .pipe $.plumber errorHandler: util.onError 11 | .pipe $.concat 'ext.js' 12 | .pipe $.uglify() 13 | .pipe $.size {title: 'Minified ext libs'} 14 | .pipe gulp.dest "#{paths.static.min}/script" 15 | 16 | 17 | gulp.task 'ext:dev', false, -> 18 | gulp.src config.ext 19 | .pipe $.plumber errorHandler: util.onError 20 | .pipe $.sourcemaps.init() 21 | .pipe $.concat 'ext.js' 22 | .pipe $.sourcemaps.write() 23 | .pipe gulp.dest "#{paths.static.dev}/script" 24 | -------------------------------------------------------------------------------- /gulp/tasks/script.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 | is_coffee = (file) -> 9 | return file.path.indexOf('.coffee') > 0 10 | 11 | 12 | gulp.task 'script', false, -> 13 | gulp.src config.script 14 | .pipe $.plumber errorHandler: util.onError 15 | .pipe $.if is_coffee, $.coffee() 16 | .pipe $.concat 'script.js' 17 | .pipe $.uglify() 18 | .pipe $.size {title: 'Minified scripts'} 19 | .pipe gulp.dest "#{paths.static.min}/script" 20 | 21 | 22 | gulp.task 'script:dev', false, -> 23 | gulp.src config.script 24 | .pipe $.plumber errorHandler: util.onError 25 | .pipe $.sourcemaps.init() 26 | .pipe $.if is_coffee, $.coffee() 27 | .pipe $.concat 'script.js' 28 | .pipe $.sourcemaps.write() 29 | .pipe gulp.dest "#{paths.static.dev}/script" 30 | -------------------------------------------------------------------------------- /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,md,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': map(lambda l: flask_restful.marshal(l, marshal_table), 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 .language import * 7 | from .post import * 8 | from .vote import * 9 | from .comment import * 10 | -------------------------------------------------------------------------------- /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/comment.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from google.appengine.ext import ndb 6 | import flask 7 | import flask_restful 8 | 9 | from api import helpers 10 | import auth 11 | import model 12 | import util 13 | 14 | from main import api_v1 15 | 16 | 17 | @api_v1.resource('/comment/', endpoint='api.comment.list') 18 | class CommentListAPI(flask_restful.Resource): 19 | def get(self): 20 | comment_dbs, comment_cursor = model.Comment.get_dbs() 21 | return helpers.make_response(comment_dbs, model.Comment.FIELDS, comment_cursor) 22 | 23 | 24 | @api_v1.resource('/comment//', endpoint='api.comment') 25 | class CommentAPI(flask_restful.Resource): 26 | def get(self, comment_key): 27 | comment_db = ndb.Key(urlsafe=comment_key).get() 28 | if not comment_db: 29 | helpers.make_not_found_exception('Comment %s not found' % comment_key) 30 | return helpers.make_response(comment_db, model.Comment.FIELDS) 31 | 32 | 33 | ############################################################################### 34 | # Admin 35 | ############################################################################### 36 | @api_v1.resource('/admin/comment/', endpoint='api.admin.comment.list') 37 | class AdminCommentListAPI(flask_restful.Resource): 38 | @auth.admin_required 39 | def get(self): 40 | comment_keys = util.param('comment_keys', list) 41 | if comment_keys: 42 | comment_db_keys = [ndb.Key(urlsafe=k) for k in comment_keys] 43 | comment_dbs = ndb.get_multi(comment_db_keys) 44 | return helpers.make_response(comment_dbs, model.comment.FIELDS) 45 | 46 | comment_dbs, comment_cursor = model.Comment.get_dbs() 47 | return helpers.make_response(comment_dbs, model.Comment.FIELDS, comment_cursor) 48 | 49 | @auth.admin_required 50 | def delete(self): 51 | comment_keys = util.param('comment_keys', list) 52 | if not comment_keys: 53 | helpers.make_not_found_exception('Comment(s) %s not found' % comment_keys) 54 | comment_db_keys = [ndb.Key(urlsafe=k) for k in comment_keys] 55 | ndb.delete_multi(comment_db_keys) 56 | return flask.jsonify({ 57 | 'result': comment_keys, 58 | 'status': 'success', 59 | }) 60 | 61 | 62 | @api_v1.resource('/admin/comment//', endpoint='api.admin.comment') 63 | class AdminCommentAPI(flask_restful.Resource): 64 | @auth.admin_required 65 | def get(self, comment_key): 66 | comment_db = ndb.Key(urlsafe=comment_key).get() 67 | if not comment_db: 68 | helpers.make_not_found_exception('Comment %s not found' % comment_key) 69 | return helpers.make_response(comment_db, model.Comment.FIELDS) 70 | 71 | @auth.admin_required 72 | def delete(self, comment_key): 73 | comment_db = ndb.Key(urlsafe=comment_key).get() 74 | if not comment_db: 75 | helpers.make_not_found_exception('Comment %s not found' % comment_key) 76 | comment_db.key.delete() 77 | return helpers.make_response(comment_db, model.Comment.FIELDS) 78 | -------------------------------------------------------------------------------- /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/language.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from google.appengine.ext import ndb 6 | import flask 7 | import flask_restful 8 | 9 | from api import helpers 10 | import auth 11 | import model 12 | import util 13 | 14 | from main import api_v1 15 | 16 | 17 | @api_v1.resource('/language/', endpoint='api.language.list') 18 | class LanguageListAPI(flask_restful.Resource): 19 | def get(self): 20 | language_dbs, language_cursor = model.Language.get_dbs() 21 | return helpers.make_response(language_dbs, model.Language.FIELDS, language_cursor) 22 | 23 | 24 | @api_v1.resource('/language//', endpoint='api.language') 25 | class LanguageAPI(flask_restful.Resource): 26 | def get(self, language_key): 27 | language_db = ndb.Key(urlsafe=language_key).get() 28 | if not language_db: 29 | helpers.make_not_found_exception('Language %s not found' % language_key) 30 | return helpers.make_response(language_db, model.Language.FIELDS) 31 | 32 | 33 | ############################################################################### 34 | # Admin 35 | ############################################################################### 36 | @api_v1.resource('/admin/language/', endpoint='api.admin.language.list') 37 | class AdminLanguageListAPI(flask_restful.Resource): 38 | @auth.admin_required 39 | def get(self): 40 | language_keys = util.param('language_keys', list) 41 | if language_keys: 42 | language_db_keys = [ndb.Key(urlsafe=k) for k in language_keys] 43 | language_dbs = ndb.get_multi(language_db_keys) 44 | return helpers.make_response(language_dbs, model.language.FIELDS) 45 | 46 | language_dbs, language_cursor = model.Language.get_dbs() 47 | return helpers.make_response(language_dbs, model.Language.FIELDS, language_cursor) 48 | 49 | @auth.admin_required 50 | def delete(self): 51 | language_keys = util.param('language_keys', list) 52 | if not language_keys: 53 | helpers.make_not_found_exception('Language(s) %s not found' % language_keys) 54 | language_db_keys = [ndb.Key(urlsafe=k) for k in language_keys] 55 | ndb.delete_multi(language_db_keys) 56 | return flask.jsonify({ 57 | 'result': language_keys, 58 | 'status': 'success', 59 | }) 60 | 61 | 62 | @api_v1.resource('/admin/language//', endpoint='api.admin.language') 63 | class AdminLanguageAPI(flask_restful.Resource): 64 | @auth.admin_required 65 | def get(self, language_key): 66 | language_db = ndb.Key(urlsafe=language_key).get() 67 | if not language_db: 68 | helpers.make_not_found_exception('Language %s not found' % language_key) 69 | return helpers.make_response(language_db, model.Language.FIELDS) 70 | 71 | @auth.admin_required 72 | def delete(self, language_key): 73 | language_db = ndb.Key(urlsafe=language_key).get() 74 | if not language_db: 75 | helpers.make_not_found_exception('Language %s not found' % language_key) 76 | language_db.key.delete() 77 | return helpers.make_response(language_db, model.Language.FIELDS) 78 | -------------------------------------------------------------------------------- /main/api/v1/post.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from google.appengine.ext import ndb 6 | import flask 7 | import flask_restful 8 | 9 | from api import helpers 10 | import auth 11 | import model 12 | import util 13 | 14 | from main import api_v1 15 | 16 | 17 | @api_v1.resource('/post/', endpoint='api.post.list') 18 | class PostListAPI(flask_restful.Resource): 19 | def get(self): 20 | post_dbs, post_cursor = model.Post.get_dbs() 21 | return helpers.make_response(post_dbs, model.Post.FIELDS, post_cursor) 22 | 23 | 24 | @api_v1.resource('/post//', endpoint='api.post') 25 | class PostAPI(flask_restful.Resource): 26 | def get(self, post_key): 27 | post_db = ndb.Key(urlsafe=post_key).get() 28 | if not post_db: 29 | helpers.make_not_found_exception('Post %s not found' % post_key) 30 | return helpers.make_response(post_db, model.Post.FIELDS) 31 | 32 | 33 | ############################################################################### 34 | # Admin 35 | ############################################################################### 36 | @api_v1.resource('/admin/post/', endpoint='api.admin.post.list') 37 | class AdminPostListAPI(flask_restful.Resource): 38 | @auth.admin_required 39 | def get(self): 40 | post_keys = util.param('post_keys', list) 41 | if post_keys: 42 | post_db_keys = [ndb.Key(urlsafe=k) for k in post_keys] 43 | post_dbs = ndb.get_multi(post_db_keys) 44 | return helpers.make_response(post_dbs, model.post.FIELDS) 45 | 46 | post_dbs, post_cursor = model.Post.get_dbs() 47 | return helpers.make_response(post_dbs, model.Post.FIELDS, post_cursor) 48 | 49 | @auth.admin_required 50 | def delete(self): 51 | post_keys = util.param('post_keys', list) 52 | if not post_keys: 53 | helpers.make_not_found_exception('Post(s) %s not found' % post_keys) 54 | post_db_keys = [ndb.Key(urlsafe=k) for k in post_keys] 55 | ndb.delete_multi(post_db_keys) 56 | return flask.jsonify({ 57 | 'result': post_keys, 58 | 'status': 'success', 59 | }) 60 | 61 | 62 | @api_v1.resource('/admin/post//', endpoint='api.admin.post') 63 | class AdminPostAPI(flask_restful.Resource): 64 | @auth.admin_required 65 | def get(self, post_key): 66 | post_db = ndb.Key(urlsafe=post_key).get() 67 | if not post_db: 68 | helpers.make_not_found_exception('Post %s not found' % post_key) 69 | return helpers.make_response(post_db, model.Post.FIELDS) 70 | 71 | @auth.admin_required 72 | def delete(self, post_key): 73 | post_db = ndb.Key(urlsafe=post_key).get() 74 | if not post_db: 75 | helpers.make_not_found_exception('Post %s not found' % post_key) 76 | post_db.key.delete() 77 | return helpers.make_response(post_db, model.Post.FIELDS) 78 | -------------------------------------------------------------------------------- /main/api/v1/user.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from google.appengine.ext import ndb 6 | import flask 7 | import flask_restful 8 | 9 | from api import helpers 10 | import auth 11 | import model 12 | import util 13 | 14 | from main import api_v1 15 | 16 | 17 | @api_v1.resource('/admin/user/', endpoint='api.admin.user.list') 18 | class AdminUserListAPI(flask_restful.Resource): 19 | @auth.admin_required 20 | def get(self): 21 | user_keys = util.param('user_keys', list) 22 | if user_keys: 23 | user_db_keys = [ndb.Key(urlsafe=k) for k in user_keys] 24 | user_dbs = ndb.get_multi(user_db_keys) 25 | return helpers.make_response(user_dbs, model.User.FIELDS) 26 | 27 | user_dbs, cursors = model.User.get_dbs(prev_cursor=True) 28 | return helpers.make_response(user_dbs, model.User.FIELDS, cursors) 29 | 30 | @auth.admin_required 31 | def delete(self): 32 | user_keys = util.param('user_keys', list) 33 | if not user_keys: 34 | helpers.make_not_found_exception('User(s) %s not found' % user_keys) 35 | user_db_keys = [ndb.Key(urlsafe=k) for k in user_keys] 36 | delete_user_dbs(user_db_keys) 37 | return flask.jsonify({ 38 | 'result': user_keys, 39 | 'status': 'success', 40 | }) 41 | 42 | 43 | @api_v1.resource('/admin/user//', endpoint='api.admin.user') 44 | class AdminUserAPI(flask_restful.Resource): 45 | @auth.admin_required 46 | def get(self, user_key): 47 | user_db = ndb.Key(urlsafe=user_key).get() 48 | if not user_db: 49 | helpers.make_not_found_exception('User %s not found' % user_key) 50 | return helpers.make_response(user_db, model.User.FIELDS) 51 | 52 | @auth.admin_required 53 | def delete(self, user_key): 54 | user_db = ndb.Key(urlsafe=user_key).get() 55 | if not user_db: 56 | helpers.make_not_found_exception('User %s not found' % user_key) 57 | delete_user_dbs([user_db.key]) 58 | return helpers.make_response(user_db, model.User.FIELDS) 59 | 60 | 61 | ############################################################################### 62 | # Helpers 63 | ############################################################################### 64 | @ndb.transactional(xg=True) 65 | def delete_user_dbs(user_db_keys): 66 | ndb.delete_multi(user_db_keys) 67 | -------------------------------------------------------------------------------- /main/api/v1/vote.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from google.appengine.ext import ndb 6 | import flask 7 | import flask_restful 8 | 9 | from api import helpers 10 | import auth 11 | import model 12 | import util 13 | 14 | from main import api_v1 15 | 16 | 17 | @api_v1.resource('/vote/', endpoint='api.vote.list') 18 | class VoteListAPI(flask_restful.Resource): 19 | def get(self): 20 | vote_dbs, vote_cursor = model.Vote.get_dbs() 21 | return helpers.make_response(vote_dbs, model.Vote.FIELDS, vote_cursor) 22 | 23 | 24 | @api_v1.resource('/vote//', endpoint='api.vote') 25 | class VoteAPI(flask_restful.Resource): 26 | def get(self, vote_key): 27 | vote_db = ndb.Key(urlsafe=vote_key).get() 28 | if not vote_db: 29 | helpers.make_not_found_exception('Vote %s not found' % vote_key) 30 | return helpers.make_response(vote_db, model.Vote.FIELDS) 31 | 32 | 33 | ############################################################################### 34 | # Admin 35 | ############################################################################### 36 | @api_v1.resource('/admin/vote/', endpoint='api.admin.vote.list') 37 | class AdminVoteListAPI(flask_restful.Resource): 38 | @auth.admin_required 39 | def get(self): 40 | vote_keys = util.param('vote_keys', list) 41 | if vote_keys: 42 | vote_db_keys = [ndb.Key(urlsafe=k) for k in vote_keys] 43 | vote_dbs = ndb.get_multi(vote_db_keys) 44 | return helpers.make_response(vote_dbs, model.vote.FIELDS) 45 | 46 | vote_dbs, vote_cursor = model.Vote.get_dbs() 47 | return helpers.make_response(vote_dbs, model.Vote.FIELDS, vote_cursor) 48 | 49 | @auth.admin_required 50 | def delete(self): 51 | vote_keys = util.param('vote_keys', list) 52 | if not vote_keys: 53 | helpers.make_not_found_exception('Vote(s) %s not found' % vote_keys) 54 | vote_db_keys = [ndb.Key(urlsafe=k) for k in vote_keys] 55 | ndb.delete_multi(vote_db_keys) 56 | return flask.jsonify({ 57 | 'result': vote_keys, 58 | 'status': 'success', 59 | }) 60 | 61 | 62 | @api_v1.resource('/admin/vote//', endpoint='api.admin.vote') 63 | class AdminVoteAPI(flask_restful.Resource): 64 | @auth.admin_required 65 | def get(self, vote_key): 66 | vote_db = ndb.Key(urlsafe=vote_key).get() 67 | if not vote_db: 68 | helpers.make_not_found_exception('Vote %s not found' % vote_key) 69 | return helpers.make_response(vote_db, model.Vote.FIELDS) 70 | 71 | @auth.admin_required 72 | def delete(self, vote_key): 73 | vote_db = ndb.Key(urlsafe=vote_key).get() 74 | if not vote_db: 75 | helpers.make_not_found_exception('Vote %s not found' % vote_key) 76 | vote_db.key.delete() 77 | return helpers.make_response(vote_db, model.Vote.FIELDS) 78 | -------------------------------------------------------------------------------- /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 | 19 | error_handlers: 20 | - file: templates/error_static.html 21 | 22 | handlers: 23 | - url: /favicon.ico 24 | static_files: static/img/favicon.ico 25 | upload: static/img/favicon.ico 26 | 27 | - url: /robots.txt 28 | static_files: static/robots.txt 29 | upload: static/robots.txt 30 | 31 | - url: /p/(.*\.ttf) 32 | static_files: static/\1 33 | upload: static/(.*\.ttf) 34 | mime_type: font/ttf 35 | expiration: "365d" 36 | 37 | - url: /p/(.*\.woff2) 38 | static_files: static/\1 39 | upload: static/(.*\.woff2) 40 | mime_type: font/woff2 41 | expiration: "365d" 42 | 43 | - url: /p/ 44 | static_dir: static/ 45 | expiration: "365d" 46 | 47 | - url: /.* 48 | script: main.app 49 | secure: always 50 | redirect_http_response_code: 301 51 | 52 | skip_files: 53 | - ^(.*/)?#.*# 54 | - ^(.*/)?.*/RCS/.* 55 | - ^(.*/)?.*\.bak$ 56 | - ^(.*/)?.*\.py[co] 57 | - ^(.*/)?.*~ 58 | - ^(.*/)?Icon\r 59 | - ^(.*/)?\..* 60 | - ^(.*/)?app\.yaml 61 | - ^(.*/)?app\.yml 62 | - ^(.*/)?index\.yaml 63 | - ^(.*/)?index\.yml 64 | - ^lib/.* 65 | - ^static/dev/.* 66 | - ^static/ext/.*\.coffee 67 | - ^static/ext/.*\.css 68 | - ^static/ext/.*\.js 69 | - ^static/ext/.*\.less 70 | - ^static/ext/.*\.json 71 | - ^static/src/.* 72 | -------------------------------------------------------------------------------- /main/appengine_config.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | import sys 5 | 6 | if os.environ.get('SERVER_SOFTWARE', '').startswith('Google App Engine'): 7 | sys.path.insert(0, 'lib.zip') 8 | else: 9 | if os.name == 'nt': 10 | os.name = None 11 | sys.platform = '' 12 | 13 | import re 14 | from google.appengine.tools.devappserver2.python import stubs 15 | 16 | re_ = stubs.FakeFile._skip_files.pattern.replace('|^lib/.*', '') 17 | re_ = re.compile(re_) 18 | stubs.FakeFile._skip_files = re_ 19 | sys.path.insert(0, 'lib') 20 | sys.path.insert(0, 'libx') 21 | 22 | 23 | def webapp_add_wsgi_middleware(app): 24 | from google.appengine.ext.appstats import recording 25 | app = recording.appstats_wsgi_middleware(app) 26 | return app 27 | -------------------------------------------------------------------------------- /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 | import flask 4 | import jwt 5 | 6 | import auth 7 | import config 8 | import util 9 | 10 | from main import app 11 | 12 | 13 | azure_ad_config = dict( 14 | access_token_method='POST', 15 | access_token_url='https://login.microsoftonline.com/common/oauth2/token', 16 | authorize_url='https://login.microsoftonline.com/common/oauth2/authorize', 17 | base_url='', 18 | consumer_key=config.CONFIG_DB.azure_ad_client_id, 19 | consumer_secret=config.CONFIG_DB.azure_ad_client_secret, 20 | request_token_params={ 21 | 'scope': 'openid profile user_impersonation', 22 | }, 23 | ) 24 | 25 | azure_ad = auth.create_oauth_app(azure_ad_config, 'azure_ad') 26 | 27 | 28 | @app.route('/api/auth/callback/azure_ad/') 29 | def azure_ad_authorized(): 30 | response = azure_ad.authorized_response() 31 | print response 32 | if response is None: 33 | flask.flash('You denied the request to sign in.') 34 | return flask.redirect(util.get_next_url) 35 | id_token = response['id_token'] 36 | flask.session['oauth_token'] = (id_token, '') 37 | try: 38 | decoded_id_token = jwt.decode(id_token, verify=False) 39 | except (jwt.DecodeError, jwt.ExpiredSignature): 40 | flask.flash('You denied the request to sign in.') 41 | return flask.redirect(util.get_next_url) 42 | user_db = retrieve_user_from_azure_ad(decoded_id_token) 43 | return auth.signin_user_db(user_db) 44 | 45 | 46 | @azure_ad.tokengetter 47 | def get_azure_ad_oauth_token(): 48 | return flask.session.get('oauth_token') 49 | 50 | 51 | @app.route('/signin/azure_ad/') 52 | def signin_azure_ad(): 53 | return auth.signin_oauth(azure_ad) 54 | 55 | 56 | def retrieve_user_from_azure_ad(response): 57 | auth_id = 'azure_ad_%s' % response['oid'] 58 | email = response.get('upn', '') 59 | first_name = response.get('given_name', '') 60 | last_name = response.get('family_name', '') 61 | username = ' '.join((first_name, last_name)).strip() 62 | return auth.create_user_db( 63 | auth_id=auth_id, 64 | name='%s %s' % (first_name, last_name), 65 | username=email or username, 66 | email=email, 67 | verified=bool(email), 68 | ) 69 | -------------------------------------------------------------------------------- /main/auth/bitbucket.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | 5 | import auth 6 | import config 7 | import model 8 | import util 9 | 10 | from main import app 11 | 12 | bitbucket_config = dict( 13 | access_token_method='POST', 14 | access_token_url='https://bitbucket.org/site/oauth2/access_token', 15 | authorize_url='https://bitbucket.org/site/oauth2/authorize', 16 | base_url='https://api.bitbucket.org/2.0/', 17 | consumer_key=config.CONFIG_DB.bitbucket_key, 18 | consumer_secret=config.CONFIG_DB.bitbucket_secret, 19 | ) 20 | 21 | bitbucket = auth.create_oauth_app(bitbucket_config, 'bitbucket') 22 | 23 | 24 | @app.route('/api/auth/callback/bitbucket/') 25 | def bitbucket_authorized(): 26 | response = bitbucket.authorized_response() 27 | if response is None: 28 | flask.flash('You denied the request to sign in.') 29 | return flask.redirect(util.get_next_url()) 30 | 31 | flask.session['oauth_token'] = (response['access_token'], '') 32 | me = bitbucket.get('user') 33 | user_db = retrieve_user_from_bitbucket(me.data) 34 | return auth.signin_user_db(user_db) 35 | 36 | 37 | @bitbucket.tokengetter 38 | def get_bitbucket_oauth_token(): 39 | return flask.session.get('oauth_token') 40 | 41 | 42 | @app.route('/signin/bitbucket/') 43 | def signin_bitbucket(): 44 | return auth.signin_oauth(bitbucket) 45 | 46 | 47 | def retrieve_user_from_bitbucket(response): 48 | auth_id = 'bitbucket_%s' % response['username'] 49 | user_db = model.User.get_by('auth_ids', auth_id) 50 | if user_db: 51 | return user_db 52 | emails = bitbucket.get('user/emails').data['values'] 53 | email = ''.join([e['email'] for e in emails if e['is_primary']][0:1]) 54 | return auth.create_user_db( 55 | auth_id=auth_id, 56 | name=response['display_name'], 57 | username=response['username'], 58 | email=email, 59 | verified=bool(email), 60 | ) 61 | -------------------------------------------------------------------------------- /main/auth/dropbox.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | 5 | import auth 6 | import config 7 | import model 8 | import util 9 | 10 | from main import app 11 | 12 | dropbox_config = dict( 13 | access_token_method='POST', 14 | access_token_url='https://api.dropbox.com/1/oauth2/token', 15 | authorize_url='https://www.dropbox.com/1/oauth2/authorize', 16 | base_url='https://www.dropbox.com/1/', 17 | consumer_key=config.CONFIG_DB.dropbox_app_key, 18 | consumer_secret=config.CONFIG_DB.dropbox_app_secret, 19 | ) 20 | 21 | dropbox = auth.create_oauth_app(dropbox_config, 'dropbox') 22 | 23 | 24 | @app.route('/api/auth/callback/dropbox/') 25 | def dropbox_authorized(): 26 | response = dropbox.authorized_response() 27 | if response is None: 28 | flask.flash('You denied the request to sign in.') 29 | return flask.redirect(util.get_next_url()) 30 | flask.session['oauth_token'] = (response['access_token'], '') 31 | me = dropbox.get('account/info') 32 | user_db = retrieve_user_from_dropbox(me.data) 33 | return auth.signin_user_db(user_db) 34 | 35 | 36 | @dropbox.tokengetter 37 | def get_dropbox_oauth_token(): 38 | return flask.session.get('oauth_token') 39 | 40 | 41 | @app.route('/signin/dropbox/') 42 | def signin_dropbox(): 43 | return auth.signin_oauth(dropbox, 'https') 44 | 45 | 46 | def retrieve_user_from_dropbox(response): 47 | auth_id = 'dropbox_%s' % response['uid'] 48 | user_db = model.User.get_by('auth_ids', auth_id) 49 | if user_db: 50 | return user_db 51 | 52 | return auth.create_user_db( 53 | auth_id=auth_id, 54 | name=response['display_name'], 55 | username=response['display_name'], 56 | ) 57 | -------------------------------------------------------------------------------- /main/auth/facebook.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | 5 | import auth 6 | import config 7 | import model 8 | import util 9 | 10 | from main import app 11 | 12 | facebook_config = dict( 13 | access_token_url='/oauth/access_token', 14 | authorize_url='/oauth/authorize', 15 | base_url='https://graph.facebook.com/', 16 | consumer_key=config.CONFIG_DB.facebook_app_id, 17 | consumer_secret=config.CONFIG_DB.facebook_app_secret, 18 | request_token_params={'scope': 'email'}, 19 | ) 20 | 21 | facebook = auth.create_oauth_app(facebook_config, 'facebook') 22 | 23 | 24 | @app.route('/api/auth/callback/facebook/') 25 | def facebook_authorized(): 26 | response = facebook.authorized_response() 27 | if response is None: 28 | flask.flash('You denied the request to sign in.') 29 | return flask.redirect(util.get_next_url()) 30 | 31 | flask.session['oauth_token'] = (response['access_token'], '') 32 | me = facebook.get('/me?fields=name,email') 33 | user_db = retrieve_user_from_facebook(me.data) 34 | return auth.signin_user_db(user_db) 35 | 36 | 37 | @facebook.tokengetter 38 | def get_facebook_oauth_token(): 39 | return flask.session.get('oauth_token') 40 | 41 | 42 | @app.route('/signin/facebook/') 43 | def signin_facebook(): 44 | return auth.signin_oauth(facebook) 45 | 46 | 47 | def retrieve_user_from_facebook(response): 48 | auth_id = 'facebook_%s' % response['id'] 49 | user_db = model.User.get_by('auth_ids', auth_id) 50 | return user_db or auth.create_user_db( 51 | auth_id=auth_id, 52 | name=response['name'], 53 | username=response.get('username', response['name']), 54 | email=response.get('email', ''), 55 | verified=bool(response.get('email', '')), 56 | ) 57 | -------------------------------------------------------------------------------- /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 | import flask 4 | 5 | import auth 6 | import config 7 | import model 8 | import util 9 | 10 | from main import app 11 | 12 | github_config = dict( 13 | access_token_method='POST', 14 | access_token_url='https://github.com/login/oauth/access_token', 15 | authorize_url='https://github.com/login/oauth/authorize', 16 | base_url='https://api.github.com/', 17 | consumer_key=config.CONFIG_DB.github_client_id, 18 | consumer_secret=config.CONFIG_DB.github_client_secret, 19 | request_token_params={'scope': 'user:email'}, 20 | ) 21 | 22 | github = auth.create_oauth_app(github_config, 'github') 23 | 24 | 25 | @app.route('/api/auth/callback/github/') 26 | def github_authorized(): 27 | response = github.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 | flask.session['oauth_token'] = (response['access_token'], '') 32 | me = github.get('user') 33 | user_db = retrieve_user_from_github(me.data) 34 | return auth.signin_user_db(user_db) 35 | 36 | 37 | @github.tokengetter 38 | def get_github_oauth_token(): 39 | return flask.session.get('oauth_token') 40 | 41 | 42 | @app.route('/signin/github/') 43 | def signin_github(): 44 | return auth.signin_oauth(github) 45 | 46 | 47 | def retrieve_user_from_github(response): 48 | auth_id = 'github_%s' % str(response['id']) 49 | user_db = model.User.get_by('auth_ids', auth_id) 50 | return user_db or auth.create_user_db( 51 | auth_id=auth_id, 52 | name=response['name'] or response['login'], 53 | username=response['login'], 54 | email=response.get('email', ''), 55 | verified=bool(response.get('email', '')), 56 | ) 57 | -------------------------------------------------------------------------------- /main/auth/google.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | 5 | import auth 6 | import config 7 | import model 8 | import util 9 | 10 | from main import app 11 | 12 | google_config = dict( 13 | access_token_method='POST', 14 | access_token_url='https://accounts.google.com/o/oauth2/token', 15 | authorize_url='https://accounts.google.com/o/oauth2/auth', 16 | base_url='https://www.googleapis.com/plus/v1/people/', 17 | consumer_key=config.CONFIG_DB.google_client_id, 18 | consumer_secret=config.CONFIG_DB.google_client_secret, 19 | request_token_params={'scope': 'email profile'}, 20 | ) 21 | 22 | google = auth.create_oauth_app(google_config, 'google') 23 | 24 | 25 | @app.route('/api/auth/callback/google/') 26 | def google_authorized(): 27 | response = google.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 | me = google.get('me', data={'access_token': response['access_token']}) 34 | user_db = retrieve_user_from_google(me.data) 35 | return auth.signin_user_db(user_db) 36 | 37 | 38 | @google.tokengetter 39 | def get_google_oauth_token(): 40 | return flask.session.get('oauth_token') 41 | 42 | 43 | @app.route('/signin/google/') 44 | def signin_google(): 45 | return auth.signin_oauth(google) 46 | 47 | 48 | def retrieve_user_from_google(response): 49 | auth_id = 'google_%s' % response['id'] 50 | user_db = model.User.get_by('auth_ids', auth_id) 51 | if user_db: 52 | return user_db 53 | 54 | if 'email' in response: 55 | email = response['email'] 56 | elif 'emails' in response: 57 | email = response['emails'][0]['value'] 58 | else: 59 | email = '' 60 | 61 | if 'displayName' in response: 62 | name = response['displayName'] 63 | elif 'name' in response: 64 | names = response['name'] 65 | given_name = names.get('givenName', '') 66 | family_name = names.get('familyName', '') 67 | name = ' '.join([given_name, family_name]).strip() 68 | else: 69 | name = 'google_user_%s' % id 70 | 71 | return auth.create_user_db( 72 | auth_id=auth_id, 73 | name=name, 74 | username=email or name, 75 | email=email, 76 | verified=bool(email), 77 | ) 78 | -------------------------------------------------------------------------------- /main/auth/instagram.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | 5 | import auth 6 | import model 7 | import util 8 | 9 | from main import app 10 | 11 | instagram_config = dict( 12 | access_token_method='POST', 13 | access_token_url='https://api.instagram.com/oauth/access_token', 14 | authorize_url='https://instagram.com/oauth/authorize/', 15 | base_url='https://api.instagram.com/v1', 16 | consumer_key=model.Config.get_master_db().instagram_client_id, 17 | consumer_secret=model.Config.get_master_db().instagram_client_secret, 18 | ) 19 | 20 | instagram = auth.create_oauth_app(instagram_config, 'instagram') 21 | 22 | 23 | @app.route('/api/auth/callback/instagram/') 24 | def instagram_authorized(): 25 | response = instagram.authorized_response() 26 | if response is None: 27 | flask.flash('You denied the request to sign in.') 28 | return flask.redirect(util.get_next_url()) 29 | 30 | flask.session['oauth_token'] = (response['access_token'], '') 31 | user_db = retrieve_user_from_instagram(response['user']) 32 | return auth.signin_user_db(user_db) 33 | 34 | 35 | @instagram.tokengetter 36 | def get_instagram_oauth_token(): 37 | return flask.session.get('oauth_token') 38 | 39 | 40 | @app.route('/signin/instagram/') 41 | def signin_instagram(): 42 | return auth.signin_oauth(instagram) 43 | 44 | 45 | def retrieve_user_from_instagram(response): 46 | auth_id = 'instagram_%s' % response['id'] 47 | user_db = model.User.get_by('auth_ids', auth_id) 48 | if user_db: 49 | return user_db 50 | 51 | return auth.create_user_db( 52 | auth_id=auth_id, 53 | name=response.get('full_name', '').strip() or response.get('username'), 54 | username=response.get('username'), 55 | ) 56 | -------------------------------------------------------------------------------- /main/auth/linkedin.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | 5 | import auth 6 | import config 7 | import model 8 | import util 9 | 10 | from main import app 11 | 12 | linkedin_config = dict( 13 | access_token_method='POST', 14 | access_token_url='https://www.linkedin.com/uas/oauth2/accessToken', 15 | authorize_url='https://www.linkedin.com/uas/oauth2/authorization', 16 | base_url='https://api.linkedin.com/v1/', 17 | consumer_key=config.CONFIG_DB.linkedin_api_key, 18 | consumer_secret=config.CONFIG_DB.linkedin_secret_key, 19 | request_token_params={ 20 | 'scope': 'r_basicprofile r_emailaddress', 21 | 'state': util.uuid(), 22 | }, 23 | ) 24 | 25 | linkedin = auth.create_oauth_app(linkedin_config, 'linkedin') 26 | 27 | 28 | def change_linkedin_query(uri, headers, body): 29 | headers['x-li-format'] = 'json' 30 | return uri, headers, body 31 | 32 | 33 | linkedin.pre_request = change_linkedin_query 34 | 35 | 36 | @app.route('/api/auth/callback/linkedin/') 37 | def linkedin_authorized(): 38 | response = linkedin.authorized_response() 39 | if response is None: 40 | flask.flash('You denied the request to sign in.') 41 | return flask.redirect(util.get_next_url()) 42 | 43 | flask.session['access_token'] = (response['access_token'], '') 44 | me = linkedin.get('people/~:(id,first-name,last-name,email-address)') 45 | user_db = retrieve_user_from_linkedin(me.data) 46 | return auth.signin_user_db(user_db) 47 | 48 | 49 | @linkedin.tokengetter 50 | def get_linkedin_oauth_token(): 51 | return flask.session.get('access_token') 52 | 53 | 54 | @app.route('/signin/linkedin/') 55 | def signin_linkedin(): 56 | return auth.signin_oauth(linkedin) 57 | 58 | 59 | def retrieve_user_from_linkedin(response): 60 | auth_id = 'linkedin_%s' % response['id'] 61 | user_db = model.User.get_by('auth_ids', auth_id) 62 | if user_db: 63 | return user_db 64 | 65 | names = [response.get('firstName', ''), response.get('lastName', '')] 66 | name = ' '.join(names).strip() 67 | email = response.get('emailAddress', '') 68 | return auth.create_user_db( 69 | auth_id=auth_id, 70 | name=name, 71 | username=email or name, 72 | email=email, 73 | verified=bool(email), 74 | ) 75 | -------------------------------------------------------------------------------- /main/auth/mailru.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import hashlib 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 | mailru_config = dict( 15 | access_token_url='https://connect.mail.ru/oauth/token', 16 | authorize_url='https://connect.mail.ru/oauth/authorize', 17 | base_url='https://www.appsmail.ru/', 18 | consumer_key=config.CONFIG_DB.mailru_app_id, 19 | consumer_secret=config.CONFIG_DB.mailru_app_secret, 20 | ) 21 | 22 | mailru = auth.create_oauth_app(mailru_config, 'mailru') 23 | 24 | 25 | def mailru_sig(data): 26 | param_list = sorted(['%s=%s' % (item, data[item]) for item in data]) 27 | return hashlib.md5(''.join(param_list) + mailru.consumer_secret).hexdigest() 28 | 29 | 30 | @app.route('/api/auth/callback/mailru/') 31 | def mailru_authorized(): 32 | response = mailru.authorized_response() 33 | if response is None: 34 | flask.flash(u'You denied the request to sign in.') 35 | return flask.redirect(util.get_next_url()) 36 | 37 | access_token = response['access_token'] 38 | flask.session['oauth_token'] = (access_token, '') 39 | data = { 40 | 'method': 'users.getInfo', 41 | 'app_id': mailru.consumer_key, 42 | 'session_key': access_token, 43 | 'secure': '1', 44 | } 45 | data['sig'] = mailru_sig(data) 46 | me = mailru.get('/platform/api', data=data) 47 | user_db = retrieve_user_from_mailru(me.data[0]) 48 | return auth.signin_user_db(user_db) 49 | 50 | 51 | @mailru.tokengetter 52 | def get_mailru_oauth_token(): 53 | return flask.session.get('oauth_token') 54 | 55 | 56 | @app.route('/signin/mailru/') 57 | def signin_mailru(): 58 | return auth.signin_oauth(mailru) 59 | 60 | 61 | def retrieve_user_from_mailru(response): 62 | auth_id = 'mailru_%s' % response['uid'] 63 | user_db = model.User.get_by('auth_ids', auth_id) 64 | if user_db: 65 | return user_db 66 | name = u' '.join([ 67 | response.get('first_name', u''), 68 | response.get('last_name', u'') 69 | ]).strip() 70 | email = response.get('email', '') 71 | return auth.create_user_db( 72 | auth_id=auth_id, 73 | name=name or email, 74 | username=email or name, 75 | email=email, 76 | verified=bool(email), 77 | ) 78 | -------------------------------------------------------------------------------- /main/auth/microsoft.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | 5 | import auth 6 | import config 7 | import model 8 | import util 9 | 10 | from main import app 11 | 12 | microsoft_config = dict( 13 | access_token_method='POST', 14 | access_token_url='https://login.live.com/oauth20_token.srf', 15 | authorize_url='https://login.live.com/oauth20_authorize.srf', 16 | base_url='https://apis.live.net/v5.0/', 17 | consumer_key=config.CONFIG_DB.microsoft_client_id, 18 | consumer_secret=config.CONFIG_DB.microsoft_client_secret, 19 | request_token_params={'scope': 'wl.emails'}, 20 | ) 21 | 22 | microsoft = auth.create_oauth_app(microsoft_config, 'microsoft') 23 | 24 | 25 | @app.route('/api/auth/callback/microsoft/') 26 | def microsoft_authorized(): 27 | response = microsoft.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 | flask.session['oauth_token'] = (response['access_token'], '') 32 | me = microsoft.get('me') 33 | if me.data.get('error', {}): 34 | return 'Unknown error: error:%s error_description:%s' % ( 35 | me['error']['code'], 36 | me['error']['message'], 37 | ) 38 | user_db = retrieve_user_from_microsoft(me.data) 39 | return auth.signin_user_db(user_db) 40 | 41 | 42 | @microsoft.tokengetter 43 | def get_microsoft_oauth_token(): 44 | return flask.session.get('oauth_token') 45 | 46 | 47 | @app.route('/signin/microsoft/') 48 | def signin_microsoft(): 49 | return auth.signin_oauth(microsoft) 50 | 51 | 52 | def retrieve_user_from_microsoft(response): 53 | auth_id = 'microsoft_%s' % response['id'] 54 | user_db = model.User.get_by('auth_ids', auth_id) 55 | if user_db: 56 | return user_db 57 | email = response['emails']['preferred'] or response['emails']['account'] 58 | return auth.create_user_db( 59 | auth_id=auth_id, 60 | name=response.get('name', ''), 61 | username=email, 62 | email=email, 63 | verified=bool(email), 64 | ) 65 | -------------------------------------------------------------------------------- /main/auth/reddit.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import base64 4 | 5 | from flask_oauthlib import client 6 | from werkzeug import urls 7 | import flask 8 | 9 | import auth 10 | import config 11 | import model 12 | import util 13 | 14 | from main import app 15 | 16 | reddit_config = dict( 17 | access_token_method='POST', 18 | access_token_params={'grant_type': 'authorization_code'}, 19 | access_token_url='https://ssl.reddit.com/api/v1/access_token', 20 | authorize_url='https://ssl.reddit.com/api/v1/authorize', 21 | base_url='https://oauth.reddit.com/api/v1/', 22 | consumer_key=model.Config.get_master_db().reddit_client_id, 23 | consumer_secret=model.Config.get_master_db().reddit_client_secret, 24 | request_token_params={'scope': 'identity', 'state': util.uuid()}, 25 | ) 26 | 27 | reddit = auth.create_oauth_app(reddit_config, 'reddit') 28 | 29 | 30 | def reddit_handle_oauth2_response(): 31 | access_args = { 32 | 'code': flask.request.args.get('code'), 33 | 'client_id': reddit.consumer_key, 34 | 'redirect_uri': flask.session.get('%s_oauthredir' % reddit.name), 35 | } 36 | access_args.update(reddit.access_token_params) 37 | auth_header = 'Basic %s' % base64.b64encode( 38 | ('%s:%s' % (reddit.consumer_key, reddit.consumer_secret)).encode('latin1') 39 | ).strip().decode('latin1') 40 | response, content = reddit.http_request( 41 | reddit.expand_url(reddit.access_token_url), 42 | method=reddit.access_token_method, 43 | data=urls.url_encode(access_args), 44 | headers={ 45 | 'Authorization': auth_header, 46 | 'User-Agent': config.USER_AGENT, 47 | }, 48 | ) 49 | data = client.parse_response(response, content) 50 | if response.code not in (200, 201): 51 | raise client.OAuthException( 52 | 'Invalid response from %s' % reddit.name, 53 | type='invalid_response', data=data, 54 | ) 55 | return data 56 | 57 | 58 | reddit.handle_oauth2_response = reddit_handle_oauth2_response 59 | 60 | 61 | @app.route('/api/auth/callback/reddit/') 62 | def reddit_authorized(): 63 | response = reddit.authorized_response() 64 | if response is None or flask.request.args.get('error'): 65 | flask.flash('You denied the request to sign in.') 66 | return flask.redirect(util.get_next_url()) 67 | 68 | flask.session['oauth_token'] = (response['access_token'], '') 69 | me = reddit.request('me') 70 | user_db = retrieve_user_from_reddit(me.data) 71 | return auth.signin_user_db(user_db) 72 | 73 | 74 | @reddit.tokengetter 75 | def get_reddit_oauth_token(): 76 | return flask.session.get('oauth_token') 77 | 78 | 79 | @app.route('/signin/reddit/') 80 | def signin_reddit(): 81 | return auth.signin_oauth(reddit) 82 | 83 | 84 | def retrieve_user_from_reddit(response): 85 | auth_id = 'reddit_%s' % response['id'] 86 | user_db = model.User.get_by('auth_ids', auth_id) 87 | if user_db: 88 | return user_db 89 | 90 | return auth.create_user_db( 91 | auth_id=auth_id, 92 | name=response['name'], 93 | username=response['name'], 94 | ) 95 | -------------------------------------------------------------------------------- /main/auth/twitter.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | 5 | import auth 6 | import config 7 | import model 8 | import util 9 | 10 | from main import app 11 | 12 | twitter_config = dict( 13 | access_token_url='https://api.twitter.com/oauth/access_token', 14 | authorize_url='https://api.twitter.com/oauth/authorize', 15 | base_url='https://api.twitter.com/1.1/', 16 | consumer_key=config.CONFIG_DB.twitter_consumer_key, 17 | consumer_secret=config.CONFIG_DB.twitter_consumer_secret, 18 | request_token_url='https://api.twitter.com/oauth/request_token', 19 | ) 20 | 21 | twitter = auth.create_oauth_app(twitter_config, 'twitter') 22 | 23 | 24 | @app.route('/api/auth/callback/twitter/') 25 | def twitter_authorized(): 26 | response = twitter.authorized_response() 27 | if response is None: 28 | flask.flash('You denied the request to sign in.') 29 | return flask.redirect(util.get_next_url()) 30 | 31 | flask.session['oauth_token'] = ( 32 | response['oauth_token'], 33 | response['oauth_token_secret'], 34 | ) 35 | user_db = retrieve_user_from_twitter(response) 36 | return auth.signin_user_db(user_db) 37 | 38 | 39 | @twitter.tokengetter 40 | def get_twitter_token(): 41 | return flask.session.get('oauth_token') 42 | 43 | 44 | @app.route('/signin/twitter/') 45 | def signin_twitter(): 46 | return auth.signin_oauth(twitter) 47 | 48 | 49 | def retrieve_user_from_twitter(response): 50 | auth_id = 'twitter_%s' % response['user_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['screen_name'], 55 | username=response['screen_name'], 56 | ) 57 | -------------------------------------------------------------------------------- /main/auth/vk.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | 5 | import auth 6 | import config 7 | import model 8 | import util 9 | 10 | from main import app 11 | 12 | vk_config = dict( 13 | access_token_url='https://oauth.vk.com/access_token', 14 | authorize_url='https://oauth.vk.com/authorize', 15 | base_url='https://api.vk.com/', 16 | consumer_key=config.CONFIG_DB.vk_app_id, 17 | consumer_secret=config.CONFIG_DB.vk_app_secret, 18 | ) 19 | 20 | vk = auth.create_oauth_app(vk_config, 'vk') 21 | 22 | 23 | @app.route('/api/auth/callback/vk/') 24 | def vk_authorized(): 25 | response = vk.authorized_response() 26 | if response is None: 27 | flask.flash(u'You denied the request to sign in.') 28 | return flask.redirect(util.get_next_url()) 29 | 30 | access_token = response['access_token'] 31 | flask.session['oauth_token'] = (access_token, '') 32 | me = vk.get( 33 | '/method/users.get', 34 | data={ 35 | 'access_token': access_token, 36 | 'format': 'json', 37 | }, 38 | ) 39 | user_db = retrieve_user_from_vk(me.data['response'][0]) 40 | return auth.signin_user_db(user_db) 41 | 42 | 43 | @vk.tokengetter 44 | def get_vk_oauth_token(): 45 | return flask.session.get('oauth_token') 46 | 47 | 48 | @app.route('/signin/vk/') 49 | def signin_vk(): 50 | return auth.signin_oauth(vk) 51 | 52 | 53 | def retrieve_user_from_vk(response): 54 | auth_id = 'vk_%s' % response['uid'] 55 | user_db = model.User.get_by('auth_ids', auth_id) 56 | if user_db: 57 | return user_db 58 | 59 | name = ' '.join((response['first_name'], response['last_name'])).strip() 60 | return auth.create_user_db( 61 | auth_id=auth_id, 62 | name=name, 63 | username=name, 64 | ) 65 | -------------------------------------------------------------------------------- /main/auth/yahoo.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | 5 | import auth 6 | import model 7 | import util 8 | 9 | from main import app 10 | 11 | yahoo_config = dict( 12 | access_token_url='https://api.login.yahoo.com/oauth/v2/get_token', 13 | authorize_url='https://api.login.yahoo.com/oauth/v2/request_auth', 14 | base_url='https://query.yahooapis.com/', 15 | consumer_key=model.Config.get_master_db().yahoo_consumer_key, 16 | consumer_secret=model.Config.get_master_db().yahoo_consumer_secret, 17 | request_token_url='https://api.login.yahoo.com/oauth/v2/get_request_token', 18 | ) 19 | 20 | yahoo = auth.create_oauth_app(yahoo_config, 'yahoo') 21 | 22 | 23 | @app.route('/api/auth/callback/yahoo/') 24 | def yahoo_authorized(): 25 | response = yahoo.authorized_response() 26 | if response is None: 27 | flask.flash('You denied the request to sign in.') 28 | return flask.redirect(util.get_next_url()) 29 | 30 | flask.session['oauth_token'] = ( 31 | response['oauth_token'], 32 | response['oauth_token_secret'], 33 | ) 34 | 35 | fields = 'guid, emails, familyName, givenName, nickname' 36 | me = yahoo.get( 37 | '/v1/yql', 38 | data={ 39 | 'format': 'json', 40 | 'q': 'select %s from social.profile where guid = me;' % fields, 41 | 'realm': 'yahooapis.com', 42 | }, 43 | ) 44 | user_db = retrieve_user_from_yahoo(me.data['query']['results']['profile']) 45 | return auth.signin_user_db(user_db) 46 | 47 | 48 | @yahoo.tokengetter 49 | def get_yahoo_oauth_token(): 50 | return flask.session.get('oauth_token') 51 | 52 | 53 | @app.route('/signin/yahoo/') 54 | def signin_yahoo(): 55 | return auth.signin_oauth(yahoo) 56 | 57 | 58 | def retrieve_user_from_yahoo(response): 59 | auth_id = 'yahoo_%s' % response['guid'] 60 | user_db = model.User.get_by('auth_ids', auth_id) 61 | if user_db: 62 | return user_db 63 | 64 | names = [response.get('givenName', ''), response.get('familyName', '')] 65 | emails = response.get('emails', {}) 66 | if not isinstance(emails, list): 67 | emails = [emails] 68 | emails = [e for e in emails if 'handle' in e] 69 | emails.sort(key=lambda e: e.get('primary', False)) 70 | email = emails[0]['handle'] if emails else '' 71 | return auth.create_user_db( 72 | auth_id=auth_id, 73 | name=' '.join(names).strip() or response['nickname'], 74 | username=response['nickname'], 75 | email=email, 76 | verified=bool(email), 77 | ) 78 | -------------------------------------------------------------------------------- /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 | except (ImportError, AttributeError): 16 | pass 17 | else: 18 | from datetime import datetime 19 | 20 | CURRENT_VERSION_ID = os.environ.get('CURRENT_VERSION_ID') 21 | CURRENT_VERSION_NAME = CURRENT_VERSION_ID.split('.')[0] 22 | CURRENT_VERSION_TIMESTAMP = long(CURRENT_VERSION_ID.split('.')[1]) >> 28 23 | if DEVELOPMENT: 24 | import calendar 25 | 26 | CURRENT_VERSION_TIMESTAMP = calendar.timegm(datetime.utcnow().timetuple()) 27 | CURRENT_VERSION_DATE = datetime.utcfromtimestamp(CURRENT_VERSION_TIMESTAMP) 28 | USER_AGENT = '%s/%s' % (APPLICATION_ID, CURRENT_VERSION_ID) 29 | 30 | import model 31 | 32 | CONFIG_DB = model.Config.get_master_db() 33 | SECRET_KEY = CONFIG_DB.flask_secret_key.encode('ascii') 34 | RECAPTCHA_PUBLIC_KEY = CONFIG_DB.recaptcha_public_key 35 | RECAPTCHA_PRIVATE_KEY = CONFIG_DB.recaptcha_private_key 36 | RECAPTCHA_LIMIT = 8 37 | TRUSTED_HOSTS = CONFIG_DB.trusted_hosts 38 | 39 | TAGLINE = 'Fighting for clean code has never been easier!' 40 | 41 | DEFAULT_DB_LIMIT = 64 42 | SIGNIN_RETRY_LIMIT = 4 43 | TAG_SEPARATOR = ' ' 44 | 45 | OG_IMAGE = '/p/img/fight-640.png' 46 | -------------------------------------------------------------------------------- /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 .letsencrypt import * 9 | from .profile import * 10 | from .test import * 11 | from .welcome import * 12 | from .language import * 13 | from .post import * 14 | from .vote import * 15 | from .comment import * 16 | from .extra import * 17 | -------------------------------------------------------------------------------- /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/extra.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # This file to be integrated somewhere else eventually :) 4 | 5 | import flask 6 | 7 | import auth 8 | import model 9 | 10 | from main import app 11 | 12 | 13 | @app.route('/post//vote//', methods=['GET', 'POST']) 14 | @auth.login_required 15 | def post_vote(post_id, variant): 16 | post_db = model.Post.get_by_id(post_id) 17 | if not post_db or variant not in ['a', 'b']: 18 | flask.abort(404) 19 | user_db = auth.current_user_db() 20 | code = '%s-%s' % (post_db.key.id(), user_db.key.id()) 21 | vote_db = model.Vote.get_or_insert( 22 | code, 23 | parent=post_db.key, 24 | user_key=user_db.key, 25 | post_key=post_db.key, 26 | variant=variant, 27 | ) 28 | vote_db.variant = variant 29 | vote_db.put() 30 | 31 | return flask.redirect(flask.url_for('post_view', post_id=post_db.key.id())) 32 | -------------------------------------------------------------------------------- /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/language.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from google.appengine.ext import ndb 4 | import flask 5 | import flask_wtf 6 | import wtforms 7 | 8 | import auth 9 | import config 10 | import model 11 | import util 12 | 13 | from main import app 14 | 15 | 16 | ############################################################################### 17 | # Admin List 18 | ############################################################################### 19 | @app.route('/admin/language/') 20 | @auth.admin_required 21 | def admin_language_list(): 22 | language_dbs, language_cursor = model.Language.get_dbs( 23 | order=util.param('order') or '-modified', 24 | ) 25 | return flask.render_template( 26 | 'language/admin_language_list.html', 27 | html_class='admin-language-list', 28 | title='Language List', 29 | language_dbs=language_dbs, 30 | next_url=util.generate_next_url(language_cursor), 31 | api_url=flask.url_for('api.admin.language.list'), 32 | ) 33 | 34 | 35 | ############################################################################### 36 | # Admin Update 37 | ############################################################################### 38 | class LanguageUpdateAdminForm(flask_wtf.FlaskForm): 39 | name = wtforms.StringField( 40 | model.Language.name._verbose_name, 41 | [wtforms.validators.required(), wtforms.validators.length(max=500)], 42 | filters=[util.strip_filter], 43 | ) 44 | slug = wtforms.StringField( 45 | model.Language.slug._verbose_name, 46 | [wtforms.validators.required(), wtforms.validators.length(max=500)], 47 | filters=[util.strip_filter], 48 | ) 49 | 50 | 51 | @app.route('/admin/language/create/', methods=['GET', 'POST']) 52 | @app.route('/admin/language//update/', methods=['GET', 'POST']) 53 | @auth.admin_required 54 | def admin_language_update(language_id=0): 55 | if language_id: 56 | language_db = model.Language.get_by_id(language_id) 57 | else: 58 | language_db = model.Language() 59 | 60 | if not language_db: 61 | flask.abort(404) 62 | 63 | form = LanguageUpdateAdminForm(obj=language_db) 64 | 65 | if form.validate_on_submit(): 66 | form.populate_obj(language_db) 67 | language_db.put() 68 | return flask.redirect(flask.url_for('admin_language_list', order='-modified')) 69 | 70 | return flask.render_template( 71 | 'language/admin_language_update.html', 72 | title='%s' % language_db.name if language_id else 'New Language', 73 | html_class='admin-language-update', 74 | form=form, 75 | language_db=language_db, 76 | back_url_for='admin_language_list', 77 | api_url=flask.url_for('api.admin.language', language_key=language_db.key.urlsafe() if language_db.key else ''), 78 | ) 79 | 80 | 81 | ############################################################################### 82 | # Admin Delete 83 | ############################################################################### 84 | @app.route('/admin/language//delete/', methods=['POST']) 85 | @auth.admin_required 86 | def admin_language_delete(language_id): 87 | language_db = model.Language.get_by_id(language_id) 88 | language_db.key.delete() 89 | flask.flash('Language deleted.', category='success') 90 | return flask.redirect(flask.url_for('admin_language_list')) 91 | -------------------------------------------------------------------------------- /main/control/letsencrypt.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | 5 | import config 6 | 7 | from main import app 8 | 9 | 10 | @app.route('/.well-known/acme-challenge/') 11 | def letsencrypt(challenge): 12 | response = flask.make_response('oups', 404) 13 | if challenge == config.CONFIG_DB.letsencrypt_challenge: 14 | response = flask.make_response(config.CONFIG_DB.letsencrypt_response) 15 | response.headers['Content-Type'] = 'text/plain' 16 | return response 17 | -------------------------------------------------------------------------------- /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/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/vote.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from google.appengine.ext import ndb 4 | import flask 5 | import flask_wtf 6 | import wtforms 7 | 8 | import auth 9 | import config 10 | import model 11 | import util 12 | 13 | from main import app 14 | 15 | 16 | ############################################################################### 17 | # Admin List 18 | ############################################################################### 19 | @app.route('/admin/vote/') 20 | @auth.admin_required 21 | def admin_vote_list(): 22 | vote_dbs, vote_cursor = model.Vote.get_dbs( 23 | order=util.param('order') or '-modified', 24 | ) 25 | return flask.render_template( 26 | 'vote/admin_vote_list.html', 27 | html_class='admin-vote-list', 28 | title='Vote List', 29 | vote_dbs=vote_dbs, 30 | next_url=util.generate_next_url(vote_cursor), 31 | api_url=flask.url_for('api.admin.vote.list'), 32 | ) 33 | 34 | 35 | ############################################################################### 36 | # Admin Update 37 | ############################################################################### 38 | class VoteUpdateAdminForm(flask_wtf.FlaskForm): 39 | user_key = wtforms.SelectField( 40 | model.Vote.user_key._verbose_name, 41 | [wtforms.validators.required()], 42 | choices=[], 43 | ) 44 | post_key = wtforms.SelectField( 45 | model.Vote.post_key._verbose_name, 46 | [wtforms.validators.required()], 47 | choices=[], 48 | ) 49 | variant = wtforms.SelectField( 50 | model.Vote.variant._verbose_name, 51 | [wtforms.validators.required()], 52 | choices=[(v, v.title()) for v in model.Vote.variant._choices], 53 | ) 54 | 55 | 56 | @app.route('/admin/vote/create/', methods=['GET', 'POST']) 57 | @app.route('/admin/vote//update/', methods=['GET', 'POST']) 58 | @auth.admin_required 59 | def admin_vote_update(vote_id=0): 60 | if vote_id: 61 | vote_db = model.Vote.get_by_id(vote_id) 62 | else: 63 | vote_db = model.Vote() 64 | 65 | if not vote_db: 66 | flask.abort(404) 67 | 68 | form = VoteUpdateAdminForm(obj=vote_db) 69 | 70 | user_dbs, user_cursor = model.User.get_dbs(limit=-1) 71 | post_dbs, post_cursor = model.Post.get_dbs(limit=-1) 72 | form.user_key.choices = [(c.key.urlsafe(), c.name) for c in user_dbs] 73 | form.post_key.choices = [(c.key.urlsafe(), c.title) for c in post_dbs] 74 | if flask.request.method == 'GET' and not form.errors: 75 | form.user_key.data = vote_db.user_key.urlsafe() if vote_db.user_key else None 76 | form.post_key.data = vote_db.post_key.urlsafe() if vote_db.post_key else None 77 | 78 | if form.validate_on_submit(): 79 | form.user_key.data = ndb.Key(urlsafe=form.user_key.data) if form.user_key.data else None 80 | form.post_key.data = ndb.Key(urlsafe=form.post_key.data) if form.post_key.data else None 81 | form.populate_obj(vote_db) 82 | vote_db.put() 83 | return flask.redirect(flask.url_for('admin_vote_list', order='-modified')) 84 | 85 | return flask.render_template( 86 | 'vote/admin_vote_update.html', 87 | title='%s' % '%sVote' % ('' if vote_id else 'New '), 88 | html_class='admin-vote-update', 89 | form=form, 90 | vote_db=vote_db, 91 | back_url_for='admin_vote_list', 92 | api_url=flask.url_for('api.admin.vote', vote_key=vote_db.key.urlsafe() if vote_db.key else ''), 93 | ) 94 | 95 | 96 | ############################################################################### 97 | # Admin Delete 98 | ############################################################################### 99 | @app.route('/admin/vote//delete/', methods=['POST']) 100 | @auth.admin_required 101 | def admin_vote_delete(vote_id): 102 | vote_db = model.Vote.get_by_id(vote_id) 103 | vote_db.key.delete() 104 | flask.flash('Vote deleted.', category='success') 105 | return flask.redirect(flask.url_for('admin_vote_list')) 106 | -------------------------------------------------------------------------------- /main/control/welcome.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | 5 | import config 6 | import model 7 | import util 8 | 9 | from main import app 10 | 11 | 12 | ############################################################################### 13 | # Welcome 14 | ############################################################################### 15 | @app.route('/') 16 | def welcome(): 17 | post_dbs, post_cursor = model.Post.get_dbs() 18 | top_post_dbs, _ = model.Post.get_dbs(order='-votes', limit=4) 19 | return flask.render_template( 20 | 'welcome.html', 21 | html_class='welcome', 22 | post_dbs=post_dbs, 23 | top_post_dbs=top_post_dbs, 24 | next_url=util.generate_next_url(post_cursor), 25 | api_url=flask.url_for('api.post.list'), 26 | ) 27 | 28 | 29 | ############################################################################### 30 | # Sitemap stuff 31 | ############################################################################### 32 | @app.route('/sitemap.xml') 33 | def sitemap(): 34 | response = flask.make_response(flask.render_template( 35 | 'sitemap.xml', 36 | lastmod=config.CURRENT_VERSION_DATE.strftime('%Y-%m-%d'), 37 | )) 38 | response.headers['Content-Type'] = 'application/xml' 39 | return response 40 | 41 | 42 | ############################################################################### 43 | # Warmup request 44 | ############################################################################### 45 | @app.route('/_ah/warmup') 46 | def warmup(): 47 | # TODO: put your warmup code here 48 | return 'success' 49 | -------------------------------------------------------------------------------- /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.add_extension('jinja2_markdown.MarkdownExtension') 20 | app.jinja_env.markdowner.set_output_format('html5') 21 | app.jinja_env.globals.update( 22 | check_form_fields=util.check_form_fields, 23 | is_iterable=util.is_iterable, 24 | slugify=util.slugify, 25 | update_query_argument=util.update_query_argument, 26 | ) 27 | 28 | import auth 29 | import control 30 | import model 31 | import task 32 | 33 | from api import helpers 34 | 35 | api_v1 = helpers.Api(app, prefix='/api/v1') 36 | 37 | import api.v1 38 | 39 | if config.DEVELOPMENT: 40 | from werkzeug import debug 41 | try: 42 | app.wsgi_app = debug.DebuggedApplication( 43 | app.wsgi_app, evalex=True, pin_security=False, 44 | ) 45 | except TypeError: 46 | app.wsgi_app = debug.DebuggedApplication(app.wsgi_app, evalex=True) 47 | app.testing = False 48 | -------------------------------------------------------------------------------- /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 .language import Language 8 | from .post import Post 9 | from .vote import Vote 10 | from .comment import Comment 11 | -------------------------------------------------------------------------------- /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/comment.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 | import util 10 | 11 | 12 | class Comment(model.Base): 13 | content = ndb.TextProperty(required=True) 14 | user_key = ndb.KeyProperty(kind=model.User, required=True) 15 | post_key = ndb.KeyProperty(kind=model.Post, required=True, verbose_name=u'Post') 16 | 17 | FIELDS = { 18 | 'content': fields.String, 19 | 'user_key': fields.Key, 20 | 'post_key': fields.Key, 21 | } 22 | 23 | FIELDS.update(model.Base.FIELDS) 24 | -------------------------------------------------------------------------------- /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 | check_unique_email = ndb.BooleanProperty(default=True, verbose_name='Check for uniqueness of the verified emails') 20 | email_authentication = ndb.BooleanProperty(default=False, verbose_name='Email authentication for sign in/sign up') 21 | feedback_email = ndb.StringProperty(default='') 22 | flask_secret_key = ndb.StringProperty(default=util.uuid()) 23 | letsencrypt_challenge = ndb.StringProperty(default='', verbose_name=u'Let’s Encrypt Challenge') 24 | letsencrypt_response = ndb.StringProperty(default='', verbose_name=u'Let’s Encrypt Response') 25 | notify_on_new_user = ndb.BooleanProperty(default=True, verbose_name='Send an email notification when a user signs up') 26 | recaptcha_private_key = ndb.StringProperty(default='', verbose_name='Private Key') 27 | recaptcha_public_key = ndb.StringProperty(default='', verbose_name='Public Key') 28 | salt = ndb.StringProperty(default=util.uuid()) 29 | trusted_hosts = ndb.StringProperty(repeated=True, verbose_name='Trusted Hosts') 30 | verify_email = ndb.BooleanProperty(default=True, verbose_name='Verify user emails') 31 | 32 | @property 33 | def has_anonymous_recaptcha(self): 34 | return bool(self.anonymous_recaptcha and self.has_recaptcha) 35 | 36 | @property 37 | def has_email_authentication(self): 38 | return bool(self.email_authentication and self.feedback_email and self.verify_email) 39 | 40 | @property 41 | def has_recaptcha(self): 42 | return bool(self.recaptcha_private_key and self.recaptcha_public_key) 43 | 44 | @classmethod 45 | def get_master_db(cls): 46 | return cls.get_or_insert('master') 47 | 48 | FIELDS = { 49 | 'analytics_id': fields.String, 50 | 'announcement_html': fields.String, 51 | 'announcement_type': fields.String, 52 | 'anonymous_recaptcha': fields.Boolean, 53 | 'brand_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 | 'letsencrypt_challenge': fields.String, 59 | 'letsencrypt_response': fields.String, 60 | 'notify_on_new_user': fields.Boolean, 61 | 'recaptcha_private_key': fields.String, 62 | 'recaptcha_public_key': fields.String, 63 | 'salt': fields.String, 64 | 'trusted_hosts': fields.List(fields.String), 65 | 'verify_email': fields.Boolean, 66 | } 67 | 68 | FIELDS.update(model.Base.FIELDS) 69 | FIELDS.update(model.ConfigAuth.FIELDS) 70 | -------------------------------------------------------------------------------- /main/model/language.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 | import util 10 | 11 | 12 | class Language(model.Base): 13 | name = ndb.StringProperty(required=True) 14 | slug = ndb.StringProperty(required=True) 15 | 16 | @classmethod 17 | def get_dbs(cls, order=None, **kwargs): 18 | return super(Language, cls).get_dbs( 19 | order=order or util.param('order') or 'name', 20 | **kwargs 21 | ) 22 | 23 | def get_post_dbs(self, **kwargs): 24 | return model.Post.get_dbs(language_key=self.key, **kwargs) 25 | 26 | @classmethod 27 | def _pre_delete_hook(cls, key): 28 | language_db = key.get() 29 | post_keys = language_db.get_post_dbs(keys_only=True, limit=-1)[0] 30 | ndb.delete_multi(post_keys) 31 | 32 | FIELDS = { 33 | 'name': fields.String, 34 | 'slug': fields.String, 35 | } 36 | 37 | FIELDS.update(model.Base.FIELDS) 38 | -------------------------------------------------------------------------------- /main/model/post.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 | import util 10 | 11 | 12 | class Post(model.Base): 13 | title = ndb.StringProperty(required=True) 14 | language_key = ndb.KeyProperty(kind=model.Language, required=True, verbose_name=u'Language') 15 | variant_a = ndb.TextProperty(required=True, verbose_name='Red Corner') 16 | variant_b = ndb.TextProperty(required=True, verbose_name='Blue Corner') 17 | user_key = ndb.KeyProperty(kind=model.User, required=True) 18 | votes_a = ndb.IntegerProperty(default=0) 19 | votes_b = ndb.IntegerProperty(default=0) 20 | 21 | @ndb.ComputedProperty 22 | def votes(self): 23 | return self.votes_a + self.votes_b 24 | 25 | @ndb.ComputedProperty 26 | def votes_a_percentage(self): 27 | if self.votes > 0: 28 | return 1.0 * self.votes_a / self.votes 29 | return 0 30 | 31 | @ndb.ComputedProperty 32 | def votes_b_percentage(self): 33 | if self.votes > 0: 34 | return 1.0 * self.votes_b / self.votes 35 | return 0 36 | 37 | @classmethod 38 | def get_dbs(cls, order=None, **kwargs): 39 | return super(Post, cls).get_dbs( 40 | order=order or util.param('order') or '-created', 41 | **kwargs 42 | ) 43 | 44 | def get_vote_dbs(self, **kwargs): 45 | return model.Vote.get_dbs(post_key=self.key, **kwargs) 46 | 47 | def get_comment_dbs(self, **kwargs): 48 | return model.Comment.get_dbs(ancestor=self.key, **kwargs) 49 | 50 | @classmethod 51 | def _pre_delete_hook(cls, key): 52 | post_db = key.get() 53 | vote_keys = post_db.get_vote_dbs(keys_only=True, limit=-1)[0] 54 | comment_keys = post_db.get_comment_dbs(keys_only=True, limit=-1)[0] 55 | ndb.delete_multi(vote_keys + comment_keys) 56 | 57 | FIELDS = { 58 | 'title': fields.String, 59 | 'language_key': fields.Key, 60 | 'variant_a': fields.String, 61 | 'variant_b': fields.String, 62 | 'user_key': fields.Key, 63 | 'votes_a': fields.Integer, 64 | 'votes_b': fields.Integer, 65 | } 66 | 67 | FIELDS.update(model.Base.FIELDS) 68 | -------------------------------------------------------------------------------- /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 has_github(self): 39 | for auth_id in self.auth_ids: 40 | if auth_id.startswith('github'): 41 | return auth_id 42 | return None 43 | 44 | def avatar_url_size(self, size=None): 45 | github_id = self.has_github() 46 | if github_id: 47 | return 'https://avatars.githubusercontent.com/u/%(id)s%(size)s' % { 48 | 'id': github_id.split('_')[1], 49 | 'size': '?size=%s' % (size) if size else '', 50 | } 51 | 52 | facebook_id = self.has_facebook() 53 | if facebook_id: 54 | return 'https://graph.facebook.com/%(id)s/picture%(size)s' % { 55 | 'id': facebook_id.split('_')[1], 56 | 'size': '?width=%s&height=%s' % (size, size) if size else '', 57 | } 58 | 59 | return 'https://gravatar.com/avatar/%(hash)s?d=identicon&r=x%(size)s' % { 60 | 'hash': hashlib.md5( 61 | (self.email or self.username).encode('utf-8')).hexdigest(), 62 | 'size': '&s=%d' % size if size > 0 else '', 63 | } 64 | 65 | avatar_url = property(avatar_url_size) 66 | 67 | @classmethod 68 | def get_dbs( 69 | cls, admin=None, active=None, verified=None, permissions=None, **kwargs 70 | ): 71 | args = parser.parse({ 72 | 'admin': wf.Bool(missing=None), 73 | 'active': wf.Bool(missing=None), 74 | 'verified': wf.Bool(missing=None), 75 | 'permissions': wf.DelimitedList(wf.Str(), delimiter=',', missing=[]), 76 | }) 77 | return super(User, cls).get_dbs( 78 | admin=admin or args['admin'], 79 | active=active or args['active'], 80 | verified=verified or args['verified'], 81 | permissions=permissions or args['permissions'], 82 | **kwargs 83 | ) 84 | 85 | @classmethod 86 | def is_username_available(cls, username, self_key=None): 87 | if self_key is None: 88 | return cls.get_by('username', username) is None 89 | user_keys, _ = util.get_keys(cls.query(), username=username, limit=2) 90 | return not user_keys or self_key in user_keys and not user_keys[1:] 91 | 92 | @classmethod 93 | def is_email_available(cls, email, self_key=None): 94 | if not config.CONFIG_DB.check_unique_email: 95 | return True 96 | user_keys, _ = util.get_keys( 97 | cls.query(), email=email, verified=True, limit=2, 98 | ) 99 | return not user_keys or self_key in user_keys and not user_keys[1:] 100 | 101 | FIELDS = { 102 | 'active': fields.Boolean, 103 | 'admin': fields.Boolean, 104 | 'auth_ids': fields.List(fields.String), 105 | 'avatar_url': fields.String, 106 | 'email': fields.String, 107 | 'name': fields.String, 108 | 'permissions': fields.List(fields.String), 109 | 'username': fields.String, 110 | 'verified': fields.Boolean, 111 | } 112 | 113 | FIELDS.update(model.Base.FIELDS) 114 | -------------------------------------------------------------------------------- /main/model/vote.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 | import util 10 | 11 | 12 | class Vote(model.Base): 13 | user_key = ndb.KeyProperty(kind=model.User, required=True, verbose_name=u'User') 14 | post_key = ndb.KeyProperty(kind=model.Post, required=True, verbose_name=u'Post') 15 | variant = ndb.StringProperty(required=True, choices=['a', 'b']) 16 | 17 | FIELDS = { 18 | 'user_key': fields.Key, 19 | 'post_key': fields.Key, 20 | 'variant': fields.String, 21 | } 22 | 23 | FIELDS.update(model.Base.FIELDS) 24 | -------------------------------------------------------------------------------- /main/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/welovecoding/vote4code/be265d553af35dc6c5322ecb3f7d5b3cf7691b75/main/static/img/favicon.ico -------------------------------------------------------------------------------- /main/static/img/fight-640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/welovecoding/vote4code/be265d553af35dc6c5322ecb3f7d5b3cf7691b75/main/static/img/fight-640.png -------------------------------------------------------------------------------- /main/static/img/fight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/welovecoding/vote4code/be265d553af35dc6c5322ecb3f7d5b3cf7691b75/main/static/img/fight.png -------------------------------------------------------------------------------- /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/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 arguments.callee, 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 | -------------------------------------------------------------------------------- /main/static/src/script/post.js: -------------------------------------------------------------------------------- 1 | function initPostView() { 2 | $('pre code').each(function (i, block) { 3 | hljs.highlightBlock(block); 4 | hljs.lineNumbersBlock(block); 5 | }); 6 | } 7 | -------------------------------------------------------------------------------- /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.post-view').each -> 14 | initPostView() 15 | -------------------------------------------------------------------------------- /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/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/highlight/default.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original highlight.js style (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #F0F0F0; 12 | } 13 | 14 | 15 | /* Base color: saturation 0; */ 16 | 17 | .hljs, 18 | .hljs-subst { 19 | color: #444; 20 | } 21 | 22 | .hljs-comment { 23 | color: #888888; 24 | } 25 | 26 | .hljs-keyword, 27 | .hljs-attribute, 28 | .hljs-selector-tag, 29 | .hljs-meta-keyword, 30 | .hljs-doctag, 31 | .hljs-name { 32 | font-weight: bold; 33 | } 34 | 35 | 36 | /* User color: hue: 0 */ 37 | 38 | .hljs-type, 39 | .hljs-string, 40 | .hljs-number, 41 | .hljs-selector-id, 42 | .hljs-selector-class, 43 | .hljs-quote, 44 | .hljs-template-tag, 45 | .hljs-deletion { 46 | color: #880000; 47 | } 48 | 49 | .hljs-title, 50 | .hljs-section { 51 | color: #880000; 52 | font-weight: bold; 53 | } 54 | 55 | .hljs-regexp, 56 | .hljs-symbol, 57 | .hljs-variable, 58 | .hljs-template-variable, 59 | .hljs-link, 60 | .hljs-selector-attr, 61 | .hljs-selector-pseudo { 62 | color: #BC6060; 63 | } 64 | 65 | 66 | /* Language color: hue: 90; */ 67 | 68 | .hljs-literal { 69 | color: #78A960; 70 | } 71 | 72 | .hljs-built_in, 73 | .hljs-bullet, 74 | .hljs-code, 75 | .hljs-addition { 76 | color: #397300; 77 | } 78 | 79 | 80 | /* Meta color: hue: 200 */ 81 | 82 | .hljs-meta { 83 | color: #1f7199; 84 | } 85 | 86 | .hljs-meta-string { 87 | color: #4d99bf; 88 | } 89 | 90 | 91 | /* Misc effects */ 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /main/static/src/style/highlight/line-numbers.less: -------------------------------------------------------------------------------- 1 | .hljs-line-numbers { 2 | border-right: 1px solid #ccc; 3 | color: #999; 4 | text-align: right; 5 | user-select: none; 6 | } 7 | -------------------------------------------------------------------------------- /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/post.less: -------------------------------------------------------------------------------- 1 | .post-update { 2 | textarea { 3 | font-family: @font-family-monospace; 4 | } 5 | } 6 | 7 | .post-view { 8 | h1 { 9 | margin-top: 0; 10 | } 11 | .code-split { 12 | padding: 0 @grid-gutter-width / 2; 13 | margin-bottom: 32px; 14 | } 15 | 16 | pre { 17 | margin: 16px 0; 18 | code { 19 | overflow: auto; 20 | word-wrap: normal; 21 | white-space: pre; 22 | font-size: .9em; 23 | } 24 | } 25 | 26 | .variant-a { 27 | border: 4px solid @brand-danger; 28 | &::before { 29 | content: 'Red Corner'; 30 | left: 8px; 31 | color: @brand-danger; 32 | border-color: @brand-danger; 33 | } 34 | } 35 | .variant-b { 36 | border: 4px solid @brand-info; 37 | &::before { 38 | content: 'Blue Corner'; 39 | right: 8px; 40 | color: @brand-info; 41 | border-color: @brand-info; 42 | border-bottom: none; 43 | } 44 | } 45 | 46 | .variant { 47 | position: relative; 48 | overflow: visible; 49 | &::before { 50 | position: absolute; 51 | top: -28px; 52 | font-size: 16px; 53 | font-weight: 600; 54 | background-color: @hr-border; 55 | border-radius: 4px 4px 0 0; 56 | border-width: 4px; 57 | border-style: solid; 58 | border-bottom: none; 59 | padding: 2px 8px 0; 60 | } 61 | } 62 | 63 | .post-progress { 64 | border-radius: 0; 65 | margin-bottom: 0; 66 | } 67 | 68 | h2 { 69 | margin-top: 0; 70 | } 71 | 72 | .comment { 73 | padding: 8px; 74 | margin-bottom: @line-height-computed; 75 | border: 1px solid @hr-border; 76 | border-radius: 4px; 77 | .meta { 78 | margin-top: 12px; 79 | .user { 80 | .text-warning; 81 | } 82 | 83 | time { 84 | .pull-right; 85 | .text-success; 86 | .small; 87 | } 88 | } 89 | 90 | .markdown { 91 | h1, h2, h3, h4, h5, h6 { 92 | font-size: @font-size-h4; 93 | margin: 8px 0; 94 | } 95 | 96 | pre { 97 | padding: 0 8px; 98 | code { 99 | line-height: 1; 100 | padding: 0; 101 | } 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /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) + @padding-large-vertical * 2; 3 | @height-sm: floor(@font-size-small * 1.5) + @padding-small-vertical * 2; 4 | @height-xs: floor(@font-size-small * 1.2) + @padding-small-vertical + 1; 5 | 6 | .btn-social { 7 | position: relative; 8 | padding-left: @height-base + @padding-base-horizontal; 9 | text-align: left; 10 | white-space: nowrap; 11 | overflow: hidden; 12 | text-overflow: ellipsis; 13 | :first-child { 14 | position: absolute; 15 | left: 0; 16 | top: 0; 17 | bottom: 0; 18 | width: @height-base; 19 | line-height: @height-base + 2; 20 | font-size: 1.6em; 21 | text-align: center; 22 | border-right: 1px solid rgba(0,0,0,.2); 23 | } 24 | &.btn-lg { 25 | padding-left: @height-lg + @padding-large-horizontal; 26 | :first-child { 27 | line-height: @height-lg; 28 | width: @height-lg; 29 | font-size: 1.8em; 30 | } 31 | } 32 | &.btn-sm { 33 | padding-left: @height-sm + @padding-small-horizontal; 34 | :first-child { 35 | line-height: @height-sm; 36 | width: @height-sm; 37 | font-size: 1.4em; 38 | } 39 | } 40 | &.btn-xs { 41 | padding-left: @height-xs + @padding-small-horizontal; 42 | :first-child { 43 | line-height: @height-xs; 44 | width: @height-xs; 45 | font-size: 1.2em; 46 | } 47 | } 48 | } 49 | 50 | .btn-social-icon { 51 | .btn-social; 52 | height: @height-base + 2; 53 | width: @height-base + 2; 54 | padding-left: 0; 55 | padding-right: 0; 56 | :first-child { 57 | border: none; 58 | text-align: center; 59 | width: 100%!important; 60 | } 61 | &.btn-lg { 62 | height: @height-lg; 63 | width: @height-lg; 64 | padding-left: 0; 65 | padding-right: 0; 66 | } 67 | &.btn-sm { 68 | height: @height-sm + 2; 69 | width: @height-sm + 2; 70 | padding-left: 0; 71 | padding-right: 0; 72 | } 73 | &.btn-xs { 74 | height: @height-xs + 2; 75 | width: @height-xs + 2; 76 | padding-left: 0; 77 | padding-right: 0; 78 | } 79 | } 80 | 81 | .btn-social(@color-bg, @color: #fff) { 82 | background-color: @color-bg; 83 | .button-variant(@color, @color-bg, rgba(0,0,0,.2)); 84 | } 85 | 86 | .auth { 87 | .btn-social-icon { 88 | margin-top: 2px; 89 | margin-bottom: 2px; 90 | } 91 | } 92 | 93 | .remember { 94 | .text-center; 95 | label { 96 | display: inline-block; 97 | } 98 | } 99 | 100 | .btn-azure_ad { .btn-social(#307ea7); } 101 | .btn-bitbucket { .btn-social(#205081); } 102 | .btn-dropbox { .btn-social(#007ee5); } 103 | .btn-facebook { .btn-social(#3b5998); } 104 | .btn-github { .btn-social(#444444); } 105 | .btn-google { .btn-social(#dd4b39); } 106 | .btn-instagram { .btn-social(#3f729b); } 107 | .btn-linkedin { .btn-social(#007bb6); } 108 | .btn-mailru { .btn-social(#168de2); } 109 | .btn-microsoft { .btn-social(#2672ec); } 110 | .btn-reddit { .btn-social(#eff7ff, #000); } 111 | .btn-twitter { .btn-social(#55acee); } 112 | .btn-vk { .btn-social(#587ea3); } 113 | .btn-yahoo { .btn-social(#720e9e); } 114 | -------------------------------------------------------------------------------- /main/static/src/style/style.less: -------------------------------------------------------------------------------- 1 | @import "../../ext/font-awesome/less/font-awesome"; 2 | @import "../../ext/bootstrap/less/bootstrap"; 3 | 4 | @import "../../ext/bootswatch/flatly/variables"; 5 | @import "../../ext/bootswatch/flatly/bootswatch"; 6 | 7 | @import (less) "highlight/default.css"; 8 | @import "highlight/line-numbers"; 9 | 10 | @import "base"; 11 | @import "variables"; 12 | @import "test"; 13 | @import "mixins"; 14 | @import "footer"; 15 | @import "signin"; 16 | @import "user"; 17 | @import "vote"; 18 | @import "post"; 19 | @import "welcome"; 20 | -------------------------------------------------------------------------------- /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 | 6 | @jumbotron-padding: 32px; 7 | @jumbotron-color: #eee; 8 | @jumbotron-bg: #000; 9 | @jumbotron-heading-color: inherit; 10 | @navbar-default-bg: #000; 11 | @grid-gutter-width: 24px; 12 | -------------------------------------------------------------------------------- /main/static/src/style/vote.less: -------------------------------------------------------------------------------- 1 | .welcome { 2 | .progress { 3 | margin-bottom: 0; 4 | } 5 | .table > thead > tr > th { 6 | text-align: center; 7 | } 8 | .table > tbody > tr > td { 9 | vertical-align: middle; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /main/static/src/style/welcome.less: -------------------------------------------------------------------------------- 1 | .welcome { 2 | .jumbotron { 3 | background-image: url(/p/img/fight.png); 4 | background-position: top center; 5 | background-size: contain; 6 | background-repeat: no-repeat; 7 | margin-bottom: 0; 8 | 9 | @media (min-width: @screen-md-min) { 10 | background-position: 50%; 11 | } 12 | } 13 | 14 | h2 { 15 | margin: 48px 0 24px; 16 | text-align: center; 17 | } 18 | 19 | .top-post { 20 | display: flex; 21 | flex-flow: row wrap; 22 | margin: 24px 0; 23 | padding: 16px 0; 24 | border-bottom: 1px solid @hr-border; 25 | .post-avatar { 26 | display: flex; 27 | flex: 1; 28 | align-items: center; 29 | justify-content: center; 30 | img { 31 | width: 64px; 32 | height: 64px; 33 | border: 1px solid @hr-border; 34 | padding: 4px; 35 | border-radius: 4px; 36 | } 37 | 38 | } 39 | 40 | .post-votes { 41 | display: flex; 42 | flex: 1; 43 | align-items: center; 44 | justify-content: center; 45 | font-size: 48px; 46 | font-weight: 500; 47 | } 48 | 49 | .post-body { 50 | flex: 6; 51 | .title { 52 | font-size: 24px; 53 | font-weight: 300; 54 | } 55 | .author { 56 | font-size: 18px; 57 | font-weight: 300; 58 | } 59 | .post-progress { 60 | height: 20px; 61 | display: inline-block; 62 | width: 100%; 63 | margin: 0; 64 | .progress-bar { 65 | line-height: 20px; 66 | font-size: 14px; 67 | color: rgba(255,255,255,.2); 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /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 | {{admin_link('Language', 'language', url_for('admin_language_list'))}} 21 | {{admin_link('Post', 'code', url_for('admin_post_list'))}} 22 | {{admin_link('Vote', 'arrow-up', url_for('admin_vote_list'))}} 23 | {{admin_link('Comment', 'comments-o', url_for('admin_comment_list'))}} 24 |
25 | 26 | 32 | 33 |
34 | # if localhost 35 | {{admin_link('Localhost', 'home', localhost, 'gae')}} 36 | # endif 37 | {{admin_link('Dashboard', 'tachometer', 'https://console.cloud.google.com/home/dashboard?project=%s' % config.APPLICATION_ID, 'gae')}} 38 | {{admin_link('Datastore', 'database', 'https://console.cloud.google.com/datastore/query?project=%s' % config.APPLICATION_ID, 'gae')}} 39 | {{admin_link('Instances', 'bolt', 'https://console.cloud.google.com/appengine/instances?project=%s' % config.APPLICATION_ID, 'gae')}} 40 | {{admin_link('Versions', 'history', 'https://console.cloud.google.com/appengine/versions?project=%s' % config.APPLICATION_ID, 'gae')}} 41 | {{admin_link('Logs', 'bullhorn', 'https://console.cloud.google.com/logs?project=%s&versionId=%s' % (config.APPLICATION_ID, config.CURRENT_VERSION_NAME), 'gae')}} 42 | {{admin_link('APIs', 'wrench', 'https://console.developers.google.com/apis/library?project=%s' % config.APPLICATION_ID, 'gae')}} 43 | {{admin_link('Settings', 'cogs', 'https://console.cloud.google.com/appengine/settings?project=%s' % config.APPLICATION_ID, 'gae')}} 44 | {{admin_link('Billing', 'credit-card', 'https://console.cloud.google.com/billing', 'gae')}} 45 |
46 | 47 | # endblock 48 | 49 | 50 | # macro admin_link(title, icon, url, target='_self') 51 | 57 | # endmacro 58 | -------------------------------------------------------------------------------- /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/letsencrypt.html' 23 | # include 'admin/bit/google_analytics_tracking_id.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_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/letsencrypt.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'Let’s Encrypt (SSL)', 4 | (form.letsencrypt_challenge, form.letsencrypt_response), 5 | ''' 6 | For more information follow the 7 | instructions. 8 | ''' 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 | # if not config.CONFIG_DB.has_github 24 | {{utils.signin_button('GAE' if config.CONFIG_DB.has_google else 'Google', 'btn-google', 'fa-google', gae_signin_url, is_icon)}} 25 | # endif 26 | {{utils.signin_button('Facebook', 'btn-facebook', 'fa-facebook', facebook_signin_url, is_icon) if config.CONFIG_DB.has_facebook}} 27 | {{utils.signin_button('Twitter', 'btn-twitter', 'fa-twitter', twitter_signin_url, is_icon) if config.CONFIG_DB.has_twitter}} 28 | {{utils.signin_button('Azure AD', 'btn-azure_ad', 'fa-windows', azure_ad_signin_url, is_icon) if config.CONFIG_DB.has_azure_ad}} 29 | {{utils.signin_button('Bitbucket', 'btn-bitbucket', 'fa-bitbucket', bitbucket_signin_url, is_icon) if config.CONFIG_DB.has_bitbucket}} 30 | {{utils.signin_button('Dropbox', 'btn-dropbox', 'fa-dropbox', dropbox_signin_url, is_icon) if config.CONFIG_DB.has_dropbox}} 31 | {{utils.signin_button('GitHub', 'btn-github', 'fa-github', github_signin_url, is_icon) if config.CONFIG_DB.has_github}} 32 | {{utils.signin_button('Google', 'btn-google', 'fa-google-plus', google_signin_url, is_icon) if config.CONFIG_DB.has_google}} 33 | {{utils.signin_button('Instagram', 'btn-instagram', 'fa-instagram', instagram_signin_url, is_icon) if config.CONFIG_DB.has_instagram}} 34 | {{utils.signin_button('LinkedIn', 'btn-linkedin', 'fa-linkedin', linkedin_signin_url, is_icon) if config.CONFIG_DB.has_linkedin}} 35 | {{utils.signin_button('Mail.Ru', 'btn-mailru', 'fa-at', mailru_signin_url, is_icon) if config.CONFIG_DB.has_mailru}} 36 | {{utils.signin_button('Microsoft', 'btn-microsoft', 'fa-windows', microsoft_signin_url, is_icon) if config.CONFIG_DB.has_microsoft}} 37 | {{utils.signin_button('Reddit', 'btn-reddit', 'fa-reddit', reddit_signin_url, is_icon) if config.CONFIG_DB.has_reddit}} 38 | {{utils.signin_button('VK', 'btn-vk', 'fa-vk', vk_signin_url, is_icon) if config.CONFIG_DB.has_vk}} 39 | {{utils.signin_button('Yahoo!', 'btn-yahoo', 'fa-yahoo', yahoo_signin_url, is_icon) if config.CONFIG_DB.has_yahoo}} 40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 | # else 48 |
49 |

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

50 | Please sign out 51 | first if you want to sign in with a different account. 52 |
53 | # endif 54 | # endblock 55 | -------------------------------------------------------------------------------- /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 | # include 'bit/graph.html' 6 | 7 | # block title 8 | {{title + ' |' if title}} 9 | # endblock 10 | {{config.CONFIG_DB.brand_name}} 11 | 12 | # include 'bit/style.html' 13 | # block head 14 | # endblock 15 | # include 'bit/analytics.html' 16 | 17 | 18 | 19 | # include 'bit/header.html' 20 | # include 'bit/announcement.html' 21 | # block header 22 | # endblock 23 |
24 | # include 'bit/notifications.html' 25 | # block content 26 | # endblock 27 |
28 | # include 'bit/footer.html' 29 | # include 'bit/script.html' 30 | # block scripts 31 | # endblock 32 | 33 | 34 | -------------------------------------------------------------------------------- /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/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 |

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

13 |
14 |
15 | -------------------------------------------------------------------------------- /main/templates/bit/graph.html: -------------------------------------------------------------------------------- 1 | 2 | # if title and html_class == 'welcome' 3 | 4 | # else 5 | 6 | # endif 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /main/templates/bit/header.html: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /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/comment/admin_comment_list.html: -------------------------------------------------------------------------------- 1 | # extends 'admin/admin_base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block head 5 | {{utils.prefetch_link(next_url)}} 6 | # endblock 7 | 8 | # block admin_content 9 |
    10 | 15 |
    16 | 17 |
    18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | # for comment_db in comment_dbs 30 | 31 | 32 | 33 | 34 | 39 | 47 | 48 | # endfor 49 | 50 |
    {{utils.order_by_link('content', 'Content')}}{{utils.order_by_link('user_key', 'User Key')}}{{utils.order_by_link('post_key', 'Post')}}{{utils.order_by_link('user_key', 'User')}}{{utils.order_by_link('modified', 'Modified')}}
    {{utils.order_by_link('created', 'Created')}}
    51 |
    52 | 53 | {{utils.next_link(next_url)}} 54 | # endblock 55 | -------------------------------------------------------------------------------- /main/templates/comment/admin_comment_update.html: -------------------------------------------------------------------------------- 1 | # extends 'admin/admin_base.html' 2 | # import 'macro/forms.html' as forms 3 | # import 'macro/utils.html' as utils 4 | 5 | 6 | # block admin_content 7 |
    8 |
    9 |
    10 | {{form.csrf_token}} 11 | {{forms.textarea_field(form.content, autofocus=True)}} 12 | {{forms.select_field(form.post_key)}} 13 | 20 |
    21 |
    22 | # if comment_db.key 23 |
    24 |
    25 | 26 |
    27 |
    28 | # endif 29 |
    30 | # endblock 31 | -------------------------------------------------------------------------------- /main/templates/comment/comment_list.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block head 5 | {{utils.prefetch_link(next_url)}} 6 | # endblock 7 | 8 | # block content 9 | 15 |
    16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | # for comment_db in comment_dbs 25 | 26 | 27 | 28 | 29 | # endfor 30 | 31 |
    {{utils.order_by_link('content', 'Content')}}{{utils.order_by_link('post_key', 'Post')}}
    32 |
    33 | 34 | {{utils.next_link(next_url)}} 35 | # endblock 36 | -------------------------------------------------------------------------------- /main/templates/comment/comment_update.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 | 17 |
    18 |
    19 |
    20 | {{form.csrf_token}} 21 | {{forms.textarea_field(form.content, autofocus=True)}} 22 | {{forms.select_field(form.post_key)}} 23 | 30 |
    31 |
    32 |
    33 | # endblock 34 | -------------------------------------------------------------------------------- /main/templates/comment/comment_view.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 | 21 |
    22 |
    23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
    Content{{comment_db.content}}
    Post{{comment_db.post_key.get().title if comment_db.post_key else ''}}
    35 |
    36 |
    37 | # endblock 38 | -------------------------------------------------------------------------------- /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/language/admin_language_list.html: -------------------------------------------------------------------------------- 1 | # extends 'admin/admin_base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block head 5 | {{utils.prefetch_link(next_url)}} 6 | # endblock 7 | 8 | # block admin_content 9 |
    10 | 15 |
    16 | 17 |
    18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | # for language_db in language_dbs 28 | 29 | 30 | 31 | 39 | 40 | # endfor 41 | 42 |
    {{utils.order_by_link('name', 'Name')}}{{utils.order_by_link('slug', 'Slug')}}{{utils.order_by_link('modified', 'Modified')}}
    {{utils.order_by_link('created', 'Created')}}
    43 |
    44 | 45 | {{utils.next_link(next_url)}} 46 | # endblock 47 | -------------------------------------------------------------------------------- /main/templates/language/admin_language_update.html: -------------------------------------------------------------------------------- 1 | # extends 'admin/admin_base.html' 2 | # import 'macro/forms.html' as forms 3 | # import 'macro/utils.html' as utils 4 | 5 | 6 | # block admin_content 7 |
    8 |
    9 |
    10 | {{form.csrf_token}} 11 | {{forms.text_field(form.name, autofocus=True)}} 12 | {{forms.text_field(form.slug)}} 13 | 20 |
    21 |
    22 | # if language_db.key 23 |
    24 |
    25 | 26 |
    27 |
    28 | # endif 29 |
    30 | # endblock 31 | -------------------------------------------------------------------------------- /main/templates/language/language_list.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block head 5 | {{utils.prefetch_link(next_url)}} 6 | # endblock 7 | 8 | # block content 9 | 12 |
    13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | # for language_db in language_dbs 21 | 22 | 23 | 24 | # endfor 25 | 26 |
    {{utils.order_by_link('name', 'Name')}}
    27 |
    28 | 29 | {{utils.next_link(next_url)}} 30 | # endblock 31 | -------------------------------------------------------------------------------- /main/templates/language/language_view.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 |
    Name{{language_db.name}}
    23 |
    24 |
    25 | 26 | # set post_dbs, post_cursor = language_db.get_post_dbs(limit=-1) 27 |
    28 |

    Post List

    29 |
    30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | # for post_db in post_dbs 41 | 42 | 43 | 44 | 45 | 46 | 47 | # endfor 48 | 49 |
    TitleLanguageVariant AVariant B
    50 |
    51 |
    52 | # endblock 53 | -------------------------------------------------------------------------------- /main/templates/post/admin_post_list.html: -------------------------------------------------------------------------------- 1 | # extends 'admin/admin_base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block head 5 | {{utils.prefetch_link(next_url)}} 6 | # endblock 7 | 8 | # block admin_content 9 |
    10 | 15 |
    16 | 17 |
    18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | # for post_db in post_dbs 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 47 | 55 | 56 | # endfor 57 | 58 |
    {{utils.order_by_link('title', 'Title')}}{{utils.order_by_link('language_key', 'Language')}}{{utils.order_by_link('variant_a', 'Variant A')}}{{utils.order_by_link('variant_b', 'Variant B')}}{{utils.order_by_link('user_key', 'User Key')}}{{utils.order_by_link('votes_a', 'Votes A')}}{{utils.order_by_link('votes_b', 'Votes B')}}{{utils.order_by_link('user_key', 'User')}}{{utils.order_by_link('modified', 'Modified')}}
    {{utils.order_by_link('created', 'Created')}}
    59 |
    60 | 61 | {{utils.next_link(next_url)}} 62 | # endblock 63 | -------------------------------------------------------------------------------- /main/templates/post/admin_post_update.html: -------------------------------------------------------------------------------- 1 | # extends 'admin/admin_base.html' 2 | # import 'macro/forms.html' as forms 3 | # import 'macro/utils.html' as utils 4 | 5 | 6 | # block admin_content 7 |
    8 |
    9 |
    10 | {{form.csrf_token}} 11 | {{forms.text_field(form.title, autofocus=True)}} 12 | {{forms.select_field(form.language_key)}} 13 | {{forms.textarea_field(form.variant_a)}} 14 | {{forms.textarea_field(form.variant_b)}} 15 | 22 |
    23 |
    24 | # if post_db.key 25 |
    26 |
    27 | 28 |
    29 |
    30 | # endif 31 |
    32 | # endblock 33 | -------------------------------------------------------------------------------- /main/templates/post/post_list.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block head 5 | {{utils.prefetch_link(next_url)}} 6 | # endblock 7 | 8 | # block content 9 | 15 |
    16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | # for post_db in post_dbs 27 | 28 | 29 | 30 | 31 | 32 | 33 | # endfor 34 | 35 |
    {{utils.order_by_link('title', 'Title')}}{{utils.order_by_link('language_key', 'Language')}}{{utils.order_by_link('variant_a', 'Variant A')}}{{utils.order_by_link('variant_b', 'Variant B')}}
    36 |
    37 | 38 | {{utils.next_link(next_url)}} 39 | # endblock 40 | -------------------------------------------------------------------------------- /main/templates/post/post_update.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 | 17 |
    18 | {{form.csrf_token}} 19 |
    20 |
    21 | {{forms.text_field(form.title, autofocus=True)}} 22 |
    23 |
    24 | {{forms.select_field(form.language_key)}} 25 |

    26 | Missing a language here? 27 | 28 | Submit it. 29 | 30 |

    31 |
    32 |
    33 |
    34 |
    35 |
    36 | {{forms.textarea_field(form.variant_a, rows=16)}} 37 |
    38 |
    39 | {{forms.textarea_field(form.variant_b, rows=16)}} 40 |
    41 |
    42 | 49 |
    50 | # endblock 51 | -------------------------------------------------------------------------------- /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/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_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/vote/admin_vote_list.html: -------------------------------------------------------------------------------- 1 | # extends 'admin/admin_base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block head 5 | {{utils.prefetch_link(next_url)}} 6 | # endblock 7 | 8 | # block admin_content 9 |
    10 | 15 |
    16 | 17 |
    18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | # for vote_db in vote_dbs 29 | 30 | 31 | 32 | 33 | 41 | 42 | # endfor 43 | 44 |
    {{utils.order_by_link('user_key', 'User')}}{{utils.order_by_link('post_key', 'Post')}}{{utils.order_by_link('variant', 'Variant')}}{{utils.order_by_link('modified', 'Modified')}}
    {{utils.order_by_link('created', 'Created')}}
    45 |
    46 | 47 | {{utils.next_link(next_url)}} 48 | # endblock 49 | -------------------------------------------------------------------------------- /main/templates/vote/admin_vote_update.html: -------------------------------------------------------------------------------- 1 | # extends 'admin/admin_base.html' 2 | # import 'macro/forms.html' as forms 3 | # import 'macro/utils.html' as utils 4 | 5 | 6 | # block admin_content 7 |
    8 |
    9 |
    10 | {{form.csrf_token}} 11 | {{forms.select_field(form.user_key)}} 12 | {{forms.select_field(form.post_key)}} 13 | {{forms.select_field(form.variant)}} 14 | 21 |
    22 |
    23 | # if vote_db.key 24 |
    25 |
    26 | 27 |
    28 |
    29 | # endif 30 |
    31 | # endblock 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Panayiotis Lipiridis ", 3 | "devDependencies": { 4 | "bower": "1.8.8", 5 | "browser-sync": "2.18.8", 6 | "coffee-script": "1.12.5", 7 | "del": "2.2.2", 8 | "gulp": "3.9.1", 9 | "gulp-autoprefixer": "3.1.1", 10 | "gulp-bower": "0.0.13", 11 | "gulp-coffee": "2.3.4", 12 | "gulp-concat": "2.6.1", 13 | "gulp-cssnano": "2.1.2", 14 | "gulp-help": "1.6.1", 15 | "gulp-if": "2.0.2", 16 | "gulp-less": "3.3.0", 17 | "gulp-load-plugins": "1.5.0", 18 | "gulp-plumber": "1.1.0", 19 | "gulp-sequence": "0.4.6", 20 | "gulp-size": "2.1.0", 21 | "gulp-sourcemaps": "2.6.0", 22 | "gulp-start": "1.0.1", 23 | "gulp-uglify": "2.1.2", 24 | "gulp-util": "3.0.8", 25 | "gulp-watch": "4.3.11", 26 | "gulp-zip": "4.0.0", 27 | "less": "2.7.2", 28 | "main-bower-files": "2.13.1", 29 | "require-dir": "0.3.1", 30 | "uglify-js": "2.8.22", 31 | "yargs-parser": "7.0.0" 32 | }, 33 | "license": "MIT", 34 | "name": "gae-init", 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/gae-init" 38 | }, 39 | "scripts": { 40 | "install": "gulp init", 41 | "postinstall": "echo 'Run `gulp` to start or `gulp help` for more.'", 42 | "start": "gulp" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | blinker==1.4 2 | flask-login==0.4.0 3 | flask-oauthlib==0.9.3 4 | flask-restful==0.3.5 5 | flask-wtf==0.14.2 6 | flask==1.0 7 | jinja2-markdown 8 | markdown==2.6.5 9 | pyjwt==1.4.2 10 | unidecode==0.4.20 11 | webargs==1.6.0 12 | --------------------------------------------------------------------------------