├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── admin_views.py ├── app.py ├── dev_server.py ├── docker-run.sh ├── models.py ├── requirements.txt ├── settings.py ├── static ├── css │ └── main.css ├── image │ └── bg.jpg └── js │ └── main.js ├── templates ├── add_image.html ├── base.html ├── create_gallery.html ├── edit.html ├── form-mid.html ├── form-narrow.html ├── gallery.html ├── image.html ├── index.html ├── list.html ├── login.html ├── macro.html ├── reg.html ├── tag_picture_list.html └── user_image_list.html ├── tushe.nginx ├── tushe.py ├── tushe.supervisord_conf ├── uwsgi.ini ├── views.py └── wc.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # Darwin 61 | .DS_Store 62 | 63 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 64 | *.iml 65 | 66 | # Directory-based project format: 67 | .idea/ 68 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:15.10 2 | RUN rm /bin/sh && ln -s /bin/bash /bin/sh 3 | RUN apt-get install apt-utils -y 4 | RUN apt-get -y update && apt-get install -y git libtiff5-dev libjpeg8-dev zlib1g-dev \ 5 | libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python-tk python3-dev python3-setuptools 6 | RUN apt-get install build-essential zlib1g zlib1g-dev zlibc libruby1.9 libxml2 libxml2-dev libxslt-dev -y 7 | RUN mkdir /data; cd /data; git clone https://github.com/ericls/tushe/ 8 | RUN apt-get install python3-venv -y 9 | RUN mkdir /data/Envs; cd /data/Envs/;pyvenv-3.4 tushe --without-pip 10 | RUN ls /data/Envs/ 11 | RUN source /data/Envs/tushe/bin/activate 12 | RUN apt-get install libreadline6 libreadline6-dev lib32ncurses5-dev -y 13 | RUN apt-get install curl python3-dev -y 14 | ENV PATH /data/Envs/tushe/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin 15 | RUN curl https://bootstrap.pypa.io/get-pip.py | python 16 | RUN cd /data/tushe; pip install -r requirements.txt; pip install uwsgi 17 | RUN curl https://gist.githubusercontent.com/ericls/fe24bc7d4215362da1e1/raw/0fd1690f8e00145cc1b5a1cdbe2cbe3cafc454d8/settings.py > /data/tushe/settings.py 18 | RUN curl https://gist.githubusercontent.com/ericls/093532dfa8731a0338a6/raw/65d9d81a6be45a05e95911a7c264193025c3a0b0/uwsgi.ini > /data/tushe/uwsgi.ini 19 | EXPOSE 3333 20 | RUN mkdir -p /data/tushe/static/uploads 21 | ENTRYPOINT /data/Envs/tushe/bin/uwsgi --ini /data/tushe/uwsgi.ini -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Shen Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TUSHE 2 | 3 | 演示:http://tushe.org 4 | 5 | 图社(TUSHE)是基于 FLask 的图床和图片浏览网站源码,也可以用作套图网站。网站采用 Mongodb 作为数据库,图片也储存于 GridFS。 6 | 采用 Flask—Login 做用户认证,采用 Flask-Admin 做后台。 7 | 8 | 还用到了 Flask-Mongoengine, Flask-Bcrypt 等。见`requirements.txt`。 9 | 10 | ## 特点 11 | - 注册用户可以认领未注册用户的图片,进行相关信息编辑。 12 | - 有图册功能,可以作为套图网站。 13 | - 支持微信公众平台接口,用微信上传图片(需要认证的订阅号或者服务号)。 14 | 15 | 16 | ## Docker 自动部署 17 | 运行 repo 里面的 docker-run.sh,或者直接运行如下命令 18 | 19 | `bash -c "$(wget -O - https://raw.githubusercontent.com/ericls/tushe/master/docker-run.sh)"` 20 | 21 | 会提示输入端口号,请输入一个非常用的端口 22 | 23 | 运行完成之后, 会提示 nginx 的配置,照着配置就可以了。 24 | 25 | ## 常规部署 26 | 27 | ### 要求 28 | 29 | 1. Python3.3+ 30 | 1. pip install -r requirements.txt (Pillow 相关的支持见:[http://pillow.readthedocs.org/installation.html#linux-installation](http://pillow.readthedocs.org/installation.html#linux-installation)) 31 | 32 | ### 部署方法 33 | 提供了uwsgi supervisor 配合 nginx 的配置文件。具体请参考他们的文档。 34 | 35 | 也可以采用其他方式部署,wsgi 服务器网关接口为`tushe.app`。 36 | 37 | ## 已知问题和解决方式 38 | 39 | 由于引用了 Flask-Login 和 GridFs,所有的请求会插入 Set-Cookie 的 Header。 40 | 41 | 目前的解决方式是在 Nginx 里面对对应的目录设置 uwsgi_hide_header Set-Cookie。 42 | 43 | 另外,为了不让每次请求都从数据库读取,可以再引入 Flask-Cache 和在 Nginx 里面设置 uwsgi_cache 相关参数。 -------------------------------------------------------------------------------- /admin_views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask import redirect, url_for 4 | from flask_admin.contrib.mongoengine import ModelView 5 | from flask.ext.admin import AdminIndexView 6 | from app import login 7 | 8 | 9 | class IndexView(AdminIndexView): 10 | 11 | def _handle_view(self, name, **kwargs): 12 | if not self.is_accessible(): 13 | return redirect(url_for('light-cms.user_login')) 14 | 15 | def is_accessible(self): 16 | if not login.current_user.is_authenticated(): 17 | return False 18 | return login.current_user.is_admin 19 | 20 | 21 | class UserView(ModelView): 22 | 23 | column_labels = dict( 24 | username='用户名', 25 | password_hash='密码哈希', 26 | active='可用', 27 | is_admin='管理员' 28 | ) 29 | 30 | column_filters = ('username', 'active', 'is_admin') 31 | column_list = ('username', 'active', 'is_admin', 'email') 32 | 33 | form_create_rules = ('username', 'password', 'active', 'is_admin', 'email') 34 | form_edit_rules = form_create_rules 35 | 36 | def is_accessible(self): 37 | if not login.current_user.is_authenticated(): 38 | return False 39 | return login.current_user.is_admin 40 | 41 | 42 | class GeneralView(ModelView): 43 | 44 | def is_accessible(self): 45 | if not login.current_user.is_authenticated(): 46 | return False 47 | return login.current_user.is_admin 48 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask.ext.mongoengine import MongoEngine 3 | from flask.ext.bcrypt import Bcrypt 4 | from flask.ext import login 5 | from flask.ext.babelex import Babel 6 | import settings 7 | 8 | app = Flask(settings.APP_NAME) 9 | app.config.from_object(settings) 10 | 11 | login_manager = login.LoginManager() 12 | 13 | app.config['BCRYPT_LOG_ROUNDS'] = 1 14 | 15 | db = MongoEngine(app) 16 | bcrypt = Bcrypt(app) 17 | babel = Babel(app) 18 | 19 | 20 | @babel.localeselector 21 | def get_locale(): 22 | # Put your logic here. Application can store locale in 23 | # user profile, cookie, session, etc. 24 | return 'zh_CN' 25 | -------------------------------------------------------------------------------- /dev_server.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from tushe import app 3 | 4 | if __name__ == '__main__': 5 | app.run(debug=True, port=5000, host='0.0.0.0') 6 | -------------------------------------------------------------------------------- /docker-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if ! [ "$(id -u)" == "0" ]; then 3 | echo 'Please run as root' 4 | exit 5 | fi 6 | 7 | echo "Which port do you want Tushe application to run on?" 8 | echo -n "Enter port number: " 9 | read tushe_port 10 | 11 | 12 | if [ `which docker` ]; then 13 | echo 'Docker installed' 14 | else 15 | 16 | if [ `which wget` ]; then 17 | echo 'wget installed' 18 | else 19 | if [ `which apt-get` ]; then 20 | apt-get update 21 | apt-get install wget -y 22 | fi 23 | if [ `which yum` ]; then 24 | yum install wget -y 25 | fi 26 | fi 27 | 28 | echo -n "Installing docker" 29 | wget -qO- https://get.docker.com/ | sh 30 | fi 31 | 32 | mkdir /home/tushe/db/ -p 33 | 34 | echo "Setting up database" 35 | docker rm -f tushe_db 36 | docker run -d --name tushe_db -v '/home/tushe/db:/data/db' -e AUTH=no tutum/mongodb 37 | 38 | echo "Runing Tushe" 39 | sleep 5 40 | docker rm -f tushe 41 | docker run --name tushe -d --link tushe_db:db -p $tushe_port:3333 ericls/tushe 42 | 43 | echo " 44 | ==============NGINX CONF=============== 45 | # You can use the following lines as an 46 | # example of how to set up nginx as a 47 | # front end for Tushe 48 | 49 | server { 50 | listen 80; 51 | server_name example.org; 52 | 53 | location / { 54 | uwsgi_pass 127.0.0.1:$tushe_port; 55 | include uwsgi_params; 56 | uwsgi_param SCRIPT_NAME ''; 57 | } 58 | } 59 | 60 | # You can always re-run this script 61 | # to restart Tushe application 62 | " -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | from app import db, bcrypt 2 | from mongoengine import signals 3 | from datetime import datetime 4 | 5 | 6 | class User(db.Document): 7 | username = db.StringField(required=True, verbose_name="用户名") 8 | email = db.StringField(required=True, verbose_name="E-Mail") 9 | password = db.StringField(verbose_name='修改密码') 10 | password_hash = db.StringField() 11 | active = db.BooleanField(default=True, verbose_name="已激活") 12 | is_admin = db.BooleanField(default=False, verbose_name="管理员") 13 | 14 | @classmethod 15 | def pre_save(cls, sender, document, **kwargs): 16 | if document.password: 17 | pw_hash = bcrypt.generate_password_hash(document.password) 18 | document.password_hash = pw_hash 19 | document.password = None 20 | 21 | def set_password(self, password): 22 | pw_hash = bcrypt.generate_password_hash(password) 23 | self.password_hash = pw_hash 24 | self.save() 25 | 26 | def check_password(self, password): 27 | return bcrypt.check_password_hash(self.password_hash, password) 28 | 29 | @staticmethod 30 | def is_authenticated(): 31 | return True 32 | 33 | def is_active(self): 34 | return self.active 35 | 36 | @staticmethod 37 | def is_anonymous(): 38 | return False 39 | 40 | def get_id(self): 41 | return str(self.id) 42 | 43 | def __unicode__(self): 44 | return self.username 45 | 46 | def __repr__(self): 47 | return '' % self.username 48 | 49 | 50 | class Gallery(db.Document): 51 | gid = db.StringField(primary_key=True, unique=True) 52 | title = db.StringField() 53 | user = db.ReferenceField(User) 54 | pub_date = db.DateTimeField(default=datetime.now) 55 | 56 | @classmethod 57 | def pre_delete(cls, sender, document, **kwargs): 58 | for image in document.images: 59 | image.gallery.remove(document) 60 | image.save() 61 | 62 | @property 63 | def images(self): 64 | return Image.objects(gallery=self.gid).order_by('-pub-date') 65 | 66 | def __unicode__(self): 67 | return self.title 68 | 69 | 70 | class Image(db.Document): 71 | iid = db.StringField(primary_key=True, unique=True) 72 | image = db.ImageField(thumbnail_size=(180, 160, True)) 73 | title = db.StringField() 74 | description = db.StringField() 75 | user = db.ReferenceField(User) 76 | pub_date = db.DateTimeField(default=datetime.now) 77 | view_count = db.IntField(default=0) 78 | likes = db.IntField(default=0) 79 | dislikes = db.IntField(default=0) 80 | tags = db.ListField(db.StringField(max_length=30)) 81 | gallery = db.ListField(db.ReferenceField(Gallery)) 82 | 83 | def __unicode__(self): 84 | return self.title 85 | 86 | 87 | class Runtime(db.Document): 88 | wc_access_token = db.StringField(default='') 89 | wc_access_token_time = db.IntField(default=0) 90 | rid = db.IntField(default=0, unique=True, primary_key=True) 91 | 92 | 93 | 94 | signals.pre_save.connect(User.pre_save, sender=User) 95 | signals.pre_delete.connect(Gallery.pre_delete, sender=Gallery) 96 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Babel==1.3 2 | Flask==0.10.1 3 | Flask-Admin==1.1.0 4 | Flask-BabelEx==0.9.2 5 | Flask-Bcrypt==0.6.2 6 | Flask-Login==0.2.11 7 | Flask-WTF==0.11 8 | Jinja2==2.7.3 9 | MarkupSafe==0.23 10 | WTForms==2.0.2 11 | Werkzeug==0.10.4 12 | blinker==1.3 13 | flask-mongoengine==0.7.1 14 | gnureadline==6.3.3 15 | ipython==3.1.0 16 | itsdangerous==0.24 17 | mongoengine==0.9.0 18 | pymongo==2.8 19 | python-bcrypt==0.3.1 20 | pytz==2015.4 21 | shortuuid==0.4.2 22 | speaklater==1.3 23 | Pillow==2.8.2 24 | requests 25 | xmltodict 26 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | MONGODB_SETTINGS = { 5 | 'db': 'tushedb', # Name of the database, other available settings refer to Mongoengine documentation 6 | } 7 | 8 | APP_NAME = 'light-cms' # English Only 9 | SECRET_KEY = 'secret' # Change this in production 10 | SITE_NAME = '图社' 11 | ADMIN_URL = '/admin/' # I forgot where I used it 12 | 13 | 14 | BASE_DIR = os.path.dirname(os.path.realpath(__file__)) 15 | UPLOAD_FOLDER = os.path.join(BASE_DIR, 'static/uploads') 16 | UPLOAD_URL = '/static/uploads/' 17 | 18 | 19 | # Wechat related 20 | wc_appid = 'appid' 21 | wc_secret = 'secret' 22 | wc_id = 'wc_id' # Wechat public account id (the one you set, NOT the original id) 23 | wc_token = 'wc_token' # Wechat public 24 | 25 | # Duoshuo 26 | duoshuo_short_name = 'xxxxx' 27 | -------------------------------------------------------------------------------- /static/css/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | transition-duration: 0.3s; 4 | -webkit-transition-duration: 0.3s; 5 | -moz-transition-duration: 0.3s; 6 | } 7 | 8 | *, *:before, *:after { 9 | box-sizing: inherit; 10 | transition-duration: inherit; 11 | -webkit-transition-duration: inherit; 12 | -moz-transition-duration: inherit; 13 | } 14 | 15 | body { 16 | color: #dddddd; 17 | background: #000000 url('/static/image/bg.jpg') fixed; 18 | background-size: 100% auto; 19 | font-family: 'Open Sans','helvetica',"微软雅黑",arial,sans-serif; 20 | } 21 | 22 | a { 23 | text-decoration: none; 24 | color: inherit; 25 | } 26 | 27 | ul { 28 | margin: 0; 29 | padding: 0; 30 | } 31 | 32 | 33 | .container { 34 | width: 980px; 35 | margin: auto; 36 | } 37 | 38 | .navbar { 39 | background-color: #2b2b2b; 40 | overflow: hidden; 41 | box-shadow: 0 0 20px 1px #222222; 42 | padding-left: 50px; 43 | } 44 | 45 | .navbar li{ 46 | box-sizing: border-box; 47 | list-style-type: none; 48 | float: left; 49 | display: block; 50 | font-size: 20px; 51 | line-height: 38px; 52 | padding: 0 20px; 53 | text-transform: uppercase; 54 | text-shadow: 0 1px rgba(0,255,0,0.1); 55 | } 56 | 57 | .navbar li.active, .navbar li:hover{ 58 | border-bottom: solid 2px #669933; 59 | transition-duration: 0.3s; 60 | background-color: #222222; 61 | } 62 | 63 | .navbar li.nav-brand { 64 | padding-top: 1px; 65 | } 66 | 67 | .navbar li.nav-brand:hover{ 68 | border-bottom: inherit; 69 | background-color: inherit; 70 | } 71 | 72 | .navbar li.active a, .navbar li:hover a{ 73 | color: #fff; 74 | } 75 | 76 | .clearfix:after { 77 | content: ""; 78 | display: table; 79 | clear: both; 80 | } 81 | 82 | .uploader { 83 | margin-top: 20px; 84 | display: none; 85 | } 86 | 87 | #drop { 88 | background-color: #2b2b2b; 89 | border: none; 90 | border-radius: 5px; 91 | box-shadow: 0 0 10px 1px #222222; 92 | } 93 | 94 | .dropzone .dz-preview.dz-image-preview { 95 | background-color: inherit; 96 | } 97 | 98 | .main { 99 | margin-bottom: 15px; 100 | } 101 | 102 | .wall { 103 | margin-top: 20px; 104 | padding: 10px; 105 | background-color: #2b2b2b; 106 | border-radius: 5px; 107 | box-shadow: 0 0 10px 1px #222222; 108 | } 109 | 110 | .wall-thumb { 111 | float: left; 112 | margin: 5px; 113 | box-sizing: border-box; 114 | border: solid 1px #c8a732; 115 | overflow: hidden; 116 | height:160px; 117 | width: 180px; 118 | } 119 | 120 | .fa-green { 121 | color: #209361; 122 | } 123 | 124 | #drop:hover .fa-green { 125 | color: #0F769F; 126 | } 127 | 128 | .wall-thumb:hover { 129 | border: solid 1px #669933; 130 | box-shadow: 0 0 10px 1px #669933; 131 | } 132 | 133 | .wall-title{ 134 | display: inline-block; 135 | text-align: left; 136 | border-left: solid 10px #0F769F; 137 | margin: 0 0 5px -20px; 138 | line-height: 30px; 139 | color: #fff; 140 | background-color: #209361; 141 | padding: 0 15px 0 10px; 142 | box-shadow: 0 0 10px 1px #333; 143 | border-radius: 3px; 144 | font-weight: 100; 145 | } 146 | 147 | .wall-title:hover { 148 | border-left: solid 10px #209361; 149 | color: #209361; 150 | background-color: #fff; 151 | } 152 | 153 | .wall-title a{ 154 | color: #ffff00; 155 | } 156 | 157 | .main-img { 158 | display: block; 159 | max-width: 90%; 160 | margin: 0 auto 10px auto; 161 | border: solid 5px #222222; 162 | border-radius: 10px; 163 | } 164 | 165 | .img-description, .link-address { 166 | width: 90%; 167 | margin: 0 auto 10px auto; 168 | background-color: #363636; 169 | border-radius: 5px; 170 | padding: 10px; 171 | box-shadow: 0 0 1px 1px #222222 inset; 172 | } 173 | 174 | .link-address textarea, .form-mid textarea{ 175 | display: block; 176 | width: 100%; 177 | overflow: hidden; 178 | background-color: inherit; 179 | border: none; 180 | height: 22px; 181 | } 182 | 183 | .wall-thumb img { 184 | display: block; 185 | margin:auto; 186 | box-sizing: border-box; 187 | } 188 | 189 | .thumb-cover { 190 | height: 22px; 191 | display: none; 192 | position: relative; 193 | bottom: 22px; 194 | background-color: rgba(32, 147, 97, 0.73); 195 | font-size: small; 196 | overflow: hidden; 197 | padding: 1px 5px; 198 | font-weight: 100; 199 | } 200 | 201 | .thumb-hover{ 202 | z-index: 3; 203 | position: absolute; 204 | margin-top: -20px; 205 | background-color: #2b2b2b; 206 | border: solid 1px #494949; 207 | width: 220px; 208 | padding: 5px; 209 | margin-left: -20px; 210 | border-radius: 3px; 211 | font-size: x-small; 212 | font-weight: 100; 213 | display: none; 214 | } 215 | 216 | .user-form { 217 | display: block; 218 | margin: auto; 219 | } 220 | 221 | .wall-narrow { 222 | margin: 20px auto 0 auto; 223 | width: 40%; 224 | } 225 | 226 | .wall-mid { 227 | margin: 20px auto 0 auto; 228 | width: 70%; 229 | } 230 | 231 | .user-form input[type="submit"]{ 232 | display: block; 233 | background-color: #209361; 234 | border-style: none; 235 | margin-left: 105px; 236 | margin-top: 10px; 237 | } 238 | 239 | .input-field label{ 240 | display: inline-block; 241 | width: 100px; 242 | text-align: right; 243 | } 244 | 245 | .input-field input{ 246 | background-color: #363636; 247 | border: 1px solid #222222; 248 | margin-top: 10px; 249 | } 250 | 251 | .input-field input:focus{ 252 | background-color: #363636; 253 | border: 1px solid #209361; 254 | outline: none; 255 | } 256 | 257 | 258 | .form-mid label { 259 | width: 300px; 260 | vertical-align: middle; 261 | } 262 | 263 | .form-mid input[type="submit"]{ 264 | border: 1px solid #209361; 265 | margin-left: 305px; 266 | } 267 | 268 | .form-mid textarea { 269 | display: inline; 270 | width: 600px; 271 | height: 100px; 272 | background-color: #363636; 273 | border-radius: 5px; 274 | padding: 10px; 275 | box-shadow: 0 0 1px 1px #222222 inset; 276 | outline: none; 277 | margin-top: 10px; 278 | 279 | } 280 | 281 | footer { 282 | background-color: #2b2b2b; 283 | color: #ccc; 284 | box-shadow: 0 0 10px 1px #222222; 285 | } 286 | 287 | html, body { 288 | height: 100%; 289 | } 290 | 291 | .wrapper { 292 | min-height: 100%; 293 | /* equal to footer height */ 294 | margin-bottom: -22px; 295 | } 296 | 297 | .wrapper:after { 298 | content: ""; 299 | display: block; 300 | } 301 | 302 | footer, .wrapper:after { 303 | height: 22px; 304 | } 305 | 306 | .flashes li{ 307 | list-style-type: none; 308 | } 309 | 310 | a.claim{ 311 | color: #ffff00; 312 | } 313 | 314 | .pager { 315 | margin-top: 10px; 316 | } 317 | 318 | .pager a.page { 319 | background-color: #209361; 320 | border: solid 2px #222222; 321 | padding: 3px 8px; 322 | } 323 | 324 | .pager a.active { 325 | background-color: #0F769F; 326 | } -------------------------------------------------------------------------------- /static/image/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericls/tushe/baa51245d95a2d8fa74f0c54a00694c956becfa4/static/image/bg.jpg -------------------------------------------------------------------------------- /static/js/main.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | var thumbs = $('.wall-thumb'); 3 | thumbs.mouseenter(function(){ 4 | $('.thumb-cover', this).fadeIn(100); 5 | $('.thumb-hover', this).fadeIn(100); 6 | }); 7 | thumbs.mouseleave(function(){ 8 | $('.thumb-cover', this).fadeOut(0); 9 | $('.thumb-hover', this).fadeOut(0); 10 | }); 11 | Dropzone.autoDiscover = false; 12 | $(function() { 13 | // Now that the DOM is fully loaded, create the dropzone, and setup the 14 | // event listeners 15 | drop_settings = { 16 | maxFiles:1, 17 | uploadMultiple: false, 18 | url: "/drop/", 19 | acceptedFiles: "image/*", 20 | paramName: "file", 21 | dictDefaultMessage:"
点击 或 拖放图片到这里上传", 22 | dictFallbackMessage:"你的浏览器不支持拖放上传" 23 | }; 24 | var dz = new Dropzone(".dropzone",drop_settings); 25 | dz.on('success', function(file, response){ 26 | console.log(response); 27 | window.location.href = window.location.protocol + '//' + window.location.host + '/i/' + response['id'] + '/'; 28 | }); 29 | }); 30 | $('#upload-toggle').click(function(){ 31 | $(this).toggleClass('active'); 32 | $('.uploader').fadeToggle(100); 33 | }); 34 | }); -------------------------------------------------------------------------------- /templates/add_image.html: -------------------------------------------------------------------------------- 1 | {% extends 'form-mid.html' %} 2 | 3 | {% block form_title %} 4 | 添加图片 5 | {% endblock %} 6 | 7 | {% block form %} 8 | 10 | {% endblock %} 11 | 12 | {% block footer_ext %} 13 | 32 | {% endblock %} -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }}-图社.ORG 5 | 6 | 7 | 8 | 9 | 10 | {% block header_ext %}{% endblock %} 11 | 12 | 13 |
14 |
15 | 44 |
45 | 46 |
47 |
48 | {% block main %} 49 |
50 |
51 |
52 |
53 | {% block before_wall %} 54 | {% endblock %} 55 |
56 | {% block inside_wall %} 57 | {% endblock %} 58 |
59 | {% endblock %} 60 |
61 |
62 |
63 | 68 | 69 | 70 | 71 | 72 | {% block footer_ext %}{% endblock %} 73 | 74 | 75 | -------------------------------------------------------------------------------- /templates/create_gallery.html: -------------------------------------------------------------------------------- 1 | {% extends 'form-narrow.html' %} 2 | 3 | {% block form_title %} 4 | 创建相册 5 | {% endblock %} 6 | 7 | {% block form %} 8 |
9 |
10 | 11 | 12 |
13 | 14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /templates/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'form-mid.html' %} 2 | 3 | {% block form_title %} 4 | 图片编辑 5 | {% endblock %} 6 | 7 | {% block form %} 8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 | {% endblock %} -------------------------------------------------------------------------------- /templates/form-mid.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block main %} 4 |
5 |
6 |
7 |
8 |
9 |

