├── .gitignore ├── conf ├── nginx.conf └── supervisor.conf ├── fabfile.py ├── gather ├── __init__.py ├── account │ ├── __init__.py │ ├── api.py │ ├── forms.py │ ├── models.py │ ├── utils.py │ └── views.py ├── admin │ ├── __init__.py │ └── views.py ├── api.py ├── app.py ├── assets │ ├── javascripts │ │ ├── gather.coffee │ │ ├── libs │ │ │ ├── jquery-2.1.0.min.js │ │ │ ├── jquery.atwho.js │ │ │ ├── jquery.filedrop.js │ │ │ ├── locales │ │ │ │ └── timeago.zh-cn.coffee │ │ │ ├── timeago.coffee │ │ │ └── turbolinks.js │ │ └── turbolinks_icon.coffee │ └── stylesheets │ │ ├── card.sass │ │ ├── footer.sass │ │ ├── form.sass │ │ ├── gather.sass │ │ ├── grid.sass │ │ ├── helpers │ │ ├── functions.sass │ │ ├── responsive.sass │ │ └── variables.sass │ │ ├── icons.sass │ │ ├── nav.sass │ │ ├── sphinner.scss │ │ ├── topic.sass │ │ ├── user.sass │ │ └── vendor │ │ ├── _diff.scss │ │ ├── _font-awesome.scss │ │ ├── _jquery.atwho.scss │ │ ├── _normalize.scss │ │ ├── _pygments.scss │ │ └── bourbon │ │ ├── _bourbon-deprecated-upcoming.scss │ │ ├── _bourbon.scss │ │ ├── addons │ │ ├── _button.scss │ │ ├── _clearfix.scss │ │ ├── _font-family.scss │ │ ├── _hide-text.scss │ │ ├── _html5-input-types.scss │ │ ├── _position.scss │ │ ├── _prefixer.scss │ │ ├── _retina-image.scss │ │ ├── _size.scss │ │ ├── _timing-functions.scss │ │ └── _triangle.scss │ │ ├── css3 │ │ ├── _animation.scss │ │ ├── _appearance.scss │ │ ├── _backface-visibility.scss │ │ ├── _background-image.scss │ │ ├── _background.scss │ │ ├── _border-image.scss │ │ ├── _border-radius.scss │ │ ├── _box-sizing.scss │ │ ├── _columns.scss │ │ ├── _flex-box.scss │ │ ├── _font-face.scss │ │ ├── _hidpi-media-query.scss │ │ ├── _image-rendering.scss │ │ ├── _inline-block.scss │ │ ├── _keyframes.scss │ │ ├── _linear-gradient.scss │ │ ├── _perspective.scss │ │ ├── _placeholder.scss │ │ ├── _radial-gradient.scss │ │ ├── _transform.scss │ │ ├── _transition.scss │ │ └── _user-select.scss │ │ ├── functions │ │ ├── _compact.scss │ │ ├── _flex-grid.scss │ │ ├── _grid-width.scss │ │ ├── _linear-gradient.scss │ │ ├── _modular-scale.scss │ │ ├── _px-to-em.scss │ │ ├── _radial-gradient.scss │ │ ├── _tint-shade.scss │ │ └── _transition-property-name.scss │ │ └── helpers │ │ ├── _deprecated-webkit-gradient.scss │ │ ├── _gradient-positions-parser.scss │ │ ├── _linear-positions-parser.scss │ │ ├── _radial-arg-parser.scss │ │ ├── _radial-positions-parser.scss │ │ ├── _render-gradients.scss │ │ └── _shape-size-stripper.scss ├── extensions.py ├── filters.py ├── form.py ├── frontend │ ├── __init__.py │ └── views.py ├── node │ ├── __init__.py │ ├── api.py │ ├── forms.py │ ├── models.py │ └── views.py ├── notification │ └── __init__.py ├── public │ ├── humans.txt │ └── static │ │ ├── .webassets-manifest │ │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ └── fontawesome-webfont.woff │ │ ├── gather.css │ │ └── gather.js ├── settings │ ├── __init__.py │ ├── base.py │ ├── develop.py │ └── production.py ├── templates │ ├── account │ │ ├── find_password.html │ │ ├── find_sent.html │ │ ├── login.html │ │ ├── register.html │ │ ├── reset.html │ │ └── settings.html │ ├── email │ │ └── reset.html │ ├── feed.xml │ ├── index.html │ ├── layout.html │ ├── node │ │ ├── change.html │ │ ├── create.html │ │ ├── index.html │ │ └── node.html │ ├── snippet │ │ ├── form.html │ │ ├── nav.html │ │ ├── pagination.html │ │ ├── sidebar.html │ │ └── topic.html │ ├── topic │ │ ├── change.html │ │ ├── change_reply.html │ │ ├── create.html │ │ ├── index.html │ │ └── topic.html │ └── user │ │ ├── index.html │ │ ├── profile.html │ │ └── topic.html ├── topic │ ├── __init__.py │ ├── api.py │ ├── forms.py │ ├── models.py │ └── views.py ├── user │ ├── __init__.py │ └── views.py └── utils.py ├── gunicorn.py ├── manage.py ├── migrate-requirements.txt ├── migrate_from_pbb2.py ├── migrations ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ ├── 10c48c7e7526_.py │ ├── 2268227deebb_.py │ ├── 26dfc02ce3ff_.py │ ├── 276f5d7b1612_.py │ └── 3a19b3b5d896_.py ├── requirements.txt └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | cover/ 29 | htmlcov/ 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | .DS_Store 40 | .idea 41 | 42 | #Sphinx 43 | docs/_ 44 | 45 | #VirtualEnv 46 | .Python 47 | include/ 48 | 49 | .coveralls.yml 50 | 51 | .sass-cache 52 | gather/public/static/.webassets-cache 53 | -------------------------------------------------------------------------------- /conf/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream app_gather { 2 | server 127.0.0.1:8000 fail_timeout=0; 3 | } 4 | 5 | limit_req_zone $binary_remote_addr zone=gather_100:10m rate=100r/s; 6 | limit_req_zone $binary_remote_addr zone=gather_500:10m rate=500r/s; 7 | limit_req_zone $binary_remote_addr zone=gather_1000:10m rate=1000r/s; 8 | 9 | 10 | 11 | server { 12 | server_name gather.whouz.com; 13 | client_max_body_size 1M; 14 | listen 80; 15 | 16 | location /static { 17 | root /home/gather/Gather/gather/public; 18 | } 19 | 20 | location /member { 21 | rewrite ^/member/(.*) /user/$1 last; 22 | } 23 | 24 | location / { 25 | limit_req zone=gather_100 burst=20; 26 | limit_req zone=gather_500 burst=5; 27 | limit_req zone=gather_1000; 28 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 29 | proxy_set_header Host $http_host; 30 | proxy_set_header X-Real-IP $remote_addr; 31 | proxy_set_header X-Scheme $scheme; 32 | proxy_redirect off; 33 | 34 | proxy_pass http://app_gather; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /conf/supervisor.conf: -------------------------------------------------------------------------------- 1 | [program:Gather] 2 | command = /home/gather/Gather/bin/newrelic-admin run-program /home/gather/Gather/bin/gunicorn wsgi:application -c /home/gather/Gather/gunicorn.py 3 | directory = /home/gather/Gather/ 4 | user = gather 5 | autostart = true 6 | autorestart = true 7 | redirect_stderr = true 8 | stdout_logfile = /home/gather/logs/gunicorn.log 9 | environment = NEW_RELIC_CONFIG_FILE=/home/gather/Gather/newrelic.ini 10 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from fabric.api import * 4 | 5 | base_path = os.path.dirname(__file__) 6 | project_root = "~/Gather" 7 | pip_path = os.path.join(project_root, "bin/pip") 8 | python_path = os.path.join(project_root, "bin/python") 9 | 10 | 11 | env.user = "gather" 12 | env.hosts = ["gather.whouz.com"] 13 | 14 | 15 | def update_from_github(): 16 | with cd(project_root): 17 | run("git pull --rebase") 18 | 19 | 20 | def update_pip_requirements(): 21 | with cd(project_root): 22 | run("%s install -r requirements.txt" % pip_path) 23 | 24 | 25 | def migrate_databases(): 26 | with cd(project_root): 27 | run("%s manage.py db upgrade" % python_path) 28 | run("%s manage.py create_all" % python_path) 29 | 30 | 31 | def reload_nginx(): 32 | _current_user = env.user 33 | env.user = 'root' 34 | run("/etc/init.d/nginx reload") 35 | env.user = _current_user 36 | 37 | 38 | def restart_gunicorn(): 39 | _current_user = env.user 40 | env.user = 'root' 41 | run("supervisorctl reload") 42 | env.user = _current_user 43 | 44 | 45 | def reload_gunicorn(): 46 | run("kill -HUP `cat /tmp/gather.pid`") 47 | 48 | 49 | def clear_cache(): 50 | with cd(project_root): 51 | run("%s manage.py clear_cache" % python_path) 52 | 53 | 54 | def update(): 55 | update_from_github() 56 | migrate_databases() 57 | reload_gunicorn() 58 | clear_cache() 59 | 60 | 61 | def fullyupdate(): 62 | update_from_github() 63 | update_pip_requirements() 64 | migrate_databases() 65 | reload_nginx() 66 | reload_gunicorn() 67 | clear_cache() 68 | -------------------------------------------------------------------------------- /gather/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from gather.app import create_app 4 | -------------------------------------------------------------------------------- /gather/account/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import api 4 | 5 | from .views import bp 6 | 7 | __all__ = ("bp", "api") 8 | -------------------------------------------------------------------------------- /gather/account/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from flask import g, jsonify, request 4 | from gather.account.models import Account 5 | from gather.api import need_auth, EXCLUDE_COLUMNS 6 | from gather.extensions import api_manager 7 | 8 | 9 | __all__ = ["bp"] 10 | 11 | 12 | def patch_single_preprocessor(instance_id=None, data=None, **kw): 13 | """Accepts two arguments, `instance_id`, the primary key of the 14 | instance of the model to patch, and `data`, the dictionary of fields 15 | to change on the instance. 16 | 17 | """ 18 | return g.token_user.id == instance_id 19 | 20 | 21 | # 需要一点小 hack .. 22 | bp = api_manager.create_api_blueprint( 23 | Account, 24 | methods=["GET", "PUT"], 25 | preprocessors=dict(PUT_SINGLE=[need_auth, patch_single_preprocessor],), 26 | exclude_columns=EXCLUDE_COLUMNS 27 | ) 28 | 29 | 30 | @bp.route("/account/authorize/", methods=["POST"]) 31 | def _account_authorize(): 32 | from .forms import LoginForm 33 | form = LoginForm() 34 | if not form.validate_on_submit(): 35 | return jsonify( 36 | code=400, 37 | msg="Wrong username/password" 38 | ) 39 | user = form.user 40 | if not user.api_token: 41 | user.generate_api_token() 42 | return jsonify( 43 | code=200, 44 | token=user.api_token 45 | ) 46 | -------------------------------------------------------------------------------- /gather/account/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from flask import current_app, g, render_template, url_for 6 | from flask.ext.mail import Message 7 | from gather.form import Form 8 | from wtforms import TextField, PasswordField, TextAreaField, BooleanField 9 | from wtforms.validators import Length, Email, DataRequired, Optional, URL, Regexp 10 | from gather.utils import send_mail 11 | from gather.account.models import Account 12 | from gather.account.utils import login_user, create_reset_token 13 | 14 | 15 | class LoginForm(Form): 16 | username = TextField("用户名", validators=[ 17 | Length(max=15, message="用户名最多15个字符哟~"), 18 | DataRequired(), 19 | Regexp("^[a-zA-Z0-9]+$", message="用户名只能由英文字母和数字构成") 20 | ]) 21 | password = PasswordField("密码", validators=[ 22 | DataRequired() 23 | ]) 24 | 25 | def validate_password(self, field): 26 | account = self.username.data 27 | user = Account.query.filter_by(username=account).first() 28 | 29 | if user and user.check_password(field.data): 30 | self.user = user 31 | return user 32 | raise ValueError("用户名或密码错误") 33 | 34 | def login(self): 35 | login_user(self.user) 36 | 37 | 38 | class RegisterForm(LoginForm): 39 | email = TextField("电子邮件地址", validators=[ 40 | Email(), 41 | DataRequired() 42 | ]) 43 | 44 | def validate_password(self, field): 45 | return True 46 | 47 | def validate_username(self, field): 48 | if Account.query.filter_by(username=field.data.lower()).count(): 49 | raise ValueError("这个用户名被注册了") 50 | 51 | def validate_email(self, field): 52 | if Account.query.filter_by(email=field.data.lower()).count(): 53 | raise ValueError("这个电子邮件地址被注册了") 54 | 55 | def save(self): 56 | user = Account(**self.data) 57 | return user.save() 58 | 59 | 60 | class FindForm(Form): 61 | email = TextField("电子邮件地址", validators=[ 62 | Email(), 63 | DataRequired() 64 | ]) 65 | 66 | def validate_email(self, field): 67 | if not Account.query.filter_by(email=field.data.lower()).count(): 68 | raise ValueError("这个电子邮件地址尚未注册") 69 | 70 | def send(self): 71 | config = current_app.config 72 | email = self.email.data 73 | user = Account.query.filter_by(email=email.lower()).first_or_404() 74 | msg = Message( 75 | "找回 {username} 在 {site_name} 的密码".format( 76 | username=user.username, 77 | site_name=config["FORUM_TITLE"] 78 | ), 79 | recipients=[email], 80 | ) 81 | reset_url = "".join([ 82 | config["FORUM_URL"].rstrip("/"), 83 | url_for("account.reset"), 84 | "?token=", 85 | create_reset_token(user) 86 | ]) 87 | msg.html = render_template('email/reset.html', user=user, url=reset_url) 88 | send_mail(msg) 89 | 90 | 91 | class ResetForm(Form): 92 | password = PasswordField("密码", validators=[ 93 | DataRequired() 94 | ]) 95 | 96 | def reset(self, user): 97 | user.change_password(self.password.data) 98 | return user.save() 99 | 100 | 101 | class SettingsForm(Form): 102 | username = TextField("用户名", validators=[ 103 | Length(max=15, message="用户名最多15个字符哟~"), 104 | DataRequired(), 105 | Regexp("^[a-zA-Z0-9]+$", message="用户名只能由英文字母和数字构成") 106 | ]) 107 | website = TextField("网站", validators=[ 108 | URL(), Optional(), Length(max=100) 109 | ]) 110 | email = TextField("电子邮件地址", validators=[ 111 | Email(), 112 | DataRequired() 113 | ]) 114 | feeling_lucky = BooleanField("手气不错") 115 | description = TextAreaField("简介", validators=[ 116 | Optional(), Length(max=500) 117 | ], description="我叫王大锤,万万没想到..") 118 | css = TextAreaField("自定义 CSS", validators=[ 119 | Optional() 120 | ], description="body{display: none}") 121 | 122 | def validate_username(self, field): 123 | user = Account.query.filter_by(username=field.data.lower()).first() 124 | if user and user.id != g.user.id: 125 | raise ValueError("这个用户名被注册了") 126 | 127 | def validate_email(self, field): 128 | user = Account.query.filter_by(email=field.data.lower()).first() 129 | if user and user.id != g.user.id: 130 | raise ValueError("这个电子邮件地址被注册了") 131 | 132 | def save(self): 133 | user = Account.query.get(g.user.id) 134 | self.populate_obj(user) 135 | user.save() 136 | -------------------------------------------------------------------------------- /gather/account/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import hashlib 6 | 7 | from datetime import datetime, timedelta 8 | from flask import current_app 9 | from werkzeug import security 10 | from gather.extensions import db 11 | 12 | ROLES = { 13 | "banned": 0, 14 | "user": 1, 15 | "staff": 6, 16 | "admin": 9 17 | } 18 | 19 | 20 | class Account(db.Model): 21 | id = db.Column(db.Integer, primary_key=True) 22 | username = db.Column(db.String(100), nullable=False, unique=True, index=True) 23 | email = db.Column(db.String(100), nullable=False, unique=True, index=True) 24 | password = db.Column(db.String(100), nullable=False) 25 | role = db.Column(db.String(10), default="user") 26 | 27 | website = db.Column(db.String(100), nullable=True, default="") 28 | description = db.Column(db.String(500), nullable=True, default="") 29 | css = db.Column(db.String(), nullable=True, default="") 30 | 31 | created = db.Column(db.DateTime, default=datetime.utcnow) 32 | token = db.Column(db.String(20), nullable=True, default="") 33 | 34 | api_token = db.Column(db.String(40)) 35 | feeling_lucky = db.Column(db.Boolean, default=False, nullable=True) 36 | 37 | def __init__(self, **kwargs): 38 | self.token = self.create_token(16) 39 | 40 | if 'password' in kwargs: 41 | raw = kwargs.pop('password') 42 | self.password = self.create_password(raw) 43 | 44 | if 'username' in kwargs: 45 | username = kwargs.pop('username') 46 | self.username = username.lower() 47 | 48 | if 'email' in kwargs: 49 | email = kwargs.pop('email') 50 | self.email = email.lower() 51 | 52 | for k, v in kwargs.items(): 53 | setattr(self, k, v) 54 | 55 | def __str__(self): 56 | return self.username 57 | 58 | def __repr__(self): 59 | return '' % self.username 60 | 61 | def avatar(self, size=48): 62 | size *= 2 # Retina 63 | md5email = hashlib.md5(self.email).hexdigest() 64 | query = "%s?s=%s" % (md5email, size) 65 | return current_app.config['GRAVATAR_BASE_URL'] + query 66 | 67 | @staticmethod 68 | def create_password(raw): 69 | passwd = '%s%s' % (raw, current_app.config['PASSWORD_SECRET']) 70 | return security.generate_password_hash(passwd) 71 | 72 | @staticmethod 73 | def create_token(length=16): 74 | return security.gen_salt(length) 75 | 76 | @property 77 | def is_staff(self): 78 | return self.is_admin or self.role == "staff" 79 | 80 | @property 81 | def is_admin(self): 82 | return self.id == 1 or self.role == "admin" 83 | 84 | def check_password(self, raw): 85 | passwd = '%s%s' % (raw, current_app.config['PASSWORD_SECRET']) 86 | return security.check_password_hash(self.password, passwd) 87 | 88 | def change_password(self, raw): 89 | self.password = self.create_password(raw) 90 | self.token = self.create_token() 91 | 92 | def generate_api_token(self): 93 | token = security.gen_salt(40) 94 | while Account.query.filter_by(api_token=token).count(): 95 | token = security.gen_salt(40) 96 | self.api_token = token 97 | self.save() 98 | return self 99 | 100 | def save(self): 101 | db.session.add(self) 102 | db.session.commit() 103 | return self 104 | 105 | @classmethod 106 | def clean_junk_users(cls): 107 | a_month_ago = datetime.utcnow() - timedelta(days=30) 108 | from gather.topic.models import Topic, Reply 109 | for user in cls.query.all(): 110 | if user.created >= a_month_ago: 111 | pass 112 | if not Topic.query.filter_by(author=user).count(): 113 | if not Reply.query.filter_by(author=user).count(): 114 | db.session.delete(user) 115 | db.session.commit() 116 | -------------------------------------------------------------------------------- /gather/account/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import functools 4 | import time 5 | import hashlib 6 | import base64 7 | 8 | from flask import current_app 9 | from flask import g, request, session 10 | from flask import url_for, redirect, abort 11 | 12 | from gather.account.models import Account, ROLES 13 | 14 | 15 | class RequireRole(object): 16 | def __init__(self, role): 17 | self.role = role 18 | 19 | def __call__(self, method): 20 | @functools.wraps(method) 21 | def wrapper(*args, **kwargs): 22 | if not g.user: 23 | url = url_for("account.login") 24 | if "?" not in url: 25 | url += "?next=" + request.url 26 | return redirect(url) 27 | if self.role is None: 28 | return method(*args, **kwargs) 29 | if g.user.id == 1: 30 | return method(*args, **kwargs) 31 | if ROLES[g.user.role] < ROLES[self.role]: 32 | return abort(403) 33 | return method(*args, **kwargs) 34 | return wrapper 35 | 36 | 37 | require_login = RequireRole("user") 38 | require_staff = RequireRole("staff") 39 | require_admin = RequireRole("admin") 40 | 41 | 42 | def get_current_user(): 43 | if "id" in session and "token" in session: 44 | user = Account.query.get(int(session["id"])) 45 | if not user: 46 | return None 47 | if user.token != session["token"]: 48 | return None 49 | return user 50 | return None 51 | 52 | 53 | def login_user(user, permanent=True): 54 | if not user: 55 | return None 56 | session["id"] = user.id 57 | session["token"] = user.token 58 | if permanent: 59 | session.permanent = True 60 | return user 61 | 62 | 63 | def logout_user(): 64 | if "id" not in session: 65 | return 66 | session.pop("id") 67 | session.pop("token") 68 | 69 | 70 | def create_reset_token(user): 71 | timestamp = str(int(time.time())) 72 | user_id = str(user.id) 73 | token = "|".join([user_id, timestamp, current_app.secret_key]) 74 | hsh = hashlib.sha512(token).hexdigest() 75 | return base64.b64encode("|".join([timestamp, user_id, hsh])) 76 | 77 | 78 | def verify_reset_token(token): 79 | try: 80 | timestamp, user_id, hsh = base64.b64decode(token).split("|") 81 | except Exception: 82 | return 83 | token = "|".join([user_id, timestamp, current_app.secret_key]) 84 | if hsh != hashlib.sha512(token).hexdigest(): 85 | return 86 | return Account.query.get(int(user_id)) 87 | -------------------------------------------------------------------------------- /gather/account/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from flask import Blueprint, abort 4 | from flask import request, url_for 5 | from flask import g, render_template, redirect 6 | from gather.account.forms import (LoginForm, RegisterForm, 7 | FindForm, ResetForm, SettingsForm) 8 | from gather.account.utils import require_login, login_user, logout_user, verify_reset_token 9 | from gather.utils import require_token 10 | 11 | bp = Blueprint("account", __name__, url_prefix="/account") 12 | 13 | 14 | @bp.route("/login", methods=("GET", "POST")) 15 | def login(): 16 | next_url = request.args.get('next', "/") 17 | form = LoginForm() 18 | if form.validate_on_submit(): 19 | form.login() 20 | return redirect(next_url) 21 | return render_template("account/login.html", form=form) 22 | 23 | 24 | @bp.route("/register", methods=("GET", "POST")) 25 | def register(): 26 | next_url = request.args.get('next', url_for('.settings')) 27 | form = RegisterForm() 28 | if form.validate_on_submit(): 29 | user = form.save() 30 | login_user(user) 31 | return redirect(next_url) 32 | return render_template("account/register.html", form=form) 33 | 34 | 35 | @bp.route("/logout/") 36 | @require_login 37 | @require_token 38 | def logout(): 39 | next_url = request.args.get('next', "/") 40 | logout_user() 41 | return redirect(next_url) 42 | 43 | 44 | @bp.route("/find", methods=("GET", "POST")) 45 | def find_password(): 46 | if g.user: 47 | return redirect("/") 48 | form = FindForm() 49 | if form.validate_on_submit(): 50 | form.send() 51 | return render_template("account/find_sent.html") 52 | return render_template("account/find_password.html", form=form) 53 | 54 | 55 | @bp.route("/reset", methods=("GET", "POST")) 56 | def reset(): 57 | token = request.args.get("token", None) 58 | user = verify_reset_token(token) 59 | if not user: 60 | return abort(403) 61 | form = ResetForm() 62 | if form.validate_on_submit(): 63 | user = form.reset(user) 64 | login_user(user) 65 | return redirect("/") 66 | return render_template("account/reset.html", form=form) 67 | 68 | 69 | @bp.route("/settings", methods=("GET", "POST")) 70 | @require_login 71 | def settings(): 72 | user = g.user 73 | form = SettingsForm(obj=user) 74 | next_url = request.args.get('next', url_for('.settings')) 75 | if form.validate_on_submit(): 76 | form.save() 77 | return redirect(next_url) 78 | return render_template('account/settings.html', form=form) 79 | -------------------------------------------------------------------------------- /gather/admin/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from .views import bp 4 | 5 | __all__ = ("bp", ) -------------------------------------------------------------------------------- /gather/admin/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from flask import Blueprint 4 | 5 | bp = Blueprint("admin", __name__, url_prefix="/admin") 6 | -------------------------------------------------------------------------------- /gather/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from flask import g, request 4 | from flask.ext.restless import ProcessingException 5 | 6 | 7 | EXCLUDE_COLUMNS = [ 8 | "password", "token", "api_token", 9 | "author.password", "author.token", "author.api_token" 10 | ] 11 | 12 | 13 | def need_auth(**kw): 14 | if not g.token_user: 15 | raise ProcessingException(description='Not Authorized', code=401) 16 | -------------------------------------------------------------------------------- /gather/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import os 4 | 5 | BASEDIR = os.path.dirname(os.path.abspath(__file__)) 6 | 7 | from flask import Flask, g, request 8 | from flask.ext.turbolinks import turbolinks 9 | from jinja2 import MemcachedBytecodeCache 10 | from gather.extensions import db, assets, mail, cache, api_manager 11 | from gather.settings import load_settings 12 | 13 | 14 | def create_app(): 15 | app = Flask( 16 | __name__, 17 | static_folder=os.path.join(BASEDIR, "public", "static"), 18 | template_folder="templates" 19 | ) 20 | load_settings(app) 21 | register_extensions(app) 22 | register_blurprints(app) 23 | register_hooks(app) 24 | register_jinja(app) 25 | return app 26 | 27 | 28 | def register_extensions(app): 29 | db.init_app(app) 30 | assets.init_app(app) 31 | mail.init_app(app) 32 | turbolinks(app) 33 | cache.init_app(app) 34 | api_manager.init_app(app, flask_sqlalchemy_db=db) 35 | 36 | if app.debug: 37 | from flask_debugtoolbar import DebugToolbarExtension 38 | DebugToolbarExtension(app) 39 | 40 | 41 | def register_blurprints(app): 42 | import gather.frontend 43 | import gather.account 44 | import gather.user 45 | import gather.node 46 | import gather.topic 47 | import gather.admin 48 | 49 | app.register_blueprint(gather.frontend.bp) 50 | app.register_blueprint(gather.account.bp) 51 | app.register_blueprint(gather.user.bp) 52 | app.register_blueprint(gather.node.bp) 53 | app.register_blueprint(gather.topic.bp) 54 | app.register_blueprint(gather.admin.bp) 55 | 56 | app.register_blueprint(gather.account.api.bp) 57 | app.register_blueprint(gather.node.api.bp) 58 | app.register_blueprint(gather.topic.api.bp) 59 | app.register_blueprint(gather.topic.api.reply_bp) 60 | 61 | 62 | def register_hooks(app): 63 | from gather.account.utils import get_current_user 64 | 65 | @app.before_request 66 | def load_user(): 67 | g.user = get_current_user() 68 | from gather.account.models import Account 69 | 70 | token = request.headers.get("token", None) 71 | if token: 72 | g.token_user = Account.query.filter_by(api_token=token).first() 73 | else: 74 | g.token_user = None 75 | 76 | 77 | def register_jinja(app): 78 | from gather.filters import ( 79 | sanitize, get_site_status, 80 | content_to_html, xmldatetime, 81 | url_for_other_page, url_for_with_token 82 | ) 83 | @app.context_processor 84 | def register_context(): 85 | return dict( 86 | site_status=get_site_status, 87 | ) 88 | 89 | app.jinja_env.filters['sanitize'] = sanitize 90 | app.jinja_env.filters['content_to_html'] = content_to_html 91 | app.jinja_env.filters['xmldatetime'] = xmldatetime 92 | app.jinja_env.globals.update( 93 | url_for_other_page=url_for_other_page, 94 | url_for_with_token=url_for_with_token 95 | ) 96 | 97 | app.jinja_env.bytecode_cache = MemcachedBytecodeCache(cache) 98 | 99 | -------------------------------------------------------------------------------- /gather/assets/javascripts/gather.coffee: -------------------------------------------------------------------------------- 1 | init_at_who = -> 2 | names = [] 3 | 4 | add_name = (ele) -> 5 | name = $(ele).text() 6 | if name not in names 7 | names.push(name) 8 | 9 | for a in $('.user-link') 10 | add_name a 11 | 12 | $('textarea#content').atWho '@', {data: names} 13 | return 14 | ###, callbacks: { 15 | remote_filter: (query, callback) -> 16 | $.ajax { 17 | url: '/api/user', 18 | data: , 19 | dataType: "json", 20 | contentType: "application/json", 21 | success: (data) -> 22 | names = [] 23 | if !data.objects 24 | return callback([]) 25 | for account in data.users 26 | if account.username not in names 27 | names.push(account.username) 28 | callback(names) 29 | } 30 | } 31 | ### 32 | 33 | 34 | have_textarea = -> 35 | $("textarea").length 36 | 37 | resize = -> 38 | $("textarea").scroll -> 39 | $(this).height(this.scrollHeight) 40 | 41 | gather_main = -> 42 | if have_textarea() 43 | resize() 44 | init_at_who() 45 | $("time").timeago(selector: 'time') 46 | 47 | $('.reply-this-floor').click -> 48 | reply = $(this) 49 | floor = reply.data('floor') 50 | user = reply.data('user') 51 | 52 | reply_content = $("#content") 53 | new_text = "##{floor} @#{user} " 54 | if reply_content.val().trim().length is 0 55 | new_text += '' 56 | else 57 | new_text = "\n#{new_text}" 58 | reply_content.focus().val reply_content.val() + new_text 59 | return false 60 | 61 | $('.duixing-this-floor').click -> 62 | reply = $(this) 63 | 64 | content_textarea = $("#content") 65 | content_textarea.focus().val reply.data('content') 66 | return false 67 | 68 | random_int = (max) -> 69 | Math.floor(Math.random()*(max-1)) 70 | 71 | set_random_color = (ele) -> 72 | for child in ele.children() 73 | set_random_color($(child)) 74 | a = random_int(256) 75 | b = random_int(256) 76 | c = random_int(256) 77 | ele.css("color", "rgb(#{a}, #{b}, #{c})") 78 | a = random_int(256) 79 | b = random_int(256) 80 | c = random_int(256) 81 | ele.css("background-color", "rgb(#{a}, #{b}, #{c})") 82 | return 83 | if window.feeling_lucky 84 | set_random_color($("body")) 85 | 86 | gather_page_load = -> 87 | gather_main() 88 | if _gaq 89 | _gaq.push(['_trackPageview']) 90 | 91 | 92 | $(document).on 'page:load', gather_page_load 93 | 94 | $(document).ready gather_main 95 | -------------------------------------------------------------------------------- /gather/assets/javascripts/libs/locales/timeago.zh-cn.coffee: -------------------------------------------------------------------------------- 1 | $.fn.timeago.defaults.lang = 2 | units: 3 | second: "秒" 4 | seconds: "秒" 5 | minute: "分钟" 6 | minutes: "分钟" 7 | hour: "小时" 8 | hours: "小时" 9 | day: "天" 10 | days: "天" 11 | month: "个月" 12 | months: "个月" 13 | year: "年" 14 | years: "年" 15 | prefixes: 16 | lt: "" 17 | about: "" 18 | over: "" 19 | almost: "" 20 | ago: "" 21 | suffix: "之前" 22 | -------------------------------------------------------------------------------- /gather/assets/javascripts/libs/timeago.coffee: -------------------------------------------------------------------------------- 1 | # Smart Time Ago v0.1.1 2 | 3 | # Copyright 2012, Terry Tai, Pragmatic.ly 4 | # https://pragmatic.ly/ 5 | # Licensed under the MIT license. 6 | # https://github.com/pragmaticly/smart-time-ago/blob/master/LICENSE 7 | 8 | class TimeAgo 9 | 10 | constructor: (element, options) -> 11 | @startInterval = 60000 12 | @init(element, options) 13 | 14 | init: (element, options) -> 15 | @$element = $(element) 16 | @options = $.extend({}, $.fn.timeago.defaults, options) 17 | @updateTime() 18 | @startTimer() 19 | 20 | startTimer: -> 21 | self = @ 22 | @interval = setInterval ( -> 23 | self.refresh() 24 | ), @startInterval 25 | 26 | stopTimer: -> 27 | clearInterval(@interval) 28 | 29 | restartTimer: -> 30 | @stopTimer() 31 | @startTimer() 32 | 33 | refresh: -> 34 | @updateTime() 35 | @updateInterval() 36 | 37 | updateTime: -> 38 | self = @ 39 | @$element.findAndSelf(@options.selector).each -> 40 | timeAgoInWords = self.timeAgoInWords($(this).attr(self.options.attr)) 41 | $(this).html(timeAgoInWords) 42 | 43 | updateInterval: -> 44 | if @$element.findAndSelf(@options.selector).length > 0 45 | if @options.dir is "up" 46 | filter = ":first" 47 | else if @options.dir is "down" 48 | filter = ":last" 49 | newestTimeSrc = @$element.findAndSelf(@options.selector).filter(filter).attr(@options.attr) 50 | newestTime = @parse(newestTimeSrc) 51 | newestTimeInMinutes = @getTimeDistanceInMinutes(newestTime) 52 | 53 | if newestTimeInMinutes >= 0 and newestTimeInMinutes <= 44 and @startInterval != 60000 #1 minute 54 | @startInterval = 60000 55 | @restartTimer() 56 | else if newestTimeInMinutes >= 45 and newestTimeInMinutes <= 89 and @startInterval != 60000 * 22 #22 minutes 57 | @startInterval = 60000 * 22 58 | @restartTimer() 59 | else if newestTimeInMinutes >= 90 and newestTimeInMinutes <= 2519 and @startInterval != 60000 * 30 #half hour 60 | @startInterval = 60000 * 30 61 | @restartTimer() 62 | else if newestTimeInMinutes >= 2520 and @startInterval != 60000 * 60 * 12 #half day 63 | @startInterval = 60000 * 60 * 12 64 | @restartTimer() 65 | 66 | timeAgoInWords: (timeString) -> 67 | absolutTime = @parse(timeString) 68 | "#{@options.lang.prefixes.ago}#{@distanceOfTimeInWords(absolutTime)}#{@options.lang.suffix}" 69 | 70 | parse: (iso8601) -> 71 | timeStr = $.trim(iso8601) 72 | timeStr = timeStr.replace(/\.\d+/,"") 73 | timeStr = timeStr.replace(/-/,"/").replace(/-/,"/") 74 | timeStr = timeStr.replace(/T/," ").replace(/Z/," UTC") 75 | timeStr = timeStr.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2") 76 | new Date(timeStr); 77 | 78 | getTimeDistanceInMinutes: (absolutTime) -> 79 | timeDistance = new Date().getTime() - absolutTime.getTime() 80 | Math.round((Math.abs(timeDistance) / 1000) / 60) 81 | 82 | distanceOfTimeInWords: (absolutTime) -> 83 | #TODO support i18n. 84 | dim = @getTimeDistanceInMinutes(absolutTime) #distance in minutes 85 | 86 | if dim == 0 87 | "#{ @options.lang.prefixes.lt } #{ @options.lang.units.minute }" 88 | else if dim == 1 89 | "1 #{ @options.lang.units.minute }" 90 | else if dim >= 2 and dim <= 44 91 | "#{ dim } #{ @options.lang.units.minutes }" 92 | else if dim >= 45 and dim <= 89 93 | "#{ @options.lang.prefixes.about } 1 #{ @options.lang.units.hour }" 94 | else if dim >= 90 and dim <= 1439 95 | "#{ @options.lang.prefixes.about } #{ Math.round(dim / 60) } #{ @options.lang.units.hours }" 96 | else if dim >= 1440 and dim <= 2519 97 | "1 #{ @options.lang.units.day }" 98 | else if dim >= 2520 and dim <= 43199 99 | "#{ Math.round(dim / 1440) } #{ @options.lang.units.days }" 100 | else if dim >= 43200 and dim <= 86399 101 | "#{ @options.lang.prefixes.about } 1 #{ @options.lang.units.month }" 102 | else if dim >= 86400 and dim <= 525599 #1 yr 103 | "#{ Math.round(dim / 43200) } #{ @options.lang.units.months }" 104 | else if dim >= 525600 and dim <= 655199 #1 yr, 3 months 105 | "#{ @options.lang.prefixes.about } 1 #{ @options.lang.units.year }" 106 | else if dim >= 655200 and dim <= 914399 #1 yr, 9 months 107 | "#{ @options.lang.prefixes.over } 1 #{ @options.lang.units.year }" 108 | else if dim >= 914400 and dim <= 1051199 #2 yr minus half minute 109 | "#{ @options.lang.prefixes.almost } 2 #{ @options.lang.units.years }" 110 | else 111 | "#{ @options.lang.prefixes.about } #{ Math.round(dim / 525600) } #{ @options.lang.units.years }" 112 | 113 | $.fn.timeago = (options = {}) -> 114 | @each -> 115 | $this = $(this) 116 | data = $this.data("timeago") 117 | if (!data) 118 | $this.data("timeago", new TimeAgo(this, options)) 119 | else if (typeof options is 'string') 120 | data[options]() 121 | 122 | $.fn.findAndSelf = (selector) -> 123 | this.find(selector).add(this.filter(selector)) 124 | 125 | $.fn.timeago.Constructor = TimeAgo 126 | 127 | $.fn.timeago.defaults = 128 | selector: 'time.timeago' 129 | attr: 'datetime' 130 | dir: 'up' 131 | lang: 132 | units: 133 | second: "second" 134 | seconds: "seconds" 135 | minute: "minute" 136 | minutes: "minutes" 137 | hour: "hour" 138 | hours: "hours" 139 | day: "day" 140 | days: "days" 141 | month: "month" 142 | months: "months" 143 | year: "year" 144 | years: "years" 145 | prefixes: 146 | lt: "less than a" 147 | about: "about" 148 | over: "over" 149 | almost: "almost" 150 | ago: "" 151 | suffix: ' ago' 152 | 153 | -------------------------------------------------------------------------------- /gather/assets/javascripts/turbolinks_icon.coffee: -------------------------------------------------------------------------------- 1 | $(document).on 'page:load', -> 2 | $("#spinner").hide() 3 | 4 | $(document).on 'page:fetch', -> 5 | $("#spinner").show() 6 | 7 | $(document).ready -> 8 | $("#spinner").hide() 9 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/card.sass: -------------------------------------------------------------------------------- 1 | @import "helpers/functions" 2 | @import "helpers/responsive" 3 | 4 | div.card 5 | @extend %card 6 | 7 | div#cards-list 8 | width: 100% 9 | display: block 10 | @include flexbox 11 | @include prefix(flex-wrap, wrap) 12 | @include prefix(flex-direction, row) 13 | @include prefix(justify-content, space-between) 14 | 15 | > div 16 | @include clearfix 17 | @extend .card 18 | width: 250px 19 | max-width: 80% 20 | margin: 15px auto 21 | 22 | img 23 | float: left 24 | height: $medium-avatar-size 25 | width: $medium-avatar-size 26 | border-radius: $medium-avatar-size/2 27 | margin-right: 10px 28 | 29 | &:hover 30 | @include animation(spin .3s infinite linear) 31 | 32 | @include tablets 33 | display: inline-block 34 | width: 300px 35 | margin: 15px 36 | 37 | @include desktop 38 | margin: 15px 0 39 | 40 | 41 | 42 | #pagination-card 43 | width: 100% 44 | background-color: #fff 45 | height: 5em 46 | text-align: center 47 | 48 | ul 49 | line-height: 5em 50 | text-align: center 51 | float: none 52 | 53 | li 54 | float: none 55 | display: inline-block 56 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/footer.sass: -------------------------------------------------------------------------------- 1 | @import "helpers/functions" 2 | @import "helpers/responsive" 3 | @import "helpers/variables" 4 | 5 | #site-footer 6 | border-top: 1px solid black(0.1) 7 | margin-top: 50px 8 | padding: 20px 0 9 | text-align: center 10 | color: #7b7b7b 11 | 12 | width: 100% 13 | @include centered 14 | 15 | @include tablets 16 | width: $tablet-width 17 | 18 | @include desktop 19 | width: $desktop-width 20 | 21 | @include large 22 | width: $large-width 23 | 24 | a 25 | color: $dark-link-color 26 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/form.sass: -------------------------------------------------------------------------------- 1 | @import "vendor/bourbon/bourbon" 2 | @import "helpers/functions" 3 | @import "helpers/variables" 4 | 5 | .form-controls 6 | margin-top: 10px 7 | 8 | div.signal-form 9 | @extend %card 10 | width: 98% 11 | max-width: 500px 12 | @include centered 13 | 14 | h2 15 | text-align: center 16 | 17 | .form-controls 18 | text-align: center 19 | 20 | div.form-field 21 | width: 100% 22 | margin-bottom: 10px 23 | 24 | #content 25 | min-height: 400px 26 | 27 | .form-checkbox 28 | margin-bottom: 10px 29 | input 30 | width: auto 31 | 32 | input 33 | border: 1px solid rgb(238, 238, 238) 34 | padding: 4px 10px 35 | width: 90% 36 | 37 | textarea 38 | @extend input 39 | height: 100px 40 | overflow-y: visible 41 | 42 | select 43 | @extend input 44 | margin-left: 10px 45 | max-width: 200px 46 | -webkit-appearance: none 47 | background: #fff 48 | 49 | div.form-error 50 | color: red 51 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/gather.sass: -------------------------------------------------------------------------------- 1 | @import "vendor/bourbon/bourbon" 2 | @import "vendor/diff" 3 | 4 | @import "vendor/normalize" 5 | @import "helpers/functions" 6 | @import "helpers/responsive" 7 | @import "helpers/variables" 8 | @import "vendor/pygments" 9 | @import "vendor/jquery.atwho" 10 | @import "sphinner" 11 | 12 | @import "grid" 13 | @import "nav" 14 | @import "footer" 15 | @import "icons" 16 | @import "topic" 17 | @import "form" 18 | @import "card" 19 | @import "user" 20 | 21 | html 22 | @include tablets 23 | font-size: 87.5% 24 | 25 | body 26 | font-family: $site-font-family 27 | background-color: #eeeeee 28 | 29 | img 30 | max-width: 100% 31 | height: auto 32 | 33 | a 34 | color: #525DF0 35 | text-decoration: none 36 | 37 | &:hover 38 | color: #d70a16 39 | 40 | &:visited 41 | color: #525DF0 42 | 43 | .pagination 44 | float: right 45 | position: relative 46 | left: -9px 47 | margin-left: 0 48 | list-style: none 49 | 50 | li 51 | float: left 52 | margin-right: 2px 53 | 54 | a, span 55 | display: block 56 | padding: 7px 9px 57 | line-height: 1 58 | border-radius: 3px 59 | 60 | a 61 | color: #0f0f0f 62 | &:focus, &:hover 63 | text-decoration: none 64 | background-color: #0f0f0f 65 | color: #fff 66 | 67 | span 68 | border: 1px solid #ddd 69 | 70 | button, .button 71 | &:disabled, &.disabled 72 | opacity: 0.5 73 | cursor: not-allowed 74 | border: 1px solid black(.3) 75 | &:hover:not(:disabled) 76 | color: black(.9) 77 | border-radius: 3px 78 | padding: 7px 18px 79 | text-decoration: none 80 | color: black(.7) 81 | background-color: #fff 82 | &:visited 83 | color: black(.7) 84 | 85 | .dark-link 86 | color: $dark-link-color 87 | 88 | &:visited 89 | color: $dark-link-color 90 | 91 | &:hover 92 | border-bottom: 1px solid $dark-link-color 93 | 94 | .more 95 | @extend .pull-right, .dark-link -------------------------------------------------------------------------------- /gather/assets/stylesheets/grid.sass: -------------------------------------------------------------------------------- 1 | @import "vendor/bourbon/bourbon" 2 | @import "helpers/functions" 3 | @import "helpers/responsive" 4 | @import "helpers/variables" 5 | 6 | #wraps 7 | width: 100% 8 | @include flexbox 9 | @include centered 10 | @include clearfix 11 | @include prefix(flex-direction, column) 12 | 13 | @include tablets 14 | width: $tablet-width 15 | 16 | @include desktop 17 | @include prefix(flex-direction, row) 18 | width: $desktop-width 19 | 20 | @include large 21 | width: $large-width 22 | 23 | #main 24 | max-width: 90% 25 | @include centered 26 | @include flex(2) 27 | 28 | @include desktop 29 | margin-right: 15px 30 | 31 | #sidebar 32 | max-width: 90% 33 | @include centered 34 | @include flex(1) 35 | 36 | @include desktop 37 | margin-left: 15px 38 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/helpers/functions.sass: -------------------------------------------------------------------------------- 1 | @import "../vendor/bourbon/bourbon" 2 | @import "variables" 3 | 4 | @function black($opacity) 5 | @return rgba(0,0,0, $opacity) 6 | 7 | @function white($opacity) 8 | @return rgba(255,255,255, $opacity) 9 | 10 | @mixin centered 11 | margin-left: auto 12 | margin-right: auto 13 | 14 | @mixin prefix($property, $value) 15 | @include prefixer($property, $value, moz webkit ms o spec) 16 | 17 | @mixin flexbox 18 | display: -moz-flex 19 | display: -webkit-flex 20 | display: -ms-flex 21 | display: -o-flex 22 | display: flex 23 | 24 | @mixin flex($fg: 1, $fs: null, $fb: null) 25 | @include prefix(box-flex, $fg) 26 | @include prefix(flex, $fg $fs $fb) 27 | 28 | @mixin border-box 29 | @include prefix(box-sizing, border-box) 30 | 31 | %card 32 | @include border-box 33 | border-radius: 5px 34 | vertical-align: top 35 | overflow: hidden 36 | padding: 20px 37 | margin-bottom: 10px 38 | word-break: break-all 39 | background-color: #fff 40 | 41 | header 42 | font-size: 1.2em 43 | padding-bottom: 10px 44 | margin-bottom: 10px 45 | border-bottom: 1px solid black(.1) 46 | 47 | footer 48 | border-top: 1px solid black(.1) 49 | padding-top: 10px 50 | margin-top: 10px 51 | 52 | header + footer 53 | border-top: none 54 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/helpers/responsive.sass: -------------------------------------------------------------------------------- 1 | =tablets 2 | @media (min-width: 768px) 3 | @content 4 | 5 | =desktop 6 | @media (min-width: 992px) 7 | @content 8 | 9 | =large 10 | @media (min-width: 1200px) 11 | @content 12 | 13 | =retina 14 | @media only all and (-webkit-min-device-pixel-ratio: 1.3), only all and (min--moz-device-pixel-ratio: 1.3), only all and (-o-min-device-pixel-ratio: 1.3 / 1), only all and (min-device-pixel-ratio: 1.3), only all and (min-resolution: 1.3dppx) 15 | @content 16 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/helpers/variables.sass: -------------------------------------------------------------------------------- 1 | $tablet-width: 740px 2 | $desktop-width: 900px 3 | $large-width: 1080px 4 | 5 | $dark-link-color: #555555 6 | 7 | $small-avatar-size: 48px 8 | $medium-avatar-size: 60px 9 | $large-avatar-size: 120px 10 | 11 | $button-color: lighten(blue, 70%) 12 | $hover-red: rgba(255, 0, 0, .6) 13 | 14 | $site-font-family: "Helvetica Neue", Helvetica, Arial, "Hiragino Sans GB", "Microsoft YaHei", "Wenquanyi Micro Hei", "WenQuanYi Micro Hei Mono", "WenQuanYi Zen Hei", "WenQuanYi Zen Hei", "Apple LiGothic Medium", "SimHei", "ST Heiti", "WenQuanYi Zen Hei Sharp", sans-serif 15 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/icons.sass: -------------------------------------------------------------------------------- 1 | @import "vendor/bourbon/bourbon" 2 | @import "vendor/font-awesome" 3 | @import "helpers/variables" 4 | 5 | i.icon-settings 6 | @extend .fa 7 | @extend .fa-gear 8 | float: right 9 | color: slategray 10 | 11 | &:hover 12 | @include animation(spin 1s infinite linear) 13 | 14 | i.icon-edit 15 | @extend .fa 16 | @extend .fa-pencil 17 | color: slategray 18 | 19 | &:hover 20 | color: $hover-red 21 | 22 | i.icon-remove 23 | @extend .fa 24 | @extend .fa-trash-o 25 | color: slategray 26 | 27 | &:hover 28 | color: $hover-red 29 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/nav.sass: -------------------------------------------------------------------------------- 1 | @import "helpers/functions" 2 | @import "helpers/responsive" 3 | @import "helpers/variables" 4 | 5 | #nav 6 | vertical-align: middle 7 | @include centered 8 | width: 100% 9 | 10 | a 11 | font-size: 1.3em 12 | height: 50px 13 | line-height: 50px 14 | color: #5C6576 15 | 16 | &:hover 17 | color: lighten(#5C6576, 20%) 18 | 19 | @include tablets 20 | width: $tablet-width 21 | 22 | @include desktop 23 | width: $desktop-width 24 | 25 | @include large 26 | width: $large-width 27 | 28 | > div 29 | @include centered 30 | display: block 31 | text-align: center 32 | 33 | @include tablets 34 | text-align: left 35 | 36 | > a 37 | padding-left: 15px 38 | padding-right: 15px 39 | 40 | #nav-wrapper 41 | border-bottom: 1px solid black(.1) 42 | margin-top: 0 43 | padding-top: 0 44 | min-height: 50px 45 | margin-bottom: 20px 46 | background-color: #fff 47 | @include clearfix 48 | 49 | a 50 | text-decoration: none 51 | 52 | #nav-main 53 | @include tablets 54 | float: left 55 | 56 | i.fa 57 | display: none 58 | @include tablets 59 | display: inline-block 60 | margin-right: 3px 61 | 62 | #nav-user 63 | @include tablets 64 | float: right 65 | 66 | a#site-name 67 | color: #000000 68 | 69 | display: block 70 | text-align: center 71 | 72 | @include tablets 73 | display: inline-block 74 | float: left 75 | text-align: left 76 | margin-right: 10px 77 | margin-bottom: 0 78 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/sphinner.scss: -------------------------------------------------------------------------------- 1 | @import "helpers/functions"; 2 | 3 | #spinner { 4 | width: 50px; 5 | height: 30px; 6 | text-align: center; 7 | font-size: 10px; 8 | position: fixed; 9 | bottom: 10px; 10 | left: 10px; 11 | } 12 | 13 | #spinner > div { 14 | background-color: black(.6); 15 | height: 100%; 16 | width: 6px; 17 | display: inline-block; 18 | 19 | -webkit-animation: stretchdelay 1.2s infinite ease-in-out; 20 | animation: stretchdelay 1.2s infinite ease-in-out; 21 | } 22 | 23 | #spinner .rect2 { 24 | -webkit-animation-delay: -1.1s; 25 | animation-delay: -1.1s; 26 | } 27 | 28 | #spinner .rect3 { 29 | -webkit-animation-delay: -1.0s; 30 | animation-delay: -1.0s; 31 | } 32 | 33 | #spinner .rect4 { 34 | -webkit-animation-delay: -0.9s; 35 | animation-delay: -0.9s; 36 | } 37 | 38 | #spinner .rect5 { 39 | -webkit-animation-delay: -0.8s; 40 | animation-delay: -0.8s; 41 | } 42 | 43 | @-webkit-keyframes stretchdelay { 44 | 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } 45 | 20% { -webkit-transform: scaleY(1.0) } 46 | } 47 | 48 | @keyframes stretchdelay { 49 | 0%, 40%, 100% { 50 | transform: scaleY(0.4); 51 | -webkit-transform: scaleY(0.4); 52 | } 20% { 53 | transform: scaleY(1.0); 54 | -webkit-transform: scaleY(1.0); 55 | } 56 | } -------------------------------------------------------------------------------- /gather/assets/stylesheets/topic.sass: -------------------------------------------------------------------------------- 1 | @import "helpers/functions" 2 | @import "helpers/variables" 3 | 4 | .topic-list 5 | @include border-box 6 | @include centered 7 | @include clearfix 8 | min-height: $small-avatar-size 9 | padding-bottom: 10px 10 | padding-top: 10px 11 | border-bottom: 1px solid black(.1) 12 | padding-left: $small-avatar-size/2 + 10px 13 | margin-left: $small-avatar-size/2 14 | border-left: 1px solid black(.2) 15 | position: relative 16 | 17 | .avatar 18 | margin-left: -($small-avatar-size + 10px) 19 | height: $small-avatar-size 20 | width: $small-avatar-size 21 | border-radius: $small-avatar-size/2 22 | 23 | &:last-of-type 24 | border-bottom: none 25 | 26 | .topic-meta 27 | margin-top: 9px 28 | 29 | #topic-control 30 | float: right 31 | 32 | a 33 | margin-left: 5px 34 | 35 | @include desktop 36 | display: none 37 | 38 | header:hover > & 39 | display: block 40 | 41 | .node-label 42 | @extend .dark-link 43 | margin-left: 3px 44 | 45 | .topic-reply-count 46 | float: right 47 | line-height: 20px 48 | background-color: slateblue 49 | border-radius: 15px 50 | padding: 0 9px 51 | margin-top: $small-avatar-size/2 - 10px 52 | color: white 53 | &:hover 54 | color: white 55 | &:visited, &.visited 56 | color: white 57 | background-color: slategray 58 | 59 | .reply-list 60 | @extend .topic-list 61 | 62 | .reply-meta 63 | display: inline-block 64 | vertical-align: text-top 65 | 66 | .reply-floor 67 | color: slategray 68 | float: right 69 | margin-right: 10px 70 | 71 | .reply-control 72 | float: right 73 | margin-right: 10px 74 | 75 | a 76 | color: slategray 77 | margin-left: 5px 78 | 79 | &:visited 80 | color: slategray 81 | 82 | &:hover 83 | color: $hover-red 84 | 85 | @include desktop 86 | display: none 87 | .reply-list:hover > & 88 | display: block 89 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/user.sass: -------------------------------------------------------------------------------- 1 | @import "helpers/functions" 2 | @import "helpers/variables" 3 | 4 | .avatar 5 | float: left 6 | vertical-align: top 7 | 8 | &:hover 9 | @include animation(spin .4s infinite linear) 10 | 11 | div.sidebar-user-card 12 | @extend .card 13 | header 14 | img 15 | width: $medium-avatar-size 16 | height: $medium-avatar-size 17 | border-radius: $medium-avatar-size/2 18 | float: left 19 | margin-right: 10px 20 | 21 | &:hover 22 | @include animation(spin .3s infinite linear) 23 | @include clearfix 24 | 25 | .small 26 | padding-top: 10px 27 | 28 | div#user-profile 29 | @extend .card 30 | header 31 | text-align: center 32 | .avatar 33 | width: $large-avatar-size 34 | height: $large-avatar-size 35 | border-radius: $large-avatar-size/2 36 | float: none 37 | display: block 38 | @include centered 39 | 40 | &:hover 41 | @include animation(spin .2s infinite linear) 42 | 43 | ul#user-profile-info 44 | text-align: center 45 | list-style: none 46 | li 47 | margin-bottom: 10px 48 | i.fa 49 | margin-right: 8px 50 | 51 | .user-link 52 | @extend .dark-link -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/_diff.scss: -------------------------------------------------------------------------------- 1 | .diff { 2 | border: 1px solid #cccccc; 3 | background: none repeat scroll 0 0 #f8f8f8; 4 | font-family: 'Bitstream Vera Sans Mono','Courier',monospace; 5 | font-size: 12px; 6 | line-height: 1.4; 7 | white-space: normal; 8 | } 9 | .diff div:hover { 10 | background-color:#ffc; 11 | } 12 | .diff .control { 13 | background-color: #eaf2f5; 14 | color: #999999; 15 | } 16 | .diff .insert { 17 | background-color: #ddffdd; 18 | color: #000000; 19 | } 20 | .diff .insert .highlight { 21 | background-color: #aaffaa; 22 | color: #000000; 23 | } 24 | .diff .delete { 25 | background-color: #ffdddd; 26 | color: #000000; 27 | } 28 | .diff .delete .highlight { 29 | background-color: #ffaaaa; 30 | color: #000000; 31 | } -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/_jquery.atwho.scss: -------------------------------------------------------------------------------- 1 | #at-view { 2 | position:absolute; 3 | top: 0; 4 | left: 0; 5 | display: none; 6 | margin-top: 18px; 7 | background: white; 8 | border: 1px solid #DDD; 9 | border-radius: 3px; 10 | box-shadow: 0 0 5px rgba(0,0,0,0.1); 11 | min-width: 120px; 12 | } 13 | 14 | #at-view .cur { 15 | background: #3366FF; 16 | color: white; 17 | } 18 | #at-view .cur small { 19 | color: white; 20 | } 21 | #at-view strong { 22 | color: #3366FF; 23 | } 24 | #at-view .cur strong { 25 | color: white; 26 | font:bold; 27 | } 28 | #at-view ul { 29 | /* width: 100px; */ 30 | list-style:none; 31 | padding:0; 32 | margin:auto; 33 | } 34 | #at-view ul li { 35 | display: block; 36 | padding: 5px 10px; 37 | border-bottom: 1px solid #DDD; 38 | cursor: pointer; 39 | /* border-top: 1px solid #C8C8C8; */ 40 | } 41 | #at-view small { 42 | font-size: smaller; 43 | color: #777; 44 | font-weight: normal; 45 | } 46 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/_normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.0 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined in IE 8/9. 28 | */ 29 | 30 | article, 31 | aside, 32 | details, 33 | figcaption, 34 | figure, 35 | footer, 36 | header, 37 | hgroup, 38 | main, 39 | nav, 40 | section, 41 | summary { 42 | display: block; 43 | } 44 | 45 | /** 46 | * 1. Correct `inline-block` display not defined in IE 8/9. 47 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 48 | */ 49 | 50 | audio, 51 | canvas, 52 | progress, 53 | video { 54 | display: inline-block; /* 1 */ 55 | vertical-align: baseline; /* 2 */ 56 | } 57 | 58 | /** 59 | * Prevent modern browsers from displaying `audio` without controls. 60 | * Remove excess height in iOS 5 devices. 61 | */ 62 | 63 | audio:not([controls]) { 64 | display: none; 65 | height: 0; 66 | } 67 | 68 | /** 69 | * Address `[hidden]` styling not present in IE 8/9. 70 | * Hide the `template` element in IE, Safari, and Firefox < 22. 71 | */ 72 | 73 | [hidden], 74 | template { 75 | display: none; 76 | } 77 | 78 | /* Links 79 | ========================================================================== */ 80 | 81 | /** 82 | * Remove the gray background color from active links in IE 10. 83 | */ 84 | 85 | a { 86 | background: transparent; 87 | } 88 | 89 | /** 90 | * Improve readability when focused and also mouse hovered in all browsers. 91 | */ 92 | 93 | a:active, 94 | a:hover { 95 | outline: 0; 96 | } 97 | 98 | /* Text-level semantics 99 | ========================================================================== */ 100 | 101 | /** 102 | * Address styling not present in IE 8/9, Safari 5, and Chrome. 103 | */ 104 | 105 | abbr[title] { 106 | border-bottom: 1px dotted; 107 | } 108 | 109 | /** 110 | * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 111 | */ 112 | 113 | b, 114 | strong { 115 | font-weight: bold; 116 | } 117 | 118 | /** 119 | * Address styling not present in Safari 5 and Chrome. 120 | */ 121 | 122 | dfn { 123 | font-style: italic; 124 | } 125 | 126 | /** 127 | * Address variable `h1` font-size and margin within `section` and `article` 128 | * contexts in Firefox 4+, Safari 5, and Chrome. 129 | */ 130 | 131 | h1 { 132 | font-size: 2em; 133 | margin: 0.67em 0; 134 | } 135 | 136 | /** 137 | * Address styling not present in IE 8/9. 138 | */ 139 | 140 | mark { 141 | background: #ff0; 142 | color: #000; 143 | } 144 | 145 | /** 146 | * Address inconsistent and variable font size in all browsers. 147 | */ 148 | 149 | small { 150 | font-size: 80%; 151 | } 152 | 153 | /** 154 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 155 | */ 156 | 157 | sub, 158 | sup { 159 | font-size: 75%; 160 | line-height: 0; 161 | position: relative; 162 | vertical-align: baseline; 163 | } 164 | 165 | sup { 166 | top: -0.5em; 167 | } 168 | 169 | sub { 170 | bottom: -0.25em; 171 | } 172 | 173 | /* Embedded content 174 | ========================================================================== */ 175 | 176 | /** 177 | * Remove border when inside `a` element in IE 8/9. 178 | */ 179 | 180 | img { 181 | border: 0; 182 | } 183 | 184 | /** 185 | * Correct overflow displayed oddly in IE 9. 186 | */ 187 | 188 | svg:not(:root) { 189 | overflow: hidden; 190 | } 191 | 192 | /* Grouping content 193 | ========================================================================== */ 194 | 195 | /** 196 | * Address margin not present in IE 8/9 and Safari 5. 197 | */ 198 | 199 | figure { 200 | margin: 1em 40px; 201 | } 202 | 203 | /** 204 | * Address differences between Firefox and other browsers. 205 | */ 206 | 207 | hr { 208 | -moz-box-sizing: content-box; 209 | box-sizing: content-box; 210 | height: 0; 211 | } 212 | 213 | /** 214 | * Contain overflow in all browsers. 215 | */ 216 | 217 | pre { 218 | overflow: auto; 219 | } 220 | 221 | /** 222 | * Address odd `em`-unit font size rendering in all browsers. 223 | */ 224 | 225 | code, 226 | kbd, 227 | pre, 228 | samp { 229 | font-family: monospace, monospace; 230 | font-size: 1em; 231 | } 232 | 233 | /* Forms 234 | ========================================================================== */ 235 | 236 | /** 237 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 238 | * styling of `select`, unless a `border` property is set. 239 | */ 240 | 241 | /** 242 | * 1. Correct color not being inherited. 243 | * Known issue: affects color of disabled elements. 244 | * 2. Correct font properties not being inherited. 245 | * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. 246 | */ 247 | 248 | button, 249 | input, 250 | optgroup, 251 | select, 252 | textarea { 253 | color: inherit; /* 1 */ 254 | font: inherit; /* 2 */ 255 | margin: 0; /* 3 */ 256 | } 257 | 258 | /** 259 | * Address `overflow` set to `hidden` in IE 8/9/10. 260 | */ 261 | 262 | button { 263 | overflow: visible; 264 | } 265 | 266 | /** 267 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 268 | * All other form control elements do not inherit `text-transform` values. 269 | * Correct `button` style inheritance in Firefox, IE 8+, and Opera 270 | * Correct `select` style inheritance in Firefox. 271 | */ 272 | 273 | button, 274 | select { 275 | text-transform: none; 276 | } 277 | 278 | /** 279 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 280 | * and `video` controls. 281 | * 2. Correct inability to style clickable `input` types in iOS. 282 | * 3. Improve usability and consistency of cursor style between image-type 283 | * `input` and others. 284 | */ 285 | 286 | button, 287 | html input[type="button"], /* 1 */ 288 | input[type="reset"], 289 | input[type="submit"] { 290 | -webkit-appearance: button; /* 2 */ 291 | cursor: pointer; /* 3 */ 292 | } 293 | 294 | /** 295 | * Re-set default cursor for disabled elements. 296 | */ 297 | 298 | button[disabled], 299 | html input[disabled] { 300 | cursor: default; 301 | } 302 | 303 | /** 304 | * Remove inner padding and border in Firefox 4+. 305 | */ 306 | 307 | button::-moz-focus-inner, 308 | input::-moz-focus-inner { 309 | border: 0; 310 | padding: 0; 311 | } 312 | 313 | /** 314 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 315 | * the UA stylesheet. 316 | */ 317 | 318 | input { 319 | line-height: normal; 320 | } 321 | 322 | /** 323 | * It's recommended that you don't attempt to style these elements. 324 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 325 | * 326 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 327 | * 2. Remove excess padding in IE 8/9/10. 328 | */ 329 | 330 | input[type="checkbox"], 331 | input[type="radio"] { 332 | box-sizing: border-box; /* 1 */ 333 | padding: 0; /* 2 */ 334 | } 335 | 336 | /** 337 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 338 | * `font-size` values of the `input`, it causes the cursor style of the 339 | * decrement button to change from `default` to `text`. 340 | */ 341 | 342 | input[type="number"]::-webkit-inner-spin-button, 343 | input[type="number"]::-webkit-outer-spin-button { 344 | height: auto; 345 | } 346 | 347 | /** 348 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 349 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome 350 | * (include `-moz` to future-proof). 351 | */ 352 | 353 | input[type="search"] { 354 | -webkit-appearance: textfield; /* 1 */ 355 | -moz-box-sizing: content-box; 356 | -webkit-box-sizing: content-box; /* 2 */ 357 | box-sizing: content-box; 358 | } 359 | 360 | /** 361 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 362 | * Safari (but not Chrome) clips the cancel button when the search input has 363 | * padding (and `textfield` appearance). 364 | */ 365 | 366 | input[type="search"]::-webkit-search-cancel-button, 367 | input[type="search"]::-webkit-search-decoration { 368 | -webkit-appearance: none; 369 | } 370 | 371 | /** 372 | * Define consistent border, margin, and padding. 373 | */ 374 | 375 | fieldset { 376 | border: 1px solid #c0c0c0; 377 | margin: 0 2px; 378 | padding: 0.35em 0.625em 0.75em; 379 | } 380 | 381 | /** 382 | * 1. Correct `color` not being inherited in IE 8/9. 383 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 384 | */ 385 | 386 | legend { 387 | border: 0; /* 1 */ 388 | padding: 0; /* 2 */ 389 | } 390 | 391 | /** 392 | * Remove default vertical scrollbar in IE 8/9. 393 | */ 394 | 395 | textarea { 396 | overflow: auto; 397 | } 398 | 399 | /** 400 | * Don't inherit the `font-weight` (applied by a rule above). 401 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 402 | */ 403 | 404 | optgroup { 405 | font-weight: bold; 406 | } 407 | 408 | /* Tables 409 | ========================================================================== */ 410 | 411 | /** 412 | * Remove most spacing between table cells. 413 | */ 414 | 415 | table { 416 | border-collapse: collapse; 417 | border-spacing: 0; 418 | } 419 | 420 | td, 421 | th { 422 | padding: 0; 423 | } 424 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/_pygments.scss: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight { background: #f0f0f0; } 3 | .highlight .c { color: #60a0b0; font-style: italic } /* Comment */ 4 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 5 | .highlight .k { color: #007020; font-weight: bold } /* Keyword */ 6 | .highlight .o { color: #666666 } /* Operator */ 7 | .highlight .cm { color: #60a0b0; font-style: italic } /* Comment.Multiline */ 8 | .highlight .cp { color: #007020 } /* Comment.Preproc */ 9 | .highlight .c1 { color: #60a0b0; font-style: italic } /* Comment.Single */ 10 | .highlight .cs { color: #60a0b0; background-color: #fff0f0 } /* Comment.Special */ 11 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 12 | .highlight .ge { font-style: italic } /* Generic.Emph */ 13 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 14 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 15 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 16 | .highlight .go { color: #808080 } /* Generic.Output */ 17 | .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 18 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 19 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 20 | .highlight .gt { color: #0040D0 } /* Generic.Traceback */ 21 | .highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 22 | .highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 23 | .highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 24 | .highlight .kp { color: #007020 } /* Keyword.Pseudo */ 25 | .highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 26 | .highlight .kt { color: #902000 } /* Keyword.Type */ 27 | .highlight .m { color: #40a070 } /* Literal.Number */ 28 | .highlight .s { color: #4070a0 } /* Literal.String */ 29 | .highlight .na { color: #4070a0 } /* Name.Attribute */ 30 | .highlight .nb { color: #007020 } /* Name.Builtin */ 31 | .highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 32 | .highlight .no { color: #60add5 } /* Name.Constant */ 33 | .highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 34 | .highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 35 | .highlight .ne { color: #007020 } /* Name.Exception */ 36 | .highlight .nf { color: #06287e } /* Name.Function */ 37 | .highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ 38 | .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 39 | .highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ 40 | .highlight .nv { color: #bb60d5 } /* Name.Variable */ 41 | .highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ 42 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 43 | .highlight .mf { color: #40a070 } /* Literal.Number.Float */ 44 | .highlight .mh { color: #40a070 } /* Literal.Number.Hex */ 45 | .highlight .mi { color: #40a070 } /* Literal.Number.Integer */ 46 | .highlight .mo { color: #40a070 } /* Literal.Number.Oct */ 47 | .highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ 48 | .highlight .sc { color: #4070a0 } /* Literal.String.Char */ 49 | .highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 50 | .highlight .s2 { color: #4070a0 } /* Literal.String.Double */ 51 | .highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 52 | .highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ 53 | .highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 54 | .highlight .sx { color: #c65d09 } /* Literal.String.Other */ 55 | .highlight .sr { color: #235388 } /* Literal.String.Regex */ 56 | .highlight .s1 { color: #4070a0 } /* Literal.String.Single */ 57 | .highlight .ss { color: #517918 } /* Literal.String.Symbol */ 58 | .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 59 | .highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ 60 | .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ 61 | .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ 62 | .highlight .il { color: #40a070 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/_bourbon-deprecated-upcoming.scss: -------------------------------------------------------------------------------- 1 | //************************************************************************// 2 | // These mixins/functions are deprecated 3 | // They will be removed in the next MAJOR version release 4 | //************************************************************************// 5 | @mixin box-shadow ($shadows...) { 6 | @include prefixer(box-shadow, $shadows, spec); 7 | @warn "box-shadow is deprecated and will be removed in the next major version release"; 8 | } 9 | 10 | @mixin background-size ($lengths...) { 11 | @include prefixer(background-size, $lengths, spec); 12 | @warn "background-size is deprecated and will be removed in the next major version release"; 13 | } 14 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/_bourbon.scss: -------------------------------------------------------------------------------- 1 | // Custom Helpers 2 | @import "helpers/deprecated-webkit-gradient"; 3 | @import "helpers/gradient-positions-parser"; 4 | @import "helpers/linear-positions-parser"; 5 | @import "helpers/radial-arg-parser"; 6 | @import "helpers/radial-positions-parser"; 7 | @import "helpers/render-gradients"; 8 | @import "helpers/shape-size-stripper"; 9 | 10 | // Custom Functions 11 | @import "functions/compact"; 12 | @import "functions/flex-grid"; 13 | @import "functions/grid-width"; 14 | @import "functions/linear-gradient"; 15 | @import "functions/modular-scale"; 16 | @import "functions/px-to-em"; 17 | @import "functions/radial-gradient"; 18 | @import "functions/tint-shade"; 19 | @import "functions/transition-property-name"; 20 | 21 | // CSS3 Mixins 22 | @import "css3/animation"; 23 | @import "css3/appearance"; 24 | @import "css3/backface-visibility"; 25 | @import "css3/background"; 26 | @import "css3/background-image"; 27 | @import "css3/border-image"; 28 | @import "css3/border-radius"; 29 | @import "css3/box-sizing"; 30 | @import "css3/columns"; 31 | @import "css3/flex-box"; 32 | @import "css3/font-face"; 33 | @import "css3/hidpi-media-query"; 34 | @import "css3/image-rendering"; 35 | @import "css3/inline-block"; 36 | @import "css3/keyframes"; 37 | @import "css3/linear-gradient"; 38 | @import "css3/perspective"; 39 | @import "css3/radial-gradient"; 40 | @import "css3/transform"; 41 | @import "css3/transition"; 42 | @import "css3/user-select"; 43 | @import "css3/placeholder"; 44 | 45 | // Addons & other mixins 46 | @import "addons/button"; 47 | @import "addons/clearfix"; 48 | @import "addons/font-family"; 49 | @import "addons/hide-text"; 50 | @import "addons/html5-input-types"; 51 | @import "addons/position"; 52 | @import "addons/prefixer"; 53 | @import "addons/retina-image"; 54 | @import "addons/size"; 55 | @import "addons/timing-functions"; 56 | @import "addons/triangle"; 57 | 58 | // Soon to be deprecated Mixins 59 | @import "bourbon-deprecated-upcoming"; 60 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/addons/_button.scss: -------------------------------------------------------------------------------- 1 | @mixin button ($style: simple, $base-color: #4294f0) { 2 | 3 | @if type-of($style) == color { 4 | $base-color: $style; 5 | $style: simple; 6 | } 7 | 8 | // Grayscale button 9 | @if $base-color == grayscale($base-color) { 10 | @if $style == simple { 11 | @include simple($base-color, $grayscale: true); 12 | } 13 | 14 | @else if $style == shiny { 15 | @include shiny($base-color, $grayscale: true); 16 | } 17 | 18 | @else if $style == pill { 19 | @include pill($base-color, $grayscale: true); 20 | } 21 | } 22 | 23 | // Colored button 24 | @else { 25 | @if $style == simple { 26 | @include simple($base-color); 27 | } 28 | 29 | @else if $style == shiny { 30 | @include shiny($base-color); 31 | } 32 | 33 | @else if $style == pill { 34 | @include pill($base-color); 35 | } 36 | } 37 | 38 | &:disabled { 39 | opacity: 0.5; 40 | cursor: not-allowed; 41 | } 42 | } 43 | 44 | 45 | // Simple Button 46 | //************************************************************************// 47 | @mixin simple($base-color, $grayscale: false) { 48 | $color: hsl(0, 0, 100%); 49 | $border: adjust-color($base-color, $saturation: 9%, $lightness: -14%); 50 | $inset-shadow: adjust-color($base-color, $saturation: -8%, $lightness: 15%); 51 | $stop-gradient: adjust-color($base-color, $saturation: 9%, $lightness: -11%); 52 | $text-shadow: adjust-color($base-color, $saturation: 15%, $lightness: -18%); 53 | 54 | @if lightness($base-color) > 70% { 55 | $color: hsl(0, 0, 20%); 56 | $text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%); 57 | } 58 | 59 | @if $grayscale == true { 60 | $border: grayscale($border); 61 | $inset-shadow: grayscale($inset-shadow); 62 | $stop-gradient: grayscale($stop-gradient); 63 | $text-shadow: grayscale($text-shadow); 64 | } 65 | 66 | border: 1px solid $border; 67 | border-radius: 3px; 68 | box-shadow: inset 0 1px 0 0 $inset-shadow; 69 | color: $color; 70 | display: inline-block; 71 | font-size: 11px; 72 | font-weight: bold; 73 | @include linear-gradient ($base-color, $stop-gradient); 74 | padding: 7px 18px; 75 | text-decoration: none; 76 | text-shadow: 0 1px 0 $text-shadow; 77 | background-clip: padding-box; 78 | 79 | &:hover:not(:disabled) { 80 | $base-color-hover: adjust-color($base-color, $saturation: -4%, $lightness: -5%); 81 | $inset-shadow-hover: adjust-color($base-color, $saturation: -7%, $lightness: 5%); 82 | $stop-gradient-hover: adjust-color($base-color, $saturation: 8%, $lightness: -14%); 83 | 84 | @if $grayscale == true { 85 | $base-color-hover: grayscale($base-color-hover); 86 | $inset-shadow-hover: grayscale($inset-shadow-hover); 87 | $stop-gradient-hover: grayscale($stop-gradient-hover); 88 | } 89 | 90 | box-shadow: inset 0 1px 0 0 $inset-shadow-hover; 91 | cursor: pointer; 92 | @include linear-gradient ($base-color-hover, $stop-gradient-hover); 93 | } 94 | 95 | &:active:not(:disabled) { 96 | $border-active: adjust-color($base-color, $saturation: 9%, $lightness: -14%); 97 | $inset-shadow-active: adjust-color($base-color, $saturation: 7%, $lightness: -17%); 98 | 99 | @if $grayscale == true { 100 | $border-active: grayscale($border-active); 101 | $inset-shadow-active: grayscale($inset-shadow-active); 102 | } 103 | 104 | border: 1px solid $border-active; 105 | box-shadow: inset 0 0 8px 4px $inset-shadow-active, inset 0 0 8px 4px $inset-shadow-active, 0 1px 1px 0 #eee; 106 | } 107 | } 108 | 109 | 110 | // Shiny Button 111 | //************************************************************************// 112 | @mixin shiny($base-color, $grayscale: false) { 113 | $color: hsl(0, 0, 100%); 114 | $border: adjust-color($base-color, $red: -117, $green: -111, $blue: -81); 115 | $border-bottom: adjust-color($base-color, $red: -126, $green: -127, $blue: -122); 116 | $fourth-stop: adjust-color($base-color, $red: -79, $green: -70, $blue: -46); 117 | $inset-shadow: adjust-color($base-color, $red: 37, $green: 29, $blue: 12); 118 | $second-stop: adjust-color($base-color, $red: -56, $green: -50, $blue: -33); 119 | $text-shadow: adjust-color($base-color, $red: -140, $green: -141, $blue: -114); 120 | $third-stop: adjust-color($base-color, $red: -86, $green: -75, $blue: -48); 121 | 122 | @if lightness($base-color) > 70% { 123 | $color: hsl(0, 0, 20%); 124 | $text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%); 125 | } 126 | 127 | @if $grayscale == true { 128 | $border: grayscale($border); 129 | $border-bottom: grayscale($border-bottom); 130 | $fourth-stop: grayscale($fourth-stop); 131 | $inset-shadow: grayscale($inset-shadow); 132 | $second-stop: grayscale($second-stop); 133 | $text-shadow: grayscale($text-shadow); 134 | $third-stop: grayscale($third-stop); 135 | } 136 | 137 | border: 1px solid $border; 138 | border-bottom: 1px solid $border-bottom; 139 | border-radius: 5px; 140 | box-shadow: inset 0 1px 0 0 $inset-shadow; 141 | color: $color; 142 | display: inline-block; 143 | font-size: 14px; 144 | font-weight: bold; 145 | @include linear-gradient(top, $base-color 0%, $second-stop 50%, $third-stop 50%, $fourth-stop 100%); 146 | padding: 8px 20px; 147 | text-align: center; 148 | text-decoration: none; 149 | text-shadow: 0 -1px 1px $text-shadow; 150 | 151 | &:hover:not(:disabled) { 152 | $first-stop-hover: adjust-color($base-color, $red: -13, $green: -15, $blue: -18); 153 | $second-stop-hover: adjust-color($base-color, $red: -66, $green: -62, $blue: -51); 154 | $third-stop-hover: adjust-color($base-color, $red: -93, $green: -85, $blue: -66); 155 | $fourth-stop-hover: adjust-color($base-color, $red: -86, $green: -80, $blue: -63); 156 | 157 | @if $grayscale == true { 158 | $first-stop-hover: grayscale($first-stop-hover); 159 | $second-stop-hover: grayscale($second-stop-hover); 160 | $third-stop-hover: grayscale($third-stop-hover); 161 | $fourth-stop-hover: grayscale($fourth-stop-hover); 162 | } 163 | 164 | cursor: pointer; 165 | @include linear-gradient(top, $first-stop-hover 0%, 166 | $second-stop-hover 50%, 167 | $third-stop-hover 50%, 168 | $fourth-stop-hover 100%); 169 | } 170 | 171 | &:active:not(:disabled) { 172 | $inset-shadow-active: adjust-color($base-color, $red: -111, $green: -116, $blue: -122); 173 | 174 | @if $grayscale == true { 175 | $inset-shadow-active: grayscale($inset-shadow-active); 176 | } 177 | 178 | box-shadow: inset 0 0 20px 0 $inset-shadow-active, 0 1px 0 #fff; 179 | } 180 | } 181 | 182 | 183 | // Pill Button 184 | //************************************************************************// 185 | @mixin pill($base-color, $grayscale: false) { 186 | $color: hsl(0, 0, 100%); 187 | $border-bottom: adjust-color($base-color, $hue: 8, $saturation: -11%, $lightness: -26%); 188 | $border-sides: adjust-color($base-color, $hue: 4, $saturation: -21%, $lightness: -21%); 189 | $border-top: adjust-color($base-color, $hue: -1, $saturation: -30%, $lightness: -15%); 190 | $inset-shadow: adjust-color($base-color, $hue: -1, $saturation: -1%, $lightness: 7%); 191 | $stop-gradient: adjust-color($base-color, $hue: 8, $saturation: 14%, $lightness: -10%); 192 | $text-shadow: adjust-color($base-color, $hue: 5, $saturation: -19%, $lightness: -15%); 193 | 194 | @if lightness($base-color) > 70% { 195 | $color: hsl(0, 0, 20%); 196 | $text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%); 197 | } 198 | 199 | @if $grayscale == true { 200 | $border-bottom: grayscale($border-bottom); 201 | $border-sides: grayscale($border-sides); 202 | $border-top: grayscale($border-top); 203 | $inset-shadow: grayscale($inset-shadow); 204 | $stop-gradient: grayscale($stop-gradient); 205 | $text-shadow: grayscale($text-shadow); 206 | } 207 | 208 | border: 1px solid $border-top; 209 | border-color: $border-top $border-sides $border-bottom; 210 | border-radius: 16px; 211 | box-shadow: inset 0 1px 0 0 $inset-shadow, 0 1px 2px 0 #b3b3b3; 212 | color: $color; 213 | display: inline-block; 214 | font-size: 11px; 215 | font-weight: normal; 216 | line-height: 1; 217 | @include linear-gradient ($base-color, $stop-gradient); 218 | padding: 5px 16px; 219 | text-align: center; 220 | text-decoration: none; 221 | text-shadow: 0 -1px 1px $text-shadow; 222 | background-clip: padding-box; 223 | 224 | &:hover:not(:disabled) { 225 | $base-color-hover: adjust-color($base-color, $lightness: -4.5%); 226 | $border-bottom: adjust-color($base-color, $hue: 8, $saturation: 13.5%, $lightness: -32%); 227 | $border-sides: adjust-color($base-color, $hue: 4, $saturation: -2%, $lightness: -27%); 228 | $border-top: adjust-color($base-color, $hue: -1, $saturation: -17%, $lightness: -21%); 229 | $inset-shadow-hover: adjust-color($base-color, $saturation: -1%, $lightness: 3%); 230 | $stop-gradient-hover: adjust-color($base-color, $hue: 8, $saturation: -4%, $lightness: -15.5%); 231 | $text-shadow-hover: adjust-color($base-color, $hue: 5, $saturation: -5%, $lightness: -22%); 232 | 233 | @if $grayscale == true { 234 | $base-color-hover: grayscale($base-color-hover); 235 | $border-bottom: grayscale($border-bottom); 236 | $border-sides: grayscale($border-sides); 237 | $border-top: grayscale($border-top); 238 | $inset-shadow-hover: grayscale($inset-shadow-hover); 239 | $stop-gradient-hover: grayscale($stop-gradient-hover); 240 | $text-shadow-hover: grayscale($text-shadow-hover); 241 | } 242 | 243 | border: 1px solid $border-top; 244 | border-color: $border-top $border-sides $border-bottom; 245 | box-shadow: inset 0 1px 0 0 $inset-shadow-hover; 246 | cursor: pointer; 247 | @include linear-gradient ($base-color-hover, $stop-gradient-hover); 248 | text-shadow: 0 -1px 1px $text-shadow-hover; 249 | background-clip: padding-box; 250 | } 251 | 252 | &:active:not(:disabled) { 253 | $active-color: adjust-color($base-color, $hue: 4, $saturation: -12%, $lightness: -10%); 254 | $border-active: adjust-color($base-color, $hue: 6, $saturation: -2.5%, $lightness: -30%); 255 | $border-bottom-active: adjust-color($base-color, $hue: 11, $saturation: 6%, $lightness: -31%); 256 | $inset-shadow-active: adjust-color($base-color, $hue: 9, $saturation: 2%, $lightness: -21.5%); 257 | $text-shadow-active: adjust-color($base-color, $hue: 5, $saturation: -12%, $lightness: -21.5%); 258 | 259 | @if $grayscale == true { 260 | $active-color: grayscale($active-color); 261 | $border-active: grayscale($border-active); 262 | $border-bottom-active: grayscale($border-bottom-active); 263 | $inset-shadow-active: grayscale($inset-shadow-active); 264 | $text-shadow-active: grayscale($text-shadow-active); 265 | } 266 | 267 | background: $active-color; 268 | border: 1px solid $border-active; 269 | border-bottom: 1px solid $border-bottom-active; 270 | box-shadow: inset 0 0 6px 3px $inset-shadow-active, 0 1px 0 0 #fff; 271 | text-shadow: 0 -1px 1px $text-shadow-active; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/addons/_clearfix.scss: -------------------------------------------------------------------------------- 1 | // Micro clearfix provides an easy way to contain floats without adding additional markup 2 | // 3 | // Example usage: 4 | // 5 | // // Contain all floats within .wrapper 6 | // .wrapper { 7 | // @include clearfix; 8 | // .content, 9 | // .sidebar { 10 | // float : left; 11 | // } 12 | // } 13 | 14 | @mixin clearfix { 15 | *zoom: 1; 16 | 17 | &:before, 18 | &:after { 19 | content: " "; 20 | display: table; 21 | } 22 | 23 | &:after { 24 | clear: both; 25 | } 26 | } 27 | 28 | // Acknowledgements 29 | // Micro clearfix: [Nicolas Gallagher](http://nicolasgallagher.com/micro-clearfix-hack/) 30 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/addons/_font-family.scss: -------------------------------------------------------------------------------- 1 | $georgia: Georgia, Cambria, "Times New Roman", Times, serif; 2 | $helvetica: "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | $lucida-grande: "Lucida Grande", Tahoma, Verdana, Arial, sans-serif; 4 | $monospace: "Bitstream Vera Sans Mono", Consolas, Courier, monospace; 5 | $verdana: Verdana, Geneva, sans-serif; 6 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/addons/_hide-text.scss: -------------------------------------------------------------------------------- 1 | @mixin hide-text { 2 | color: transparent; 3 | font: 0/0 a; 4 | text-shadow: none; 5 | } 6 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/addons/_html5-input-types.scss: -------------------------------------------------------------------------------- 1 | //************************************************************************// 2 | // Generate a variable ($all-text-inputs) with a list of all html5 3 | // input types that have a text-based input, excluding textarea. 4 | // http://diveintohtml5.org/forms.html 5 | //************************************************************************// 6 | $inputs-list: 'input[type="email"]', 7 | 'input[type="number"]', 8 | 'input[type="password"]', 9 | 'input[type="search"]', 10 | 'input[type="tel"]', 11 | 'input[type="text"]', 12 | 'input[type="url"]', 13 | 14 | // Webkit & Gecko may change the display of these in the future 15 | 'input[type="color"]', 16 | 'input[type="date"]', 17 | 'input[type="datetime"]', 18 | 'input[type="datetime-local"]', 19 | 'input[type="month"]', 20 | 'input[type="time"]', 21 | 'input[type="week"]'; 22 | 23 | $unquoted-inputs-list: (); 24 | @each $input-type in $inputs-list { 25 | $unquoted-inputs-list: append($unquoted-inputs-list, unquote($input-type), comma); 26 | } 27 | 28 | $all-text-inputs: $unquoted-inputs-list; 29 | 30 | 31 | // Hover Pseudo-class 32 | //************************************************************************// 33 | $all-text-inputs-hover: (); 34 | @each $input-type in $unquoted-inputs-list { 35 | $input-type-hover: $input-type + ":hover"; 36 | $all-text-inputs-hover: append($all-text-inputs-hover, $input-type-hover, comma); 37 | } 38 | 39 | // Focus Pseudo-class 40 | //************************************************************************// 41 | $all-text-inputs-focus: (); 42 | @each $input-type in $unquoted-inputs-list { 43 | $input-type-focus: $input-type + ":focus"; 44 | $all-text-inputs-focus: append($all-text-inputs-focus, $input-type-focus, comma); 45 | } 46 | 47 | // You must use interpolation on the variable: 48 | // #{$all-text-inputs} 49 | // #{$all-text-inputs-hover} 50 | // #{$all-text-inputs-focus} 51 | 52 | // Example 53 | //************************************************************************// 54 | // #{$all-text-inputs}, textarea { 55 | // border: 1px solid red; 56 | // } 57 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/addons/_position.scss: -------------------------------------------------------------------------------- 1 | @mixin position ($position: relative, $coordinates: 0 0 0 0) { 2 | 3 | @if type-of($position) == list { 4 | $coordinates: $position; 5 | $position: relative; 6 | } 7 | 8 | $top: nth($coordinates, 1); 9 | $right: nth($coordinates, 2); 10 | $bottom: nth($coordinates, 3); 11 | $left: nth($coordinates, 4); 12 | 13 | position: $position; 14 | 15 | @if $top == auto { 16 | top: $top; 17 | } 18 | @else if not(unitless($top)) { 19 | top: $top; 20 | } 21 | 22 | @if $right == auto { 23 | right: $right; 24 | } 25 | @else if not(unitless($right)) { 26 | right: $right; 27 | } 28 | 29 | @if $bottom == auto { 30 | bottom: $bottom; 31 | } 32 | @else if not(unitless($bottom)) { 33 | bottom: $bottom; 34 | } 35 | 36 | @if $left == auto { 37 | left: $left; 38 | } 39 | @else if not(unitless($left)) { 40 | left: $left; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/addons/_prefixer.scss: -------------------------------------------------------------------------------- 1 | //************************************************************************// 2 | // Example: @include prefixer(border-radius, $radii, webkit ms spec); 3 | //************************************************************************// 4 | $prefix-for-webkit: true !default; 5 | $prefix-for-mozilla: true !default; 6 | $prefix-for-microsoft: true !default; 7 | $prefix-for-opera: true !default; 8 | $prefix-for-spec: true !default; // required for keyframe mixin 9 | 10 | @mixin prefixer ($property, $value, $prefixes) { 11 | @each $prefix in $prefixes { 12 | @if $prefix == webkit { 13 | @if $prefix-for-webkit { 14 | -webkit-#{$property}: $value; 15 | } 16 | } 17 | @else if $prefix == moz { 18 | @if $prefix-for-mozilla { 19 | -moz-#{$property}: $value; 20 | } 21 | } 22 | @else if $prefix == ms { 23 | @if $prefix-for-microsoft { 24 | -ms-#{$property}: $value; 25 | } 26 | } 27 | @else if $prefix == o { 28 | @if $prefix-for-opera { 29 | -o-#{$property}: $value; 30 | } 31 | } 32 | @else if $prefix == spec { 33 | @if $prefix-for-spec { 34 | #{$property}: $value; 35 | } 36 | } 37 | @else { 38 | @warn "Unrecognized prefix: #{$prefix}"; 39 | } 40 | } 41 | } 42 | 43 | @mixin disable-prefix-for-all() { 44 | $prefix-for-webkit: false; 45 | $prefix-for-mozilla: false; 46 | $prefix-for-microsoft: false; 47 | $prefix-for-opera: false; 48 | $prefix-for-spec: false; 49 | } 50 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/addons/_retina-image.scss: -------------------------------------------------------------------------------- 1 | @mixin retina-image($filename, $background-size, $extension: png, $retina-filename: null, $asset-pipeline: false) { 2 | @if $asset-pipeline { 3 | background-image: image-url("#{$filename}.#{$extension}"); 4 | } 5 | @else { 6 | background-image: url("#{$filename}.#{$extension}"); 7 | } 8 | 9 | @include hidpi { 10 | 11 | @if $asset-pipeline { 12 | @if $retina-filename { 13 | background-image: image-url("#{$retina-filename}.#{$extension}"); 14 | } 15 | @else { 16 | background-image: image-url("#{$filename}@2x.#{$extension}"); 17 | } 18 | } 19 | 20 | @else { 21 | @if $retina-filename { 22 | background-image: url("#{$retina-filename}.#{$extension}"); 23 | } 24 | @else { 25 | background-image: url("#{$filename}@2x.#{$extension}"); 26 | } 27 | } 28 | 29 | background-size: $background-size; 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/addons/_size.scss: -------------------------------------------------------------------------------- 1 | @mixin size($size) { 2 | @if length($size) == 1 { 3 | @if $size == auto { 4 | width: $size; 5 | height: $size; 6 | } 7 | 8 | @else if unitless($size) { 9 | width: $size + px; 10 | height: $size + px; 11 | } 12 | 13 | @else if not(unitless($size)) { 14 | width: $size; 15 | height: $size; 16 | } 17 | } 18 | 19 | // Width x Height 20 | @if length($size) == 2 { 21 | $width: nth($size, 1); 22 | $height: nth($size, 2); 23 | 24 | @if $width == auto { 25 | width: $width; 26 | } 27 | @else if not(unitless($width)) { 28 | width: $width; 29 | } 30 | @else if unitless($width) { 31 | width: $width + px; 32 | } 33 | 34 | @if $height == auto { 35 | height: $height; 36 | } 37 | @else if not(unitless($height)) { 38 | height: $height; 39 | } 40 | @else if unitless($height) { 41 | height: $height + px; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/addons/_timing-functions.scss: -------------------------------------------------------------------------------- 1 | // CSS cubic-bezier timing functions. Timing functions courtesy of jquery.easie (github.com/jaukia/easie) 2 | // Timing functions are the same as demo'ed here: http://jqueryui.com/demos/effect/easing.html 3 | 4 | // EASE IN 5 | $ease-in-quad: cubic-bezier(0.550, 0.085, 0.680, 0.530); 6 | $ease-in-cubic: cubic-bezier(0.550, 0.055, 0.675, 0.190); 7 | $ease-in-quart: cubic-bezier(0.895, 0.030, 0.685, 0.220); 8 | $ease-in-quint: cubic-bezier(0.755, 0.050, 0.855, 0.060); 9 | $ease-in-sine: cubic-bezier(0.470, 0.000, 0.745, 0.715); 10 | $ease-in-expo: cubic-bezier(0.950, 0.050, 0.795, 0.035); 11 | $ease-in-circ: cubic-bezier(0.600, 0.040, 0.980, 0.335); 12 | $ease-in-back: cubic-bezier(0.600, -0.280, 0.735, 0.045); 13 | 14 | // EASE OUT 15 | $ease-out-quad: cubic-bezier(0.250, 0.460, 0.450, 0.940); 16 | $ease-out-cubic: cubic-bezier(0.215, 0.610, 0.355, 1.000); 17 | $ease-out-quart: cubic-bezier(0.165, 0.840, 0.440, 1.000); 18 | $ease-out-quint: cubic-bezier(0.230, 1.000, 0.320, 1.000); 19 | $ease-out-sine: cubic-bezier(0.390, 0.575, 0.565, 1.000); 20 | $ease-out-expo: cubic-bezier(0.190, 1.000, 0.220, 1.000); 21 | $ease-out-circ: cubic-bezier(0.075, 0.820, 0.165, 1.000); 22 | $ease-out-back: cubic-bezier(0.175, 0.885, 0.320, 1.275); 23 | 24 | // EASE IN OUT 25 | $ease-in-out-quad: cubic-bezier(0.455, 0.030, 0.515, 0.955); 26 | $ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1.000); 27 | $ease-in-out-quart: cubic-bezier(0.770, 0.000, 0.175, 1.000); 28 | $ease-in-out-quint: cubic-bezier(0.860, 0.000, 0.070, 1.000); 29 | $ease-in-out-sine: cubic-bezier(0.445, 0.050, 0.550, 0.950); 30 | $ease-in-out-expo: cubic-bezier(1.000, 0.000, 0.000, 1.000); 31 | $ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.150, 0.860); 32 | $ease-in-out-back: cubic-bezier(0.680, -0.550, 0.265, 1.550); 33 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/addons/_triangle.scss: -------------------------------------------------------------------------------- 1 | @mixin triangle ($size, $color, $direction) { 2 | height: 0; 3 | width: 0; 4 | 5 | @if ($direction == up) or ($direction == down) or ($direction == right) or ($direction == left) { 6 | border-color: transparent; 7 | border-style: solid; 8 | border-width: $size / 2; 9 | 10 | @if $direction == up { 11 | border-bottom-color: $color; 12 | 13 | } @else if $direction == right { 14 | border-left-color: $color; 15 | 16 | } @else if $direction == down { 17 | border-top-color: $color; 18 | 19 | } @else if $direction == left { 20 | border-right-color: $color; 21 | } 22 | } 23 | 24 | @else if ($direction == up-right) or ($direction == up-left) { 25 | border-top: $size solid $color; 26 | 27 | @if $direction == up-right { 28 | border-left: $size solid transparent; 29 | 30 | } @else if $direction == up-left { 31 | border-right: $size solid transparent; 32 | } 33 | } 34 | 35 | @else if ($direction == down-right) or ($direction == down-left) { 36 | border-bottom: $size solid $color; 37 | 38 | @if $direction == down-right { 39 | border-left: $size solid transparent; 40 | 41 | } @else if $direction == down-left { 42 | border-right: $size solid transparent; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_animation.scss: -------------------------------------------------------------------------------- 1 | // http://www.w3.org/TR/css3-animations/#the-animation-name-property- 2 | // Each of these mixins support comma separated lists of values, which allows different transitions for individual properties to be described in a single style rule. Each value in the list corresponds to the value at that same position in the other properties. 3 | 4 | // Official animation shorthand property. 5 | @mixin animation ($animations...) { 6 | @include prefixer(animation, $animations, webkit moz spec); 7 | } 8 | 9 | // Individual Animation Properties 10 | @mixin animation-name ($names...) { 11 | @include prefixer(animation-name, $names, webkit moz spec); 12 | } 13 | 14 | 15 | @mixin animation-duration ($times...) { 16 | @include prefixer(animation-duration, $times, webkit moz spec); 17 | } 18 | 19 | 20 | @mixin animation-timing-function ($motions...) { 21 | // ease | linear | ease-in | ease-out | ease-in-out 22 | @include prefixer(animation-timing-function, $motions, webkit moz spec); 23 | } 24 | 25 | 26 | @mixin animation-iteration-count ($values...) { 27 | // infinite | 28 | @include prefixer(animation-iteration-count, $values, webkit moz spec); 29 | } 30 | 31 | 32 | @mixin animation-direction ($directions...) { 33 | // normal | alternate 34 | @include prefixer(animation-direction, $directions, webkit moz spec); 35 | } 36 | 37 | 38 | @mixin animation-play-state ($states...) { 39 | // running | paused 40 | @include prefixer(animation-play-state, $states, webkit moz spec); 41 | } 42 | 43 | 44 | @mixin animation-delay ($times...) { 45 | @include prefixer(animation-delay, $times, webkit moz spec); 46 | } 47 | 48 | 49 | @mixin animation-fill-mode ($modes...) { 50 | // none | forwards | backwards | both 51 | @include prefixer(animation-fill-mode, $modes, webkit moz spec); 52 | } 53 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_appearance.scss: -------------------------------------------------------------------------------- 1 | @mixin appearance ($value) { 2 | @include prefixer(appearance, $value, webkit moz ms o spec); 3 | } 4 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_backface-visibility.scss: -------------------------------------------------------------------------------- 1 | //************************************************************************// 2 | // Backface-visibility mixin 3 | //************************************************************************// 4 | @mixin backface-visibility($visibility) { 5 | @include prefixer(backface-visibility, $visibility, webkit spec); 6 | } 7 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_background-image.scss: -------------------------------------------------------------------------------- 1 | //************************************************************************// 2 | // Background-image property for adding multiple background images with 3 | // gradients, or for stringing multiple gradients together. 4 | //************************************************************************// 5 | 6 | @mixin background-image($images...) { 7 | background-image: _add-prefix($images, webkit); 8 | background-image: _add-prefix($images); 9 | } 10 | 11 | @function _add-prefix($images, $vendor: false) { 12 | $images-prefixed: (); 13 | $gradient-positions: false; 14 | @for $i from 1 through length($images) { 15 | $type: type-of(nth($images, $i)); // Get type of variable - List or String 16 | 17 | // If variable is a list - Gradient 18 | @if $type == list { 19 | $gradient-type: nth(nth($images, $i), 1); // linear or radial 20 | $gradient-pos: null; 21 | $gradient-args: null; 22 | 23 | @if ($gradient-type == linear) or ($gradient-type == radial) { 24 | $gradient-pos: nth(nth($images, $i), 2); // Get gradient position 25 | $gradient-args: nth(nth($images, $i), 3); // Get actual gradient (red, blue) 26 | } 27 | @else { 28 | $gradient-args: nth(nth($images, $i), 2); // Get actual gradient (red, blue) 29 | } 30 | 31 | $gradient-positions: _gradient-positions-parser($gradient-type, $gradient-pos); 32 | $gradient: _render-gradients($gradient-positions, $gradient-args, $gradient-type, $vendor); 33 | $images-prefixed: append($images-prefixed, $gradient, comma); 34 | } 35 | // If variable is a string - Image 36 | @else if $type == string { 37 | $images-prefixed: join($images-prefixed, nth($images, $i), comma); 38 | } 39 | } 40 | @return $images-prefixed; 41 | } 42 | 43 | //Examples: 44 | //@include background-image(linear-gradient(top, orange, red)); 45 | //@include background-image(radial-gradient(50% 50%, cover circle, orange, red)); 46 | //@include background-image(url("/images/a.png"), linear-gradient(orange, red)); 47 | //@include background-image(url("image.png"), linear-gradient(orange, red), url("image.png")); 48 | //@include background-image(linear-gradient(hsla(0, 100%, 100%, 0.25) 0%, hsla(0, 100%, 100%, 0.08) 50%, transparent 50%), linear-gradient(orange, red)); 49 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_background.scss: -------------------------------------------------------------------------------- 1 | //************************************************************************// 2 | // Background property for adding multiple backgrounds using shorthand 3 | // notation. 4 | //************************************************************************// 5 | 6 | @mixin background( 7 | $background-1 , $background-2: false, 8 | $background-3: false, $background-4: false, 9 | $background-5: false, $background-6: false, 10 | $background-7: false, $background-8: false, 11 | $background-9: false, $background-10: false, 12 | $fallback: false 13 | ) { 14 | $backgrounds: compact($background-1, $background-2, 15 | $background-3, $background-4, 16 | $background-5, $background-6, 17 | $background-7, $background-8, 18 | $background-9, $background-10); 19 | 20 | $fallback-color: false; 21 | @if (type-of($fallback) == color) or ($fallback == "transparent") { 22 | $fallback-color: $fallback; 23 | } 24 | @else { 25 | $fallback-color: _extract-background-color($backgrounds); 26 | } 27 | 28 | @if $fallback-color { 29 | background-color: $fallback-color; 30 | } 31 | background: _background-add-prefix($backgrounds, webkit); 32 | background: _background-add-prefix($backgrounds); 33 | } 34 | 35 | @function _extract-background-color($backgrounds) { 36 | $final-bg-layer: nth($backgrounds, length($backgrounds)); 37 | @if type-of($final-bg-layer) == list { 38 | @for $i from 1 through length($final-bg-layer) { 39 | $value: nth($final-bg-layer, $i); 40 | @if type-of($value) == color { 41 | @return $value; 42 | } 43 | } 44 | } 45 | @return false; 46 | } 47 | 48 | @function _background-add-prefix($backgrounds, $vendor: false) { 49 | $backgrounds-prefixed: (); 50 | 51 | @for $i from 1 through length($backgrounds) { 52 | $shorthand: nth($backgrounds, $i); // Get member for current index 53 | $type: type-of($shorthand); // Get type of variable - List (gradient) or String (image) 54 | 55 | // If shorthand is a list (gradient) 56 | @if $type == list { 57 | $first-member: nth($shorthand, 1); // Get first member of shorthand 58 | 59 | // Linear Gradient 60 | @if index(linear radial, nth($first-member, 1)) { 61 | $gradient-type: nth($first-member, 1); // linear || radial 62 | $gradient-args: false; 63 | $gradient-positions: false; 64 | $shorthand-start: false; 65 | @if type-of($first-member) == list { // Linear gradient plus additional shorthand values - lg(red,orange)repeat,... 66 | $gradient-positions: nth($first-member, 2); 67 | $gradient-args: nth($first-member, 3); 68 | $shorthand-start: 2; 69 | } 70 | @else { // Linear gradient only - lg(red,orange),... 71 | $gradient-positions: nth($shorthand, 2); 72 | $gradient-args: nth($shorthand, 3); // Get gradient (red, blue) 73 | } 74 | 75 | $gradient-positions: _gradient-positions-parser($gradient-type, $gradient-positions); 76 | $gradient: _render-gradients($gradient-positions, $gradient-args, $gradient-type, $vendor); 77 | 78 | // Append any additional shorthand args to gradient 79 | @if $shorthand-start { 80 | @for $j from $shorthand-start through length($shorthand) { 81 | $gradient: join($gradient, nth($shorthand, $j), space); 82 | } 83 | } 84 | $backgrounds-prefixed: append($backgrounds-prefixed, $gradient, comma); 85 | } 86 | // Image with additional properties 87 | @else { 88 | $backgrounds-prefixed: append($backgrounds-prefixed, $shorthand, comma); 89 | } 90 | } 91 | // If shorthand is a simple string (color or image) 92 | @else if $type == string { 93 | $backgrounds-prefixed: join($backgrounds-prefixed, $shorthand, comma); 94 | } 95 | } 96 | @return $backgrounds-prefixed; 97 | } 98 | 99 | //Examples: 100 | //@include background(linear-gradient(top, orange, red)); 101 | //@include background(radial-gradient(circle at 40% 40%, orange, red)); 102 | //@include background(url("/images/a.png") no-repeat, linear-gradient(orange, red)); 103 | //@include background(url("image.png") center center, linear-gradient(orange, red), url("image.png")); 104 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_border-image.scss: -------------------------------------------------------------------------------- 1 | @mixin border-image($images) { 2 | -webkit-border-image: _border-add-prefix($images, webkit); 3 | -moz-border-image: _border-add-prefix($images, moz); 4 | -o-border-image: _border-add-prefix($images, o); 5 | border-image: _border-add-prefix($images); 6 | } 7 | 8 | @function _border-add-prefix($images, $vendor: false) { 9 | $border-image: null; 10 | $images-type: type-of(nth($images, 1)); 11 | $first-var: nth(nth($images, 1), 1); // Get type of Gradient (Linear || radial) 12 | 13 | // If input is a gradient 14 | @if $images-type == string { 15 | @if ($first-var == "linear") or ($first-var == "radial") { 16 | $gradient-type: nth($images, 1); // Get type of gradient (linear || radial) 17 | $gradient-pos: nth($images, 2); // Get gradient position 18 | $gradient-args: nth($images, 3); // Get actual gradient (red, blue) 19 | $gradient-positions: _gradient-positions-parser($gradient-type, $gradient-pos); 20 | $border-image: _render-gradients($gradient-positions, $gradient-args, $gradient-type, $vendor); 21 | } 22 | // If input is a URL 23 | @else { 24 | $border-image: $images; 25 | } 26 | } 27 | // If input is gradient or url + additional args 28 | @else if $images-type == list { 29 | $type: type-of(nth($images, 1)); // Get type of variable - List or String 30 | 31 | // If variable is a list - Gradient 32 | @if $type == list { 33 | $gradient: nth($images, 1); 34 | $gradient-type: nth($gradient, 1); // Get type of gradient (linear || radial) 35 | $gradient-pos: nth($gradient, 2); // Get gradient position 36 | $gradient-args: nth($gradient, 3); // Get actual gradient (red, blue) 37 | $gradient-positions: _gradient-positions-parser($gradient-type, $gradient-pos); 38 | $border-image: _render-gradients($gradient-positions, $gradient-args, $gradient-type, $vendor); 39 | 40 | @for $i from 2 through length($images) { 41 | $border-image: append($border-image, nth($images, $i)); 42 | } 43 | } 44 | } 45 | @return $border-image; 46 | } 47 | 48 | //Examples: 49 | // @include border-image(url("image.png")); 50 | // @include border-image(url("image.png") 20 stretch); 51 | // @include border-image(linear-gradient(45deg, orange, yellow)); 52 | // @include border-image(linear-gradient(45deg, orange, yellow) stretch); 53 | // @include border-image(linear-gradient(45deg, orange, yellow) 20 30 40 50 stretch round); 54 | // @include border-image(radial-gradient(top, cover, orange, yellow, orange)); 55 | 56 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_border-radius.scss: -------------------------------------------------------------------------------- 1 | //************************************************************************// 2 | // Shorthand Border-radius mixins 3 | //************************************************************************// 4 | @mixin border-top-radius($radii) { 5 | @include prefixer(border-top-left-radius, $radii, spec); 6 | @include prefixer(border-top-right-radius, $radii, spec); 7 | } 8 | 9 | @mixin border-bottom-radius($radii) { 10 | @include prefixer(border-bottom-left-radius, $radii, spec); 11 | @include prefixer(border-bottom-right-radius, $radii, spec); 12 | } 13 | 14 | @mixin border-left-radius($radii) { 15 | @include prefixer(border-top-left-radius, $radii, spec); 16 | @include prefixer(border-bottom-left-radius, $radii, spec); 17 | } 18 | 19 | @mixin border-right-radius($radii) { 20 | @include prefixer(border-top-right-radius, $radii, spec); 21 | @include prefixer(border-bottom-right-radius, $radii, spec); 22 | } 23 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_box-sizing.scss: -------------------------------------------------------------------------------- 1 | @mixin box-sizing ($box) { 2 | // content-box | border-box | inherit 3 | @include prefixer(box-sizing, $box, webkit moz spec); 4 | } 5 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_columns.scss: -------------------------------------------------------------------------------- 1 | @mixin columns($arg: auto) { 2 | // || 3 | @include prefixer(columns, $arg, webkit moz spec); 4 | } 5 | 6 | @mixin column-count($int: auto) { 7 | // auto || integer 8 | @include prefixer(column-count, $int, webkit moz spec); 9 | } 10 | 11 | @mixin column-gap($length: normal) { 12 | // normal || length 13 | @include prefixer(column-gap, $length, webkit moz spec); 14 | } 15 | 16 | @mixin column-fill($arg: auto) { 17 | // auto || length 18 | @include prefixer(columns-fill, $arg, webkit moz spec); 19 | } 20 | 21 | @mixin column-rule($arg) { 22 | // || || 23 | @include prefixer(column-rule, $arg, webkit moz spec); 24 | } 25 | 26 | @mixin column-rule-color($color) { 27 | @include prefixer(column-rule-color, $color, webkit moz spec); 28 | } 29 | 30 | @mixin column-rule-style($style: none) { 31 | // none | hidden | dashed | dotted | double | groove | inset | inset | outset | ridge | solid 32 | @include prefixer(column-rule-style, $style, webkit moz spec); 33 | } 34 | 35 | @mixin column-rule-width ($width: none) { 36 | @include prefixer(column-rule-width, $width, webkit moz spec); 37 | } 38 | 39 | @mixin column-span($arg: none) { 40 | // none || all 41 | @include prefixer(column-span, $arg, webkit moz spec); 42 | } 43 | 44 | @mixin column-width($length: auto) { 45 | // auto || length 46 | @include prefixer(column-width, $length, webkit moz spec); 47 | } 48 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_flex-box.scss: -------------------------------------------------------------------------------- 1 | // CSS3 Flexible Box Model and property defaults 2 | 3 | // Custom shorthand notation for flexbox 4 | @mixin box($orient: inline-axis, $pack: start, $align: stretch) { 5 | @include display-box; 6 | @include box-orient($orient); 7 | @include box-pack($pack); 8 | @include box-align($align); 9 | } 10 | 11 | @mixin display-box { 12 | display: -webkit-box; 13 | display: -moz-box; 14 | display: box; 15 | } 16 | 17 | @mixin box-orient($orient: inline-axis) { 18 | // horizontal|vertical|inline-axis|block-axis|inherit 19 | @include prefixer(box-orient, $orient, webkit moz spec); 20 | } 21 | 22 | @mixin box-pack($pack: start) { 23 | // start|end|center|justify 24 | @include prefixer(box-pack, $pack, webkit moz spec); 25 | } 26 | 27 | @mixin box-align($align: stretch) { 28 | // start|end|center|baseline|stretch 29 | @include prefixer(box-align, $align, webkit moz spec); 30 | } 31 | 32 | @mixin box-direction($direction: normal) { 33 | // normal|reverse|inherit 34 | @include prefixer(box-direction, $direction, webkit moz spec); 35 | } 36 | 37 | @mixin box-lines($lines: single) { 38 | // single|multiple 39 | @include prefixer(box-lines, $lines, webkit moz spec); 40 | } 41 | 42 | @mixin box-ordinal-group($int: 1) { 43 | @include prefixer(box-ordinal-group, $int, webkit moz spec); 44 | } 45 | 46 | @mixin box-flex($value: 0.0) { 47 | @include prefixer(box-flex, $value, webkit moz spec); 48 | } 49 | 50 | @mixin box-flex-group($int: 1) { 51 | @include prefixer(box-flex-group, $int, webkit moz spec); 52 | } 53 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_font-face.scss: -------------------------------------------------------------------------------- 1 | // Order of the includes matters, and it is: normal, bold, italic, bold+italic. 2 | 3 | @mixin font-face($font-family, $file-path, $weight: normal, $style: normal, $asset-pipeline: false ) { 4 | @font-face { 5 | font-family: $font-family; 6 | font-weight: $weight; 7 | font-style: $style; 8 | 9 | @if $asset-pipeline == true { 10 | src: font-url('#{$file-path}.eot'); 11 | src: font-url('#{$file-path}.eot?#iefix') format('embedded-opentype'), 12 | font-url('#{$file-path}.woff') format('woff'), 13 | font-url('#{$file-path}.ttf') format('truetype'), 14 | font-url('#{$file-path}.svg##{$font-family}') format('svg'); 15 | } @else { 16 | src: url('#{$file-path}.eot'); 17 | src: url('#{$file-path}.eot?#iefix') format('embedded-opentype'), 18 | url('#{$file-path}.woff') format('woff'), 19 | url('#{$file-path}.ttf') format('truetype'), 20 | url('#{$file-path}.svg##{$font-family}') format('svg'); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_hidpi-media-query.scss: -------------------------------------------------------------------------------- 1 | // HiDPI mixin. Default value set to 1.3 to target Google Nexus 7 (http://bjango.com/articles/min-device-pixel-ratio/) 2 | @mixin hidpi($ratio: 1.3) { 3 | @media only screen and (-webkit-min-device-pixel-ratio: $ratio), 4 | only screen and (min--moz-device-pixel-ratio: $ratio), 5 | only screen and (-o-min-device-pixel-ratio: #{$ratio}/1), 6 | only screen and (min-resolution: #{round($ratio*96)}dpi), 7 | only screen and (min-resolution: #{$ratio}dppx) { 8 | @content; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_image-rendering.scss: -------------------------------------------------------------------------------- 1 | @mixin image-rendering ($mode:optimizeQuality) { 2 | 3 | @if ($mode == optimize-contrast) { 4 | image-rendering: -moz-crisp-edges; 5 | image-rendering: -o-crisp-edges; 6 | image-rendering: -webkit-optimize-contrast; 7 | image-rendering: optimize-contrast; 8 | } 9 | 10 | @else { 11 | image-rendering: $mode; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_inline-block.scss: -------------------------------------------------------------------------------- 1 | // Legacy support for inline-block in IE7 (maybe IE6) 2 | @mixin inline-block { 3 | display: inline-block; 4 | vertical-align: baseline; 5 | zoom: 1; 6 | *display: inline; 7 | *vertical-align: auto; 8 | } 9 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_keyframes.scss: -------------------------------------------------------------------------------- 1 | // Adds keyframes blocks for supported prefixes, removing redundant prefixes in the block's content 2 | @mixin keyframes($name) { 3 | $original-prefix-for-webkit: $prefix-for-webkit; 4 | $original-prefix-for-mozilla: $prefix-for-mozilla; 5 | $original-prefix-for-microsoft: $prefix-for-microsoft; 6 | $original-prefix-for-opera: $prefix-for-opera; 7 | $original-prefix-for-spec: $prefix-for-spec; 8 | 9 | @if $original-prefix-for-webkit { 10 | @include disable-prefix-for-all(); 11 | $prefix-for-webkit: true; 12 | @-webkit-keyframes #{$name} { 13 | @content; 14 | } 15 | } 16 | @if $original-prefix-for-mozilla { 17 | @include disable-prefix-for-all(); 18 | $prefix-for-mozilla: true; 19 | @-moz-keyframes #{$name} { 20 | @content; 21 | } 22 | } 23 | @if $original-prefix-for-opera { 24 | @include disable-prefix-for-all(); 25 | $prefix-for-opera: true; 26 | @-o-keyframes #{$name} { 27 | @content; 28 | } 29 | } 30 | @if $original-prefix-for-spec { 31 | @include disable-prefix-for-all(); 32 | $prefix-for-spec: true; 33 | @keyframes #{$name} { 34 | @content; 35 | } 36 | } 37 | 38 | $prefix-for-webkit: $original-prefix-for-webkit; 39 | $prefix-for-mozilla: $original-prefix-for-mozilla; 40 | $prefix-for-microsoft: $original-prefix-for-microsoft; 41 | $prefix-for-opera: $original-prefix-for-opera; 42 | $prefix-for-spec: $original-prefix-for-spec; 43 | } 44 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_linear-gradient.scss: -------------------------------------------------------------------------------- 1 | @mixin linear-gradient($pos, $G1, $G2: false, 2 | $G3: false, $G4: false, 3 | $G5: false, $G6: false, 4 | $G7: false, $G8: false, 5 | $G9: false, $G10: false, 6 | $deprecated-pos1: left top, 7 | $deprecated-pos2: left bottom, 8 | $fallback: false) { 9 | // Detect what type of value exists in $pos 10 | $pos-type: type-of(nth($pos, 1)); 11 | $pos-spec: null; 12 | $pos-degree: null; 13 | 14 | // If $pos is missing from mixin, reassign vars and add default position 15 | @if ($pos-type == color) or (nth($pos, 1) == "transparent") { 16 | $G10: $G9; $G9: $G8; $G8: $G7; $G7: $G6; $G6: $G5; 17 | $G5: $G4; $G4: $G3; $G3: $G2; $G2: $G1; $G1: $pos; 18 | $pos: null; 19 | } 20 | 21 | @if $pos { 22 | $positions: _linear-positions-parser($pos); 23 | $pos-degree: nth($positions, 1); 24 | $pos-spec: nth($positions, 2); 25 | } 26 | 27 | $full: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); 28 | 29 | // Set $G1 as the default fallback color 30 | $fallback-color: nth($G1, 1); 31 | 32 | // If $fallback is a color use that color as the fallback color 33 | @if (type-of($fallback) == color) or ($fallback == "transparent") { 34 | $fallback-color: $fallback; 35 | } 36 | 37 | background-color: $fallback-color; 38 | background-image: _deprecated-webkit-gradient(linear, $deprecated-pos1, $deprecated-pos2, $full); // Safari <= 5.0 39 | background-image: -webkit-linear-gradient($pos-degree $full); // Safari 5.1+, Chrome 40 | background-image: unquote("linear-gradient(#{$pos-spec}#{$full})"); 41 | } 42 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_perspective.scss: -------------------------------------------------------------------------------- 1 | @mixin perspective($depth: none) { 2 | // none | 3 | @include prefixer(perspective, $depth, webkit moz spec); 4 | } 5 | 6 | @mixin perspective-origin($value: 50% 50%) { 7 | @include prefixer(perspective-origin, $value, webkit moz spec); 8 | } 9 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_placeholder.scss: -------------------------------------------------------------------------------- 1 | $placeholders: '-webkit-input-placeholder', 2 | '-moz-placeholder', 3 | '-ms-input-placeholder'; 4 | 5 | @mixin placeholder { 6 | @each $placeholder in $placeholders { 7 | @if $placeholder == "-webkit-input-placeholder" { 8 | &::#{$placeholder} { 9 | @content; 10 | } 11 | } 12 | @else if $placeholder == "-moz-placeholder" { 13 | // FF 18- 14 | &:#{$placeholder} { 15 | @content; 16 | } 17 | 18 | // FF 19+ 19 | &::#{$placeholder} { 20 | @content; 21 | } 22 | } 23 | @else { 24 | &:#{$placeholder} { 25 | @content; 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_radial-gradient.scss: -------------------------------------------------------------------------------- 1 | // Requires Sass 3.1+ 2 | @mixin radial-gradient($G1, $G2, 3 | $G3: false, $G4: false, 4 | $G5: false, $G6: false, 5 | $G7: false, $G8: false, 6 | $G9: false, $G10: false, 7 | $pos: null, 8 | $shape-size: null, 9 | $deprecated-pos1: center center, 10 | $deprecated-pos2: center center, 11 | $deprecated-radius1: 0, 12 | $deprecated-radius2: 460, 13 | $fallback: false) { 14 | 15 | $data: _radial-arg-parser($G1, $G2, $pos, $shape-size); 16 | $G1: nth($data, 1); 17 | $G2: nth($data, 2); 18 | $pos: nth($data, 3); 19 | $shape-size: nth($data, 4); 20 | 21 | $full: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); 22 | 23 | // Strip deprecated cover/contain for spec 24 | $shape-size-spec: _shape-size-stripper($shape-size); 25 | 26 | // Set $G1 as the default fallback color 27 | $first-color: nth($full, 1); 28 | $fallback-color: nth($first-color, 1); 29 | 30 | @if (type-of($fallback) == color) or ($fallback == "transparent") { 31 | $fallback-color: $fallback; 32 | } 33 | 34 | // Add Commas and spaces 35 | $shape-size: if($shape-size, '#{$shape-size}, ', null); 36 | $pos: if($pos, '#{$pos}, ', null); 37 | $pos-spec: if($pos, 'at #{$pos}', null); 38 | $shape-size-spec: if(($shape-size-spec != ' ') and ($pos == null), '#{$shape-size-spec}, ', '#{$shape-size-spec} '); 39 | 40 | background-color: $fallback-color; 41 | background-image: _deprecated-webkit-gradient(radial, $deprecated-pos1, $deprecated-pos2, $full, $deprecated-radius1, $deprecated-radius2); // Safari <= 5.0 && IOS 4 42 | background-image: -webkit-radial-gradient(unquote(#{$pos}#{$shape-size}#{$full})); 43 | background-image: unquote("radial-gradient(#{$shape-size-spec}#{$pos-spec}#{$full})"); 44 | } 45 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_transform.scss: -------------------------------------------------------------------------------- 1 | @mixin transform($property: none) { 2 | // none | 3 | @include prefixer(transform, $property, webkit moz ms o spec); 4 | } 5 | 6 | @mixin transform-origin($axes: 50%) { 7 | // x-axis - left | center | right | length | % 8 | // y-axis - top | center | bottom | length | % 9 | // z-axis - length 10 | @include prefixer(transform-origin, $axes, webkit moz ms o spec); 11 | } 12 | 13 | @mixin transform-style ($style: flat) { 14 | @include prefixer(transform-style, $style, webkit moz ms o spec); 15 | } 16 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_transition.scss: -------------------------------------------------------------------------------- 1 | // Shorthand mixin. Supports multiple parentheses-deliminated values for each variable. 2 | // Example: @include transition (all, 2.0s, ease-in-out); 3 | // @include transition ((opacity, width), (1.0s, 2.0s), ease-in, (0, 2s)); 4 | // @include transition ($property:(opacity, width), $delay: (1.5s, 2.5s)); 5 | 6 | @mixin transition ($properties...) { 7 | @if length($properties) >= 1 { 8 | @include prefixer(transition, $properties, webkit moz spec); 9 | } 10 | 11 | @else { 12 | $properties: all 0.15s ease-out 0; 13 | @include prefixer(transition, $properties, webkit moz spec); 14 | } 15 | } 16 | 17 | @mixin transition-property ($properties...) { 18 | -webkit-transition-property: transition-property-names($properties, 'webkit'); 19 | -moz-transition-property: transition-property-names($properties, 'moz'); 20 | transition-property: transition-property-names($properties, false); 21 | } 22 | 23 | @mixin transition-duration ($times...) { 24 | @include prefixer(transition-duration, $times, webkit moz spec); 25 | } 26 | 27 | @mixin transition-timing-function ($motions...) { 28 | // ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier() 29 | @include prefixer(transition-timing-function, $motions, webkit moz spec); 30 | } 31 | 32 | @mixin transition-delay ($times...) { 33 | @include prefixer(transition-delay, $times, webkit moz spec); 34 | } 35 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/css3/_user-select.scss: -------------------------------------------------------------------------------- 1 | @mixin user-select($arg: none) { 2 | @include prefixer(user-select, $arg, webkit moz ms spec); 3 | } 4 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/functions/_compact.scss: -------------------------------------------------------------------------------- 1 | // Remove `false` values from a list 2 | 3 | @function compact($vars...) { 4 | $list: (); 5 | @each $var in $vars { 6 | @if $var { 7 | $list: append($list, $var, comma); 8 | } 9 | } 10 | @return $list; 11 | } 12 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/functions/_flex-grid.scss: -------------------------------------------------------------------------------- 1 | // Flexible grid 2 | @function flex-grid($columns, $container-columns: $fg-max-columns) { 3 | $width: $columns * $fg-column + ($columns - 1) * $fg-gutter; 4 | $container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter; 5 | @return percentage($width / $container-width); 6 | } 7 | 8 | // Flexible gutter 9 | @function flex-gutter($container-columns: $fg-max-columns, $gutter: $fg-gutter) { 10 | $container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter; 11 | @return percentage($gutter / $container-width); 12 | } 13 | 14 | // The $fg-column, $fg-gutter and $fg-max-columns variables must be defined in your base stylesheet to properly use the flex-grid function. 15 | // This function takes the fluid grid equation (target / context = result) and uses columns to help define each. 16 | // 17 | // The calculation presumes that your column structure will be missing the last gutter: 18 | // 19 | // -- column -- gutter -- column -- gutter -- column 20 | // 21 | // $fg-column: 60px; // Column Width 22 | // $fg-gutter: 25px; // Gutter Width 23 | // $fg-max-columns: 12; // Total Columns For Main Container 24 | // 25 | // div { 26 | // width: flex-grid(4); // returns (315px / 995px) = 31.65829%; 27 | // margin-left: flex-gutter(); // returns (25px / 995px) = 2.51256%; 28 | // 29 | // p { 30 | // width: flex-grid(2, 4); // returns (145px / 315px) = 46.031746%; 31 | // float: left; 32 | // margin: flex-gutter(4); // returns (25px / 315px) = 7.936508%; 33 | // } 34 | // 35 | // blockquote { 36 | // float: left; 37 | // width: flex-grid(2, 4); // returns (145px / 315px) = 46.031746%; 38 | // } 39 | // } -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/functions/_grid-width.scss: -------------------------------------------------------------------------------- 1 | @function grid-width($n) { 2 | @return $n * $gw-column + ($n - 1) * $gw-gutter; 3 | } 4 | 5 | // The $gw-column and $gw-gutter variables must be defined in your base stylesheet to properly use the grid-width function. 6 | // 7 | // $gw-column: 100px; // Column Width 8 | // $gw-gutter: 40px; // Gutter Width 9 | // 10 | // div { 11 | // width: grid-width(4); // returns 520px; 12 | // margin-left: $gw-gutter; // returns 40px; 13 | // } 14 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/functions/_linear-gradient.scss: -------------------------------------------------------------------------------- 1 | @function linear-gradient($pos, $gradients...) { 2 | $type: linear; 3 | $pos-type: type-of(nth($pos, 1)); 4 | 5 | // if $pos doesn't exist, fix $gradient 6 | @if ($pos-type == color) or (nth($pos, 1) == "transparent") { 7 | $gradients: zip($pos $gradients); 8 | $pos: false; 9 | } 10 | 11 | $type-gradient: $type, $pos, $gradients; 12 | @return $type-gradient; 13 | } 14 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/functions/_modular-scale.scss: -------------------------------------------------------------------------------- 1 | @function modular-scale($value, $increment, $ratio) { 2 | @if $increment > 0 { 3 | @for $i from 1 through $increment { 4 | $value: ($value * $ratio); 5 | } 6 | } 7 | 8 | @if $increment < 0 { 9 | $increment: abs($increment); 10 | @for $i from 1 through $increment { 11 | $value: ($value / $ratio); 12 | } 13 | } 14 | 15 | @return $value; 16 | } 17 | 18 | // div { 19 | // Increment Up GR with positive value 20 | // font-size: modular-scale(14px, 1, 1.618); // returns: 22.652px 21 | // 22 | // Increment Down GR with negative value 23 | // font-size: modular-scale(14px, -1, 1.618); // returns: 8.653px 24 | // 25 | // Can be used with ceil(round up) or floor(round down) 26 | // font-size: floor( modular-scale(14px, 1, 1.618) ); // returns: 22px 27 | // font-size: ceil( modular-scale(14px, 1, 1.618) ); // returns: 23px 28 | // } 29 | // 30 | // modularscale.com 31 | 32 | @function golden-ratio($value, $increment) { 33 | @return modular-scale($value, $increment, 1.618) 34 | } 35 | 36 | // div { 37 | // font-size: golden-ratio(14px, 1); // returns: 22.652px 38 | // } 39 | // 40 | // goldenratiocalculator.com 41 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/functions/_px-to-em.scss: -------------------------------------------------------------------------------- 1 | // Convert pixels to ems 2 | // eg. for a relational value of 12px write em(12) when the parent is 16px 3 | // if the parent is another value say 24px write em(12, 24) 4 | 5 | @function em($pxval, $base: 16) { 6 | @return ($pxval / $base) * 1em; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/functions/_radial-gradient.scss: -------------------------------------------------------------------------------- 1 | // This function is required and used by the background-image mixin. 2 | @function radial-gradient($G1, $G2, 3 | $G3: false, $G4: false, 4 | $G5: false, $G6: false, 5 | $G7: false, $G8: false, 6 | $G9: false, $G10: false, 7 | $pos: null, 8 | $shape-size: null) { 9 | 10 | $data: _radial-arg-parser($G1, $G2, $pos, $shape-size); 11 | $G1: nth($data, 1); 12 | $G2: nth($data, 2); 13 | $pos: nth($data, 3); 14 | $shape-size: nth($data, 4); 15 | 16 | $type: radial; 17 | $gradient: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); 18 | 19 | $type-gradient: $type, $shape-size $pos, $gradient; 20 | @return $type-gradient; 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/functions/_tint-shade.scss: -------------------------------------------------------------------------------- 1 | // Add percentage of white to a color 2 | @function tint($color, $percent){ 3 | @return mix(white, $color, $percent); 4 | } 5 | 6 | // Add percentage of black to a color 7 | @function shade($color, $percent){ 8 | @return mix(black, $color, $percent); 9 | } 10 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/functions/_transition-property-name.scss: -------------------------------------------------------------------------------- 1 | // Return vendor-prefixed property names if appropriate 2 | // Example: transition-property-names((transform, color, background), moz) -> -moz-transform, color, background 3 | //************************************************************************// 4 | @function transition-property-names($props, $vendor: false) { 5 | $new-props: (); 6 | 7 | @each $prop in $props { 8 | $new-props: append($new-props, transition-property-name($prop, $vendor), comma); 9 | } 10 | 11 | @return $new-props; 12 | } 13 | 14 | @function transition-property-name($prop, $vendor: false) { 15 | // put other properties that need to be prefixed here aswell 16 | @if $vendor and $prop == transform { 17 | @return unquote('-'+$vendor+'-'+$prop); 18 | } 19 | @else { 20 | @return $prop; 21 | } 22 | } -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/helpers/_deprecated-webkit-gradient.scss: -------------------------------------------------------------------------------- 1 | // Render Deprecated Webkit Gradient - Linear || Radial 2 | //************************************************************************// 3 | @function _deprecated-webkit-gradient($type, 4 | $deprecated-pos1, $deprecated-pos2, 5 | $full, 6 | $deprecated-radius1: false, $deprecated-radius2: false) { 7 | $gradient-list: (); 8 | $gradient: false; 9 | $full-length: length($full); 10 | $percentage: false; 11 | $gradient-type: $type; 12 | 13 | @for $i from 1 through $full-length { 14 | $gradient: nth($full, $i); 15 | 16 | @if length($gradient) == 2 { 17 | $color-stop: color-stop(nth($gradient, 2), nth($gradient, 1)); 18 | $gradient-list: join($gradient-list, $color-stop, comma); 19 | } 20 | @else if $gradient != null { 21 | @if $i == $full-length { 22 | $percentage: 100%; 23 | } 24 | @else { 25 | $percentage: ($i - 1) * (100 / ($full-length - 1)) + "%"; 26 | } 27 | $color-stop: color-stop(unquote($percentage), $gradient); 28 | $gradient-list: join($gradient-list, $color-stop, comma); 29 | } 30 | } 31 | 32 | @if $type == radial { 33 | $gradient: -webkit-gradient(radial, $deprecated-pos1, $deprecated-radius1, $deprecated-pos2, $deprecated-radius2, $gradient-list); 34 | } 35 | @else if $type == linear { 36 | $gradient: -webkit-gradient(linear, $deprecated-pos1, $deprecated-pos2, $gradient-list); 37 | } 38 | @return $gradient; 39 | } 40 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/helpers/_gradient-positions-parser.scss: -------------------------------------------------------------------------------- 1 | @function _gradient-positions-parser($gradient-type, $gradient-positions) { 2 | @if $gradient-positions 3 | and ($gradient-type == linear) 4 | and (type-of($gradient-positions) != color) { 5 | $gradient-positions: _linear-positions-parser($gradient-positions); 6 | } 7 | @else if $gradient-positions 8 | and ($gradient-type == radial) 9 | and (type-of($gradient-positions) != color) { 10 | $gradient-positions: _radial-positions-parser($gradient-positions); 11 | } 12 | @return $gradient-positions; 13 | } 14 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/helpers/_linear-positions-parser.scss: -------------------------------------------------------------------------------- 1 | @function _linear-positions-parser($pos) { 2 | $type: type-of(nth($pos, 1)); 3 | $spec: null; 4 | $degree: null; 5 | $side: null; 6 | $corner: null; 7 | $length: length($pos); 8 | // Parse Side and corner positions 9 | @if ($length > 1) { 10 | @if nth($pos, 1) == "to" { // Newer syntax 11 | $side: nth($pos, 2); 12 | 13 | @if $length == 2 { // eg. to top 14 | // Swap for backwards compatability 15 | $degree: _position-flipper(nth($pos, 2)); 16 | } 17 | @else if $length == 3 { // eg. to top left 18 | $corner: nth($pos, 3); 19 | } 20 | } 21 | @else if $length == 2 { // Older syntax ("top left") 22 | $side: _position-flipper(nth($pos, 1)); 23 | $corner: _position-flipper(nth($pos, 2)); 24 | } 25 | 26 | @if ("#{$side} #{$corner}" == "left top") or ("#{$side} #{$corner}" == "top left") { 27 | $degree: _position-flipper(#{$side}) _position-flipper(#{$corner}); 28 | } 29 | @else if ("#{$side} #{$corner}" == "right top") or ("#{$side} #{$corner}" == "top right") { 30 | $degree: _position-flipper(#{$side}) _position-flipper(#{$corner}); 31 | } 32 | @else if ("#{$side} #{$corner}" == "right bottom") or ("#{$side} #{$corner}" == "bottom right") { 33 | $degree: _position-flipper(#{$side}) _position-flipper(#{$corner}); 34 | } 35 | @else if ("#{$side} #{$corner}" == "left bottom") or ("#{$side} #{$corner}" == "bottom left") { 36 | $degree: _position-flipper(#{$side}) _position-flipper(#{$corner}); 37 | } 38 | $spec: to $side $corner; 39 | } 40 | @else if $length == 1 { 41 | // Swap for backwards compatability 42 | @if $type == string { 43 | $degree: $pos; 44 | $spec: to _position-flipper($pos); 45 | } 46 | @else { 47 | $degree: -270 - $pos; //rotate the gradient opposite from spec 48 | $spec: $pos; 49 | } 50 | } 51 | $degree: unquote($degree + ","); 52 | $spec: unquote($spec + ","); 53 | @return $degree $spec; 54 | } 55 | 56 | @function _position-flipper($pos) { 57 | @return if($pos == left, right, null) 58 | if($pos == right, left, null) 59 | if($pos == top, bottom, null) 60 | if($pos == bottom, top, null); 61 | } 62 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/helpers/_radial-arg-parser.scss: -------------------------------------------------------------------------------- 1 | @function _radial-arg-parser($G1, $G2, $pos, $shape-size) { 2 | @each $value in $G1, $G2 { 3 | $first-val: nth($value, 1); 4 | $pos-type: type-of($first-val); 5 | $spec-at-index: null; 6 | 7 | // Determine if spec was passed to mixin 8 | @if type-of($value) == list { 9 | $spec-at-index: if(index($value, at), index($value, at), false); 10 | } 11 | @if $spec-at-index { 12 | @if $spec-at-index > 1 { 13 | @for $i from 1 through ($spec-at-index - 1) { 14 | $shape-size: $shape-size nth($value, $i); 15 | } 16 | @for $i from ($spec-at-index + 1) through length($value) { 17 | $pos: $pos nth($value, $i); 18 | } 19 | } 20 | @else if $spec-at-index == 1 { 21 | @for $i from ($spec-at-index + 1) through length($value) { 22 | $pos: $pos nth($value, $i); 23 | } 24 | } 25 | $G1: false; 26 | } 27 | 28 | // If not spec calculate correct values 29 | @else { 30 | @if ($pos-type != color) or ($first-val != "transparent") { 31 | @if ($pos-type == number) 32 | or ($first-val == "center") 33 | or ($first-val == "top") 34 | or ($first-val == "right") 35 | or ($first-val == "bottom") 36 | or ($first-val == "left") { 37 | 38 | $pos: $value; 39 | 40 | @if $pos == $G1 { 41 | $G1: false; 42 | } 43 | } 44 | 45 | @else if 46 | ($first-val == "ellipse") 47 | or ($first-val == "circle") 48 | or ($first-val == "closest-side") 49 | or ($first-val == "closest-corner") 50 | or ($first-val == "farthest-side") 51 | or ($first-val == "farthest-corner") 52 | or ($first-val == "contain") 53 | or ($first-val == "cover") { 54 | 55 | $shape-size: $value; 56 | 57 | @if $value == $G1 { 58 | $G1: false; 59 | } 60 | 61 | @else if $value == $G2 { 62 | $G2: false; 63 | } 64 | } 65 | } 66 | } 67 | } 68 | @return $G1, $G2, $pos, $shape-size; 69 | } 70 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/helpers/_radial-positions-parser.scss: -------------------------------------------------------------------------------- 1 | @function _radial-positions-parser($gradient-pos) { 2 | $shape-size: nth($gradient-pos, 1); 3 | $pos: nth($gradient-pos, 2); 4 | $shape-size-spec: _shape-size-stripper($shape-size); 5 | 6 | $pre-spec: unquote(if($pos, "#{$pos}, ", null)) 7 | unquote(if($shape-size, "#{$shape-size},", null)); 8 | $pos-spec: if($pos, "at #{$pos}", null); 9 | 10 | $spec: "#{$shape-size-spec} #{$pos-spec}"; 11 | 12 | // Add comma 13 | @if ($spec != ' ') { 14 | $spec: "#{$spec}," 15 | } 16 | 17 | @return $pre-spec $spec; 18 | } 19 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/helpers/_render-gradients.scss: -------------------------------------------------------------------------------- 1 | // User for linear and radial gradients within background-image or border-image properties 2 | 3 | @function _render-gradients($gradient-positions, $gradients, $gradient-type, $vendor: false) { 4 | $pre-spec: null; 5 | $spec: null; 6 | $vendor-gradients: null; 7 | @if $gradient-type == linear { 8 | @if $gradient-positions { 9 | $pre-spec: nth($gradient-positions, 1); 10 | $spec: nth($gradient-positions, 2); 11 | } 12 | } 13 | @else if $gradient-type == radial { 14 | $pre-spec: nth($gradient-positions, 1); 15 | $spec: nth($gradient-positions, 2); 16 | } 17 | 18 | @if $vendor { 19 | $vendor-gradients: -#{$vendor}-#{$gradient-type}-gradient(#{$pre-spec} $gradients); 20 | } 21 | @else if $vendor == false { 22 | $vendor-gradients: "#{$gradient-type}-gradient(#{$spec} #{$gradients})"; 23 | $vendor-gradients: unquote($vendor-gradients); 24 | } 25 | @return $vendor-gradients; 26 | } 27 | -------------------------------------------------------------------------------- /gather/assets/stylesheets/vendor/bourbon/helpers/_shape-size-stripper.scss: -------------------------------------------------------------------------------- 1 | @function _shape-size-stripper($shape-size) { 2 | $shape-size-spec: null; 3 | @each $value in $shape-size { 4 | @if ($value == "cover") or ($value == "contain") { 5 | $value: null; 6 | } 7 | $shape-size-spec: "#{$shape-size-spec} #{$value}"; 8 | } 9 | @return $shape-size-spec; 10 | } 11 | -------------------------------------------------------------------------------- /gather/extensions.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | __all__ = ["db", "assets"] 4 | 5 | from flask.ext.sqlalchemy import SQLAlchemy 6 | from flask.ext.assets import Environment, Bundle 7 | from flask.ext.mail import Mail 8 | from flask.ext.cache import Cache 9 | from flask.ext.restless import APIManager 10 | 11 | db = SQLAlchemy() 12 | assets = Environment() 13 | 14 | js_all = Bundle( 15 | "javascripts/libs/jquery-2.1.0.min.js", 16 | "javascripts/libs/turbolinks.js", 17 | "javascripts/libs/jquery.atwho.js", 18 | Bundle( 19 | "javascripts/libs/timeago.coffee", 20 | "javascripts/libs/locales/timeago.zh-cn.coffee", 21 | "javascripts/turbolinks_icon.coffee", 22 | "javascripts/gather.coffee", 23 | filters="coffeescript" 24 | ), 25 | filters="rjsmin", 26 | output="gather.js" 27 | ) 28 | assets.register("js_all", js_all) 29 | 30 | css_all = Bundle( 31 | "stylesheets/gather.sass", 32 | depends=["stylesheets/*.sass", "stylesheets/*.scss"], 33 | filters=("sass", "cssmin"), 34 | output="gather.css" 35 | ) 36 | assets.register("css_all", css_all) 37 | 38 | mail = Mail() 39 | cache = Cache() 40 | api_manager = APIManager() -------------------------------------------------------------------------------- /gather/filters.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import re 4 | import bleach 5 | import datetime 6 | 7 | from flask import url_for, g 8 | from flask.ext.sqlalchemy import models_committed 9 | from markupsafe import Markup 10 | from tornado.escape import xhtml_escape, to_unicode, _URL_RE 11 | from pygments import highlight 12 | from pygments.formatters import HtmlFormatter 13 | from pygments.lexers import get_lexer_by_name, TextLexer 14 | from gather.account.models import Account 15 | from gather.node.models import Node 16 | from gather.topic.models import Topic, Reply 17 | from gather.extensions import cache 18 | from gather.utils import gen_action_token 19 | 20 | 21 | _CODE_RE = re.compile(r'```(\w+)(.+?)```', re.S) 22 | _MENTION_RE = re.compile(r'((?:^|\W)@\w+)') 23 | _FLOOR_RE = re.compile(r'((?:^|[^&])#\d+)') 24 | _EMAIL_RE = re.compile(r'([A-Za-z0-9-+.]+@[A-Za-z0-9-.]+)(\s|$)') 25 | formatter = HtmlFormatter() 26 | 27 | 28 | def get_site_status(): 29 | account, node, topic, reply = cache.get_many( 30 | 'status-account', 'status-node', 'status-topic', 'status-reply' 31 | ) 32 | if not account: 33 | account = Account.query.count() 34 | cache.set('status-account', account) 35 | if not node: 36 | node = Node.query.count() 37 | cache.set('status-node', node) 38 | if not topic: 39 | topic = Topic.query.count() 40 | cache.set('status-topic', topic) 41 | if not reply: 42 | reply = Reply.query.count() 43 | cache.set('status-reply', reply) 44 | return dict( 45 | account=account, 46 | node=node, 47 | topic=topic, 48 | reply=reply, 49 | ) 50 | 51 | 52 | def _clear_cache(sender, changes): 53 | for model, operation in changes: 54 | if isinstance(model, Account) and operation != 'update': 55 | cache.delete('status-account') 56 | if isinstance(model, Node) and operation != 'update': 57 | cache.delete('status-node') 58 | if isinstance(model, Topic) and operation != 'update': 59 | cache.delete('status-topic') 60 | if isinstance(model, Reply) and operation != 'update': 61 | cache.delete('status-reply') 62 | 63 | 64 | models_committed.connect(_clear_cache) 65 | 66 | 67 | def sanitize(content): 68 | return bleach.linkify(content) 69 | 70 | 71 | @cache.memoize(timeout=3600*24) 72 | def content_to_html(text, extra_params='rel="nofollow"'): 73 | if not text.strip(): 74 | return "" 75 | if extra_params: 76 | extra_params = " " + extra_params.strip() 77 | 78 | def make_link(m): 79 | url = m.group(1) 80 | proto = m.group(2) 81 | 82 | href = m.group(1) 83 | if not proto: 84 | href = "http://" + href # no proto specified, use http 85 | 86 | params = extra_params 87 | 88 | if '.' in href: 89 | name_extension = href.split('.')[-1].lower() 90 | if name_extension in ('jpg', 'png', 'gif', 'jpeg'): 91 | return u'' % href 92 | 93 | return u'%s' % (href, params, url) 94 | 95 | def cover_email(m): 96 | data = {'mail': m.group(1), 97 | 'end': m.group(2)} 98 | return u'%(mail)s%(end)s' % data 99 | 100 | def convert_mention(m): 101 | data = {} 102 | data['begin'], data['user'] = m.group(1).split('@') 103 | data["user_profile"] = url_for("user.profile", name=data['user']) 104 | t = u'%(begin)s' \ 105 | u'@%(user)s' 106 | return t % data 107 | 108 | def convert_floor(m): 109 | data = {} 110 | data['begin'], data['floor'] = m.group(1).split('#') 111 | t = u'%(begin)s#%(floor)s' 113 | return t % data 114 | 115 | def highligt(m): 116 | try: 117 | name = m.group(1) 118 | lexer = get_lexer_by_name(name) 119 | except ValueError: 120 | lexer = TextLexer() 121 | text = m.group(2).replace('"', '"').replace('&', '&') 122 | text = text.replace('<', '<').replace('>', '>') 123 | text = text.replace(' ', ' ') 124 | return highlight(text, lexer, formatter) 125 | 126 | text = to_unicode(xhtml_escape(text)).replace(' ', ' ') 127 | text = _CODE_RE.sub(highligt, text).replace('\n', '
') 128 | text = _MENTION_RE.sub(convert_mention, text) 129 | text = _EMAIL_RE.sub(cover_email, text) 130 | text = _FLOOR_RE.sub(convert_floor, text) 131 | return Markup(_URL_RE.sub(make_link, text)) 132 | 133 | 134 | def xmldatetime(value): 135 | if not isinstance(value, datetime.datetime): 136 | return value 137 | return value.strftime('%Y-%m-%dT%H:%M:%SZ') 138 | 139 | 140 | def url_for_other_page(page): 141 | from flask import request, url_for 142 | args = request.view_args.copy() 143 | args["page"] = int(page) 144 | return url_for(request.endpoint, **args) 145 | 146 | 147 | def url_for_with_token(endpoint, **values): 148 | if not g.user: 149 | return 150 | values["token"] = gen_action_token(40) 151 | return url_for(endpoint, **values) 152 | -------------------------------------------------------------------------------- /gather/form.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from flask import g, _request_ctx_stack 6 | from flask.ext.wtf import Form as _Form 7 | from flask.ext.wtf.csrf import generate_csrf, validate_csrf 8 | from flask.ext.wtf.form import _Auto 9 | from wtforms import ValidationError 10 | from gather.extensions import cache 11 | 12 | 13 | class Form(_Form): 14 | """ 15 | 让 CSRF 成为基于 Cache 的一次性字符 16 | """ 17 | 18 | def __init__(self, formdata=_Auto, obj=None, prefix='', csrf_context=None, 19 | secret_key=None, csrf_enabled=None, *args, **kwargs): 20 | self.obj = obj 21 | if csrf_enabled is None: 22 | ctx = _request_ctx_stack.top 23 | if ctx is not None: 24 | if ctx.request.path.startswith("/api/"): 25 | csrf_enabled = False 26 | # Disbale CSRF on API Pages 27 | super(Form, self).__init__(formdata, obj, prefix, 28 | csrf_context=csrf_context, 29 | secret_key=secret_key, 30 | csrf_enabled=csrf_enabled, 31 | *args, **kwargs) 32 | 33 | def generate_csrf_token(self, csrf_context=None): 34 | if not self.csrf_enabled: 35 | return None 36 | csrf = generate_csrf(self.SECRET_KEY, self.TIME_LIMIT) 37 | if g.user: 38 | cache_value = g.user.id 39 | else: 40 | cache_value = 0 41 | cache.set("csrf_%s" % csrf, cache_value, self.TIME_LIMIT) 42 | return csrf 43 | 44 | def validate_csrf_token(self, field): 45 | if not self.csrf_enabled: 46 | return True 47 | if not self.validate_csrf_data(field.data): 48 | raise ValidationError("Wrong CSRF Token") 49 | 50 | def validate_csrf_data(self, data): 51 | if not validate_csrf(data, self.SECRET_KEY, self.TIME_LIMIT): 52 | return False 53 | key = "csrf_%s" % data 54 | cache_value = cache.get(key) 55 | cache.delete(key) 56 | if cache_value is not None: 57 | if cache_value == 0: 58 | return True 59 | return cache_value == g.user.id 60 | return False 61 | -------------------------------------------------------------------------------- /gather/frontend/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from gather.frontend.views import bp 4 | 5 | __all__ = ["bp"] 6 | -------------------------------------------------------------------------------- /gather/frontend/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from flask import Blueprint, render_template, make_response 4 | 5 | bp = Blueprint("frontend", __name__, url_prefix="") 6 | 7 | 8 | @bp.route("/") 9 | def index(): 10 | from gather.topic.models import Topic 11 | topics = Topic.query.order_by(Topic.updated.desc()).limit(5) 12 | return render_template("index.html", topics=topics) 13 | 14 | 15 | @bp.route("/feed") 16 | def feed(): 17 | from gather.topic.models import Topic 18 | topics = Topic.query.order_by(Topic.updated.desc()).limit(15) 19 | response = make_response(render_template("feed.xml", topics=topics)) 20 | response.mimetype = "application/atom+xml" 21 | return response 22 | 23 | -------------------------------------------------------------------------------- /gather/node/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import api 4 | 5 | from .views import bp 6 | 7 | __all__ = ("bp", "api") 8 | -------------------------------------------------------------------------------- /gather/node/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from gather.api import EXCLUDE_COLUMNS 3 | 4 | from gather.extensions import api_manager 5 | from gather.node.models import Node 6 | 7 | 8 | bp = api_manager.create_api_blueprint( 9 | Node, 10 | methods=["GET"], 11 | exclude_columns=EXCLUDE_COLUMNS 12 | ) 13 | -------------------------------------------------------------------------------- /gather/node/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from gather.form import Form 6 | from wtforms import TextField, TextAreaField 7 | from wtforms.validators import DataRequired, Optional, Length, URL 8 | from .models import Node 9 | 10 | 11 | class ChangeNodeForm(Form): 12 | name = TextField("节点名称", validators=[ 13 | DataRequired() 14 | ]) 15 | slug = TextField("Slug", validators=[ 16 | DataRequired() 17 | ], description="就是会出现在 URL 里面的那坨字母~" 18 | ) 19 | description = TextAreaField("简介", validators=[ 20 | Optional(), Length(max=500) 21 | ], description="喵") 22 | icon = TextField("节点图标", validators=[ 23 | Optional(), URL() 24 | ]) 25 | 26 | def validate_parent_node(self, field): 27 | if field.data and field.data == self.obj: 28 | raise ValueError("父节点不能是自己= =") 29 | 30 | def save(self): 31 | node = self.obj 32 | self.populate_obj(node) 33 | return node.save() 34 | 35 | 36 | class CreateNodeForm(ChangeNodeForm): 37 | def validate_name(self, field): 38 | if Node.query.filter_by(name=field.data.lower()).count(): 39 | raise ValueError("这个节点名被注册了") 40 | 41 | def validate_slug(self, field): 42 | if Node.query.filter_by(slug=field.data.lower()).count(): 43 | raise ValueError("这个 Slug 被注册了") 44 | 45 | def create(self): 46 | node = Node(**self.data) 47 | self.node = node 48 | return node.save() 49 | -------------------------------------------------------------------------------- /gather/node/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from gather.extensions import db 6 | 7 | 8 | class Node(db.Model): 9 | id = db.Column(db.Integer, primary_key=True) 10 | name = db.Column(db.String(100), nullable=False, unique=True, index=True) 11 | slug = db.Column(db.String(100), nullable=False, unique=True, index=True) 12 | description = db.Column(db.String(500), nullable=True, default="") 13 | icon = db.Column(db.String(100), nullable=True, default="") 14 | 15 | def __str__(self): 16 | return self.name 17 | 18 | def __repr__(self): 19 | return '' % self.name 20 | 21 | @classmethod 22 | def query_all(cls): 23 | return cls.query.order_by(Node.name.asc()).all() 24 | 25 | def save(self): 26 | db.session.add(self) 27 | db.session.commit() 28 | return self 29 | 30 | def delete(self): 31 | from gather.topic.models import Topic 32 | for topic in Topic.query.filter_by(node=self).all(): 33 | topic.delete() 34 | db.session.delete(self) 35 | db.session.commit() 36 | return self 37 | -------------------------------------------------------------------------------- /gather/node/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from flask import Blueprint 4 | from flask import render_template, redirect, url_for 5 | 6 | from gather.account.utils import require_staff 7 | from .forms import CreateNodeForm, ChangeNodeForm 8 | from .models import Node 9 | 10 | bp = Blueprint("node", __name__, url_prefix="/node") 11 | 12 | 13 | @bp.route("/") 14 | def index(): 15 | items = Node.query_all() 16 | return render_template("node/index.html", items=items) 17 | 18 | 19 | @bp.route("/create", methods=("GET", "POST")) 20 | @require_staff 21 | def create(): 22 | form = CreateNodeForm() 23 | if form.validate_on_submit(): 24 | print "validate" 25 | form.create() 26 | return redirect(url_for(".node", slug=form.node.slug)) 27 | else: 28 | print form.errors 29 | return render_template("node/create.html", form=form) 30 | 31 | 32 | @bp.route("/", defaults={'page': 1}) 33 | @bp.route("//page/") 34 | def node(slug, page): 35 | from gather.topic.models import Topic 36 | node = Node.query.filter_by(slug=slug).first_or_404() 37 | topics = Topic.query.filter_by(node=node) 38 | paginator = topics.order_by(Topic.updated.desc()).paginate(page) 39 | return render_template("node/node.html", node=node, paginator=paginator) 40 | 41 | 42 | @bp.route("//change", methods=("GET", "POST")) 43 | def change(slug): 44 | node = Node.query.filter_by(slug=slug).first_or_404() 45 | form = ChangeNodeForm(obj=node) 46 | if form.validate_on_submit(): 47 | form.save() 48 | return redirect(url_for(".node", slug=node.slug)) 49 | return render_template("node/change.html", form=form) 50 | -------------------------------------------------------------------------------- /gather/notification/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | -------------------------------------------------------------------------------- /gather/public/humans.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whtsky/Gather/09a83d64b8e792964215b1a257531ee0d9d8b931/gather/public/humans.txt -------------------------------------------------------------------------------- /gather/public/static/.webassets-manifest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whtsky/Gather/09a83d64b8e792964215b1a257531ee0d9d8b931/gather/public/static/.webassets-manifest -------------------------------------------------------------------------------- /gather/public/static/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whtsky/Gather/09a83d64b8e792964215b1a257531ee0d9d8b931/gather/public/static/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /gather/public/static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whtsky/Gather/09a83d64b8e792964215b1a257531ee0d9d8b931/gather/public/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /gather/public/static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whtsky/Gather/09a83d64b8e792964215b1a257531ee0d9d8b931/gather/public/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /gather/public/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whtsky/Gather/09a83d64b8e792964215b1a257531ee0d9d8b931/gather/public/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /gather/settings/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import os 4 | 5 | BASEDIR = os.path.abspath(os.path.dirname(__file__)) 6 | 7 | 8 | def load_develop_settings(app): 9 | app.config.from_pyfile(os.path.join(BASEDIR, "develop.py")) 10 | 11 | 12 | def load_production_settings(app): 13 | app.config.from_pyfile(os.path.join(BASEDIR, "production.py")) 14 | 15 | 16 | def load_settings(app): 17 | app.config.from_pyfile(os.path.join(BASEDIR, "base.py")) 18 | load_develop_settings(app) 19 | import getpass 20 | if getpass.getuser() == app.config["PRODUCTION_USER"]: 21 | load_production_settings(app) 22 | -------------------------------------------------------------------------------- /gather/settings/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | gather_base_dir = os.path.dirname(os.path.abspath(os.path.dirname(__file__))) 4 | 5 | FORUM_TITLE = "Gather" 6 | 7 | FORUM_DOMAIN = "127.0.0.1" 8 | FORUM_URL = "http://%s" % FORUM_DOMAIN 9 | MAIL_DEFAULT_SENDER = "no-reply@%s" % FORUM_DOMAIN 10 | 11 | PRODUCTION_USER = "gather" 12 | 13 | GRAVATAR_BASE_URL = "http://cn.gravatar.com/avatar/" 14 | 15 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True 16 | 17 | ASSETS_LOAD_PATH = [os.path.join(gather_base_dir, "assets")] 18 | -------------------------------------------------------------------------------- /gather/settings/develop.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import os 3 | 4 | DEBUG = True 5 | 6 | SECRET_KEY = "develop" 7 | 8 | """ 9 | SQLALCHEMY_DATABASE_URI = 'sqlite:///%s' % os.path.join( 10 | os.getcwd(), 'db.sqlite' 11 | ) 12 | """ 13 | 14 | SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://postgres@localhost/gather" 15 | 16 | 17 | PASSWORD_SECRET = "develop" 18 | 19 | CACHE_TYPE = "simple" 20 | -------------------------------------------------------------------------------- /gather/settings/production.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | DEBUG = False 4 | 5 | SENTRY_DSN = "" 6 | SECRET_KEY = "reset this" 7 | 8 | PASSWORD_SECRET = "reset this" 9 | 10 | SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://gather@localhost/gather" 11 | 12 | GOOGLE_ANALYTICS = "" 13 | 14 | CACHE_TYPE = "memcached" 15 | CACHE_KEY_PREFIX = "gather_" 16 | -------------------------------------------------------------------------------- /gather/templates/account/find_password.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}找回密码{% endblock %} 4 | 5 | {% block content %} 6 |
7 |

