├── main ├── api │ ├── __init__.py │ ├── v1 │ │ ├── __init__.py │ │ ├── config.py │ │ ├── auth.py │ │ ├── repo.py │ │ ├── account.py │ │ └── user.py │ ├── fields.py │ └── helpers.py ├── static │ ├── img │ │ └── favicon.ico │ ├── src │ │ ├── script │ │ │ ├── site │ │ │ │ ├── admin.coffee │ │ │ │ ├── gh.coffee │ │ │ │ ├── app.coffee │ │ │ │ ├── auth.coffee │ │ │ │ └── user.coffee │ │ │ └── common │ │ │ │ ├── api.coffee │ │ │ │ └── util.coffee │ │ └── style │ │ │ ├── variables.less │ │ │ ├── user.less │ │ │ ├── mixins.less │ │ │ ├── style.less │ │ │ ├── footer.less │ │ │ ├── base.less │ │ │ ├── gh.less │ │ │ ├── test.less │ │ │ └── signin.less │ └── robots.txt ├── queue.yaml ├── model │ ├── __init__.py │ ├── repo.py │ ├── base.py │ ├── account.py │ ├── user.py │ ├── config.py │ └── config_auth.py ├── templates │ ├── admin │ │ ├── test │ │ │ ├── test_one.html │ │ │ ├── test_badge.html │ │ │ ├── test_label.html │ │ │ ├── test_pagination.html │ │ │ ├── test_button.html │ │ │ ├── test_heading.html │ │ │ ├── test_social.html │ │ │ ├── test_font.html │ │ │ ├── test_filter.html │ │ │ ├── test_paragraph.html │ │ │ ├── test.html │ │ │ ├── test_responsive.html │ │ │ ├── test_alert.html │ │ │ ├── test_grid.html │ │ │ ├── test_table.html │ │ │ └── test_form.html │ │ ├── bit │ │ │ ├── github.html │ │ │ ├── yahoo_oauth.html │ │ │ ├── google_analytics_tracking_id.html │ │ │ ├── vk_oauth.html │ │ │ ├── bitbucket_oauth.html │ │ │ ├── recaptcha.html │ │ │ ├── letsencrypt.html │ │ │ ├── reddit_oauth.html │ │ │ ├── facebook_oauth.html │ │ │ ├── twitter_oauth.html │ │ │ ├── dropbox_oauth.html │ │ │ ├── github_oauth.html │ │ │ ├── linkedin_oauth.html │ │ │ ├── instagram_oauth.html │ │ │ ├── microsoft_oauth.html │ │ │ ├── security.html │ │ │ └── google_oauth.html │ │ ├── admin_base.html │ │ ├── admin_auth.html │ │ ├── admin_config.html │ │ └── admin.html │ ├── bit │ │ ├── contact_menu.html │ │ ├── style.html │ │ ├── meta.html │ │ ├── notifications.html │ │ ├── announcement.html │ │ ├── script.html │ │ ├── limit_bar.html │ │ ├── analytics.html │ │ ├── footer.html │ │ ├── user_menu.html │ │ └── header.html │ ├── account │ │ ├── list_repo.html │ │ ├── list_person.html │ │ ├── list_organization.html │ │ ├── list_organization_bit.html │ │ ├── item_account_bit.html │ │ ├── list_person_bit.html │ │ ├── list_new.html │ │ ├── list_repo_bit.html │ │ ├── admin_account_list.html │ │ └── view.html │ ├── error.html │ ├── profile │ │ ├── profile_update.html │ │ ├── profile_password.html │ │ ├── profile.html │ │ └── profile_base.html │ ├── auth │ │ ├── signup_form.html │ │ ├── signin_form.html │ │ └── auth.html │ ├── user │ │ ├── user_email_field.html │ │ ├── user_activate.html │ │ ├── user_forgot.html │ │ ├── user_reset.html │ │ ├── user_update.html │ │ └── user_merge.html │ ├── base.html │ ├── error_static.html │ ├── feedback.html │ ├── welcome.html │ ├── sitemap.xml │ ├── repo │ │ └── admin_repo_list.html │ └── macro │ │ ├── utils.html │ │ └── forms.html ├── __init__.py ├── control │ ├── __init__.py │ ├── letsencrypt.py │ ├── repo.py │ ├── error.py │ ├── feedback.py │ ├── test.py │ ├── account.py │ ├── gh.py │ ├── profile.py │ └── welcome.py ├── auth │ ├── __init__.py │ ├── gae.py │ ├── dropbox.py │ ├── twitter.py │ ├── vk.py │ ├── facebook.py │ ├── instagram.py │ ├── github.py │ ├── bitbucket.py │ ├── microsoft.py │ ├── linkedin.py │ ├── google.py │ ├── yahoo.py │ └── reddit.py ├── appengine_config.py ├── cron.yaml ├── main.py ├── cache.py ├── app.yaml └── config.py ├── .bowerrc ├── gulpfile.js ├── gulp ├── util.coffee ├── config.coffee ├── tasks │ ├── ext.coffee │ ├── script.coffee │ ├── style.coffee │ ├── watch.coffee │ ├── clean.coffee │ ├── dep.coffee │ └── build.coffee └── paths.coffee ├── requirements.txt ├── .editorconfig ├── .gitignore ├── .hgignore ├── gulpfile.coffee ├── bower.json ├── package.json ├── LICENSE └── README.md /main/api/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | require('coffee-script/register'); 2 | require('./gulpfile.coffee'); 3 | -------------------------------------------------------------------------------- /main/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chubin/github-stats/HEAD/main/static/img/favicon.ico -------------------------------------------------------------------------------- /main/queue.yaml: -------------------------------------------------------------------------------- 1 | queue: 2 | - name: default 3 | rate: 30/s 4 | bucket_size: 60 5 | max_concurrent_requests: 20 6 | -------------------------------------------------------------------------------- /main/static/src/script/site/admin.coffee: -------------------------------------------------------------------------------- 1 | window.init_admin_config = -> 2 | # do something with the admin page here 3 | -------------------------------------------------------------------------------- /main/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /signout/ 3 | Disallow: /signin/*/ 4 | Disallow: /api/ 5 | Disallow: /_ah/ 6 | -------------------------------------------------------------------------------- /main/api/v1/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from .auth import * 4 | from .config import * 5 | from .user import * 6 | from .account import * 7 | from .repo import * 8 | -------------------------------------------------------------------------------- /main/static/src/script/site/gh.coffee: -------------------------------------------------------------------------------- 1 | window.init_gh_view = -> 2 | if $('#status').data('status') is 'syncing' 3 | setTimeout -> 4 | location.reload() 5 | , 8000 6 | -------------------------------------------------------------------------------- /gulp/util.coffee: -------------------------------------------------------------------------------- 1 | $ = do require 'gulp-load-plugins' 2 | 3 | onError = (err) -> 4 | do $.util.beep 5 | console.log err 6 | this.emit 'end' 7 | 8 | module.exports = {onError} 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | blinker==1.4 2 | flask-login==0.3.2 3 | flask-oauthlib==0.9.2 4 | flask-restful==0.3.5 5 | flask-wtf==0.12 6 | flask==0.10.1 7 | pygithub==1.26.0 8 | unidecode==0.4.19 9 | webargs==1.2.0 10 | -------------------------------------------------------------------------------- /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 .account import Account 8 | from .repo import Repo 9 | -------------------------------------------------------------------------------- /.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 | 2 | 3 | *.pyc 4 | *.pyo 5 | .DS_Store 6 | .git* 7 | .hg* 8 | bower_components 9 | main/index.yaml 10 | main/lib 11 | main/lib.zip 12 | main/static/dev 13 | main/static/ext 14 | main/static/min 15 | node_modules 16 | temp/ 17 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_one.html: -------------------------------------------------------------------------------- 1 | # extends 'admin/admin_base.html' 2 | 3 | 4 | # block admin_content 5 |
6 | # include 'test/test_%s.html' % test 7 |
8 | # endblock 9 | -------------------------------------------------------------------------------- /main/templates/admin/bit/github.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'GitHub', 4 | (form.github_username, form.github_password), 5 | ''' 6 | This is not OK.. and should be changed :) 7 | ''' 8 | ) 9 | }} 10 | -------------------------------------------------------------------------------- /main/templates/bit/contact_menu.html: -------------------------------------------------------------------------------- 1 | # if config.CONFIG_DB.feedback_email 2 |
  • 3 | Feedback 4 |
  • 5 | # endif 6 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | *.pyc 4 | *.pyo 5 | .DS_Store 6 | .git* 7 | .hg* 8 | bower_components 9 | main/index.yaml 10 | main/lib 11 | main/lib.zip 12 | main/static/dev 13 | main/static/ext 14 | main/static/min 15 | node_modules 16 | temp/ 17 | -------------------------------------------------------------------------------- /main/templates/bit/style.html: -------------------------------------------------------------------------------- 1 | # if config.DEVELOPMENT 2 | 3 | # else 4 | 5 | # endif 6 | -------------------------------------------------------------------------------- /main/static/src/style/variables.less: -------------------------------------------------------------------------------- 1 | @fa-font-path: "/p/ext/font-awesome/fonts"; 2 | @icon-font-path: "/p/ext/bootstrap/fonts/"; 3 | @octicons-font-path: "/p/ext/octicons/octicons"; 4 | 5 | 6 | @footer-height: @line-height-computed * 5; 7 | -------------------------------------------------------------------------------- /main/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | gae-init 4 | ~~~~~~~~ 5 | 6 | Google App Engine with Bootstrap, Flask and tons of other cool features. 7 | 8 | https://github.com/gae-init 9 | https://gae-init.appspot.com 10 | 11 | """ 12 | 13 | __version__ = '4.1.3' 14 | -------------------------------------------------------------------------------- /main/templates/bit/meta.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_badge.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /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/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/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/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/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 .gh import * 9 | from .letsencrypt import * 10 | from .profile import * 11 | from .test import * 12 | from .welcome import * 13 | from .account import * 14 | from .repo import * 15 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_label.html: -------------------------------------------------------------------------------- 1 | Default 2 | Primary 3 | Success 4 | Warning 5 | Danger 6 | Info 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/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 | -------------------------------------------------------------------------------- /gulpfile.coffee: -------------------------------------------------------------------------------- 1 | gulp = require('gulp-help') require 'gulp' 2 | requireDir = require('require-dir') './gulp/tasks' 3 | $ = do require 'gulp-load-plugins' 4 | 5 | gulp.task 'default', 6 | 'Start the local server, watch for changes and reload browser automatically. 7 | For available options refer to "run" task.', 8 | $.sequence 'run', ['watch', 'reload'] 9 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/account/list_repo.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block content 5 |
    6 |
    7 |

    {{title}} (top {{limit}})

    8 | # include 'account/list_repo_bit.html' 9 | # include 'bit/limit_bar.html' 10 |
    11 |
    12 | # endblock 13 | -------------------------------------------------------------------------------- /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/auth/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from .auth import * 4 | from .bitbucket import * 5 | from .dropbox import * 6 | from .facebook import * 7 | from .github import * 8 | from .gae import * 9 | from .google import * 10 | from .instagram import * 11 | from .linkedin import * 12 | from .microsoft import * 13 | from .reddit import * 14 | from .twitter import * 15 | from .vk import * 16 | from .yahoo import * 17 | -------------------------------------------------------------------------------- /main/templates/account/list_person.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block content 5 |
    6 |
    7 |

    {{title}} (top {{limit}})

    8 | # include 'account/list_person_bit.html' 9 | # include 'bit/limit_bar.html' 10 |
    11 |
    12 | # endblock 13 | -------------------------------------------------------------------------------- /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/account/list_organization.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block content 5 |
    6 |
    7 |

    {{title}} (top {{limit}})

    8 | # include 'account/list_organization_bit.html' 9 | # include 'bit/limit_bar.html' 10 |
    11 |
    12 | # endblock 13 | -------------------------------------------------------------------------------- /main/templates/admin/bit/security.html: -------------------------------------------------------------------------------- 1 | {{ 2 | forms.panel_fields( 3 | 'Security', 4 | (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/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/api/v1/config.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from flask.ext import 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('/config/', endpoint='api.config') 16 | class ConfigAPI(restful.Resource): 17 | @auth.admin_required 18 | def get(self): 19 | return helpers.make_response(config.CONFIG_DB, model.Config.FIELDS) 20 | -------------------------------------------------------------------------------- /main/static/src/style/style.less: -------------------------------------------------------------------------------- 1 | @import "../../ext/font-awesome/less/font-awesome"; 2 | @import "../../ext/bootstrap/less/bootstrap"; 3 | @import "../../ext/octicons/octicons/octicons"; 4 | 5 | @import "../../ext/bootswatch/cerulean/variables"; 6 | @import "../../ext/bootswatch/cerulean/bootswatch"; 7 | 8 | @import "base"; 9 | @import "variables"; 10 | @import "test"; 11 | @import "mixins"; 12 | @import "footer"; 13 | @import "signin"; 14 | @import "user"; 15 | @import "gh"; 16 | -------------------------------------------------------------------------------- /main/templates/admin/test/test_button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /main/templates/bit/script.html: -------------------------------------------------------------------------------- 1 | # if config.DEVELOPMENT 2 | 3 | 4 | 5 | # else 6 | 7 | 8 | # endif 9 | -------------------------------------------------------------------------------- /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/static/src/script/site/app.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | init_common() 3 | 4 | $ -> $('html.welcome').each -> 5 | LOG('init welcome') 6 | 7 | $ -> $('html.auth').each -> 8 | init_auth() 9 | 10 | $ -> $('html.feedback').each -> 11 | 12 | $ -> $('html.user-list').each -> 13 | init_user_list() 14 | 15 | $ -> $('html.user-merge').each -> 16 | init_user_merge() 17 | 18 | $ -> $('html.admin-config').each -> 19 | init_admin_config() 20 | 21 | $ -> $('html.gh-view').each -> 22 | init_gh_view() 23 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gae-init", 3 | "dependencies": { 4 | "bootstrap": "~3", 5 | "bootswatch": "~3", 6 | "font-awesome": "~4", 7 | "octicons": "~3", 8 | "jquery": "~2", 9 | "moment": "~2" 10 | }, 11 | "overrides": { 12 | "bootstrap": { 13 | "main": ["less/**", "fonts/*", "js/*"] 14 | }, 15 | "bootswatch": { 16 | "main": ["**/*.less"] 17 | }, 18 | "font-awesome": { 19 | "main": ["less/*", "fonts/*"] 20 | }, 21 | "octicons": { 22 | "main": ["octicons/*"] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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/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/bit/limit_bar.html: -------------------------------------------------------------------------------- 1 |
    2 | Top 64 3 | Top 256 4 | Top 512 5 | Top 1024 6 |
    7 | -------------------------------------------------------------------------------- /main/templates/bit/analytics.html: -------------------------------------------------------------------------------- 1 | # if not current_user.admin and config.CONFIG_DB.analytics_id 2 | 10 | # endif 11 | -------------------------------------------------------------------------------- /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/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/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 | {{forms.text_field(form.github)}} 10 | 11 |
    12 | 15 |
    16 |
    17 | # endblock 18 | -------------------------------------------------------------------------------- /gulp/config.coffee: -------------------------------------------------------------------------------- 1 | paths = require './paths' 2 | 3 | config = 4 | ext: [ 5 | "#{paths.static.ext}/jquery/dist/jquery.js" 6 | "#{paths.static.ext}/moment/moment.js" 7 | "#{paths.static.ext}/bootstrap/js/alert.js" 8 | "#{paths.static.ext}/bootstrap/js/button.js" 9 | "#{paths.static.ext}/bootstrap/js/transition.js" 10 | "#{paths.static.ext}/bootstrap/js/collapse.js" 11 | "#{paths.static.ext}/bootstrap/js/dropdown.js" 12 | "#{paths.static.ext}/bootstrap/js/tooltip.js" 13 | ] 14 | style: [ 15 | "#{paths.src.style}/style.less" 16 | ] 17 | script: [ 18 | "#{paths.src.script}/**/*.coffee" 19 | ] 20 | 21 | module.exports = config 22 | -------------------------------------------------------------------------------- /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 | import re 10 | from google.appengine.tools.devappserver2.python import stubs 11 | 12 | re_ = stubs.FakeFile._skip_files.pattern.replace('|^lib/.*', '') 13 | re_ = re.compile(re_) 14 | stubs.FakeFile._skip_files = re_ 15 | sys.path.insert(0, 'lib') 16 | sys.path.insert(0, 'libx') 17 | 18 | 19 | def webapp_add_wsgi_middleware(app): 20 | from google.appengine.ext.appstats import recording 21 | app = recording.appstats_wsgi_middleware(app) 22 | return app 23 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /gulp/tasks/ext.coffee: -------------------------------------------------------------------------------- 1 | gulp = require('gulp-help') require 'gulp' 2 | $ = do 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 do $.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 do $.sourcemaps.init 21 | .pipe $.concat 'ext.js' 22 | .pipe do $.sourcemaps.write 23 | .pipe gulp.dest "#{paths.static.dev}/script" 24 | -------------------------------------------------------------------------------- /gulp/tasks/script.coffee: -------------------------------------------------------------------------------- 1 | gulp = require('gulp-help') require 'gulp' 2 | $ = do require 'gulp-load-plugins' 3 | config = require '../config' 4 | paths = require '../paths' 5 | util = require '../util' 6 | 7 | 8 | gulp.task 'script', false, -> 9 | gulp.src config.script 10 | .pipe $.plumber errorHandler: util.onError 11 | .pipe $.coffee() 12 | .pipe $.concat 'script.js' 13 | .pipe do $.uglify 14 | .pipe $.size {title: 'Minified scripts'} 15 | .pipe gulp.dest "#{paths.static.min}/script" 16 | 17 | 18 | gulp.task 'script:dev', false, -> 19 | gulp.src config.script 20 | .pipe $.plumber errorHandler: util.onError 21 | .pipe do $.sourcemaps.init 22 | .pipe $.coffee() 23 | .pipe $.concat 'script.js' 24 | .pipe do $.sourcemaps.write 25 | .pipe gulp.dest "#{paths.static.dev}/script" 26 | -------------------------------------------------------------------------------- /gulp/tasks/style.coffee: -------------------------------------------------------------------------------- 1 | gulp = require('gulp-help') require 'gulp' 2 | $ = do 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 do $.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 do $.sourcemaps.init 23 | .pipe do $.less 24 | .pipe $.autoprefixer {map: true} 25 | .pipe do $.sourcemaps.write 26 | .pipe gulp.dest "#{paths.static.dev}/style" 27 | -------------------------------------------------------------------------------- /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/cron.yaml: -------------------------------------------------------------------------------- 1 | cron: 2 | - description: status=synced 3 | url: /admin/cron/sync/?status=synced&limit=32 4 | schedule: every 10 minutes 5 | 6 | - description: status=error 7 | url: /admin/cron/sync/?status=error&limit=32 8 | schedule: every 4 hours 9 | 10 | - description: status=syncing 11 | url: /admin/cron/sync/?status=syncing&limit=32 12 | schedule: every 4 hours 13 | 14 | - description: status=new 15 | url: /admin/cron/sync/?status=new&limit=32 16 | schedule: every 2 hours 17 | 18 | - description: fetch > 2000 stars 19 | url: /admin/cron/repo/?stars=2000&page=1 20 | schedule: every 4 hours 21 | 22 | - description: fetch > 2000 stars 23 | url: /admin/cron/repo/?stars=2000&page=2 24 | schedule: every 4 hours 25 | 26 | - description: cleanup 27 | url: /admin/cron/repo/cleanup/?days=4 28 | schedule: every day 01:00 29 | -------------------------------------------------------------------------------- /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_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/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # include 'bit/meta.html' 5 | 6 | # block title 7 | {{title + ' |' if title}} 8 | # endblock 9 | {{config.CONFIG_DB.brand_name}} 10 | 11 | # include 'bit/style.html' 12 | # block head 13 | # endblock 14 | # include 'bit/analytics.html' 15 | 16 | 17 | 18 | # include 'bit/header.html' 19 | # include 'bit/announcement.html' 20 | # block header 21 | # endblock 22 |
    23 | # include 'bit/notifications.html' 24 | # block content 25 | # endblock 26 |
    27 | # include 'bit/footer.html' 28 | # include 'bit/script.html' 29 | # block scripts 30 | # endblock 31 | 32 | 33 | -------------------------------------------------------------------------------- /main/templates/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/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, 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 | -------------------------------------------------------------------------------- /gulp/tasks/watch.coffee: -------------------------------------------------------------------------------- 1 | gulp = require('gulp-help') require 'gulp' 2 | $ = do require 'gulp-load-plugins' 3 | paths = require '../paths' 4 | 5 | 6 | gulp.task 'reload', false, -> 7 | do $.livereload.listen 8 | gulp.watch([ 9 | "#{paths.static.dev}/**/*.{css,js}" 10 | "#{paths.main}/**/*.{html,py}" 11 | ]).on 'change', $.livereload.changed 12 | 13 | 14 | gulp.task 'ext_watch_rebuild', false, (callback) -> 15 | $.sequence('clean:dev', 'copy_bower_files', 'ext:dev', 'style:dev') callback 16 | 17 | 18 | gulp.task 'watch', false, -> 19 | gulp.watch 'requirements.txt', ['pip'] 20 | gulp.watch 'package.json', ['npm'] 21 | gulp.watch 'bower.json', ['ext_watch_rebuild'] 22 | gulp.watch 'gulp/config.coffee', ['ext:dev', 'style:dev', 'script:dev'] 23 | gulp.watch paths.static.ext, ['ext:dev'] 24 | gulp.watch "#{paths.src.script}/**/*.coffee", ['script:dev'] 25 | gulp.watch "#{paths.src.style}/**/*.less", ['style:dev'] 26 | -------------------------------------------------------------------------------- /main/control/repo.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from flask.ext import wtf 4 | from google.appengine.ext import ndb 5 | import flask 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/repo/') 20 | @auth.admin_required 21 | def admin_repo_list(): 22 | repo_dbs, repo_cursor = model.Repo.get_dbs( 23 | order=util.param('order') or '-modified', 24 | ) 25 | return flask.render_template( 26 | 'repo/admin_repo_list.html', 27 | html_class='admin-repo-list', 28 | title='Repo List', 29 | repo_dbs=repo_dbs, 30 | next_url=util.generate_next_url(repo_cursor), 31 | api_url=flask.url_for('api.admin.repo.list'), 32 | ) 33 | -------------------------------------------------------------------------------- /gulp/paths.coffee: -------------------------------------------------------------------------------- 1 | paths = 2 | main: 'main' 3 | 4 | dep: {} 5 | py: {} 6 | static: {} 7 | src: {} 8 | temp: {} 9 | 10 | 11 | paths.temp.root = 'temp' 12 | paths.temp.storage = "#{paths.temp.root}/storage" 13 | paths.temp.venv = "#{paths.temp.root}/venv" 14 | 15 | paths.dep.bower_components = 'bower_components' 16 | paths.dep.node_modules = 'node_modules' 17 | paths.dep.py = "#{paths.temp.root}/venv" 18 | paths.dep.py_guard = "#{paths.temp.root}/pip.guard" 19 | 20 | paths.py.lib = "#{paths.main}/lib" 21 | paths.py.lib_file = "#{paths.py.lib}.zip" 22 | 23 | paths.static.root = "#{paths.main}/static" 24 | paths.static.dev = "#{paths.static.root}/dev" 25 | paths.static.ext = "#{paths.static.root}/ext" 26 | paths.static.min = "#{paths.static.root}/min" 27 | 28 | paths.src.root = "#{paths.static.root}/src" 29 | paths.src.script = "#{paths.src.root}/script" 30 | paths.src.style = "#{paths.src.root}/style" 31 | 32 | 33 | module.exports = paths 34 | -------------------------------------------------------------------------------- /main/templates/bit/footer.html: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /main/main.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import flask 4 | 5 | import config 6 | import util 7 | 8 | app = flask.Flask(__name__) 9 | app.config.from_object(config) 10 | app.jinja_env.line_statement_prefix = '#' 11 | app.jinja_env.line_comment_prefix = '##' 12 | app.jinja_env.globals.update( 13 | check_form_fields=util.check_form_fields, 14 | is_iterable=util.is_iterable, 15 | slugify=util.slugify, 16 | update_query_argument=util.update_query_argument, 17 | ) 18 | 19 | import auth 20 | import control 21 | import model 22 | import task 23 | 24 | from api import helpers 25 | 26 | api_v1 = helpers.Api(app, prefix='/api/v1') 27 | 28 | import api.v1 29 | 30 | if config.DEVELOPMENT: 31 | from werkzeug import debug 32 | try: 33 | app.wsgi_app = debug.DebuggedApplication( 34 | app.wsgi_app, evalex=True, pin_security=False, 35 | ) 36 | except TypeError: 37 | app.wsgi_app = debug.DebuggedApplication(app.wsgi_app, evalex=True) 38 | app.testing = False 39 | -------------------------------------------------------------------------------- /main/templates/feedback.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/forms.html' as forms 3 | 4 | # block content 5 | 13 | 14 |
    15 |
    16 |
    17 | {{form.csrf_token}} 18 | 19 | {{forms.textarea_field(form.message, rows=8, autofocus=True)}} 20 | {{forms.email_field(form.email)}} 21 | {{forms.recaptcha_field(form.recaptcha)}} 22 | 23 | 26 |
    27 |
    28 |
    29 | # endblock 30 | -------------------------------------------------------------------------------- /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/bit/user_menu.html: -------------------------------------------------------------------------------- 1 | # if current_user.id > 0 2 | 18 | # else 19 |
  • 20 | Sign in with GitHub 21 |
  • 22 | # endif 23 | -------------------------------------------------------------------------------- /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.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 |

    {{user_db.github}}

    16 |
    17 |

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

    21 | # endblock 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /main/templates/account/list_organization_bit.html: -------------------------------------------------------------------------------- 1 | 2 | # if order 3 | 4 | 5 | 6 | 7 | 14 | 21 | 22 | 23 | 24 | # endif 25 | 26 | # for account_db in organization_dbs 27 | # set index = loop.index 28 | # include 'account/item_account_bit.html' 29 | # endfor 30 | 31 |
    RankName 8 | # if 'star' in order 9 | Stars 10 | # else 11 | Stars 12 | # endif 13 |
    32 | -------------------------------------------------------------------------------- /main/static/src/style/gh.less: -------------------------------------------------------------------------------- 1 | .text-muted { 2 | color: fade(@gray-light, 48%); 3 | } 4 | 5 | 6 | .gh-list { 7 | .avatar { 8 | width: 18px; 9 | height: 18px; 10 | border-radius: 2px; 11 | margin-right: 4px; 12 | } 13 | 14 | a { 15 | display: block; 16 | } 17 | } 18 | 19 | .gh-view { 20 | h3 { 21 | margin-top: 4px; 22 | } 23 | .avatar { 24 | border-radius: 6px; 25 | width: 100%; 26 | max-width: 320px; 27 | } 28 | } 29 | 30 | 31 | .lead .octicon { 32 | } 33 | 34 | .octicon { 35 | h1 &, .h1 & {font-size: @font-size-h1;} 36 | h2 &, .h2 & {font-size: @font-size-h2;} 37 | h3 &, .h3 & {font-size: @font-size-h3;} 38 | h4 &, .h4 & {font-size: @font-size-h4;} 39 | h5 &, .h5 & {font-size: @font-size-h5;} 40 | h6 &, .h6 & {font-size: @font-size-h6;} 41 | 42 | .lead & { 43 | font-size: floor((@font-size-base * 1.15)); 44 | font-weight: 300; 45 | line-height: 1.4; 46 | 47 | @media (min-width: @screen-sm-min) { 48 | font-size: (@font-size-base * 1.5); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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/welcome.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block content 5 |
    6 |
    7 |

    People

    8 | # include 'account/list_person_bit.html' 9 | 12 |
    13 |
    14 |

    Organizations

    15 | # include 'account/list_organization_bit.html' 16 | 19 |
    20 |
    21 |

    Repositories

    22 | # include 'account/list_repo_bit.html' 23 | 26 |
    27 |
    28 | # endblock 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gae-init", 3 | "author": "Panayiotis Lipiridis ", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/gae-init" 8 | }, 9 | "scripts": { 10 | "install": "gulp init" 11 | }, 12 | "dependencies": { 13 | "coffee-script": "~1", 14 | "less": "~2", 15 | "uglify-js": "~2" 16 | }, 17 | "devDependencies": { 18 | "bower": "~1", 19 | "del": "~2", 20 | "gulp": "~3", 21 | "gulp-autoprefixer": "~3", 22 | "gulp-bower": "~0", 23 | "gulp-coffee": "~2", 24 | "gulp-concat": "~2", 25 | "gulp-cssnano": "~2", 26 | "gulp-help": "~1", 27 | "gulp-less": "~3", 28 | "gulp-livereload": "~3", 29 | "gulp-load-plugins": "~1", 30 | "gulp-plumber": "~1", 31 | "gulp-sequence": "~0", 32 | "gulp-size": "~2", 33 | "gulp-sourcemaps": "~1", 34 | "gulp-start": "~1", 35 | "gulp-uglify": "~1", 36 | "gulp-util": "~3", 37 | "gulp-zip": "~3", 38 | "main-bower-files": "~2", 39 | "yargs-parser": "~2", 40 | "require-dir": "~0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /main/templates/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{url_for('welcome', _external=True)}} 6 | {{lastmod}} 7 | daily 8 | 0.8 9 | 10 | 11 | {{url_for('feedback', _external=True)}} 12 | {{lastmod}} 13 | weekly 14 | 0.8 15 | 16 | 17 | {{url_for('person', _external=True)}} 18 | {{lastmod}} 19 | daily 20 | 0.8 21 | 22 | 23 | {{url_for('organization', _external=True)}} 24 | {{lastmod}} 25 | daily 26 | 0.8 27 | 28 | 29 | {{url_for('repo', _external=True)}} 30 | {{lastmod}} 31 | daily 32 | 0.8 33 | 34 | 35 | -------------------------------------------------------------------------------- /main/api/fields.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import urllib 4 | 5 | from flask.ext.restful import fields 6 | from flask.ext.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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /main/api/v1/auth.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from flask.ext import restful 6 | from webargs.flaskparser import parser 7 | from webargs import fields as wf 8 | import flask 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(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/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 | logging.exception(e) 24 | try: 25 | e.code 26 | except AttributeError: 27 | e.code = 500 28 | e.name = 'Internal Server Error' 29 | 30 | if flask.request.path.startswith('/api/'): 31 | return helpers.handle_error(e) 32 | 33 | return flask.render_template( 34 | 'error.html', 35 | title='Error %d (%s)!!1' % (e.code, e.name), 36 | html_class='error-page', 37 | error=e, 38 | ), e.code 39 | 40 | 41 | if config.PRODUCTION: 42 | @app.errorhandler(Exception) 43 | def production_error_handler(e): 44 | return error_handler(e) 45 | -------------------------------------------------------------------------------- /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_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/bitbucket_oauth.html' 11 | # include 'admin/bit/dropbox_oauth.html' 12 | # include 'admin/bit/facebook_oauth.html' 13 | # include 'admin/bit/github_oauth.html' 14 | # include 'admin/bit/google_oauth.html' 15 | # include 'admin/bit/instagram_oauth.html' 16 | # include 'admin/bit/linkedin_oauth.html' 17 | # include 'admin/bit/microsoft_oauth.html' 18 | # include 'admin/bit/reddit_oauth.html' 19 | # include 'admin/bit/twitter_oauth.html' 20 | # include 'admin/bit/vk_oauth.html' 21 | # include 'admin/bit/yahoo_oauth.html' 22 |
    23 | 26 |
    27 |
    28 |
    29 | # endblock 30 | -------------------------------------------------------------------------------- /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/templates/account/item_account_bit.html: -------------------------------------------------------------------------------- 1 | 2 | {{index}}. 3 | 4 | 5 | {{account_db.name}} {{account_db.username}} 6 | 7 | {{account_db.stars_hu}} 8 | # if order 9 | {{account_db.public_repos_hu}} 10 | # if not account_db.organization 11 | {{account_db.followers_hu}} 12 | # endif 13 | {{account_db.language or ''}} 14 | # endif 15 | 16 | -------------------------------------------------------------------------------- /main/templates/account/list_person_bit.html: -------------------------------------------------------------------------------- 1 | 2 | # if order 3 | 4 | 5 | 6 | 7 | 14 | 21 | 28 | 29 | 30 | 31 | # endif 32 | 33 | # for account_db in person_dbs 34 | # set index = loop.index 35 | # include 'account/item_account_bit.html' 36 | # endfor 37 | 38 |
    RankName 8 | # if 'star' in order 9 | Stars 10 | # else 11 | Stars 12 | # endif 13 |
    39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /gulp/tasks/dep.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | gulp = require('gulp-help') require 'gulp' 3 | main_bower_files = require 'main-bower-files' 4 | $ = do 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 do $.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 do 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 | -------------------------------------------------------------------------------- /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/model/repo.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from google.appengine.ext import ndb 6 | 7 | from api import fields 8 | import model 9 | 10 | 11 | class Repo(model.Base): 12 | account_username = ndb.StringProperty(required=True) 13 | avatar_url = ndb.StringProperty(required=True, verbose_name=u'Avatar URL', indexed=False) 14 | description = ndb.StringProperty(default='', indexed=False) 15 | fork = ndb.BooleanProperty(default=False) 16 | forks = ndb.IntegerProperty(default=0) 17 | language = ndb.StringProperty(default='') 18 | name = ndb.StringProperty(required=True) 19 | stars = ndb.IntegerProperty(default=0) 20 | 21 | @ndb.ComputedProperty 22 | def stars_hu(self): 23 | return '{:,}'.format(self.stars) 24 | 25 | @ndb.ComputedProperty 26 | def forks_hu(self): 27 | return '{:,}'.format(self.forks) 28 | 29 | @ndb.ComputedProperty 30 | def url(self): 31 | return 'https://github.com/%s/%s' % (self.account_username, self.name) 32 | 33 | FIELDS = { 34 | 'account_username': fields.String, 35 | 'avatar_url': fields.String, 36 | 'description': fields.String, 37 | 'forks': fields.Integer, 38 | 'language': fields.String, 39 | 'name': fields.String, 40 | 'stars': fields.Integer, 41 | } 42 | 43 | FIELDS.update(model.Base.FIELDS) 44 | -------------------------------------------------------------------------------- /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_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/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/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 | 'key': fields.Key, 41 | 'id': fields.Id, 42 | 'version': fields.Integer, 43 | 'created': fields.DateTime, 44 | 'modified': fields.DateTime, 45 | } 46 | -------------------------------------------------------------------------------- /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/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 | 50 | skip_files: 51 | - ^(.*/)?#.*# 52 | - ^(.*/)?.*/RCS/.* 53 | - ^(.*/)?.*\.bak$ 54 | - ^(.*/)?.*\.py[co] 55 | - ^(.*/)?.*~ 56 | - ^(.*/)?Icon\r 57 | - ^(.*/)?\..* 58 | - ^(.*/)?app\.yaml 59 | - ^(.*/)?app\.yml 60 | - ^(.*/)?index\.yaml 61 | - ^(.*/)?index\.yml 62 | - ^lib/.* 63 | - ^static/dev/.* 64 | - ^static/ext/.*\.coffee 65 | - ^static/ext/.*\.css 66 | - ^static/ext/.*\.js 67 | - ^static/ext/.*\.less 68 | - ^static/ext/.*\.json 69 | - ^static/src/.* 70 | -------------------------------------------------------------------------------- /main/control/feedback.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from flask.ext import wtf 4 | import flask 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(wtf.Form): 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 = 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/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 | # include 'admin/bit/github.html' 26 |
    27 |
    28 |
    29 |
    30 |
    31 |
    32 | 35 |
    36 |
    37 |
    38 | # endblock 39 | -------------------------------------------------------------------------------- /main/templates/account/list_new.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/utils.html' as utils 3 | 4 | # block content 5 |
    6 |
    7 |

    {{title}}

    8 | 9 | 10 | # for account_db in person_dbs if account_db.stars > 2000 11 | # set index = loop.index 12 | 13 | 14 | 18 | 19 | 24 | 25 | # endfor 26 | 27 |
    28 |
    29 |
    30 | # endblock 31 | -------------------------------------------------------------------------------- /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 | 38 | DEFAULT_DB_LIMIT = 64 39 | MAX_DB_LIMIT = 256 40 | SIGNIN_RETRY_LIMIT = 4 41 | TAG_SEPARATOR = ' ' 42 | 43 | ILLEGAL_KEYS = ['__settings__'] 44 | ERRORS = ['failed', 'error', '404'] 45 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | Change on Gravatar 41 |

    42 | # endif 43 |
    44 | 45 |
    46 | # block profile_content 47 | # endblock 48 |
    49 |
    50 | # endblock 51 | -------------------------------------------------------------------------------- /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/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 | if user_db: 51 | user_db.github = response.get('login') 52 | user_db.put() 53 | return user_db or auth.create_user_db( 54 | auth_id=auth_id, 55 | name=response['name'] or response['login'], 56 | username=response['login'], 57 | email=response.get('email', ''), 58 | verified=bool(response.get('email', '')), 59 | github=response['login'], 60 | ) 61 | -------------------------------------------------------------------------------- /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/templates/account/list_repo_bit.html: -------------------------------------------------------------------------------- 1 | # import 'macro/utils.html' as utils 2 | 3 | 4 | # if order 5 | 6 | 7 | 8 | 9 | 16 | 23 | 24 | 25 | 26 | # endif 27 | 28 | # for repo_db in repo_dbs 29 | 30 | 31 | 35 | 36 | # if order 37 | 38 | 39 | # endif 40 | 41 | # endfor 42 | 43 |
    RankName 10 | # if 'star' in order 11 | Stars 12 | # else 13 | Stars 14 | # endif 15 |
    44 | -------------------------------------------------------------------------------- /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/api/helpers.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from datetime import datetime 4 | import logging 5 | 6 | from flask.ext import restful 7 | from werkzeug import exceptions 8 | import flask 9 | 10 | import util 11 | 12 | 13 | class Api(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: 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': 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/model/account.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from google.appengine.ext import ndb 6 | 7 | from api import fields 8 | import model 9 | 10 | 11 | class Account(model.Base): 12 | avatar_url = ndb.StringProperty(required=True, verbose_name=u'Avatar URL', indexed=False) 13 | email = ndb.StringProperty(default='') 14 | followers = ndb.IntegerProperty(default=0) 15 | forks = ndb.IntegerProperty(default=0) 16 | joined = ndb.DateTimeProperty() 17 | name = ndb.StringProperty(required=True) 18 | organization = ndb.BooleanProperty(default=False) 19 | public_repos = ndb.IntegerProperty(default=0) 20 | rank = ndb.IntegerProperty(default=0) 21 | stars = ndb.IntegerProperty(default=0) 22 | status = ndb.StringProperty(default='new', choices=['new', 'synced', 'syncing', 'error', 'failed', '404']) 23 | username = ndb.StringProperty(required=True) 24 | language = ndb.StringProperty() 25 | 26 | @ndb.ComputedProperty 27 | def stars_hu(self): 28 | return '{:,}'.format(self.stars) 29 | 30 | @ndb.ComputedProperty 31 | def forks_hu(self): 32 | return '{:,}'.format(self.forks) 33 | 34 | @ndb.ComputedProperty 35 | def followers_hu(self): 36 | return '{:,}'.format(self.followers) 37 | 38 | @ndb.ComputedProperty 39 | def public_repos_hu(self): 40 | return '{:,}'.format(self.public_repos) 41 | 42 | def get_repo_dbs(self, **kwargs): 43 | return model.Repo.get_dbs( 44 | ancestor=self.key, 45 | order='-stars', 46 | **kwargs 47 | ) 48 | 49 | FIELDS = { 50 | 'avatar_url': fields.String, 51 | 'email': fields.String, 52 | 'followers': fields.Integer, 53 | 'forks': fields.Integer, 54 | 'joined': fields.DateTime, 55 | 'name': fields.String, 56 | 'organization': fields.Boolean, 57 | 'public_repos': fields.Integer, 58 | 'rank': fields.Integer, 59 | 'stars': fields.Integer, 60 | 'status': fields.String, 61 | 'username': fields.String, 62 | } 63 | 64 | FIELDS.update(model.Base.FIELDS) 65 | -------------------------------------------------------------------------------- /gulp/tasks/build.coffee: -------------------------------------------------------------------------------- 1 | gulp = require('gulp-help') require 'gulp' 2 | yargs = require 'yargs-parser' 3 | $ = do require 'gulp-load-plugins' 4 | paths = require '../paths' 5 | 6 | 7 | gulp.task 'build', 8 | "Build project to prepare it for a deployment. Minify CSS & JS files and pack 9 | Python dependencies into #{paths.py.lib_file}.", 10 | $.sequence 'clean:min', 'init', 'ext', ['script', 'style', 'zip'] 11 | 12 | 13 | gulp.task 'rebuild', 14 | 'Re-build project from scratch. Equivalent to "reset" and "build" tasks.', 15 | $.sequence 'reset', 'build' 16 | 17 | 18 | gulp.task 'deploy', 'Deploy project to Google App Engine.', ['build'], -> 19 | options = yargs process.argv, configuration: 20 | 'boolean-negation': false 21 | 'camel-case-expansion': false 22 | delete options['_'] 23 | options_str = '' 24 | for k of options 25 | if options[k] == true 26 | options[k] = '' 27 | options_str += " #{if k.length > 1 then '-' else ''}-#{k} #{options[k]}" 28 | 29 | gulp.src('run.py').pipe $.start [{ 30 | match: /run.py$/ 31 | cmd: "gcloud preview app deploy main/*.yaml#{options_str}" 32 | }] 33 | 34 | 35 | gulp.task 'run', 36 | 'Start the local server. Available options:\n 37 | -o HOST - the host to start the dev_appserver.py\n 38 | -p PORT - the port to start the dev_appserver.py\n 39 | -a="..." - all following args are passed to dev_appserver.py\n', -> 40 | $.sequence('init', ['ext:dev', 'script:dev', 'style:dev']) -> 41 | argv = process.argv.slice 2 42 | 43 | known_options = 44 | default: 45 | p: '' 46 | o: '' 47 | a: '' 48 | 49 | options = yargs(argv) 50 | options_str = '-s' 51 | for k of known_options.default 52 | if options[k] 53 | if k == 'a' 54 | options_str += " --appserver-args \"#{options[k]}\"" 55 | else 56 | options_str += " -#{k} #{options[k]}" 57 | 58 | gulp.src('run.py').pipe $.start [{ 59 | match: /run.py$/ 60 | cmd: "python run.py #{options_str}" 61 | }] 62 | -------------------------------------------------------------------------------- /main/api/v1/repo.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from google.appengine.ext import ndb 6 | from flask.ext import restful 7 | import flask 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('/repo/', endpoint='api.repo.list') 18 | class RepoListAPI(restful.Resource): 19 | def get(self): 20 | repo_dbs, repo_cursor = model.Repo.get_dbs() 21 | return helpers.make_response(repo_dbs, model.Repo.FIELDS, repo_cursor) 22 | 23 | 24 | @api_v1.resource('/repo//', endpoint='api.repo') 25 | class RepoAPI(restful.Resource): 26 | def get(self, repo_key): 27 | repo_db = ndb.Key(urlsafe=repo_key).get() 28 | if not repo_db: 29 | helpers.make_not_found_exception('Repo %s not found' % repo_key) 30 | return helpers.make_response(repo_db, model.Repo.FIELDS) 31 | 32 | 33 | ############################################################################### 34 | # Admin 35 | ############################################################################### 36 | @api_v1.resource('/admin/repo/', endpoint='api.admin.repo.list') 37 | class AdminRepoListAPI(restful.Resource): 38 | @auth.admin_required 39 | def get(self): 40 | repo_keys = util.param('repo_keys', list) 41 | if repo_keys: 42 | repo_db_keys = [ndb.Key(urlsafe=k) for k in repo_keys] 43 | repo_dbs = ndb.get_multi(repo_db_keys) 44 | return helpers.make_response(repo_dbs, model.repo.FIELDS) 45 | 46 | repo_dbs, repo_cursor = model.Repo.get_dbs() 47 | return helpers.make_response(repo_dbs, model.Repo.FIELDS, repo_cursor) 48 | 49 | 50 | @api_v1.resource('/admin/repo//', endpoint='api.admin.repo') 51 | class AdminRepoAPI(restful.Resource): 52 | @auth.admin_required 53 | def get(self, repo_key): 54 | repo_db = ndb.Key(urlsafe=repo_key).get() 55 | if not repo_db: 56 | helpers.make_not_found_exception('Repo %s not found' % repo_key) 57 | return helpers.make_response(repo_db, model.Repo.FIELDS) 58 | -------------------------------------------------------------------------------- /main/templates/repo/admin_repo_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 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | # for repo_db in repo_dbs 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 39 | 40 | # endfor 41 | 42 |
    {{utils.order_by_link('name', 'Name')}}{{utils.order_by_link('description', 'Description')}}{{utils.order_by_link('stars', 'Stars')}}{{utils.order_by_link('forks', 'Forks')}}{{utils.order_by_link('fork', 'Fork')}}{{utils.order_by_link('language', 'Language')}}{{utils.order_by_link('modified', 'Modified')}}
    {{utils.order_by_link('created', 'Created')}}
    {{repo_db.name}}
    {{repo_db.account_username}}
    {{repo_db.description}}{{repo_db.stars_hu}}{{repo_db.forks_hu}}{{repo_db.fork}}{{repo_db.language}} 32 |
    35 | 38 |
    43 |
    44 | 45 | {{utils.next_link(next_url)}} 46 | # endblock 47 | -------------------------------------------------------------------------------- /main/api/v1/account.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from google.appengine.ext import ndb 6 | from flask.ext import restful 7 | import flask 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('/account/', endpoint='api.account.list') 18 | class AccountListAPI(restful.Resource): 19 | def get(self): 20 | account_dbs, account_cursor = model.Account.get_dbs() 21 | return helpers.make_response(account_dbs, model.Account.FIELDS, account_cursor) 22 | 23 | 24 | @api_v1.resource('/account//', endpoint='api.account') 25 | class AccountAPI(restful.Resource): 26 | def get(self, account_key): 27 | account_db = ndb.Key(urlsafe=account_key).get() 28 | if not account_db: 29 | helpers.make_not_found_exception('Account %s not found' % account_key) 30 | return helpers.make_response(account_db, model.Account.FIELDS) 31 | 32 | 33 | ############################################################################### 34 | # Admin 35 | ############################################################################### 36 | @api_v1.resource('/admin/account/', endpoint='api.admin.account.list') 37 | class AdminAccountListAPI(restful.Resource): 38 | @auth.admin_required 39 | def get(self): 40 | account_keys = util.param('account_keys', list) 41 | if account_keys: 42 | account_db_keys = [ndb.Key(urlsafe=k) for k in account_keys] 43 | account_dbs = ndb.get_multi(account_db_keys) 44 | return helpers.make_response(account_dbs, model.account.FIELDS) 45 | 46 | account_dbs, account_cursor = model.Account.get_dbs() 47 | return helpers.make_response(account_dbs, model.Account.FIELDS, account_cursor) 48 | 49 | 50 | @api_v1.resource('/admin/account//', endpoint='api.admin.account') 51 | class AdminAccountAPI(restful.Resource): 52 | @auth.admin_required 53 | def get(self, account_key): 54 | account_db = ndb.Key(urlsafe=account_key).get() 55 | if not account_db: 56 | helpers.make_not_found_exception('Account %s not found' % account_key) 57 | return helpers.make_response(account_db, model.Account.FIELDS) 58 | -------------------------------------------------------------------------------- /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/templates/account/admin_account_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 | 42 | 43 | {{utils.next_link(next_url)}} 44 | # endblock 45 | -------------------------------------------------------------------------------- /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/api/v1/user.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from google.appengine.ext import ndb 6 | from flask.ext import restful 7 | import flask 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(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(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/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_password_show_button() 8 | init_time() 9 | init_announcement() 10 | init_row_link() 11 | 12 | 13 | window.init_loading_button = -> 14 | $('body').on 'click', '.btn-loading', -> 15 | $(this).button 'loading' 16 | 17 | 18 | window.init_password_show_button = -> 19 | $('body').on 'click', '.btn-password-show', -> 20 | $target = $($(this).data 'target') 21 | $target.focus() 22 | if $(this).hasClass 'active' 23 | $target.attr 'type', 'password' 24 | else 25 | $target.attr 'type', 'text' 26 | 27 | 28 | window.init_time = -> 29 | if $('time').length > 0 30 | recalculate = -> 31 | $('time[datetime]').each -> 32 | date = moment.utc $(this).attr 'datetime' 33 | diff = moment().diff date , 'days' 34 | if diff > 25 35 | $(this).text date.local().format 'YYYY-MM-DD' 36 | else 37 | $(this).text date.fromNow() 38 | $(this).attr 'title', date.local().format 'dddd, MMMM Do YYYY, HH:mm:ss Z' 39 | setTimeout arguments.callee, 1000 * 45 40 | recalculate() 41 | 42 | 43 | window.init_announcement = -> 44 | $('.alert-announcement button.close').click -> 45 | sessionStorage?.setItem 'closedAnnouncement', $('.alert-announcement').html() 46 | 47 | if sessionStorage?.getItem('closedAnnouncement') != $('.alert-announcement').html() 48 | $('.alert-announcement').show() 49 | 50 | 51 | window.init_row_link = -> 52 | $('body').on 'click', '.row-link', (event) -> 53 | if event.ctrlKey or event.metaKey or event.which is 2 or $(this).data 'target' 54 | window.open $(this).data 'href' 55 | else 56 | window.location.href = $(this).data 'href' 57 | 58 | $('body').on 'click', '.not-link', (e) -> 59 | e.stopPropagation() 60 | 61 | 62 | window.clear_notifications = -> 63 | $('#notifications').empty() 64 | 65 | 66 | window.show_notification = (message, category='warning') -> 67 | clear_notifications() 68 | return if not message 69 | 70 | $('#notifications').append """ 71 |
    72 | 73 | #{message} 74 |
    75 | """ 76 | -------------------------------------------------------------------------------- /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/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('Account List', 'github-alt', url_for('admin_account_list'))}} 21 | {{admin_link('Repo List', 'github', url_for('admin_repo_list'))}} 22 |
    23 | 24 | 30 | 31 |
    32 | # if localhost 33 | {{admin_link('Localhost', 'home', localhost, 'gae')}} 34 | # endif 35 | {{admin_link('Dashboard', 'tachometer', 'https://console.cloud.google.com/home/dashboard?project=%s' % config.APPLICATION_ID, 'gae')}} 36 | {{admin_link('Datastore', 'database', 'https://console.cloud.google.com/datastore/query?project=%s' % config.APPLICATION_ID, 'gae')}} 37 | {{admin_link('Instances', 'bolt', 'https://console.cloud.google.com/appengine/instances?project=%s' % config.APPLICATION_ID, 'gae')}} 38 | {{admin_link('Versions', 'archive', 'https://console.cloud.google.com/appengine/versions?project=%s' % config.APPLICATION_ID, 'gae')}} 39 | {{admin_link('Logs', 'bullhorn', 'https://console.cloud.google.com/logs?project=%s&versionId=%s' % (config.APPLICATION_ID, config.CURRENT_VERSION_NAME), 'gae')}} 40 | {{admin_link('APIs', 'wrench', 'https://console.developers.google.com/apis/library?project=%s' % config.APPLICATION_ID, 'gae')}} 41 | {{admin_link('Settings', 'cogs', 'https://console.cloud.google.com/settings?project=%s' % config.APPLICATION_ID, 'gae')}} 42 | {{admin_link('Billing', 'credit-card', 'https://console.cloud.google.com/billing', 'gae')}} 43 |
    44 | 45 | # endblock 46 | 47 | 48 | # macro admin_link(title, icon, url, target='_self') 49 | 55 | # endmacro 56 | -------------------------------------------------------------------------------- /main/templates/bit/header.html: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /main/auth/reddit.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import base64 4 | 5 | from flask.ext.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/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 | github = ndb.StringProperty(default='') 29 | 30 | def has_permission(self, perm): 31 | return self.admin or perm in self.permissions 32 | 33 | def avatar_url_size(self, size=None): 34 | return '//gravatar.com/avatar/%(hash)s?d=identicon&r=x%(size)s' % { 35 | 'hash': hashlib.md5( 36 | (self.email or self.username).encode('utf-8')).hexdigest(), 37 | 'size': '&s=%d' % size if size > 0 else '', 38 | } 39 | 40 | avatar_url = property(avatar_url_size) 41 | 42 | @classmethod 43 | def get_dbs( 44 | cls, admin=None, active=None, verified=None, permissions=None, **kwargs 45 | ): 46 | args = parser.parse({ 47 | 'admin': wf.Bool(missing=None), 48 | 'active': wf.Bool(missing=None), 49 | 'verified': wf.Bool(missing=None), 50 | 'permissions': wf.DelimitedList(wf.Str(), delimiter=',', missing=[]), 51 | }) 52 | return super(User, cls).get_dbs( 53 | admin=admin or args['admin'], 54 | active=active or args['active'], 55 | verified=verified or args['verified'], 56 | permissions=permissions or args['permissions'], 57 | **kwargs 58 | ) 59 | 60 | @classmethod 61 | def is_username_available(cls, username, self_key=None): 62 | if self_key is None: 63 | return cls.get_by('username', username) is None 64 | user_keys, _ = util.get_keys(cls.query(), username=username, limit=2) 65 | return not user_keys or self_key in user_keys and not user_keys[1:] 66 | 67 | @classmethod 68 | def is_email_available(cls, email, self_key=None): 69 | if not config.CONFIG_DB.check_unique_email: 70 | return True 71 | user_keys, _ = util.get_keys( 72 | cls.query(), email=email, verified=True, limit=2, 73 | ) 74 | return not user_keys or self_key in user_keys and not user_keys[1:] 75 | 76 | FIELDS = { 77 | 'active': fields.Boolean, 78 | 'admin': fields.Boolean, 79 | 'auth_ids': fields.List(fields.String), 80 | 'avatar_url': fields.String, 81 | 'email': fields.String, 82 | 'name': fields.String, 83 | 'permissions': fields.List(fields.String), 84 | 'username': fields.String, 85 | 'verified': fields.Boolean, 86 | } 87 | 88 | FIELDS.update(model.Base.FIELDS) 89 | -------------------------------------------------------------------------------- /main/templates/account/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 |
    8 |
    9 |

    10 | Avatar of {{account_db.name}} 11 |

    12 |

    {{account_db.name}}

    13 |

    14 | 15 | github.com/{{account_db.username}} 16 | 17 |

    18 |
    19 |

    {{account_db.stars_hu}}

    20 |

    {{account_db.forks_hu}}

    21 | # if not account_db.organization 22 |

    {{account_db.followers_hu}}

    23 | # endif 24 | # if account_db.language 25 |

    {{account_db.language}}

    26 | # endif 27 | # if account_db.joined 28 |
    29 |

    {{account_db.joined.strftime('%b %d, %Y')}}

    30 | # endif 31 |
    32 |

    33 | 34 | 37 | ({{account_db.status}}) 38 |

    39 |
    40 |
    41 | 42 | 43 | 44 | 47 | 48 | 49 | 50 | # for repo_db in repo_dbs 51 | 52 | 56 | 57 | 58 | 59 | 60 | # endfor 61 | 62 |
    45 | {{account_db.public_repos_hu}} Repositories 46 |
    63 | {{utils.next_link(next_url, prev_url)}} 64 |
    65 |
    66 | # endblock 67 | -------------------------------------------------------------------------------- /main/control/test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from flask.ext import wtf 4 | import flask 5 | import wtforms 6 | 7 | import auth 8 | import util 9 | 10 | from main import app 11 | 12 | TESTS = [ 13 | 'responsive', 14 | 'grid', 15 | 'heading', 16 | 'paragraph', 17 | 'font', 18 | 'table', 19 | 'form', 20 | 'button', 21 | 'alert', 22 | 'badge', 23 | 'label', 24 | 'filter', 25 | 'pagination', 26 | 'social', 27 | ] 28 | 29 | 30 | class TestForm(wtf.Form): 31 | name = wtforms.StringField( 32 | 'Text', 33 | [wtforms.validators.required()], filters=[util.strip_filter], 34 | description='This is a very important field', 35 | ) 36 | number = wtforms.IntegerField('Integer', [wtforms.validators.optional()]) 37 | email = wtforms.StringField( 38 | 'Email', 39 | [wtforms.validators.optional(), wtforms.validators.email()], 40 | filters=[util.email_filter], 41 | ) 42 | date = wtforms.DateField('Date', [wtforms.validators.optional()]) 43 | textarea = wtforms.TextAreaField('Textarea') 44 | boolean = wtforms.BooleanField( 45 | 'Render it as Markdown', 46 | [wtforms.validators.optional()], 47 | ) 48 | password = wtforms.PasswordField( 49 | 'Password', 50 | [wtforms.validators.optional(), wtforms.validators.length(min=6)], 51 | ) 52 | password_visible = wtforms.StringField( 53 | 'Password visible', 54 | [wtforms.validators.optional(), wtforms.validators.length(min=6)], 55 | description='Visible passwords for the win!' 56 | ) 57 | prefix = wtforms.StringField('Prefix', [wtforms.validators.optional()]) 58 | suffix = wtforms.StringField('Suffix', [wtforms.validators.required()]) 59 | both = wtforms.IntegerField('Both', [wtforms.validators.required()]) 60 | select = wtforms.SelectField( 61 | 'Language', 62 | [wtforms.validators.optional()], 63 | choices=[(s, s.title()) for s in ['english', 'greek', 'spanish']] 64 | ) 65 | checkboxes = wtforms.SelectMultipleField( 66 | 'User permissions', 67 | [wtforms.validators.required()], 68 | choices=[(c, c.title()) for c in ['admin', 'moderator', 'slave']] 69 | ) 70 | radios = wtforms.SelectField( 71 | 'Choose your weapon', 72 | [wtforms.validators.optional()], 73 | choices=[(r, r.title()) for r in ['gun', 'knife', 'chainsaw', 'sword']] 74 | ) 75 | public = wtforms.StringField('Public Key', [wtforms.validators.optional()]) 76 | private = wtforms.StringField('Private Key', [wtforms.validators.optional()]) 77 | recaptcha = wtf.RecaptchaField() 78 | 79 | 80 | @app.route('/admin/test//', methods=['GET', 'POST']) 81 | @app.route('/admin/test/', methods=['GET', 'POST']) 82 | @auth.admin_required 83 | def admin_test(test=None): 84 | if test and test not in TESTS: 85 | flask.abort(404) 86 | form = TestForm() 87 | if form.validate_on_submit(): 88 | pass 89 | 90 | return flask.render_template( 91 | 'admin/test/test_one.html' if test else 'admin/test/test.html', 92 | title='Test: %s' % test.title() if test else 'Test', 93 | html_class='test', 94 | form=form, 95 | test=test, 96 | tests=TESTS, 97 | back_url_for='admin_test' if test else None, 98 | ) 99 | -------------------------------------------------------------------------------- /main/templates/auth/auth.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 | # if current_user.id == 0 14 |
    15 |
    16 | # if config.CONFIG_DB.has_email_authentication 17 | # if form_type == 'signin' 18 | # include 'auth/signin_form.html' 19 | # else 20 | # include 'auth/signup_form.html' 21 | # endif 22 | or 23 | # endif 24 | 25 | # set is_icon = config.CONFIG_DB.has_email_authentication 26 |
    27 | {{utils.signin_button('GAE' if config.CONFIG_DB.has_google else 'Google', 'btn-google', 'fa-google', gae_signin_url, is_icon)}} 28 | {{utils.signin_button('Facebook', 'btn-facebook', 'fa-facebook', facebook_signin_url, is_icon) if config.CONFIG_DB.has_facebook}} 29 | {{utils.signin_button('Twitter', 'btn-twitter', 'fa-twitter', twitter_signin_url, is_icon) if config.CONFIG_DB.has_twitter}} 30 | {{utils.signin_button('Bitbucket', 'btn-bitbucket', 'fa-bitbucket', bitbucket_signin_url, is_icon) if config.CONFIG_DB.has_bitbucket}} 31 | {{utils.signin_button('Dropbox', 'btn-dropbox', 'fa-dropbox', dropbox_signin_url, is_icon) if config.CONFIG_DB.has_dropbox}} 32 | {{utils.signin_button('GitHub', 'btn-github', 'fa-github', github_signin_url, is_icon) if config.CONFIG_DB.has_github}} 33 | {{utils.signin_button('Google', 'btn-google', 'fa-google-plus', google_signin_url, is_icon) if config.CONFIG_DB.has_google}} 34 | {{utils.signin_button('Instagram', 'btn-instagram', 'fa-instagram', instagram_signin_url, is_icon) if config.CONFIG_DB.has_instagram}} 35 | {{utils.signin_button('LinkedIn', 'btn-linkedin', 'fa-linkedin', linkedin_signin_url, is_icon) if config.CONFIG_DB.has_linkedin}} 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/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 | verify_email = ndb.BooleanProperty(default=True, verbose_name='Verify user emails') 30 | github_username = ndb.StringProperty(default='', verbose_name='GitHub Username') 31 | github_password = ndb.StringProperty(default='', verbose_name='GitHub Password') 32 | 33 | @property 34 | def has_anonymous_recaptcha(self): 35 | return bool(self.anonymous_recaptcha and self.has_recaptcha) 36 | 37 | @property 38 | def has_email_authentication(self): 39 | return bool(self.email_authentication and self.feedback_email and self.verify_email) 40 | 41 | @property 42 | def has_recaptcha(self): 43 | return bool(self.recaptcha_private_key and self.recaptcha_public_key) 44 | 45 | @classmethod 46 | def get_master_db(cls): 47 | return cls.get_or_insert('master') 48 | 49 | FIELDS = { 50 | 'analytics_id': fields.String, 51 | 'announcement_html': fields.String, 52 | 'announcement_type': fields.String, 53 | 'anonymous_recaptcha': fields.Boolean, 54 | 'brand_name': fields.String, 55 | 'check_unique_email': fields.Boolean, 56 | 'email_authentication': fields.Boolean, 57 | 'feedback_email': fields.String, 58 | 'flask_secret_key': fields.String, 59 | 'letsencrypt_challenge': fields.String, 60 | 'letsencrypt_response': fields.String, 61 | 'notify_on_new_user': fields.Boolean, 62 | 'recaptcha_private_key': fields.String, 63 | 'recaptcha_public_key': fields.String, 64 | 'salt': fields.String, 65 | 'verify_email': fields.Boolean, 66 | } 67 | 68 | FIELDS.update(model.Base.FIELDS) 69 | FIELDS.update(model.ConfigAuth.FIELDS) 70 | -------------------------------------------------------------------------------- /main/control/account.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from flask.ext import wtf 4 | from google.appengine.ext import ndb 5 | import flask 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/account/') 20 | @auth.admin_required 21 | def admin_account_list(): 22 | account_dbs, account_cursor = model.Account.get_dbs( 23 | order=util.param('order') or '-modified', 24 | ) 25 | return flask.render_template( 26 | 'account/admin_account_list.html', 27 | html_class='admin-account-list', 28 | title='Account List', 29 | account_dbs=account_dbs, 30 | next_url=util.generate_next_url(account_cursor), 31 | api_url=flask.url_for('api.admin.account.list'), 32 | ) 33 | 34 | 35 | ############################################################################### 36 | # Admin Update 37 | ############################################################################### 38 | class AccountUpdateAdminForm(wtf.Form): 39 | username = wtforms.StringField( 40 | model.Account.username._verbose_name, 41 | [wtforms.validators.required()], 42 | filters=[util.strip_filter], 43 | ) 44 | name = wtforms.StringField( 45 | model.Account.name._verbose_name, 46 | [wtforms.validators.required()], 47 | filters=[util.strip_filter], 48 | ) 49 | stars = wtforms.IntegerField( 50 | model.Account.stars._verbose_name, 51 | [wtforms.validators.optional()], 52 | ) 53 | rank = wtforms.IntegerField( 54 | model.Account.rank._verbose_name, 55 | [wtforms.validators.optional()], 56 | ) 57 | organization = wtforms.BooleanField( 58 | model.Account.organization._verbose_name, 59 | [wtforms.validators.optional()], 60 | ) 61 | status = wtforms.StringField( 62 | model.Account.status._verbose_name, 63 | [wtforms.validators.optional()], 64 | filters=[util.strip_filter], 65 | ) 66 | 67 | 68 | @app.route('/admin/account/create/', methods=['GET', 'POST']) 69 | @app.route('/admin/account//update/', methods=['GET', 'POST']) 70 | @auth.admin_required 71 | def admin_account_update(account_id=0): 72 | if account_id: 73 | account_db = model.Account.get_by_id(account_id) 74 | else: 75 | account_db = model.Account() 76 | 77 | if not account_db: 78 | flask.abort(404) 79 | 80 | form = AccountUpdateAdminForm(obj=account_db) 81 | 82 | if form.validate_on_submit(): 83 | form.populate_obj(account_db) 84 | account_db.put() 85 | return flask.redirect(flask.url_for('admin_account_list', order='-modified')) 86 | 87 | return flask.render_template( 88 | 'account/admin_account_update.html', 89 | title=account_db.username if account_id else 'New Account', 90 | html_class='admin-account-update', 91 | form=form, 92 | account_db=account_db, 93 | back_url_for='admin_account_list', 94 | api_url=flask.url_for('api.admin.account', account_key=account_db.key.urlsafe() if account_db.key else ''), 95 | ) 96 | -------------------------------------------------------------------------------- /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, 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-bitbucket { .btn-social(#205081); } 101 | .btn-dropbox { .btn-social(#007ee5); } 102 | .btn-facebook { .btn-social(#3b5998); } 103 | .btn-github { .btn-social(#444444); } 104 | .btn-google { .btn-social(#dd4b39); } 105 | .btn-instagram { .btn-social(#3f729b); } 106 | .btn-linkedin { .btn-social(#007bb6); } 107 | .btn-microsoft { .btn-social(#2672ec); } 108 | .btn-reddit { .btn-social(#eff7ff, #000); } 109 | .btn-twitter { .btn-social(#55acee); } 110 | .btn-vk { .btn-social(#587ea3); } 111 | .btn-yahoo { .btn-social(#720e9e); } 112 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GitHub Stats 2 | ============ 3 | 4 | [![Slack Status](https://gae-init-slack.herokuapp.com/badge.svg)](https://gae-init-slack.herokuapp.com) 5 | 6 | > GitHub Stats is just a fun project based on [gae-init](http://docs.gae-init.appspot.com) 7 | 8 | Requirements 9 | ------------ 10 | 11 | - [Google App Engine SDK for Python][] 12 | - [Node.js][], [pip][], [virtualenv][] 13 | - [OS X][] or [Linux][] or [Windows][] 14 | 15 | Make sure you have all of the above or refer to the docs on how to 16 | [install the requirements](http://docs.gae-init.appspot.com/requirement/). 17 | 18 | Running the Development Environment 19 | ----------------------------------- 20 | 21 | ```bash 22 | $ cd /path/to/github-stats 23 | $ gulp 24 | ``` 25 | 26 | To test it visit `http://localhost:8080/` in your browser. 27 | 28 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 29 | 30 | For a complete list of commands: 31 | 32 | ```bash 33 | $ gulp help 34 | ``` 35 | 36 | Initializing or Resetting the project 37 | ------------------------------------ 38 | 39 | ```bash 40 | $ cd /path/to/github-stats 41 | $ npm install 42 | $ gulp 43 | ``` 44 | 45 | If something goes wrong you can always do: 46 | 47 | ```bash 48 | $ gulp reset 49 | $ npm install 50 | $ gulp 51 | ``` 52 | 53 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 54 | 55 | To install [Gulp][] as a global package: 56 | 57 | ```bash 58 | $ npm install -g gulp 59 | ``` 60 | 61 | Deploying on Google App Engine 62 | ------------------------------ 63 | 64 | ```bash 65 | $ gulp deploy 66 | $ gulp deploy --project=foo 67 | $ gulp deploy --project=foo --version=bar 68 | ``` 69 | 70 | Tech Stack 71 | ---------- 72 | 73 | - [Google App Engine][], [NDB][] 74 | - [Jinja2][], [Flask][], [Flask-RESTful][], [Flask-WTF][] 75 | - [CoffeeScript][], [Less][] 76 | - [Bootstrap][], [Font Awesome][], [Social Buttons][] 77 | - [jQuery][], [Moment.js][] 78 | - [OpenID][] sign in (Google, Facebook, Twitter and more) 79 | - [Python 2.7][], [pip][], [virtualenv][] 80 | - [Gulp][], [Bower][] 81 | 82 | [bootstrap]: http://getbootstrap.com/ 83 | [bower]: http://bower.io/ 84 | [coffeescript]: http://coffeescript.org/ 85 | [flask-restful]: https://flask-restful.readthedocs.org 86 | [flask-wtf]: https://flask-wtf.readthedocs.org 87 | [flask]: http://flask.pocoo.org/ 88 | [font awesome]: http://fortawesome.github.com/Font-Awesome/ 89 | [google app engine sdk for python]: https://developers.google.com/appengine/downloads 90 | [google app engine]: https://developers.google.com/appengine/ 91 | [gulp]: http://gulpjs.com 92 | [jinja2]: http://jinja.pocoo.org/docs/ 93 | [jquery]: https://jquery.com/ 94 | [less]: http://lesscss.org/ 95 | [lesscss]: http://lesscss.org/ 96 | [linux]: http://www.ubuntu.com 97 | [moment.js]: http://momentjs.com/ 98 | [ndb]: https://developers.google.com/appengine/docs/python/ndb/ 99 | [node.js]: http://nodejs.org/ 100 | [openid]: http://en.wikipedia.org/wiki/OpenID 101 | [os x]: http://www.apple.com/osx/ 102 | [pip]: http://www.pip-installer.org/ 103 | [python 2.7]: https://developers.google.com/appengine/docs/python/python27/using27 104 | [social buttons]: http://lipis.github.io/bootstrap-social/ 105 | [virtualenv]: http://www.virtualenv.org/ 106 | [windows]: http://windows.microsoft.com/ 107 | -------------------------------------------------------------------------------- /main/control/gh.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from datetime import datetime 4 | from datetime import timedelta 5 | import json 6 | 7 | import flask 8 | from google.appengine.ext import ndb 9 | from google.appengine.ext import deferred 10 | from google.appengine.api import urlfetch 11 | import github 12 | 13 | from api import helpers 14 | import auth 15 | import config 16 | import model 17 | import util 18 | import task 19 | 20 | from main import app 21 | 22 | 23 | @app.route('//') 24 | @app.route('/') 25 | def gh_account(username, repo=None): 26 | username_ = username.lower() 27 | account_db = model.Account.get_by_id(username_) 28 | 29 | if not account_db: 30 | g = github.Github(config.CONFIG_DB.github_username, config.CONFIG_DB.github_password) 31 | try: 32 | account = g.get_user(username_) 33 | except github.GithubException as error: 34 | return flask.abort(error.status) 35 | 36 | account_db = model.Account.get_or_insert( 37 | account.login, 38 | avatar_url=account.avatar_url.split('?')[0], 39 | email=account.email or '', 40 | followers=account.followers, 41 | joined=account.created_at, 42 | name=account.name or account.login, 43 | organization=account.type == 'Organization', 44 | public_repos=account.public_repos, 45 | username=account.login, 46 | ) 47 | 48 | if account_db.username != username or repo: 49 | return flask.redirect(flask.url_for('gh_account', username=account_db.username)) 50 | 51 | task.queue_account(account_db) 52 | repo_dbs, repo_cursor = account_db.get_repo_dbs() 53 | 54 | return flask.render_template( 55 | 'account/view.html', 56 | html_class='gh-view', 57 | title=account_db.name, 58 | account_db=account_db, 59 | repo_dbs=repo_dbs, 60 | next_url=util.generate_next_url(repo_cursor), 61 | username=account_db.username, 62 | ) 63 | 64 | ############################################################################### 65 | # Cron Stuff 66 | ############################################################################### 67 | @app.route('/admin/cron/repo/') 68 | def gh_admin_top(): 69 | if config.PRODUCTION and 'X-Appengine-Cron' not in flask.request.headers: 70 | flask.abort(403) 71 | stars = util.param('stars', int) or 10000 72 | page = util.param('page', int) or 1 73 | per_page = util.param('per_page', int) or 100 74 | # TODO: fix formatting 75 | result = urlfetch.fetch('https://api.github.com/search/repositories?q=stars:>=%s&sort=stars&order=asc&page=%d&per_page=%d' % (stars, page, per_page)) 76 | if result.status_code == 200: 77 | repos = json.loads(result.content) 78 | else: 79 | flask.abort(result.status_code) 80 | 81 | for repo in repos['items']: 82 | account = repo['owner'] 83 | account_db = model.Account.get_or_insert( 84 | account['login'], 85 | avatar_url=account['avatar_url'].split('?')[0], 86 | email=account['email'] if 'email' in account else '', 87 | name=account['login'], 88 | followers=account['followers'] if 'followers' in account else 0, 89 | organization=account['type'] == 'Organization', 90 | username=account['login'], 91 | ) 92 | 93 | return 'OK %d of %d' % (len(repos['items']), repos['total_count']) 94 | 95 | 96 | @app.route('/admin/cron/sync/') 97 | def admin_cron(): 98 | if config.PRODUCTION and 'X-Appengine-Cron' not in flask.request.headers: 99 | flask.abort(403) 100 | account_dbs, account_cursor = model.Account.get_dbs( 101 | order=util.param('order') or 'modified', 102 | status=util.param('status'), 103 | ) 104 | 105 | for account_db in account_dbs: 106 | task.queue_account(account_db) 107 | 108 | return 'OK' 109 | 110 | 111 | @app.route('/admin/cron/repo/cleanup/') 112 | def admin_repo_cleanup(): 113 | if config.PRODUCTION and 'X-Appengine-Cron' not in flask.request.headers: 114 | flask.abort(403) 115 | task.queue_repo_cleanup(util.param('days', int) or 5) 116 | return 'OK' 117 | -------------------------------------------------------------------------------- /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/control/profile.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from flask.ext import wtf 4 | import flask 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(wtf.Form): 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 | github = wtforms.StringField( 46 | model.User.github._verbose_name, 47 | [wtforms.validators.optional()], filters=[util.strip_filter], 48 | ) 49 | 50 | 51 | @app.route('/profile/update/', methods=['GET', 'POST']) 52 | @auth.login_required 53 | def profile_update(): 54 | user_db = auth.current_user_db() 55 | form = ProfileUpdateForm(obj=user_db) 56 | 57 | if form.validate_on_submit(): 58 | email = form.email.data 59 | if email and not user_db.is_email_available(email, user_db.key): 60 | form.email.errors.append('This email is already taken.') 61 | 62 | if not form.errors: 63 | send_verification = not user_db.token or user_db.email != email 64 | form.populate_obj(user_db) 65 | if send_verification: 66 | user_db.verified = False 67 | task.verify_email_notification(user_db) 68 | user_db.put() 69 | return flask.redirect(flask.url_for('profile')) 70 | 71 | return flask.render_template( 72 | 'profile/profile_update.html', 73 | title=user_db.name, 74 | html_class='profile-update', 75 | form=form, 76 | user_db=user_db, 77 | ) 78 | 79 | 80 | ############################################################################### 81 | # Profile Password 82 | ############################################################################### 83 | class ProfilePasswordForm(wtf.Form): 84 | old_password = wtforms.StringField( 85 | 'Old Password', [wtforms.validators.required()], 86 | ) 87 | new_password = wtforms.StringField( 88 | 'New Password', 89 | [wtforms.validators.required(), wtforms.validators.length(min=6)] 90 | ) 91 | 92 | 93 | @app.route('/profile/password/', methods=['GET', 'POST']) 94 | @auth.login_required 95 | def profile_password(): 96 | if not config.CONFIG_DB.has_email_authentication: 97 | flask.abort(418) 98 | user_db = auth.current_user_db() 99 | form = ProfilePasswordForm(obj=user_db) 100 | 101 | if not user_db.password_hash: 102 | del form.old_password 103 | 104 | if form.validate_on_submit(): 105 | errors = False 106 | old_password = form.old_password.data if form.old_password else None 107 | new_password = form.new_password.data 108 | if new_password or old_password: 109 | if user_db.password_hash: 110 | if util.password_hash(user_db, old_password) != user_db.password_hash: 111 | form.old_password.errors.append('Invalid current password') 112 | errors = True 113 | 114 | if not (form.errors or errors): 115 | user_db.password_hash = util.password_hash(user_db, new_password) 116 | flask.flash('Your password has been changed.', category='success') 117 | 118 | if not (form.errors or errors): 119 | user_db.put() 120 | return flask.redirect(flask.url_for('profile')) 121 | 122 | return flask.render_template( 123 | 'profile/profile_password.html', 124 | title=user_db.name, 125 | html_class='profile-password', 126 | form=form, 127 | user_db=user_db, 128 | ) 129 | -------------------------------------------------------------------------------- /main/control/welcome.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from datetime import datetime 4 | from google.appengine.ext import ndb 5 | import flask 6 | 7 | import config 8 | import model 9 | import util 10 | 11 | from main import app 12 | 13 | 14 | ############################################################################### 15 | # Welcome 16 | ############################################################################### 17 | @app.route('/') 18 | def welcome(): 19 | if util.param('username'): 20 | return flask.redirect(flask.url_for('gh_account', username=util.param('username'))) 21 | person_dbs, person_cursor = model.Account.get_dbs( 22 | order='-stars', 23 | organization=False, 24 | ) 25 | 26 | organization_dbs, organization_cursor = model.Account.get_dbs( 27 | order='-stars', 28 | organization=True, 29 | ) 30 | 31 | repo_dbs, repo_cursor = model.Repo.get_dbs( 32 | order='-stars', 33 | ) 34 | 35 | return flask.render_template( 36 | 'welcome.html', 37 | html_class='welcome', 38 | title='Top People, Organizations and Repositories', 39 | person_dbs=person_dbs, 40 | organization_dbs=organization_dbs, 41 | repo_dbs=repo_dbs, 42 | ) 43 | 44 | 45 | @app.route('/new/') 46 | def new_accounts(): 47 | person_dbs, person_cursor = model.Account.get_dbs( 48 | order='-created', 49 | limit=128, 50 | ) 51 | 52 | return flask.make_response(flask.render_template( 53 | 'account/list_new.html', 54 | title='Latest Additions', 55 | html_class='account-new', 56 | person_dbs=person_dbs, 57 | )) 58 | 59 | 60 | @app.route('/people/') 61 | def person(): 62 | limit = int(util.param('limit', int) or flask.request.cookies.get('limit') or config.MAX_DB_LIMIT) 63 | order = util.param('order') or '-stars' 64 | if 'repo' in order: 65 | order = '-public_repos' 66 | elif 'follower' in order: 67 | order = '-followers' 68 | 69 | person_dbs, person_cursor = model.Account.get_dbs( 70 | order=order, 71 | organization=False, 72 | limit=limit, 73 | ) 74 | 75 | response = flask.make_response(flask.render_template( 76 | 'account/list_person.html', 77 | title='Top People', 78 | html_class='account-person', 79 | person_dbs=person_dbs, 80 | order=order, 81 | limit=limit, 82 | )) 83 | response.set_cookie('limit', str(limit)) 84 | return response 85 | 86 | 87 | @app.route('/organizations/') 88 | def organization(): 89 | limit = int(util.param('limit', int) or flask.request.cookies.get('limit') or config.MAX_DB_LIMIT) 90 | order = util.param('order') or '-stars' 91 | if 'repo' in order: 92 | order = '-public_repos' 93 | 94 | organization_dbs, organization_cursor = model.Account.get_dbs( 95 | order=order, 96 | organization=True, 97 | limit=limit, 98 | ) 99 | 100 | response = flask.make_response(flask.render_template( 101 | 'account/list_organization.html', 102 | title='Top Organizations', 103 | html_class='account-organization', 104 | organization_dbs=organization_dbs, 105 | order=order, 106 | limit=limit, 107 | )) 108 | response.set_cookie('limit', str(limit)) 109 | return response 110 | 111 | 112 | @app.route('/repositories/') 113 | def repo(): 114 | limit = int(util.param('limit', int) or flask.request.cookies.get('limit') or config.MAX_DB_LIMIT) 115 | order = util.param('order') or '-stars' 116 | if 'fork' in order: 117 | order = '-forks' 118 | repo_dbs, repo_cursor = model.Repo.get_dbs( 119 | order=order, 120 | limit=limit, 121 | ) 122 | 123 | response = flask.make_response(flask.render_template( 124 | 'account/list_repo.html', 125 | title='Top Repositories', 126 | html_class='account-repo', 127 | repo_dbs=repo_dbs, 128 | order=order.replace('-', ''), 129 | limit=limit, 130 | )) 131 | response.set_cookie('limit', str(limit)) 132 | return response 133 | 134 | 135 | ############################################################################### 136 | # Sitemap stuff 137 | ############################################################################### 138 | @app.route('/sitemap.xml') 139 | def sitemap(): 140 | response = flask.make_response(flask.render_template( 141 | 'sitemap.xml', 142 | lastmod=config.CURRENT_VERSION_DATE.strftime('%Y-%m-%d'), 143 | )) 144 | response.headers['Content-Type'] = 'application/xml' 145 | return response 146 | 147 | 148 | ############################################################################### 149 | # Warmup request 150 | ############################################################################### 151 | @app.route('/_ah/warmup') 152 | def warmup(): 153 | # TODO: put your warmup code here 154 | return 'success' 155 | -------------------------------------------------------------------------------- /main/templates/user/user_merge.html: -------------------------------------------------------------------------------- 1 | # extends 'base.html' 2 | # import 'macro/forms.html' as forms 3 | # import 'macro/utils.html' as utils 4 | 5 | 6 | # block content 7 | 13 |
    14 |
    15 |
    16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | # for user_db in user_dbs 29 | 30 | 38 | 39 | 40 | 45 | 56 | 57 | 58 | # endfor 59 | 60 |
    NameUsernameEmailCreated Permissions
    31 | 32 | Avatar of {{user_db.name}} 33 | {{user_db.name}} 34 | # if current_user.id == user_db.key.id() 35 | 36 | # endif 37 | {{user_db.username}}{{user_db.email}} 41 | 44 | 46 | # if user_db.admin 47 | admin 48 | # endif 49 | # if not user_db.active 50 | inactive 51 | # endif 52 | # for permission in user_db.permissions 53 | {{permission}} 54 | # endfor 55 | {{utils.auth_icons(user_db)}}
    61 |
    62 |
    63 |
    64 |
    65 |
    66 |
    67 |
    68 | {{form.csrf_token}} 69 | {{forms.hidden_field(form.user_keys)}} 70 | {{forms.hidden_field(form.user_key)}} 71 | {{forms.hidden_field(form.username)}} 72 | 73 | {{forms.text_field(form.username, disabled=True)}} 74 | {{forms.text_field(form.name)}} 75 | {{forms.email_field(form.email)}} 76 |
    77 |
    78 |
    79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | # for auth_id in auth_ids 89 | 90 | 91 | 92 | 93 | # endfor 94 | 95 |
    Auth ID
    {{utils.auth_icon(auth_id)}}{{auth_id}}
    96 |
    97 |
    98 |
    99 |
    100 |
      101 |
    • Select the user's entity that you want to keep (the other entities will be deactivated)
    • 102 |
    • Before merging make sure the entities with references to the user are being taking care of
    • 103 |
    • For deactivated users the 3rd party associated accounts will be cleared
    • 104 |
    105 |
    106 |
    107 |
    108 |
    109 | 112 |
    113 |
    114 |
    115 | # endblock 116 | -------------------------------------------------------------------------------- /main/model/config_auth.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | from google.appengine.ext import ndb 6 | 7 | from api import fields 8 | import model 9 | 10 | 11 | class ConfigAuth(object): 12 | bitbucket_key = ndb.StringProperty(default='', verbose_name='Key') 13 | bitbucket_secret = ndb.StringProperty(default='', verbose_name='Secret') 14 | dropbox_app_key = ndb.StringProperty(default='', verbose_name='App Key') 15 | dropbox_app_secret = ndb.StringProperty(default='', verbose_name='App Secret') 16 | facebook_app_id = ndb.StringProperty(default='', verbose_name='App ID') 17 | facebook_app_secret = ndb.StringProperty(default='', verbose_name='App Secret') 18 | github_client_id = ndb.StringProperty(default='', verbose_name='Client ID') 19 | github_client_secret = ndb.StringProperty(default='', verbose_name='Client Secret') 20 | google_client_id = ndb.StringProperty(default='', verbose_name='Client ID') 21 | google_client_secret = ndb.StringProperty(default='', verbose_name='Client Secret') 22 | instagram_client_id = ndb.StringProperty(default='', verbose_name='Client ID') 23 | instagram_client_secret = ndb.StringProperty(default='', verbose_name='Client Secret') 24 | linkedin_api_key = ndb.StringProperty(default='', verbose_name='API Key') 25 | linkedin_secret_key = ndb.StringProperty(default='', verbose_name='Secret Key') 26 | microsoft_client_id = ndb.StringProperty(default='', verbose_name='Client ID') 27 | microsoft_client_secret = ndb.StringProperty(default='', verbose_name='Client Secret') 28 | reddit_client_id = ndb.StringProperty(default='', verbose_name='Client ID') 29 | reddit_client_secret = ndb.StringProperty(default='', verbose_name='Client Secret') 30 | twitter_consumer_key = ndb.StringProperty(default='', verbose_name='Consumer Key') 31 | twitter_consumer_secret = ndb.StringProperty(default='', verbose_name='Consumer Secret') 32 | vk_app_id = ndb.StringProperty(default='', verbose_name='App ID') 33 | vk_app_secret = ndb.StringProperty(default='', verbose_name='App Secret') 34 | yahoo_consumer_key = ndb.StringProperty(default='', verbose_name='Consumer Key') 35 | yahoo_consumer_secret = ndb.StringProperty(default='', verbose_name='Consumer Secret') 36 | 37 | @property 38 | def has_bitbucket(self): 39 | return bool(self.bitbucket_key and self.bitbucket_secret) 40 | 41 | @property 42 | def has_dropbox(self): 43 | return bool(self.dropbox_app_key and self.dropbox_app_secret) 44 | 45 | @property 46 | def has_facebook(self): 47 | return bool(self.facebook_app_id and self.facebook_app_secret) 48 | 49 | @property 50 | def has_google(self): 51 | return bool(self.google_client_id and self.google_client_secret) 52 | 53 | @property 54 | def has_github(self): 55 | return bool(self.github_client_id and self.github_client_secret) 56 | 57 | @property 58 | def has_instagram(self): 59 | return bool(self.instagram_client_id and self.instagram_client_secret) 60 | 61 | @property 62 | def has_linkedin(self): 63 | return bool(self.linkedin_api_key and self.linkedin_secret_key) 64 | 65 | @property 66 | def has_microsoft(self): 67 | return bool(self.microsoft_client_id and self.microsoft_client_secret) 68 | 69 | @property 70 | def has_reddit(self): 71 | return bool(self.reddit_client_id and self.reddit_client_secret) 72 | 73 | @property 74 | def has_twitter(self): 75 | return bool(self.twitter_consumer_key and self.twitter_consumer_secret) 76 | 77 | @property 78 | def has_vk(self): 79 | return bool(self.vk_app_id and self.vk_app_secret) 80 | 81 | @property 82 | def has_yahoo(self): 83 | return bool(self.yahoo_consumer_key and self.yahoo_consumer_secret) 84 | 85 | FIELDS = { 86 | 'bitbucket_key': fields.String, 87 | 'bitbucket_secret': fields.String, 88 | 'dropbox_app_key': fields.String, 89 | 'dropbox_app_secret': fields.String, 90 | 'facebook_app_id': fields.String, 91 | 'facebook_app_secret': fields.String, 92 | 'github_client_id': fields.String, 93 | 'github_client_secret': fields.String, 94 | 'google_client_id': fields.String, 95 | 'google_client_secret': fields.String, 96 | 'instagram_client_id': fields.String, 97 | 'instagram_client_secret': fields.String, 98 | 'linkedin_api_key': fields.String, 99 | 'linkedin_secret_key': fields.String, 100 | 'microsoft_client_id': fields.String, 101 | 'microsoft_client_secret': fields.String, 102 | 'reddit_client_id': fields.String, 103 | 'reddit_client_secret': fields.String, 104 | 'twitter_consumer_key': fields.String, 105 | 'twitter_consumer_secret': fields.String, 106 | 'vk_app_id': fields.String, 107 | 'vk_app_secret': fields.String, 108 | 'yahoo_consumer_key': fields.String, 109 | 'yahoo_consumer_secret': fields.String, 110 | } 111 | 112 | FIELDS.update(model.Base.FIELDS) 113 | -------------------------------------------------------------------------------- /main/templates/macro/utils.html: -------------------------------------------------------------------------------- 1 | # macro order_by_link(property, title, ignore='cursor', hash=None) 2 | # if request.args.get('order') == property 3 | {{title}} 4 | 5 | # elif request.args.get('order') == '-' + property 6 | {{title}} 7 | 8 | #else 9 | {{title}} 10 | #endif 11 | # endmacro 12 | 13 | 14 | # macro filter_by_link(property, value, icon=None, ignore='cursor', is_list=False, hash=None, label=None) 15 | # set value = '%s' % value 16 | 18 | # if icon 19 | 20 | # elif label 21 | {{label|safe}} 22 | # else 23 | {{value}} 24 | # endif 25 | 26 | # endmacro 27 | 28 | 29 | # macro back_link(title, route) 30 | 31 | 32 | 33 | # endmacro 34 | 35 | 36 | # macro next_link(next_url, prev_url=None, next_caption='', prev_caption='') 37 | # if next_url or prev_url 38 | 46 | # endif 47 | # endmacro 48 | 49 | 50 | # macro prefetch_link(url) 51 | # if url 52 | 53 | 54 | # endif 55 | # endmacro 56 | 57 | 58 | # macro signin_button(brand, class_btn, class_icon, url, is_icon=False) 59 | # set caption = 'Sign in with %s' % brand 60 | 61 | 62 | {{caption if not is_icon}} 63 | 64 | # endmacro 65 | 66 | 67 | # macro auth_icon(auth_id) 68 | # if auth_id == 'email_auth' 69 | 70 | # elif auth_id.startswith('bitbucket') 71 | 72 | # elif auth_id.startswith('dropbox') 73 | 74 | # elif auth_id.startswith('facebook') 75 | 76 | # elif auth_id.startswith('github') 77 | 78 | # elif auth_id.startswith('google') 79 | 80 | # elif auth_id.startswith('federated') 81 | 82 | # elif auth_id.startswith('instagram') 83 | 84 | # elif auth_id.startswith('linkedin') 85 | 86 | # elif auth_id.startswith('microsoft') 87 | 88 | # elif auth_id.startswith('reddit') 89 | 90 | # elif auth_id.startswith('twitter') 91 | 92 | # elif auth_id.startswith('yahoo') 93 | 94 | # else 95 | 96 | # endif 97 | # endmacro 98 | 99 | 100 | # macro auth_icons(user_db, max=0) 101 | # set count = user_db.auth_ids|length 102 | # set max = 3 if max > 0 and max < 3 else max 103 | # if user_db.password_hash 104 | # set max = max - 1 if max else max 105 | {{auth_icon('email_auth')}} 106 | # endif 107 | # set max = max - 1 if max and count > max else max 108 | # set more = count - max if max else 0 109 | # for auth_id in user_db.auth_ids 110 | # if not max or loop.index0 < max 111 | {{auth_icon(auth_id)}} 112 | # elif max and loop.index0 == max 113 | 114 | # endif 115 | # endfor 116 | # endmacro 117 | 118 | 119 | # macro html_element(name, content) 120 | <{{name}} 121 | #- for arg in kwargs 122 | {{arg}}="{{kwargs[arg]}}" 123 | #- endfor 124 | > 125 | #- if content 126 | {{content}} 127 | #- endif 128 | # endmacro 129 | -------------------------------------------------------------------------------- /main/templates/macro/forms.html: -------------------------------------------------------------------------------- 1 | # macro field_errors(field) 2 | # for error in field.errors 3 |

    {{error}}

    4 | # endfor 5 | # endmacro 6 | 7 | 8 | # macro field_description(field) 9 | # if field.description 10 |

    {{field.description}}

    11 | # endif 12 | # endmacro 13 | 14 | 15 | # macro field_optional(field) 16 | # if not field.flags.required 17 | (optional) 18 | # endif 19 | # endmacro 20 | 21 | 22 | # macro input_field(field, prefix='', suffix='') 23 |
    24 | {{field.label(class='control-label')}} 25 | {{field_optional(field)}} 26 | # if prefix or suffix 27 |
    28 | # if prefix 29 | {{prefix}} 30 | # endif 31 | {{field(class='form-control', **kwargs)}} 32 | # if suffix 33 | {{suffix}} 34 | # endif 35 |
    36 | # else 37 | {{field(class='form-control', **kwargs)}} 38 | # endif 39 | {{field_errors(field)}} 40 | {{field_description(field)}} 41 |
    42 | # endmacro 43 | 44 | 45 | # macro text_field(field) 46 | {{input_field(field, type='text', **kwargs)}} 47 | # endmacro 48 | 49 | 50 | # macro password_field(field) 51 | {{input_field(field, type='password', **kwargs)}} 52 | # endmacro 53 | 54 | 55 | # macro password_visible_field(field, size='') 56 |
    57 | {{field.label(class='control-label')}} 58 | {{field_optional(field)}} 59 |
    60 | {{field(class='form-control', type='password', autocomplete='off', **kwargs)}} 61 | 62 | 65 | 66 |
    67 | {{field_errors(field)}} 68 | {{field_description(field)}} 69 |
    70 | # endmacro 71 | 72 | 73 | # macro number_field(field) 74 | {{input_field(field, type='number', **kwargs)}} 75 | # endmacro 76 | 77 | 78 | # macro date_field(field) 79 | {{input_field(field, type='date', **kwargs)}} 80 | # endmacro 81 | 82 | 83 | # macro datetime_field(field) 84 | {{input_field(field, type='datetime-local', **kwargs)}} 85 | # endmacro 86 | 87 | 88 | # macro email_field(field) 89 | {{input_field(field, type='email', **kwargs)}} 90 | # endmacro 91 | 92 | 93 | # macro select_field(field) 94 | {{input_field(field, **kwargs)}} 95 | # endmacro 96 | 97 | 98 | # macro hidden_field(field) 99 | {{field(type='hidden', **kwargs)}} 100 | # endmacro 101 | 102 | 103 | # macro textarea_field(field, rows=4) 104 |
    105 | {{field.label(class='control-label')}} 106 | {{field_optional(field)}} 107 | {{field(class='form-control', rows=rows, **kwargs)}} 108 | {{field_errors(field)}} 109 | {{field_description(field)}} 110 |
    111 | # endmacro 112 | 113 | 114 | # macro checkbox_field(field) 115 |
    116 | 119 | {{field_errors(field)}} 120 | {{field_description(field)}} 121 |
    122 | # endmacro 123 | 124 | 125 | # macro list_input_field(field, type) 126 |
    127 | {{field.label(class='control-label')}} 128 | {{field_optional(field)}} 129 | {{field_description(field)}} 130 | # for key, value in field.choices 131 |
    132 | 139 |
    140 | # endfor 141 | {{field_errors(field)}} 142 |
    143 | # endmacro 144 | 145 | 146 | # macro multiple_checkbox_field(field) 147 | {{list_input_field(field, 'checkbox')}} 148 | # endmacro 149 | 150 | 151 | # macro radio_field(field) 152 | {{list_input_field(field, 'radio')}} 153 | # endmacro 154 | 155 | 156 | # macro recaptcha_field(field) 157 | # if field 158 |
    159 | {{field.widget.server}} 160 | {{field.widget.script_url}} 161 | {{field.widget.frame_url}} 162 | {{field}} 163 | {{field_errors(field)}} 164 | {{field_description(field)}} 165 |
    166 | # endif 167 | # endmacro 168 | 169 | 170 | # macro panel_fields(name, fields, help) 171 |
    172 |
    173 |

    174 | {{name}} 175 | 176 |

    177 |
    178 |
    179 |
    180 | # if is_iterable(fields) 181 | # for field in fields: 182 | {{text_field(field)}} 183 | # endfor 184 | #else 185 | {{text_field(fields)}} 186 | #endif 187 | # if help 188 |

    {{help|safe}}

    189 | # endif 190 |
    191 |
    192 |
    193 | # endmacro 194 | 195 | 196 | # macro data_loading_text(text='Please wait..', icon='fa fa-spin fa-spinner') 197 | # if icon 198 | data-loading-text=" {{text}}" 199 | # else 200 | data-loading-text="{{text}}" 201 | # endif 202 | # endmacro 203 | --------------------------------------------------------------------------------