10 | {% block form_title %} 11 | 12 | {% endblock %} 13 |

14 | {% with messages = get_flashed_messages(with_categories=true) %} 15 | {% if messages %} 16 | 21 | {% endif %} 22 | {% endwith %} 23 | {% block form %} 24 | {% endblock %} 25 | 26 |
27 | {% endblock %} -------------------------------------------------------------------------------- /templates/form-narrow.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block main %} 4 |
5 |
6 |
7 |
8 |
9 |

10 | {% block form_title %} 11 | 12 | {% endblock %} 13 |

14 | {% with messages = get_flashed_messages(with_categories=true) %} 15 | {% if messages %} 16 | 21 | {% endif %} 22 | {% endwith %} 23 | {% block form %} 24 | {% endblock %} 25 | 26 |
27 | {% endblock %} -------------------------------------------------------------------------------- /templates/gallery.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block inside_wall %} 4 |

图册:{{ gallery.title }}

5 | {% for image in gallery.images %} 6 | 7 | {{ image.title }} 8 | 9 |
10 | {% if image.user == current_user %} 11 | 从相册移除 12 | {% endif %} 13 | {{ image.description }} 14 | {% if image.tags %} 15 | {% for tag in image.tags %} 16 | {{ tag }} 17 | {% endfor %} 18 | {% endif %} 19 |
20 | {% endfor %} 21 |