找回密码

8 | {% from "snippet/form.html" import form_field %} 9 |
10 | {{ form.csrf_token }} 11 | {{ form_field(form.email) }} 12 |
13 | 14 |
15 |
16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /gather/templates/account/find_sent.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}找回密码{% endblock %} 4 | 5 | {% block content %} 6 |
7 | 找回密码链接已发出 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /gather/templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% from "snippet/form.html" import form_field %} 3 | 4 | {% block title %}登录{% endblock %} 5 | 6 | {% from "snippet/nav.html" import navigation %} 7 | {% block nav %}{{ navigation('login') }}{% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |

登录

13 | {{ form.csrf_token }} 14 | {{ form_field(form.username) }} 15 | {{ form_field(form.password) }} 16 |
17 | 18 | 忘记密码了? 19 |
20 |
21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /gather/templates/account/register.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% from "snippet/form.html" import form_field %} 3 | 4 | {% block title %}注册{% endblock %} 5 | 6 | {% from "snippet/nav.html" import navigation %} 7 | {% block nav %}{{ navigation('register') }}{% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |

注册

13 | {{ form.csrf_token }} 14 | {{ form_field(form.username) }} 15 | {{ form_field(form.email) }} 16 | {{ form_field(form.password) }} 17 |
18 | 19 | 已经有帐号了? 20 |
21 |
22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /gather/templates/account/reset.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}重设密码{% endblock %} 4 | 5 | {% block content %} 6 |
7 |

重设密码

8 | {% from "snippet/form.html" import form_field %} 9 |
10 | {{ form.csrf_token }} 11 | {{ form_field(form.password) }} 12 |
13 | 14 |
15 |
16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /gather/templates/account/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% from "snippet/form.html" import form_field, checkbox_field %} 3 | 4 | {% block title %}设置{% endblock %} 5 | 6 | {% from "snippet/nav.html" import navigation %} 7 | {% block nav %}{{ navigation('settings') }}{% endblock %} 8 | 9 | {% block content %} 10 |
11 |
设置
12 |
13 | {{ form.csrf_token }} 14 | {{ form_field(form.username) }} 15 | {{ form_field(form.email) }} 16 | {{ form_field(form.website) }} 17 | {{ checkbox_field(form.feeling_lucky) }} 18 | {{ form_field(form.description) }} 19 | {{ form_field(form.css) }} 20 |
21 | 22 |
23 |
24 |
25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /gather/templates/email/reset.html: -------------------------------------------------------------------------------- 1 |

你好,{{ user.username }}

2 | 3 |

我们刚刚收到了你重置的账号的申请,你可以点击下面的链接重置你的密码:

4 | 5 |

{{ url }}

6 | 7 |

如果你没有提出过密码修改申请,请忽略此邮件。我们不会对你的帐户进行任何变更。