评论

22 | 23 |
24 | 25 | {% endblock %} 26 | 27 | {% block footer_ext %} 28 | 29 | 40 | 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /templates/image.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block inside_wall %} 4 |

图片名称:{{ image.title }} 5 | {% if image.user == current_user %} 6 | [编辑] 7 | {% endif %} 8 |

9 | {{ image.title }} 10 |

图片说明 11 | {% if image.user == current_user %} 12 | [编辑] 13 | {% endif %} 14 |

15 |
16 | {{ image.description }} 17 | {% if image.gallery %} 18 |

19 | 所属相册: {% for gallery in image.gallery %} 20 | 21 | {{ gallery.title }} 22 | 23 | {% endfor %} 24 |

25 | {% endif %} 26 | {% if image.user.is_active() %} 27 |

作者:{{ image.user }}

28 | {% else %} 29 |
此图野生,欢迎认领!(认领后可编辑) 30 | {% endif %} 31 |
32 |

图片标签

33 |
34 | {% for tag in image.tags %} 35 | {{ tag }} 36 | {% endfor %} 37 |
38 |

链接地址

39 |
40 |

站内链接:

41 | 42 |

图片地址:

43 | 44 |

HTML 引用:

45 | 48 |
49 |

评论

50 | 51 |
52 | 53 | {% endblock %} 54 | 55 | {% block footer_ext %} 56 | 57 | 68 | 69 | {% endblock %} 70 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import('macro.html') as macro %} 3 | 4 | 5 | {% block before_wall %} 6 |
7 | {{ macro.gallery_list(galleries, '最新图册') }} 8 |
9 | {% endblock %} 10 | 11 | 12 | {% block inside_wall %} 13 | {{ macro.image_list_without_page(images, '热门图片') }} 14 | {% endblock %} -------------------------------------------------------------------------------- /templates/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import('macro.html') as macro %} 3 | 4 | {% block inside_wall %} 5 | {{ macro.image_list(images, wall_title) }} 6 | {% endblock %} -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'form-narrow.html' %} 2 | 3 | 4 | {% block form_title %} 5 | 登录 6 | {% endblock %} 7 | 8 | 9 | {% block form %} 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /templates/macro.html: -------------------------------------------------------------------------------- 1 | {% macro image_list(images, wall_title) -%} 2 |