8 | 9 | {{ config["FORUM_TITLE"] }} 10 | -------------------------------------------------------------------------------- /gather/templates/feed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ config.FORUM_TITLE }} 4 | 5 | 6 | {{ url_for(".index", _external=True) }} 7 | {% for topic in topics %} 8 | 9 | <![CDATA[{{ topic.title }}]]> 10 | 11 | {{ url_for("topic.topic", topic_id=topic.id, _external=True) }} 12 | 13 | 16 | 17 | 18 | {% endfor %} 19 | 20 | -------------------------------------------------------------------------------- /gather/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}首页{% endblock %} 4 | 5 | {% from "snippet/nav.html" import navigation %} 6 | {% block nav %}{{ navigation() }}{% endblock %} 7 | 8 | {% block main %} 9 |
10 |
最近更新
11 | {% from "snippet/topic.html" import topic_list %} 12 | {{ topic_list(topics, to_last_page=True) }} 13 | 16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /gather/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title %}{% endblock %}{% if paginator %} · Page {{ paginator.page }}{% endif %} · Gather 5 | 6 | 7 | {% assets "css_all" %} 8 | 9 | {% endassets %} 10 | {% if config.GOOGLE_ANALYTICS and not config.DEBUG %} 11 | 27 | {% endif %} 28 | {% assets "js_all" %} 29 | 30 | {% endassets %} 31 | {% block head %} 32 | {% endblock %} 33 | 34 | 35 | {% if g.user and g.user.css %} 36 | 39 | {% endif %} 40 | {% if g.user %} 41 | 45 | {% endif %} 46 | 53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {% block content %} 62 | 63 |
64 | {% block main %} 65 | {% endblock %} 66 |
67 | 68 | 83 | 84 | {% endblock %} 85 |
86 | 87 | {% block before_footer %}{% endblock %} 88 | 89 | 94 | {% block js %} 95 | {% endblock %} 96 | 97 | -------------------------------------------------------------------------------- /gather/templates/node/change.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}修改节点{% endblock %} 4 | 5 | {% from "snippet/nav.html" import navigation %} 6 | {% block nav %}{{ navigation('node') }}{% endblock %} 7 | 8 | {% block content %} 9 | {% from "snippet/form.html" import form_field, description_field %} 10 |
11 |
修改节点
12 |
13 | {{ form.csrf_token }} 14 | {{ form_field(form.name) }} 15 | {{ form_field(form.slug) }} 16 | {{ form_field(form.icon) }} 17 | {{ form_field(form.description) }} 18 |
19 | 20 |
21 |
22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /gather/templates/node/create.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}创建节点{% endblock %} 4 | 5 | {% from "snippet/nav.html" import navigation %} 6 | {% block nav %}{{ navigation('node') }}{% endblock %} 7 | 8 | {% block content %} 9 | {% from "snippet/form.html" import form_field, description_field %} 10 |
11 |
创建节点
12 |
13 | {{ form.csrf_token }} 14 | {{ form_field(form.name) }} 15 | {{ form_field(form.slug) }} 16 | {{ form_field(form.icon) }} 17 | {{ form_field(form.description) }} 18 |
19 | 20 |
21 |
22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /gather/templates/node/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}节点{% endblock %} 4 | 5 | {% from "snippet/nav.html" import navigation %} 6 | {% block nav %}{{ navigation('node') }}{% endblock %} 7 | 8 | {% block content %} 9 |
10 | {% for node in items %} 11 |
12 | {% if node.icon %} 13 | 14 | {% endif %} 15 |
16 | {{ node.name }} 17 | {% if g.user and g.user.is_staff %} 18 | 19 | {% endif %} 20 |
21 | {{ node.description|content_to_html }} 22 |
23 | {% endfor %} 24 | {% if g.user and g.user.is_staff %} 25 |
26 | 创建节点 27 |
28 | {% endif %} 29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /gather/templates/node/node.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}{{ node.name }}{% endblock %} 4 | 5 | {% from "snippet/nav.html" import navigation %} 6 | {% block nav %}{{ navigation('node') }}{% endblock %} 7 | 8 | {% block head %} 9 | 10 | {% endblock %} 11 | 12 | {% block main %} 13 |
14 |
15 | {{ node.name }} 16 |
17 | {% from "snippet/topic.html" import topic_list %} 18 | {{ topic_list(paginator.items) }} 19 | 20 |
21 | {% from "snippet/pagination.html" import pagination %} 22 | {{ pagination(paginator, url_for(".node", slug=node.slug)) }} 23 |
24 |
25 | {% endblock %} 26 | 27 | {% block sidebar %} 28 | {% from "snippet/sidebar.html" import node_sidebar %} 29 | {{ node_sidebar(node) }} 30 | {% endblock %} -------------------------------------------------------------------------------- /gather/templates/snippet/form.html: -------------------------------------------------------------------------------- 1 | {% macro form_field(field) %} 2 |
3 | {{ field.label }} 4 | {% if field.description %} 5 | {{ field(placeholder=field.description) }} 6 | {% else %} 7 | {{ field(placeholder=field.label.text) }} 8 | {% endif %} 9 | {% if field.errors %} 10 |