{{ wall_title }}

3 |
4 | {% for image in images.items %} 5 |
6 | 7 | {{ image.title }} 8 | 9 |
10 | {{ image.title }} 11 |
12 |
13 | {{ image.description }} 14 |
15 |
16 | {% endfor %} 17 |
18 | {{ pager(images, view_name=request.endpoint, **kwargs) }} 19 | {%- endmacro %} 20 | 21 | 22 | {% macro gallery_list(galleries, wall_title) -%} 23 |

{{ wall_title }}

24 |
25 | {% for gallery in galleries %} 26 | {% if gallery.images %} 27 |
28 | 29 | {{ gallery }} 30 | 31 |
32 | {{ gallery.title }}[{{ gallery.images.count() }}P] 33 |
34 |
35 | {% endif %} 36 | {% endfor %} 37 |
38 | {%- endmacro %} 39 | 40 | 41 | {% macro image_list_without_page(images, wall_title) -%} 42 |

{{ wall_title }}

43 |
44 | {% for image in images %} 45 |
46 | 47 | {{ image.title }} 48 | 49 |
50 | {{ image.title }} 51 |
52 |
53 | {{ image.description }} 54 |
55 |
56 | {% endfor %} 57 |
58 | {%- endmacro %} 59 | 60 | {% macro pager(objects, view_name) %} 61 |
62 | {% if objects.has_prev %} 63 | {% if objects.prev_num == 1 %} 64 | 上一页 65 | {% else %} 66 | 上一页 67 | {% endif %} 68 | {% endif %} 69 | {% for page in objects.iter_pages() %} 70 | {% if page %} 71 | {% if page != objects.page %} 72 | {% if page == 1 %} 73 | {{ page }} 74 | {% else %} 75 | {{ page }} 76 | {% endif %} 77 | {% else %} 78 | {{ page }} 79 | {% endif %} 80 | {% else %} 81 | ... 82 | {% endif %} 83 | {% endfor %} 84 | {% if objects.has_next %} 85 | 下一页 86 | {% endif %} 87 |
88 | {% endmacro %} 89 | -------------------------------------------------------------------------------- /templates/reg.html: -------------------------------------------------------------------------------- 1 | {% extends 'form-narrow.html' %} 2 | 3 | {% block form_title %} 4 | 注册 5 | {% endblock %} 6 | 7 | {% block form %} 8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 | {% endblock %} -------------------------------------------------------------------------------- /templates/tag_picture_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import('macro.html') as macro %} 3 | 4 | {% block inside_wall %} 5 | {{ macro.image_list(images, wall_title, tag=tag) }} 6 | {% endblock %} -------------------------------------------------------------------------------- /templates/user_image_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import('macro.html') as macro %} 3 | 4 | {% block inside_wall %} 5 | {{ macro.image_list(images, wall_title, username=username) }} 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /tushe.nginx: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | set $root_path /data/tushe/; 4 | 5 | listen 80; 6 | server_name example.org; 7 | 8 | location /static/ { 9 | root $root_path; 10 | expires 30d; 11 | add_header Cache-Control "public"; 12 | } 13 | 14 | location / { 15 | uwsgi_pass 127.0.0.1:3333; 16 | include uwsgi_params; 17 | uwsgi_param SCRIPT_NAME ''; 18 | } 19 | 20 | location ~ ^/(p|thumb)/ { 21 | 22 | 23 | # uwsgi_cache related settings are recommended. 24 | # See: http://nginx.org/en/docs/http/ngx_http_uwsgi_module.html#uwsgi_cache 25 | 26 | uwsgi_pass 127.0.0.1:3333; 27 | uwsgi_hide_header Set-Cookie; 28 | include uwsgi_params; 29 | uwsgi_param SCRIPT_NAME ''; 30 | expires 30d; 31 | add_header Cache-Control "public"; 32 | } 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /tushe.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from app import app, login_manager 3 | from flask.ext.admin import Admin 4 | from flask.ext.admin.contrib.fileadmin import FileAdmin 5 | from views import light_cms 6 | from wc import wc 7 | from admin_views import UserView, IndexView, GeneralView 8 | from models import User, Image, Gallery 9 | import settings 10 | 11 | 12 | login_manager.init_app(app) 13 | 14 | app.register_blueprint(light_cms) 15 | app.register_blueprint(wc) 16 | 17 | admin = Admin(app, name="{}后台管理".format(settings.SITE_NAME), index_view=IndexView(endpoint='admin')) 18 | admin.add_view(UserView(User, name='用户')) 19 | admin.add_view(FileAdmin(settings.UPLOAD_FOLDER, settings.UPLOAD_URL, name='媒体文件')) 20 | admin.add_view(GeneralView(Image, name='图片')) 21 | admin.add_view(GeneralView(Gallery, name='图册')) 22 | -------------------------------------------------------------------------------- /tushe.supervisord_conf: -------------------------------------------------------------------------------- 1 | # Copy to the end of /etc/supervisord.conf 2 | [program:tushe] 3 | command = /data/Envs/tushe/bin/uwsgi --ini /data/tushe/uwsgi.ini 4 | stopsignal=QUIT 5 | autostart=true 6 | autorestart=true 7 | stdout_logfile=/data/logs/tushe/access.log 8 | redirect_stderr=true 9 | -------------------------------------------------------------------------------- /uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | socket=127.0.0.1:3333 3 | py-autoreload=3 4 | chdir=/data/tushe/ 5 | virtualenv=/data/Envs/tushe/ 6 | module=tushe:app 7 | master=True 8 | workers=4 9 | pidfile=/data/tushe/uwsgi-master.pid 10 | max-requests=5000 -------------------------------------------------------------------------------- /views.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, request, flash, redirect, url_for, Response, jsonify 2 | from flask.ext import login 3 | from app import login_manager 4 | from models import User, Image, Gallery 5 | import shortuuid 6 | import settings 7 | 8 | light_cms = Blueprint('light-cms', __name__, template_folder='templates') 9 | system_user, _ = User.objects.get_or_create(username='SYSTEM', email='1@1.com', active=False) 10 | 11 | 12 | @light_cms.route('/') 13 | def index(): 14 | return render_template('index.html', 15 | title='首页', 16 | images=Image.objects(gallery__not__exists=True).order_by('-pub_date')[:30], 17 | galleries=Gallery.objects().order_by('-pub_date'), 18 | ) 19 | 20 | 21 | @light_cms.route('/images/') 22 | @light_cms.route('/images/page//') 23 | def square(page=1): 24 | return render_template('list.html', 25 | title='图片广场', 26 | images=Image.objects(gallery__not__exists=True).order_by('-pub_date').paginate(page=page, 27 | per_page=30), 28 | wall_title='全部图片', 29 | ) 30 | 31 | 32 | @light_cms.route('/tag//') 33 | @light_cms.route('/tag//page//') 34 | def tag_view(tag, page=1): 35 | images = Image.objects(tags=tag) 36 | return render_template('tag_picture_list.html', 37 | title=tag, 38 | images=images.order_by('-pub_date').paginate(page=page, per_page=30), 39 | wall_title='tag: %s 的全部图片' % tag, 40 | tag=tag, 41 | ) 42 | 43 | 44 | @light_cms.route('/user//') 45 | @light_cms.route('/user//page//') 46 | def user_picture(username, page=1): 47 | u = User.objects.get(username=username) 48 | images = Image.objects(user=u) 49 | return render_template('user_image_list.html', 50 | title='%s 的全部图片' % username, 51 | images=images.order_by('-pub_date').paginate(page=page, per_page=30), 52 | wall_title='%s 的全部图片' % username, 53 | username=username, 54 | ) 55 | 56 | 57 | @light_cms.route('/login/', methods=['GET', 'POST']) 58 | def user_login(): 59 | if request.method == 'GET': 60 | login.logout_user() 61 | return render_template('login.html', title='用户注册') 62 | if request.method == 'POST': 63 | username, password = request.form['username'], request.form['password'] 64 | if (not username) or (not password): 65 | flash('用户名密码不能为空', 'info') 66 | return redirect(url_for('light-cms.user_login')) 67 | u = User.objects(username=username).first() 68 | if not u: 69 | flash('用户名不存在', 'warning') 70 | return redirect(url_for('light-cms.user_login')) 71 | if u.check_password(password): 72 | login.login_user(u, remember=True) 73 | flash('登陆成功', 'success') 74 | return redirect(url_for('light-cms.index')) 75 | else: 76 | flash('密码错误', 'warning') 77 | return redirect(url_for('light-cms.user_login')) 78 | 79 | 80 | @light_cms.route('/reg/', methods=['GET', 'POST']) 81 | def user_reg(): 82 | if request.method == 'GET': 83 | login.logout_user() 84 | return render_template('reg.html', title='用户注册') 85 | if request.method == 'POST': 86 | username, password, email = request.form['username'], request.form['password'], request.form['email'] 87 | if (not username) or (not password) or (not email): 88 | flash('用户名密码不能为空', 'info') 89 | return redirect(url_for('light-cms.user_reg')) 90 | u = User.objects(username=username).first() 91 | if u: 92 | flash('用户名已存在', 'warning') 93 | return redirect(url_for('light-cms.user_reg')) 94 | if ('@' not in email) or (email.endswith('.')): 95 | flash('E-Mail 不合法', 'warning') 96 | return redirect(url_for('light-cms.user_reg')) 97 | u = User(username=username, email=email, password=password) 98 | u.save() 99 | login.login_user(u, remember=True) 100 | return redirect(url_for('light-cms.index')) 101 | 102 | 103 | @light_cms.route('/i//', methods=['GET', 'POST']) 104 | def image(iid): 105 | i = Image.objects.get_or_404(iid=iid) 106 | if request.method == 'GET': 107 | return render_template('image.html', image=i, title=i.title) 108 | 109 | 110 | @light_cms.route('/p//') 111 | def display_image(iid): 112 | i = Image.objects.get_or_404(iid=iid) 113 | return Response(i.image.read(), mimetype='image') 114 | 115 | 116 | @light_cms.route('/thumb//') 117 | def display_thumbnail(iid): 118 | i = Image.objects.get_or_404(iid=iid) 119 | return Response(i.image.thumbnail.read(), mimetype='image') 120 | 121 | 122 | @light_cms.route('/i//edit/', methods=['GET', 'POST']) 123 | def edit_image(iid): 124 | i = Image.objects.get_or_404(iid=iid) 125 | if i.user.username != login.current_user.username: 126 | return redirect(url_for('.image', iid=iid)) 127 | if request.method == 'GET': 128 | return render_template('edit.html', title='编辑图片信息', image=i) 129 | if request.method == 'POST': 130 | form = request.form 131 | title, desc, tags = form['title'], form['desc'], form['tags'] 132 | tags = tags.split() 133 | i.title = title 134 | i.description = desc 135 | i.tags = tags 136 | i.save() 137 | return redirect(url_for('.image', iid=iid)) 138 | 139 | 140 | @light_cms.route('/i//claim/') 141 | def claim_image(iid): 142 | i = Image.objects.get_or_404(iid=iid) 143 | if not login.current_user.is_active(): 144 | flash('请登录后再认领图片哦。么么哒~(づ ̄3 ̄)づ') 145 | return redirect(url_for('light-cms.user_login')) 146 | if not i.user.is_active(): 147 | i.user = login.current_user._get_current_object() 148 | i.save() 149 | return redirect(url_for('.image', iid=iid)) 150 | 151 | 152 | @light_cms.route('/drop/', methods=['GET', 'POST']) 153 | def drop(): 154 | file = request.files['file'] 155 | i = Image() 156 | i.title = file.filename 157 | i.image = file 158 | uuid = shortuuid.ShortUUID().random(length=6) 159 | while Image.objects(iid=uuid): 160 | uuid = shortuuid.ShortUUID().random(length=6) 161 | i.iid = uuid 162 | if login.current_user.is_active(): 163 | i.user = login.current_user._get_current_object() 164 | else: 165 | i.user = system_user 166 | i.description = '' 167 | i.tags = [] 168 | i.save() 169 | return jsonify(id=uuid) 170 | 171 | 172 | @light_cms.route('/g//') 173 | def gallery_view(gid): 174 | gallery = Gallery.objects.get_or_404(gid=gid) 175 | return render_template('gallery.html', gallery=gallery, title='图册: %s' % gallery.title) 176 | 177 | 178 | @light_cms.route('/g//delete//') 179 | def remove_image_from_gallery(gid, iid): 180 | g = Gallery.objects.get_or_404(gid=gid) 181 | i = Image.objects.get_or_404(iid=iid) 182 | if g.user == i.user == login.current_user._get_current_object(): 183 | i.gallery.remove(g) 184 | i.save() 185 | return redirect(url_for('light-cms.gallery_view', gid=gid)) 186 | 187 | 188 | @light_cms.route('/create_gallery/', methods=['GET', 'POST']) 189 | def create_gallery(): 190 | if not login.current_user.is_active(): 191 | flash('请登录后再搞相册哦~') 192 | return redirect(url_for('light-cms.user_login')) 193 | if request.method == 'GET': 194 | return render_template('create_gallery.html', title='创建相册') 195 | if request.method == 'POST': 196 | uuid = shortuuid.ShortUUID().random(length=6) 197 | while Gallery.objects(gid=uuid): 198 | uuid = shortuuid.ShortUUID().random(length=6) 199 | title = request.form['title'] 200 | g = Gallery() 201 | g.user = login.current_user._get_current_object() 202 | g.gid = uuid 203 | g.title = title 204 | g.save() 205 | return redirect(url_for('light-cms.add_image_to_gallery', gid=g.gid)) 206 | 207 | 208 | @light_cms.route('/g//add/') 209 | def add_image_to_gallery(gid): 210 | g = Gallery.objects.get_or_404(gid=gid) 211 | return render_template('add_image.html', title='添加图片', gallery=g) 212 | 213 | 214 | @light_cms.route('/g//drop/', methods=['GET', 'POST']) 215 | def gallery_drop(gid): 216 | if not login.current_user.is_active(): 217 | flash('请登录后再搞相册哦~') 218 | return redirect(url_for('light-cms.user_login')) 219 | g = Gallery.objects.get_or_404(gid=gid) 220 | file = request.files['file'] 221 | i = Image() 222 | i.gallery.append(g) 223 | i.title = file.filename 224 | i.image = file 225 | uuid = shortuuid.ShortUUID().random(length=6) 226 | while Image.objects(iid=uuid): 227 | uuid = shortuuid.ShortUUID().random(length=6) 228 | i.iid = uuid 229 | i.user = login.current_user._get_current_object() 230 | i.description = '' 231 | i.tags = [] 232 | i.save() 233 | return jsonify(id=uuid) 234 | 235 | 236 | @login_manager.user_loader 237 | def load_user(user_id): 238 | user = User.objects(id=user_id).first() 239 | return user 240 | 241 | 242 | @light_cms.context_processor 243 | def processor(): 244 | def get_settings(): 245 | return settings 246 | return {'SETTINGS': get_settings()} 247 | -------------------------------------------------------------------------------- /wc.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, url_for 2 | from models import Image, Runtime, User 3 | import settings 4 | import requests 5 | import hashlib 6 | import xmltodict 7 | import time 8 | import shortuuid 9 | 10 | appid = settings.wc_appid 11 | secret = settings.wc_secret 12 | wc_id = settings.wc_id 13 | wc_token = settings.wc_token 14 | 15 | wc = Blueprint('wc', __name__, template_folder='templates', url_prefix='/wc/') 16 | system_user, _ = User.objects.get_or_create(username='SYSTEM', email='1@1.com', active=False) 17 | 18 | try: # may be replaced by get_or_create() 19 | runtime = Runtime.objects.get(rid=0) 20 | except: 21 | runtime = Runtime(rid=0).save() 22 | 23 | 24 | def update_access_token(): 25 | d = requests.get( 26 | 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s' % ( 27 | appid, secret 28 | ) 29 | ).json() 30 | if not d.get('errcode'): 31 | runtime.wc_access_token = d['access_token'] 32 | runtime.wc_access_token_time = int(time.time()) 33 | runtime.save() 34 | 35 | 36 | def current_access_token(): 37 | if int(time.time()) - runtime.wc_access_token_time >= 7000: 38 | update_access_token() 39 | return runtime.wc_access_token 40 | 41 | 42 | 43 | 44 | @wc.route('/', methods=['get']) 45 | def main(): 46 | signature = request.args.get('signature', 's') 47 | string = request.args.get('echostr', 'e') 48 | timestamp = request.args.get('timestamp', 't') 49 | nonce = request.args.get('nonce', 'n') 50 | token = wc_token 51 | temp_string = ''.join(sorted([token, timestamp, nonce])) 52 | hash_object = hashlib.sha1(temp_string.encode('utf-8')) 53 | hex_dig = hash_object.hexdigest() 54 | if hex_dig == signature: 55 | return string 56 | else: 57 | return '' 58 | 59 | 60 | def xml_response(data): 61 | d = {'xml': data} 62 | return xmltodict.unparse(d) 63 | 64 | 65 | def send_text(to, msg): 66 | res = dict() 67 | res['ToUserName'] = to 68 | res['FromUserName'] = wc_id 69 | res['CreateTime'] = int(time.time()) 70 | res['MsgType'] = 'text' 71 | res['Content'] = msg 72 | return xml_response(res) 73 | 74 | 75 | @wc.route('/', methods=['post']) 76 | def receive(): 77 | data = request.data 78 | data = xmltodict.parse(data)['xml'] 79 | if data['MsgType'] == 'text': 80 | return send_text(data['FromUserName'], 'hi') 81 | if data['MsgType'] == 'image': 82 | token = current_access_token() 83 | file_url = 'https://api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s' % (token, data['MediaId']) 84 | file = requests.get(file_url, stream=True).raw 85 | i = Image() 86 | i.image = file 87 | uuid = shortuuid.ShortUUID().random(length=6) 88 | while Image.objects(iid=uuid): 89 | uuid = shortuuid.ShortUUID().random(length=6) 90 | i.iid = uuid 91 | i.title = data['MediaId'] 92 | i.user = system_user 93 | i.description = '' 94 | i.tags = [] 95 | i.save() 96 | return send_text( 97 | data['FromUserName'], '上传成功!图片地址:%s%s' % ( 98 | request.url_root[:-1], url_for('light-cms.image', iid=i.iid) 99 | ) 100 | ) 101 | --------------------------------------------------------------------------------