{{ field.errors[0] }}

11 | {% endif %} 12 |
13 | {% endmacro %} 14 | 15 | {% macro checkbox_field(field) %} 16 |
17 | {{ field }} 18 | {{ field.label }} 19 |
20 | {% endmacro %} 21 | 22 | -------------------------------------------------------------------------------- /gather/templates/snippet/nav.html: -------------------------------------------------------------------------------- 1 | {% macro navigation(current='') %} 2 | 16 | 17 | 38 | {% endmacro %} 39 | -------------------------------------------------------------------------------- /gather/templates/snippet/pagination.html: -------------------------------------------------------------------------------- 1 | {% macro pagination(paginator, url) %} 2 |
    3 | {% if paginator.has_prev %} 4 |
  • 5 | {% endif %} 6 | {% for page in paginator.iter_pages() %} 7 | {% if page %} 8 | {% if page == paginator.page %} 9 |
  • {{ page }}
  • 10 | {% else %} 11 |
  • {{ page }}
  • 12 | {% endif %} 13 | {% else %} 14 |
  • 15 | {% endif %} 16 | {% endfor %} 17 | {% if paginator.has_next %} 18 |
  • 19 | {% endif %} 20 |
21 | {% endmacro %} 22 | -------------------------------------------------------------------------------- /gather/templates/snippet/sidebar.html: -------------------------------------------------------------------------------- 1 | {% macro node_sidebar(node) %} 2 |
3 |
4 | {{ node.name }} 5 | {% if g.user and g.user.is_staff %} 6 | 7 | {% endif %} 8 |
9 | {% if node.description %} 10 |
11 | {{ node.description|content_to_html }} 12 |
13 | {% endif %} 14 | 17 |
18 | {% endmacro %} 19 | 20 | {% macro user_sidebar(user) %} 21 | 41 | {% endmacro %} -------------------------------------------------------------------------------- /gather/templates/snippet/topic.html: -------------------------------------------------------------------------------- 1 | {% macro topic_info(topic) %} 2 | {{ topic.author }} · 3 | {{ topic.node.name }} · 4 | 发布 5 | {% if topic.changed %} 6 | · 修改 7 | {% endif %} 8 | {% endmacro %} 9 | 10 | {% macro topic_list(topics, to_last_page=False) %} 11 | {% for topic in topics %} 12 |
13 | {% if to_last_page %} 14 | {% set topic_url = url_for("topic.topic", topic_id=topic.id, page=topic.last_page) %} 15 | {% else %} 16 | {% set topic_url = url_for("topic.topic", topic_id=topic.id) %} 17 | {% endif %} 18 | 19 | {% set topic_url = topic_url + "#reply_count_%s" % topic.replies.count() %} 20 | 21 | 22 | {{ topic.replies.count() }} 23 | 24 | 25 | {% set author_url = url_for("user.profile", name=topic.author.username) %} 26 | 27 | {{ topic.author.username }} 28 | 29 |
30 | {{ topic.title }} 31 |
32 | {{ topic_info(topic) }} 33 |
34 |
35 |
36 | {% endfor %} 37 | {% endmacro %} 38 | -------------------------------------------------------------------------------- /gather/templates/topic/change.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}修改话题{% endblock %} 4 | 5 | {% from "snippet/nav.html" import navigation %} 6 | {% block nav %}{{ navigation('topic') }}{% endblock %} 7 | 8 | {% block content %} 9 | {% from "snippet/form.html" import form_field %} 10 |
11 |
修改话题
12 |
13 | {{ form.csrf_token }} 14 | {{ form_field(form.node) }} 15 | {{ form_field(form.title) }} 16 | {{ form_field(form.content) }} 17 |
18 | 19 |
20 |
21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /gather/templates/topic/change_reply.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}修改回复{% endblock %} 4 | 5 | {% from "snippet/nav.html" import navigation %} 6 | {% block nav %}{{ navigation('topic') }}{% endblock %} 7 | 8 | {% block content %} 9 | {% from "snippet/form.html" import form_field %} 10 |
11 |
修改回复
12 |
13 | {{ form.csrf_token }} 14 | {{ form_field(form.content) }} 15 |
16 | 17 |
18 |
19 |
20 | {% endblock %} -------------------------------------------------------------------------------- /gather/templates/topic/create.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}新建话题{% endblock %} 4 | 5 | {% from "snippet/nav.html" import navigation %} 6 | {% block nav %}{{ navigation('topic') }}{% endblock %} 7 | 8 | {% block content %} 9 | {% from "snippet/form.html" import form_field, select_field %} 10 |
11 |
新建话题
12 |
13 | {{ form.csrf_token }} 14 | {{ form_field(form.node) }} 15 | {{ form_field(form.title) }} 16 | {{ form_field(form.content) }} 17 |
18 | 19 |
20 |
21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /gather/templates/topic/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}话题列表{% endblock %} 4 | 5 | {% from "snippet/nav.html" import navigation %} 6 | {% block nav %}{{ navigation('topic') }}{% endblock %} 7 | 8 | {% block main %} 9 |
10 |
话题列表
11 | {% from "snippet/topic.html" import topic_list %} 12 | {{ topic_list(paginator.items) }} 13 | 14 |
15 | {% from "snippet/pagination.html" import pagination %} 16 | {{ pagination(paginator, url_for(".index")) }} 17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /gather/templates/topic/topic.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}{{ topic.title }}{% endblock %} 4 | 5 | {% from "snippet/nav.html" import navigation %} 6 | {% block nav %}{{ navigation('topic') }}{% endblock %} 7 | 8 | {% block head %} 9 | 10 | {% endblock %} 11 | 12 | {% block main %} 13 |
14 |
15 | {{ topic.title }} 16 | {% if g.user and g.user.is_staff %} 17 |
18 | 19 | {% if g.user.is_admin %} 20 | 21 | {% endif %} 22 |
23 | {% endif %} 24 |
25 | {% if topic.content %} 26 |
27 | {{ topic.content|content_to_html }} 28 |
29 | {% endif %} 30 |
31 | {% from "snippet/topic.html" import topic_info %} 32 | {{ topic_info(topic) }} 33 |
34 |
35 | {% if paginator.items %} 36 | {% set floor_num = (paginator.page - 1) * paginator.per_page + 1 %} 37 |
38 | {% for reply in paginator.items %} 39 | {% set author_url = url_for("user.profile", name=reply.author.username) %} 40 |
41 | 42 | {{ reply.author.username }} 43 | 44 | #{{ floor_num }} 45 |
46 | {% if reply.content|length < 50 %} 47 | 队形 48 | {% endif %} 49 | 50 | @ 51 | 52 | {% if g.user and g.user.is_staff %} 53 | 54 | {% endif %} 55 |
56 |
57 |
58 | {{ reply.author.username }} 发布于 59 | 60 | {% if reply.changed %} 61 | | 修改于 62 | {% endif %} 63 |
64 |
65 | {{ reply.content|content_to_html }} 66 |
67 |
68 |
69 | {% set floor_num = floor_num + 1 %} 70 | {% endfor %} 71 |
72 | {% from "snippet/pagination.html" import pagination %} 73 | {{ pagination(paginator, url_for(".topic", topic_id=topic.id)) }} 74 |
75 |
76 | {% endif %} 77 | {% if g.user %} 78 |
79 |
回复
80 | {% from "snippet/form.html" import form_field %} 81 |
82 | {{ form.csrf_token }} 83 | {{ form.content }} 84 |
85 | 86 |
87 |
88 |
89 | {% endif %} 90 | {% endblock %} 91 | 92 | {% block sidebar %} 93 | {% from "snippet/sidebar.html" import user_sidebar, node_sidebar %} 94 | {{ user_sidebar(topic.author) }} 95 | {{ node_sidebar(topic.node) }} 96 | {% endblock %} -------------------------------------------------------------------------------- /gather/templates/user/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}用户{% endblock %} 4 | 5 | {% from "snippet/nav.html" import navigation %} 6 | {% block nav %}{{ navigation('user') }}{% endblock %} 7 | 8 | {% block content %} 9 |
10 | {% for user in paginator.items %} 11 |
12 | {% set user_profile = url_for('.profile', name=user.username) %} 13 |
14 | {{ user.username }} 15 |
16 | {% if user.website %} 17 | {{ user.website }} 18 | {% else %} 19 | 注册于: {{ user.created.strftime("%Y-%m-%d") }} 20 | {% endif %} 21 |
22 | {% endfor %} 23 |
24 | {% endblock %} 25 | 26 | {% block before_footer %} 27 |
28 | {% from "snippet/pagination.html" import pagination %} 29 | {{ pagination(paginator, url_for(".index")) }} 30 |
31 | {% endblock %} -------------------------------------------------------------------------------- /gather/templates/user/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}{{ user.username }}{% endblock %} 4 | {% block head %} 5 | 6 | {% endblock %} 7 | 8 | {% from "snippet/nav.html" import navigation %} 9 | {% block nav %}{{ navigation('user') }}{% endblock %} 10 | 11 | {% block main %} 12 |
13 |
14 | {{ user.username }} 15 |

{{ user.username }}

16 | {{ user.description|content_to_html }} 17 |
18 | 19 |
    20 |
  • 21 | 第 {{ user.id }} 名用户 注册于 {{ user.created.strftime("%Y-%m-%d") }} 22 |
  • 23 | {% if user.website %} 24 |
  • 25 | {{ user.website }} 26 |
  • 27 | {% endif %} 28 |
29 | 30 | {% if g.user and g.user.is_admin and g.user != user %} 31 | 38 | {% endif %} 39 |
40 | 41 |
42 | {% from "snippet/topic.html" import topic_list %} 43 |
最近发布的主题
44 | {{ topic_list(topics) }} 45 | 48 |
49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /gather/templates/user/topic.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}{{ user.username }} 发布的话题{% endblock %} 4 | {% block head %} 5 | 6 | {% endblock %} 7 | 8 | {% from "snippet/nav.html" import navigation %} 9 | {% block nav %}{{ navigation('user') }}{% endblock %} 10 | 11 | {% block main %} 12 |
13 |
{{ user.username }} 发布的话题
14 | {% from "snippet/topic.html" import topic_list %} 15 | {{ topic_list(paginator.items) }} 16 | 17 | {% from "snippet/pagination.html" import pagination %} 18 | {{ pagination(paginator, url_for(".topic", name=user.username)) }} 19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /gather/topic/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import api 4 | 5 | from .views import bp 6 | 7 | __all__ = ("bp", "api") 8 | -------------------------------------------------------------------------------- /gather/topic/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from flask import g, jsonify 3 | 4 | from gather.api import need_auth, EXCLUDE_COLUMNS 5 | 6 | from gather.extensions import api_manager 7 | from gather.topic.models import Topic, Reply 8 | 9 | 10 | bp = api_manager.create_api_blueprint( 11 | Topic, 12 | methods=["GET", "POST"], 13 | preprocessors={ 14 | 'POST': [need_auth], 15 | }, 16 | include_methods=["have_read"], 17 | exclude_columns=EXCLUDE_COLUMNS 18 | ) 19 | 20 | 21 | @bp.route("/topic//mark_read") 22 | def _mark_read_for_topic(topic_id): 23 | need_auth() 24 | topic = Topic.query.get_or_404(topic_id) 25 | topic.mark_read(g.token_user) 26 | return jsonify({"code": 200}) 27 | 28 | 29 | def _update_topic_updated(result=None, **kw): 30 | if not result: 31 | return 32 | reply = Reply.query.get(result["id"]) 33 | reply.topic.updated = reply.created 34 | reply.topic.clear_read() 35 | reply.topic.save() 36 | 37 | 38 | reply_bp = api_manager.create_api_blueprint( 39 | Reply, 40 | methods=["POST"], 41 | preprocessors={ 42 | 'POST': [need_auth], 43 | }, 44 | postprocessors={ 45 | 'POST': [_update_topic_updated] 46 | }, 47 | exclude_columns=EXCLUDE_COLUMNS 48 | ) 49 | -------------------------------------------------------------------------------- /gather/topic/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from datetime import datetime 6 | from flask import g 7 | from gather.form import Form 8 | from wtforms import TextField, TextAreaField 9 | from wtforms.ext.sqlalchemy.fields import QuerySelectField 10 | from wtforms.validators import Required, Optional, Length 11 | from ghdiff import diff 12 | from gather.node.models import Node 13 | from .models import Topic, Reply, History 14 | 15 | 16 | class CreateTopicForm(Form): 17 | node = QuerySelectField( 18 | "节点", 19 | validators=[Required()], 20 | query_factory=Node.query_all, 21 | get_pk=lambda a: a.id, 22 | get_label=lambda a: a.name 23 | ) 24 | title = TextField("标题", validators=[ 25 | Required(), 26 | Length(max=100) 27 | ]) 28 | content = TextAreaField("正文", validators=[Optional()]) 29 | 30 | def create(self): 31 | topic = Topic( 32 | **self.data 33 | ) 34 | return topic.save() 35 | 36 | 37 | class ChangeTopicForm(CreateTopicForm): 38 | def save(self, topic): 39 | if self.title.data != topic.title: 40 | title = self.title.data 41 | history = History( 42 | diff_content="将标题从 %s 修改为 %s" % (topic.title, title), 43 | topic=topic, 44 | author=g.user 45 | ) 46 | history.save() 47 | topic.title = title 48 | if self.node.data != topic.node: 49 | node = self.node.data 50 | history = History( 51 | diff_content="从 %s 移动到 %s" % (topic.node.name, node.name), 52 | topic=topic, 53 | author=g.user 54 | ) 55 | history.save() 56 | topic.node = node 57 | if self.content.data != topic.content: 58 | content = self.content.data 59 | history = History( 60 | diff_content=diff(topic.content, content, css=False), 61 | topic=topic, 62 | author=g.user 63 | ) 64 | history.save() 65 | topic.content = content 66 | topic.changed = datetime.utcnow() 67 | return topic.save() 68 | 69 | 70 | class ReplyForm(Form): 71 | content = TextAreaField("正文", [ 72 | Required(), 73 | Length(max=10000), 74 | ]) 75 | 76 | def create(self, topic): 77 | reply = Reply( 78 | content=self.content.data, 79 | topic=topic, 80 | author=g.user 81 | ) 82 | return reply.save() 83 | 84 | 85 | class ChangeReplyForm(ReplyForm): 86 | def save(self, reply): 87 | if self.content.data != reply.content: 88 | content = self.content.data 89 | history = History( 90 | diff_content=diff(reply.content, content, css=False), 91 | reply=reply, 92 | author=g.user 93 | ) 94 | history.save() 95 | reply.content = content 96 | return reply.save() 97 | -------------------------------------------------------------------------------- /gather/topic/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from datetime import datetime 4 | from flask import g 5 | from gather.extensions import cache 6 | from gather.account.models import Account 7 | from gather.node.models import Node 8 | from gather.extensions import db 9 | 10 | 11 | class ReadTopic(db.Model): 12 | __table_args__ = ( 13 | db.UniqueConstraint( 14 | 'user_id', 'topic_id', name='uc_user_read_topic' 15 | ), 16 | ) 17 | id = db.Column(db.Integer, primary_key=True) 18 | user_id = db.Column(db.Integer, db.ForeignKey('account.id'), index=True) 19 | user = db.relationship(Account) 20 | topic_id = db.Column(db.Integer, db.ForeignKey('topic.id'), index=True) 21 | topic = db.relationship("Topic") 22 | 23 | 24 | def _get_author_id(): 25 | return g.user and g.user.id or g.token_user.id 26 | 27 | 28 | class Reply(db.Model): 29 | id = db.Column(db.Integer, primary_key=True) 30 | content = db.Column(db.Text(), nullable=True, default="") 31 | author_id = db.Column( 32 | db.Integer, 33 | db.ForeignKey('account.id'), nullable=False, 34 | default=_get_author_id 35 | ) 36 | author = db.relationship(Account) 37 | topic_id = db.Column( 38 | db.Integer, 39 | db.ForeignKey('topic.id'), index=True, nullable=False 40 | ) 41 | topic = db.relationship("Topic") 42 | created = db.Column(db.DateTime, default=datetime.utcnow) 43 | changed = db.Column( 44 | db.DateTime, 45 | nullable=True, 46 | onupdate=datetime.utcnow 47 | ) 48 | 49 | def have_read(self): 50 | return False 51 | 52 | def to_dict(self): 53 | return { 54 | "id": self.id, 55 | "content": self.content, 56 | "author": self.author_id, 57 | "topic": self.topic_id, 58 | "created": self.created, 59 | "changed": self.changed 60 | } 61 | 62 | def save(self): 63 | if self.id: 64 | # Update reply 65 | self.changed = datetime.utcnow() 66 | else: 67 | topic = self.topic 68 | topic.updated = datetime.utcnow() 69 | topic.clear_read() 70 | topic.save() 71 | db.session.add(self) 72 | db.session.commit() 73 | return self 74 | 75 | 76 | class Topic(db.Model): 77 | id = db.Column(db.Integer, primary_key=True) 78 | title = db.Column(db.String(100), nullable=False) 79 | content = db.Column(db.Text(), nullable=True, default="") 80 | author_id = db.Column( 81 | db.Integer, 82 | db.ForeignKey('account.id'), index=True, nullable=False, 83 | default=_get_author_id 84 | ) 85 | author = db.relationship(Account) 86 | node_id = db.Column( 87 | db.Integer, 88 | db.ForeignKey('node.id'), index=True, nullable=False 89 | ) 90 | node = db.relationship(Node) 91 | created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) 92 | replies = db.relationship( 93 | "Reply", lazy='dynamic', 94 | order_by=Reply.id.asc() 95 | ) 96 | updated = db.Column( 97 | db.DateTime, 98 | default=datetime.utcnow, 99 | onupdate=datetime.utcnow 100 | ) 101 | changed = db.Column( 102 | db.DateTime, 103 | nullable=True 104 | ) 105 | 106 | def __str__(self): 107 | return self.title 108 | 109 | def __repr__(self): 110 | return '' % self.title 111 | 112 | @property 113 | def last_page(self): 114 | return Reply.query.filter_by(topic=self).paginate(1).pages or 1 115 | 116 | @property 117 | def read_cache_key(self): 118 | return "read_topic_%s" % self.id 119 | 120 | def have_read(self, user=None): 121 | if not user: 122 | user = g.user or g.token_user 123 | read_list = cache.get(self.read_cache_key) 124 | if read_list and user.id in read_list: 125 | return True 126 | return ReadTopic.query.filter_by(topic=self, user=user).count() 127 | 128 | def mark_read(self, user): 129 | if self.have_read(user): 130 | return 131 | read_list = cache.get(self.read_cache_key) 132 | if read_list: 133 | read_list.append(user.id) 134 | else: 135 | read_list = [user.id] 136 | cache.set(self.read_cache_key, read_list) 137 | read_mark = ReadTopic(topic=self, user=user) 138 | db.session.add(read_mark) 139 | db.session.commit() 140 | 141 | def clear_read(self): 142 | cache.delete(self.read_cache_key) 143 | ReadTopic.query.filter_by(topic=self).delete() 144 | 145 | def save(self): 146 | db.session.add(self) 147 | db.session.commit() 148 | return self 149 | 150 | def delete(self): 151 | Reply.query.filter_by(topic=self).delete() 152 | self.clear_read() 153 | db.session.delete(self) 154 | db.session.commit() 155 | return self 156 | 157 | 158 | class History(db.Model): 159 | id = db.Column(db.Integer, primary_key=True) 160 | diff_content = db.Column(db.Text(), nullable=True, default="") 161 | author_id = db.Column(db.Integer, db.ForeignKey('account.id')) 162 | author = db.relationship(Account) 163 | topic_id = db.Column(db.Integer, db.ForeignKey('topic.id'), index=True) 164 | topic = db.relationship(Topic) 165 | reply_id = db.Column(db.Integer, db.ForeignKey('reply.id'), index=True) 166 | reply = db.relationship(Reply) 167 | created = db.Column(db.DateTime, default=datetime.utcnow) 168 | 169 | def save(self): 170 | db.session.add(self) 171 | db.session.commit() 172 | return self -------------------------------------------------------------------------------- /gather/topic/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from flask import Blueprint 4 | from flask import url_for, g, redirect, render_template, abort, request 5 | from gather.utils import require_token 6 | from gather.account.utils import require_login, require_staff, require_admin 7 | from gather.node.models import Node 8 | from .forms import CreateTopicForm, ChangeTopicForm, ReplyForm, ChangeReplyForm 9 | from .models import Topic, Reply 10 | 11 | bp = Blueprint("topic", __name__, url_prefix="/topic") 12 | 13 | 14 | @bp.route("/", defaults={'page': 1}) 15 | @bp.route('/page/') 16 | def index(page): 17 | paginator = Topic.query.order_by(Topic.created.desc()).paginate(page) 18 | return render_template('topic/index.html', paginator=paginator) 19 | 20 | 21 | @bp.route("/create", methods=("GET", "POST")) 22 | @require_login 23 | def create(): 24 | form = CreateTopicForm() 25 | if "node" in request.args: 26 | try: 27 | nid = int(request.args.get("node")) 28 | except ValueError: 29 | pass 30 | else: 31 | form.node.data = Node.query.get_or_404(nid) 32 | if form.validate_on_submit(): 33 | topic = form.create() 34 | return redirect(url_for(".topic", topic_id=topic.id)) 35 | return render_template("topic/create.html", form=form) 36 | 37 | 38 | @bp.route("/", methods=("GET", "POST"), defaults={'page': 1}) 39 | @bp.route("//page/", methods=("GET", "POST")) 40 | def topic(topic_id, page): 41 | topic = Topic.query.get_or_404(topic_id) 42 | form = ReplyForm() 43 | if g.user and form.validate_on_submit(): 44 | form.create(topic=topic) 45 | return redirect(url_for(".topic", topic_id=topic.id, page=topic.last_page)) 46 | replies = Reply.query.filter_by(topic=topic).order_by(Reply.id.asc()) 47 | paginator = replies.paginate(page) 48 | if g.user: 49 | topic.mark_read(g.user) 50 | return render_template( 51 | "topic/topic.html", topic=topic, form=form, 52 | paginator=paginator 53 | ) 54 | 55 | 56 | @bp.route("//remove/") 57 | @require_admin 58 | @require_token 59 | def remove_topic(topic_id): 60 | topic = Topic.query.get_or_404(topic_id) 61 | topic.delete() 62 | return redirect("/") 63 | 64 | 65 | @bp.route("//change", methods=("GET", "POST")) 66 | @require_staff 67 | def change_topic(topic_id): 68 | topic = Topic.query.get_or_404(topic_id) 69 | form = ChangeTopicForm(obj=topic) 70 | if form.validate_on_submit(): 71 | topic = form.save(topic=topic) 72 | return redirect(url_for(".topic", topic_id=topic.id)) 73 | return render_template("topic/change.html", form=form) 74 | 75 | 76 | @bp.route("///change", methods=("GET", "POST")) 77 | @require_staff 78 | def change_reply(topic_id, reply_id): 79 | topic = Topic.query.get_or_404(topic_id) 80 | reply = Reply.query.get_or_404(reply_id) 81 | if reply.topic != topic: 82 | abort(233) 83 | form = ChangeReplyForm(obj=reply) 84 | if form.validate_on_submit(): 85 | form.save(reply=reply) 86 | return redirect(url_for(".topic", topic_id=topic.id)) 87 | return render_template("topic/change_reply.html", form=form) 88 | -------------------------------------------------------------------------------- /gather/user/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from .views import bp 4 | 5 | __all__ = ("bp", ) -------------------------------------------------------------------------------- /gather/user/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from flask import Blueprint, redirect, url_for 4 | from flask import render_template 5 | 6 | from gather.utils import require_token 7 | from gather.account.models import db, Account 8 | from gather.account.utils import require_admin 9 | from gather.topic.models import Topic 10 | 11 | bp = Blueprint("user", __name__, url_prefix="/user") 12 | 13 | 14 | @bp.route("/", defaults={'page': 1}) 15 | @bp.route('/page/') 16 | def index(page): 17 | accounts = Account.query.order_by(Account.id.desc()) 18 | paginator = accounts.paginate(page, per_page=18) 19 | return render_template("user/index.html", paginator=paginator) 20 | 21 | 22 | @bp.route("/") 23 | def profile(name): 24 | user = Account.query.filter_by(username=name).first_or_404() 25 | topics = Topic.query.filter_by(author=user).order_by(Topic.id.desc())[:5] 26 | return render_template("user/profile.html", user=user, topics=topics) 27 | 28 | 29 | @bp.route("//topic", defaults={'page': 1}) 30 | @bp.route('//topic/page/') 31 | def topic(name, page): 32 | user = Account.query.filter_by(username=name).first_or_404() 33 | paginator = Topic.query.filter_by(author=user).\ 34 | order_by(Topic.created.desc()).paginate(page) 35 | return render_template('user/topic.html', user=user, paginator=paginator) 36 | 37 | 38 | @bp.route("//promote/") 39 | @require_admin 40 | @require_token 41 | def promote(name): 42 | user = Account.query.filter_by(username=name).first_or_404() 43 | user.role = "staff" 44 | db.session.add(user) 45 | db.session.commit() 46 | return redirect(url_for(".profile", name=user.username)) 47 | 48 | 49 | @bp.route("//demote/") 50 | @require_admin 51 | @require_token 52 | def demote(name): 53 | user = Account.query.filter_by(username=name).first_or_404() 54 | user.role = "user" 55 | db.session.add(user) 56 | db.session.commit() 57 | return redirect(url_for(".profile", name=user.username)) -------------------------------------------------------------------------------- /gather/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import functools 4 | 5 | from flask import current_app, request, abort, g 6 | from werkzeug.security import gen_salt 7 | from gather.extensions import mail, cache 8 | 9 | 10 | def send_mail(msg): 11 | if current_app.debug: 12 | print msg.html 13 | else: 14 | mail.send(msg) 15 | 16 | 17 | def gen_action_token(length=40): 18 | if not g.user: 19 | return 20 | user = g.user 21 | token = gen_salt(length=length) 22 | cache.set("action_token_{}".format(token), user.id, timeout=300) 23 | return token 24 | 25 | 26 | def verify_action_token(token): 27 | if not g.user: 28 | return False 29 | user = g.user 30 | key = "action_token_{}".format(token) 31 | user_id = cache.get(key) 32 | if user_id: 33 | cache.delete(key) 34 | return user_id == user.id 35 | return False 36 | 37 | 38 | def require_token(method): 39 | @functools.wraps(method) 40 | def wrapper(*args, **kwargs): 41 | token = kwargs.pop("token", "") 42 | if not verify_action_token(token): 43 | return abort(403) 44 | return method(*args, **kwargs) 45 | 46 | return wrapper -------------------------------------------------------------------------------- /gunicorn.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import multiprocessing 4 | 5 | workers = multiprocessing.cpu_count() * 2 + 1 6 | 7 | import gevent.monkey 8 | gevent.monkey.patch_all() 9 | 10 | worker_class = 'gevent' 11 | bind = "127.0.0.1:8000" 12 | 13 | pidfile = "/tmp/gather.pid" 14 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | from flask.ext.script import Manager 2 | from flask.ext.migrate import Migrate, MigrateCommand 3 | from flask.ext.assets import ManageAssets 4 | from livereload import Server 5 | 6 | from gather import create_app 7 | from gather.extensions import db 8 | 9 | app = create_app() 10 | migrate = Migrate(app, db) 11 | 12 | manager = Manager(app) 13 | manager.add_command("db", MigrateCommand) 14 | manager.add_command("assets", ManageAssets()) 15 | 16 | 17 | @manager.command 18 | def create_all(): 19 | db.create_all() 20 | 21 | 22 | @manager.command 23 | def clear_cache(): 24 | from gather.extensions import cache 25 | with app.app_context(): 26 | cache.clear() 27 | 28 | 29 | @manager.command 30 | def clean_junk_users(): 31 | from gather.account.models import Account 32 | with app.app_context(): 33 | Account.clean_junk_users() 34 | 35 | 36 | @manager.command 37 | def livereload(): 38 | db.create_all() 39 | app.debug = True 40 | server = Server(app) 41 | server.watch("gather/*.py") 42 | server.watch("gather/templates/*.html") 43 | server.watch("gather/assets/stylesheets/*.sass") 44 | server.watch("gather/assets/stylesheets/*/*.sass") 45 | server.watch("gather/assets/javascripts/*.coffee") 46 | server.serve(port=8000) 47 | 48 | if __name__ == "__main__": 49 | manager.run() 50 | -------------------------------------------------------------------------------- /migrate-requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | pymongo==2.6.3 3 | -------------------------------------------------------------------------------- /migrate_from_pbb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import datetime 6 | 7 | from pymongo import MongoClient 8 | from gather import create_app 9 | from gather.extensions import db 10 | from gather.account.models import Account 11 | from gather.node.models import Node 12 | from gather.topic.models import Topic, Reply 13 | 14 | app = create_app() 15 | 16 | mongo_database = MongoClient()["forum"] 17 | 18 | 19 | def role(num): 20 | if num == 1: 21 | return "user" 22 | elif num >= 5: 23 | return "admin" 24 | return "staff" 25 | 26 | 27 | def timestamp_to_datetime(t): 28 | return datetime.datetime.fromtimestamp(t) 29 | 30 | 31 | def main(): 32 | for pbb_member in mongo_database.members.find(sort=[('created', 1)]): 33 | account = Account( 34 | username=pbb_member["name"].lower(), 35 | email=pbb_member["email"], 36 | website=pbb_member["website"], 37 | description=pbb_member["description"], 38 | role=role(pbb_member["role"]), 39 | created=timestamp_to_datetime(pbb_member["created"]), 40 | password="need-to-reset" 41 | ) 42 | print "Migrating Account %s" % account.username 43 | account.create_password(str(pbb_member["_id"])) 44 | db.session.add(account) 45 | db.session.commit() 46 | 47 | for pbb_node in mongo_database.nodes.find(): 48 | node = Node( 49 | name=pbb_node["title"], 50 | slug=pbb_node["name"], 51 | description=pbb_node["description"] 52 | ) 53 | db.session.add(node) 54 | db.session.commit() 55 | 56 | for pbb_topic in mongo_database.topics.find(sort=[('last_reply_time', 1)]): 57 | topic = Topic( 58 | title=pbb_topic["title"], 59 | content=pbb_topic["content"], 60 | author=Account.query.filter_by(username=pbb_topic["author"].lower()).first(), 61 | node=Node.query.filter_by(slug=pbb_topic["node"]).first(), 62 | created=timestamp_to_datetime(pbb_topic["created"]), 63 | updated=timestamp_to_datetime(pbb_topic["last_reply_time"]) 64 | ) 65 | print "Migrating Topic %s by %s" % (topic.title, topic.author.username) 66 | db.session.add(topic) 67 | db.session.commit() 68 | for pbb_reply in mongo_database.replies.find({'topic': str(pbb_topic["_id"])}, 69 | sort=[('index', 1)]): 70 | reply = Reply( 71 | content=pbb_reply["content"], 72 | author=Account.query.filter_by(username=pbb_reply["author"].lower()).first(), 73 | topic=topic, 74 | created=timestamp_to_datetime(pbb_reply["created"]) 75 | ) 76 | print "Migrating Reply %s by %s" % (reply.content, reply.author.username) 77 | 78 | db.session.add(reply) 79 | db.session.commit() 80 | 81 | 82 | with app.app_context(): 83 | db.drop_all() 84 | db.create_all() 85 | main() 86 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | 6 | # this is the Alembic Config object, which provides 7 | # access to the values within the .ini file in use. 8 | config = context.config 9 | 10 | # Interpret the config file for Python logging. 11 | # This line sets up loggers basically. 12 | fileConfig(config.config_file_name) 13 | 14 | # add your model's MetaData object here 15 | # for 'autogenerate' support 16 | # from myapp import mymodel 17 | # target_metadata = mymodel.Base.metadata 18 | from flask import current_app 19 | config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI')) 20 | target_metadata = current_app.extensions['migrate'].db.metadata 21 | 22 | # other values from the config, defined by the needs of env.py, 23 | # can be acquired: 24 | # my_important_option = config.get_main_option("my_important_option") 25 | # ... etc. 26 | 27 | def run_migrations_offline(): 28 | """Run migrations in 'offline' mode. 29 | 30 | This configures the context with just a URL 31 | and not an Engine, though an Engine is acceptable 32 | here as well. By skipping the Engine creation 33 | we don't even need a DBAPI to be available. 34 | 35 | Calls to context.execute() here emit the given string to the 36 | script output. 37 | 38 | """ 39 | url = config.get_main_option("sqlalchemy.url") 40 | context.configure(url=url) 41 | 42 | with context.begin_transaction(): 43 | context.run_migrations() 44 | 45 | def run_migrations_online(): 46 | """Run migrations in 'online' mode. 47 | 48 | In this scenario we need to create an Engine 49 | and associate a connection with the context. 50 | 51 | """ 52 | engine = engine_from_config( 53 | config.get_section(config.config_ini_section), 54 | prefix='sqlalchemy.', 55 | poolclass=pool.NullPool) 56 | 57 | connection = engine.connect() 58 | context.configure( 59 | connection=connection, 60 | target_metadata=target_metadata 61 | ) 62 | 63 | try: 64 | with context.begin_transaction(): 65 | context.run_migrations() 66 | finally: 67 | connection.close() 68 | 69 | if context.is_offline_mode(): 70 | run_migrations_offline() 71 | else: 72 | run_migrations_online() 73 | 74 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = ${repr(up_revision)} 11 | down_revision = ${repr(down_revision)} 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | ${imports if imports else ""} 16 | 17 | def upgrade(): 18 | ${upgrades if upgrades else "pass"} 19 | 20 | 21 | def downgrade(): 22 | ${downgrades if downgrades else "pass"} 23 | -------------------------------------------------------------------------------- /migrations/versions/10c48c7e7526_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 10c48c7e7526 4 | Revises: 3a19b3b5d896 5 | Create Date: 2014-02-14 22:58:36.297509 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '10c48c7e7526' 11 | down_revision = '3a19b3b5d896' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('account', sa.Column('api_token', sa.String(length=40), nullable=True)) 20 | ### end Alembic commands ### 21 | 22 | 23 | def downgrade(): 24 | ### commands auto generated by Alembic - please adjust! ### 25 | op.drop_column('account', 'api_token') 26 | ### end Alembic commands ### 27 | -------------------------------------------------------------------------------- /migrations/versions/2268227deebb_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 2268227deebb 4 | Revises: None 5 | Create Date: 2014-02-13 16:51:09.794989 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '2268227deebb' 11 | down_revision = None 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.alter_column('reply', 'author_id', 20 | existing_type=sa.INTEGER(), 21 | nullable=False) 22 | op.alter_column('reply', 'topic_id', 23 | existing_type=sa.INTEGER(), 24 | nullable=False) 25 | op.alter_column('topic', 'author_id', 26 | existing_type=sa.INTEGER(), 27 | nullable=False) 28 | op.alter_column('topic', 'created', 29 | existing_type=sa.DATETIME(), 30 | nullable=False) 31 | op.alter_column('topic', 'node_id', 32 | existing_type=sa.INTEGER(), 33 | nullable=False) 34 | ### end Alembic commands ### 35 | 36 | 37 | def downgrade(): 38 | ### commands auto generated by Alembic - please adjust! ### 39 | op.alter_column('topic', 'node_id', 40 | existing_type=sa.INTEGER(), 41 | nullable=True) 42 | op.alter_column('topic', 'created', 43 | existing_type=sa.DATETIME(), 44 | nullable=True) 45 | op.alter_column('topic', 'author_id', 46 | existing_type=sa.INTEGER(), 47 | nullable=True) 48 | op.alter_column('reply', 'topic_id', 49 | existing_type=sa.INTEGER(), 50 | nullable=True) 51 | op.alter_column('reply', 'author_id', 52 | existing_type=sa.INTEGER(), 53 | nullable=True) 54 | ### end Alembic commands ### 55 | -------------------------------------------------------------------------------- /migrations/versions/26dfc02ce3ff_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 26dfc02ce3ff 4 | Revises: 10c48c7e7526 5 | Create Date: 2014-02-15 16:43:56.552255 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '26dfc02ce3ff' 11 | down_revision = '10c48c7e7526' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('account', sa.Column('feeling_lucky', sa.Boolean(), nullable=True)) 20 | ### end Alembic commands ### 21 | 22 | 23 | def downgrade(): 24 | ### commands auto generated by Alembic - please adjust! ### 25 | op.drop_column('account', 'feeling_lucky') 26 | ### end Alembic commands ### 27 | -------------------------------------------------------------------------------- /migrations/versions/276f5d7b1612_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 276f5d7b1612 4 | Revises: 26dfc02ce3ff 5 | Create Date: 2014-02-15 22:16:23.234823 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '276f5d7b1612' 11 | down_revision = '26dfc02ce3ff' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.create_unique_constraint('uc_user_read_topic', 'read_topic', ['user_id', 'topic_id']) 20 | ### end Alembic commands ### 21 | 22 | 23 | def downgrade(): 24 | ### commands auto generated by Alembic - please adjust! ### 25 | op.drop_constraint('uc_user_read_topic', 'read_topic') 26 | ### end Alembic commands ### 27 | -------------------------------------------------------------------------------- /migrations/versions/3a19b3b5d896_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 3a19b3b5d896 4 | Revises: 2268227deebb 5 | Create Date: 2014-02-14 21:25:56.726301 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '3a19b3b5d896' 11 | down_revision = '2268227deebb' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('account', sa.Column('css', sa.String(), nullable=True)) 20 | ### end Alembic commands ### 21 | 22 | 23 | def downgrade(): 24 | ### commands auto generated by Alembic - please adjust! ### 25 | op.drop_column('account', 'css') 26 | ### end Alembic commands ### 27 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==0.6.3 2 | aniso8601==0.82 3 | backports.ssl-match-hostname==3.4.0.2 4 | bleach==1.4 5 | blinker==1.3 6 | chardet==2.2.1 7 | cssmin==0.2.0 8 | ecdsa==0.10 9 | Fabric==1.8.3 10 | Flask-Assets==0.9 11 | Flask-Cache==0.13.1 12 | Flask-DebugToolbar==0.9.0 13 | Flask-Mail==0.9.0 14 | Flask-Migrate==1.2.0 15 | Flask-Restless==0.13.1 16 | Flask-Script==2.0.5 17 | Flask-SQLAlchemy==1.0 18 | Flask-Turbolinks==0.2.0 19 | Flask-WTF==0.9.5 20 | Flask==0.10.1 21 | gevent==1.0.1 22 | ghdiff==0.3 23 | greenlet==0.4.2 24 | gunicorn==18.0 25 | houdini.py==0.1.0 26 | html5lib==0.999 27 | itsdangerous==0.23 28 | Jinja2==2.7.2 29 | livereload==2.2.0 30 | Mako==0.9.1 31 | MarkupSafe==0.18 32 | mimerender==0.5.4 33 | newrelic==2.20.0.17 34 | paramiko==1.12.1 35 | pip-tools==0.3.4 36 | psycopg2==2.5.3 37 | pycrypto==2.6.1 38 | Pygments==1.6 39 | pylibmc==1.3.0 40 | python-dateutil==2.2 41 | python-mimeparse==0.1.4 42 | pytz==2013.9 43 | raven==5.0.0 44 | six==1.5.2 45 | SQLAlchemy==0.9.2 46 | tornado==3.2 47 | webassets==0.9 48 | Werkzeug==0.9.4 49 | WTForms==1.0.5 50 | -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | from gather import create_app 2 | from gather.settings import load_production_settings 3 | 4 | application = create_app() 5 | load_production_settings(application) 6 | 7 | if application.config.get("SENTRY_DSN", None): 8 | from raven.contrib.flask import Sentry 9 | Sentry(application) 10 | --------------------------------------------------------------------------------