├── .gitignore ├── Dockerfile ├── LICENSE ├── README.org ├── README_zh.org ├── config.example ├── docker-compose.yml ├── forums ├── __init__.py ├── admin │ ├── __init__.py │ ├── forums.py │ ├── message.py │ ├── permission.py │ ├── topic.py │ ├── user.py │ └── views.py ├── api │ ├── __init__.py │ ├── collect │ │ ├── __init__.py │ │ ├── db.py │ │ └── views.py │ ├── follow │ │ ├── __init__.py │ │ └── views.py │ ├── forms.py │ ├── forums │ │ ├── __init__.py │ │ ├── db.py │ │ └── views.py │ ├── message │ │ ├── __init__.py │ │ ├── db.py │ │ └── views.py │ ├── search │ │ ├── __init__.py │ │ └── views.py │ ├── setting │ │ ├── __init__.py │ │ └── views.py │ ├── tag │ │ ├── __init__.py │ │ ├── db.py │ │ └── views.py │ ├── topic │ │ ├── __init__.py │ │ ├── db.py │ │ ├── permissions.py │ │ └── views.py │ ├── upload │ │ ├── __init__.py │ │ └── views.py │ ├── user │ │ ├── __init__.py │ │ ├── db.py │ │ └── views.py │ └── utils.py ├── app.py ├── common │ ├── __init__.py │ ├── helper.py │ ├── middleware.py │ ├── models.py │ ├── records.py │ ├── response.py │ ├── utils.py │ └── views.py ├── count.py ├── default.py ├── docs │ ├── __init__.py │ ├── static │ │ ├── flask-avatar │ │ │ ├── .buildinfo │ │ │ ├── _sources │ │ │ │ ├── index.txt │ │ │ │ └── introduction.txt │ │ │ ├── _static │ │ │ │ ├── ajax-loader.gif │ │ │ │ ├── alabaster.css │ │ │ │ ├── basic.css │ │ │ │ ├── comment-bright.png │ │ │ │ ├── comment-close.png │ │ │ │ ├── comment.png │ │ │ │ ├── custom.css │ │ │ │ ├── doctools.js │ │ │ │ ├── down-pressed.png │ │ │ │ ├── down.png │ │ │ │ ├── file.png │ │ │ │ ├── jquery-1.11.1.js │ │ │ │ ├── jquery.js │ │ │ │ ├── minus.png │ │ │ │ ├── plus.png │ │ │ │ ├── pygments.css │ │ │ │ ├── searchtools.js │ │ │ │ ├── underscore-1.3.1.js │ │ │ │ ├── underscore.js │ │ │ │ ├── up-pressed.png │ │ │ │ ├── up.png │ │ │ │ └── websupport.js │ │ │ ├── genindex.html │ │ │ ├── index.html │ │ │ ├── introduction.html │ │ │ ├── objects.inv │ │ │ ├── search.html │ │ │ └── searchindex.js │ │ └── flask-maple │ │ │ ├── .buildinfo │ │ │ ├── _sources │ │ │ ├── advanced.txt │ │ │ ├── auth.txt │ │ │ ├── bootstrap.txt │ │ │ ├── error.txt │ │ │ ├── index.txt │ │ │ ├── insroduction.txt │ │ │ └── introduction.txt │ │ │ ├── _static │ │ │ ├── ajax-loader.gif │ │ │ ├── alabaster.css │ │ │ ├── basic.css │ │ │ ├── comment-bright.png │ │ │ ├── comment-close.png │ │ │ ├── comment.png │ │ │ ├── custom.css │ │ │ ├── doctools.js │ │ │ ├── down-pressed.png │ │ │ ├── down.png │ │ │ ├── file.png │ │ │ ├── jquery-1.11.1.js │ │ │ ├── jquery.js │ │ │ ├── minus.png │ │ │ ├── plus.png │ │ │ ├── pygments.css │ │ │ ├── searchtools.js │ │ │ ├── underscore-1.3.1.js │ │ │ ├── underscore.js │ │ │ ├── up-pressed.png │ │ │ ├── up.png │ │ │ └── websupport.js │ │ │ ├── advanced.html │ │ │ ├── auth.html │ │ │ ├── bootstrap.html │ │ │ ├── error.html │ │ │ ├── genindex.html │ │ │ ├── index.html │ │ │ ├── introduction.html │ │ │ ├── objects.inv │ │ │ ├── search.html │ │ │ └── searchindex.js │ ├── templates │ │ └── docs │ │ │ └── doc_list.html │ └── views.py ├── extension │ ├── __init__.py │ ├── babel.py │ ├── login.py │ └── maple.py ├── jinja.py ├── permission.py ├── subdomain.py └── utils.py ├── gunicorn.conf ├── requirements.txt ├── runserver.py ├── screenshooter ├── ask.png ├── board.png └── index.png ├── script ├── upgrade.py └── upgrade_count.py ├── static ├── assets │ ├── home.css │ └── home.js ├── avatars │ ├── honmaple-14670891852944.png │ ├── honmaple-14685857008552.png │ └── qwert-14823290629699.png ├── favicon.ico ├── images │ ├── Moo.png │ ├── bg1.jpg │ ├── header.png │ └── snow.jpg ├── libs │ └── js │ │ └── org.js ├── robots.txt ├── socketio │ ├── chat.css │ └── chat.js └── styles │ ├── following.js │ ├── forums.js │ ├── mine.css │ ├── monokai.css │ ├── topic.js │ └── upload.js ├── templates ├── auth │ ├── forget.html │ ├── login.html │ └── register.html ├── base │ ├── base.html │ ├── form.html │ ├── head.html │ ├── header.html │ ├── link.html │ ├── paginate.html │ └── panel.html ├── board │ ├── _macro.html │ ├── board.html │ ├── board_list.html │ └── chat.html ├── collect │ ├── collect.html │ ├── collect_list.html │ ├── create.html │ ├── delete.html │ └── edit.html ├── follow │ ├── _macro.html │ ├── following_collects.html │ ├── following_tags.html │ ├── following_topics.html │ ├── following_users.html │ └── none.html ├── forums │ ├── _macro.html │ ├── about.html │ ├── contact.html │ ├── help.html │ ├── index.html │ ├── message.html │ └── userlist.html ├── maple │ └── footer.html ├── search │ ├── form.html │ ├── result.html │ └── search.html ├── setting │ ├── _macro.html │ ├── babel.html │ ├── password.html │ ├── privacy.html │ └── setting.html ├── tag │ ├── _macro.html │ ├── panel.html │ ├── tag.html │ └── tag_list.html ├── topic │ ├── _list_macro.html │ ├── _topic.html │ ├── ask.html │ ├── ask │ │ ├── _macro.html │ │ └── form.html │ ├── collect.html │ ├── edit.html │ ├── item │ │ ├── _macro.html │ │ ├── body.html │ │ └── heading.html │ ├── itemlist │ │ └── _macro.html │ ├── panel.html │ ├── reply │ │ ├── _macro.html │ │ ├── form.html │ │ └── itemlist.html │ ├── topic.html │ ├── topic_good.html │ ├── topic_list.html │ └── topic_top.html └── user │ ├── _macro.html │ ├── base.html │ ├── followers.html │ ├── info.html │ ├── replies.html │ ├── user.html │ └── user_list.html └── translations ├── babel.cfg └── zh └── LC_MESSAGES ├── messages.mo └── messages.po /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | **/config/ 3 | migrations/ 4 | backup/ 5 | forums1/ 6 | fabfile.py 7 | **/__pycache__/ 8 | **/avatar/ 9 | *.swp 10 | *.log 11 | *.db 12 | *.pyc 13 | **/.webassets-cache/ 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | ENV TZ "Asia/Shanghai" 4 | ENV LANG "C.UTF-8" 5 | 6 | RUN sed -i 's/archive.ubuntu.com/mirrors.163.com/g' /etc/apt/sources.list && \ 7 | sed -i 's/security.ubuntu.com/mirrors.163.com/g' /etc/apt/sources.list && \ 8 | echo $TZ > /etc/timezone && \ 9 | apt update && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends \ 10 | python3-pip python3-setuptools python3-wheel python3.6 tzdata && \ 11 | ln -s /usr/bin/python3.6 /usr/bin/python && ln -s /usr/bin/pip3 /usr/bin/pip 12 | 13 | RUN mkdir -p /web/logs 14 | WORKDIR /web 15 | 16 | ADD requirements.txt /web/requirements.txt 17 | RUN pip3 install -r requirements.txt -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com 18 | 19 | ADD runserver.py /web/runserver.py 20 | ADD forums /web/forums 21 | ADD templates /web/templates 22 | ADD static /web/static 23 | ADD translations /web/translations 24 | 25 | CMD ["gunicorn","-b","0.0.0.0:8000","runserver:app"] 26 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * maple-bbs 2 | 3 | [[LICENSE][https://img.shields.io/badge/license-GPL3.0-blue.svg]] 4 | [[https://www.python.org][https://img.shields.io/badge/python-3.4-brightgreen.svg]] 5 | 6 | [[README.org][English]] | [[README_zh.org][中文]] 7 | 8 | [[https://raw.githubusercontent.com/honmaple/maple-bbs/master/screenshooter/index.png]] 9 | [[https://raw.githubusercontent.com/honmaple/maple-bbs/master/screenshooter/board.png]] 10 | [[https://raw.githubusercontent.com/honmaple/maple-bbs/master/screenshooter/ask.png]] 11 | 12 | 13 | This is a free,open-source forums system based on the flask 14 | 15 | *If you have used maple-bbs before 2017-4-1,please use upgrade script to upgrade data* 16 | 17 | *important !* : please modify script to configure as your own database. 18 | #+BEGIN_SRC sh 19 | # session1:old database 20 | # session2:new database 21 | python upgrade.py 22 | python upgrade_count.py 23 | #+END_SRC 24 | 25 | ** Features 26 | + Register & login & forget password 27 | + Board and tags 28 | + Collect 29 | + Like replies 30 | + Follow tags,users,topics 31 | + Privacy setting 32 | + Choice markdown to ask 33 | + Tags rss 34 | + Avatar 35 | + Full text search with whoosh 36 | 37 | ** Installation 38 | 39 | #+BEGIN_SRC sh 40 | mkvirtualenv forums 41 | #+END_SRC 42 | 43 | *** Install necessary package 44 | #+BEGIN_SRC python 45 | pip install -r requirements.txt 46 | #+END_SRC 47 | 48 | *** Config 49 | #+BEGIN_SRC shell 50 | mv config.example config.py 51 | #+END_SRC 52 | *remember to modify config file.* 53 | 54 | *** Init sql 55 | #+BEGIN_SRC python 56 | python runserver.py db init 57 | python runserver.py db migrate -m "first migrate" 58 | python runserver.py db upgrade 59 | #+END_SRC 60 | Or 61 | #+BEGIN_SRC sh 62 | python runserver.py initdb 63 | #+END_SRC 64 | 65 | *** create full text index 66 | #+BEGIN_SRC sh 67 | python runserver.py create_index 68 | #+END_SRC 69 | *** Create admin account 70 | #+BEGIN_SRC shell 71 | python runserver.py create_user 72 | #+END_SRC 73 | 74 | *** Login and visit admin 75 | *Ok* ,visit forums.localhost:8000/admin to add something 76 | 77 | ** Demo 78 | Please visit [[https://forums.honmaple.org][forums.honmaple.org]] 79 | 80 | ** License 81 | maple-bbs is open-sourced software licensed under the GPL3 license 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /README_zh.org: -------------------------------------------------------------------------------- 1 | * maple-bbs 2 | 3 | [[LICENSE][https://img.shields.io/badge/license-GPL3.0-blue.svg]] 4 | [[https://www.python.org][https://img.shields.io/badge/python-3.4-brightgreen.svg]] 5 | 6 | [[https://raw.githubusercontent.com/honmaple/maple-bbs/master/screenshooter/index.png]] 7 | [[https://raw.githubusercontent.com/honmaple/maple-bbs/master/screenshooter/board.png]] 8 | [[https://raw.githubusercontent.com/honmaple/maple-bbs/master/screenshooter/ask.png]] 9 | 10 | *如果你在4月1日之前部署过,请使用升级脚本升级你的数据* 11 | 12 | *重要!*,请修改升级脚本为你自己的数据库 13 | #+BEGIN_SRC sh 14 | # session1:old database 15 | # session2:new database 16 | python upgrade.py 17 | python upgrade_count.py 18 | #+END_SRC 19 | 20 | ** 功能 21 | + 注册,登陆,忘记密码 22 | + 版块及节点分类 23 | + 主题收藏 24 | + 回复点赞 25 | + 关注用户,节点,主题 26 | + 隐私设置 27 | + 可选markdown提问 28 | + 节点rss 29 | + 头像... 30 | + 全文搜索(基于whoosh) 31 | 32 | ** 安装 33 | 创建虚拟环境 34 | 35 | #+BEGIN_SRC sh 36 | mkvirtualenv forums 37 | #+END_SRC 38 | 39 | *** 安装需要的包 40 | #+BEGIN_SRC python 41 | pip install -r requirements.txt 42 | #+END_SRC 43 | 44 | *** 配置 45 | #+BEGIN_SRC shell 46 | mv config.example config.py 47 | #+END_SRC 48 | *记得修改配置文件* 49 | 50 | *** 初始化数据库 51 | #+BEGIN_SRC python 52 | python runserver.py db init 53 | python runserver.py db migrate -m "first migrate" 54 | python runserver.py db upgrade 55 | #+END_SRC 56 | 或者 57 | #+BEGIN_SRC sh 58 | python runserver.py initdb 59 | #+END_SRC 60 | 61 | *** 创建全文搜索索引 62 | #+BEGIN_SRC sh 63 | python runserver.py create_index 64 | #+END_SRC 65 | 66 | *** 创建管理员账户 67 | #+BEGIN_SRC shell 68 | python runserver.py create_user 69 | #+END_SRC 70 | 71 | *** 登陆并访问后台 72 | #+BEGIN_SRC sh 73 | python runserver.py 74 | #+END_SRC 75 | 然后访问*forums.localhost:8000/admin*去增加一些东西 76 | 77 | ** 演示 78 | 请访问 [[https://forums.honmaple.org][forums.honmaple.org]] 79 | 80 | ** License 81 | maple-bbs is open-sourced software licensed under the GPL3 license 82 | -------------------------------------------------------------------------------- /config.example: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding=UTF-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016-2019 jianglin 5 | # File Name: config.example 6 | # Author: jianglin 7 | # Email: mail@honmaple.com 8 | # Created: 2016-05-20 12:31:46 (CST) 9 | # Last Update: Monday 2022-12-12 16:40:46 (CST) 10 | # By: jianglin 11 | # Description: 12 | # ************************************************************************** 13 | from datetime import timedelta 14 | from os import path 15 | 16 | PATH = path.abspath(path.dirname(__file__)) 17 | DEBUG = True 18 | SECRET_KEY = 'secret key' 19 | SECURITY_PASSWORD_SALT = 'you will never guess' 20 | SECRET_KEY_SALT = 'you will never guess' 21 | 22 | # avatar upload directory 23 | AVATAR_FOLDER = path.join(PATH, 'avatars') 24 | # avatar generate range 25 | AVATAR_RANGE = [122, 512] 26 | 27 | # for development use localhost:5000 28 | # for production use xxx.com 29 | # SERVER_NAME = 'localhost:5000' 30 | 31 | # remember me to save cookies 32 | PERMANENT_SESSION_LIFETIME = timedelta(days=3) 33 | REMEMBER_COOKIE_DURATION = timedelta(days=3) 34 | ONLINE_LAST_MINUTES = 5 35 | 36 | # You want show how many topics per page 37 | PER_PAGE = 12 38 | 39 | # Use cache 40 | CACHE_TYPE = 'null' 41 | CACHE_DEFAULT_TIMEOUT = 60 42 | CACHE_KEY_PREFIX = 'cache:' 43 | CACHE_REDIS_HOST = '127.0.0.1' 44 | CACHE_REDIS_PORT = '6379' 45 | CACHE_REDIS_PASSWORD = 'your password' 46 | CACHE_REDIS_DB = 2 47 | 48 | # Redis setting 49 | REDIS = { 50 | 'host': 'redis', 51 | 'db': 1, 52 | 'password': 'your password', 53 | 'decode_responses': True 54 | } 55 | 56 | # some middleware 57 | MIDDLEWARE = [ 58 | 'forums.common.middleware.GlobalMiddleware', 59 | 'forums.common.middleware.OnlineMiddleware' 60 | ] 61 | 62 | # Mail such as qq 63 | MAIL_SERVER = 'smtp.qq.com' 64 | MAIL_PORT = 25 65 | MAIL_USE_TLS = True 66 | MAIL_USE_SSL = False 67 | MAIL_USERNAME = "your email" 68 | MAIL_PASSWORD = "your password" 69 | MAIL_DEFAULT_SENDER = 'your email' 70 | # MAIL_SUPPRESS_SEND = True 71 | 72 | SERVER_NAME = 'localhost:8000' 73 | SUBDOMAIN = {'forums': True, 'docs': True} 74 | 75 | # logging setting 76 | LOGGING = { 77 | 'info': 'logs/info.log', 78 | 'error': 'logs/error.log', 79 | 'send_mail': False, 80 | 'toaddrs': [], 81 | 'subject': 'Your Application Failed', 82 | 'formatter': ''' 83 | Message type: %(levelname)s 84 | Location: %(pathname)s:%(lineno)d 85 | Module: %(module)s 86 | Function: %(funcName)s 87 | Time: %(asctime)s 88 | 89 | Message: 90 | 91 | %(message)s 92 | ''' 93 | } 94 | 95 | # Sql 96 | SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:password@localhost/your_db' 97 | # SQLALCHEMY_DATABASE_URI = 'sqlite:///test.db' 98 | 99 | MSEARCH_INDEX_NAME = 'msearch' 100 | MSEARCH_BACKEND = 'whoosh' 101 | # SQLALCHEMY_ECHO = True 102 | # SQLALCHEMY_DATABASE_URI = 'sqlite:///test.db' 103 | # SQLALCHEMY_DATABASE_URI = 'mysql://username:password@server/db' 104 | BABEL_DEFAULT_LOCALE = 'en' 105 | BABEL_DEFAULT_TIMEZONE = 'UTC' 106 | BABEL_TRANSLATION_DIRECTORIES = path.join(PATH, 'translations') 107 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.2" 2 | services: 3 | redis: 4 | image: redis:alpine 5 | ports: 6 | - 6379:6379 7 | postgres: 8 | image: postgres:10 9 | restart: always 10 | ports: 11 | - 5433:5432 12 | environment: 13 | - POSTGRES_PASSWORD=test 14 | forums: 15 | build: 16 | context: . 17 | ports: 18 | - 8000:8000 19 | links: 20 | - redis 21 | - postgres 22 | depends_on: 23 | - redis 24 | - postgres 25 | volumes: 26 | - ./config.py:/web/config.py 27 | -------------------------------------------------------------------------------- /forums/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2017 jianglin 5 | # File Name: __init__.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2017-01-25 20:10:50 (CST) 9 | # Last Update: Thursday 2018-07-26 09:57:28 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | import os 14 | 15 | from flask import Flask 16 | 17 | from forums import app as ap, extension 18 | from forums import jinja, subdomain 19 | from forums import api, docs, admin 20 | from flask_maple import auth 21 | 22 | 23 | def create_app(config): 24 | path = os.path.dirname(__file__) 25 | templates = os.path.abspath(os.path.join(path, os.pardir, 'templates')) 26 | static = os.path.abspath(os.path.join(path, os.pardir, 'static')) 27 | 28 | app = Flask(__name__, template_folder=templates, static_folder=static) 29 | app.config.from_object(config) 30 | 31 | subdomain.init_app(app) 32 | ap.init_app(app) 33 | jinja.init_app(app) 34 | extension.init_app(app) 35 | admin.init_app(app) 36 | # router 37 | auth.init_app(app) 38 | api.init_app(app) 39 | docs.init_app(app) 40 | return app 41 | -------------------------------------------------------------------------------- /forums/admin/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: __init__.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-10-28 10:02:51 (CST) 9 | # Last Update:星期三 2017-12-13 16:13:45 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask_admin import Admin 14 | from forums.admin import forums, user, topic, message, permission 15 | 16 | admin = Admin(name='HonMaple', template_mode='bootstrap3') 17 | 18 | 19 | def init_app(app): 20 | admin.init_app(app) 21 | forums.init_admin(admin) 22 | user.init_admin(admin) 23 | topic.init_admin(admin) 24 | message.init_admin(admin) 25 | permission.init_admin(admin) 26 | -------------------------------------------------------------------------------- /forums/admin/forums.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: forums.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-12-17 13:12:23 (CST) 9 | # Last Update: Monday 2019-05-06 23:37:00 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from .views import BaseView 14 | from forums.extension import db 15 | from forums.api.forums.db import Board 16 | from forums.api.tag.db import Tags 17 | 18 | 19 | class BoardView(BaseView): 20 | form_excluded_columns = ('topics') 21 | 22 | 23 | class TagView(BaseView): 24 | column_searchable_list = ['name'] 25 | form_excluded_columns = ('topics', 'followers') 26 | 27 | 28 | def init_admin(admin): 29 | admin.add_view( 30 | BoardView( 31 | Board, 32 | db.session, 33 | name='管理版块', 34 | endpoint='admin_board', 35 | category='管理社区')) 36 | admin.add_view( 37 | TagView( 38 | Tags, 39 | db.session, 40 | name='管理节点', 41 | endpoint='admin_tag', 42 | category='管理社区')) 43 | -------------------------------------------------------------------------------- /forums/admin/message.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2017 jianglin 5 | # File Name: message.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2017-04-01 18:52:43 (CST) 9 | # Last Update:星期五 2017-11-10 11:06:11 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from .views import BaseView 14 | from forums.extension import db 15 | from forums.api.message.db import MessageText, Message 16 | 17 | 18 | class MessageTextView(BaseView): 19 | pass 20 | 21 | 22 | class MessageView(BaseView): 23 | pass 24 | # column_searchable_list = ['name'] 25 | # form_excluded_columns = ('topics', 'followers') 26 | 27 | 28 | def init_admin(admin): 29 | admin.add_view( 30 | MessageView( 31 | Message, 32 | db.session, 33 | name='管理通知', 34 | endpoint='admin_message', 35 | category='管理通知')) 36 | admin.add_view( 37 | MessageTextView( 38 | MessageText, 39 | db.session, 40 | name='管理内容', 41 | endpoint='admin_message_text', 42 | category='管理通知')) 43 | -------------------------------------------------------------------------------- /forums/admin/permission.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: permission.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-12-17 09:39:36 (CST) 9 | # Last Update:星期一 2017-12-25 17:48:59 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from .views import BaseView 14 | from forums.extension import db 15 | from forums.api.user.db import Group, Permission 16 | 17 | 18 | class GroupView(BaseView): 19 | column_editable_list = ['name'] 20 | 21 | 22 | class PermissionView(BaseView): 23 | column_searchable_list = ('resource', 'groups.name') 24 | column_filters = ['groups.name', 'resource_type'] 25 | column_editable_list = ['code'] 26 | 27 | 28 | def init_admin(admin): 29 | admin.add_view( 30 | GroupView( 31 | Group, 32 | db.session, 33 | name='管理用户组', 34 | endpoint='admin_groups', 35 | category='管理权限')) 36 | admin.add_view( 37 | PermissionView( 38 | Permission, 39 | db.session, 40 | name='管理权限', 41 | endpoint='admin_permiss', 42 | category='管理权限')) 43 | -------------------------------------------------------------------------------- /forums/admin/topic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: topic.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-12-17 13:01:43 (CST) 9 | # Last Update:星期五 2017-11-10 11:06:21 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from .views import BaseView 14 | from forums.extension import db 15 | from forums.api.topic.db import Topic, Reply 16 | from forums.api.collect.db import Collect 17 | 18 | 19 | class TopicView(BaseView): 20 | column_searchable_list = ('title', 'content', 'author.username') 21 | column_filters = ['created_at', 'is_good', 'is_top', 'author.username'] 22 | column_exclude_list = ['content'] 23 | column_editable_list = ['title', 'is_good', 'is_top', 'content_type'] 24 | column_default_sort = 'created_at' 25 | column_formatters = dict( 26 | content=lambda v, c, m, p: m.content[:100] + '...', 27 | content_type=lambda v, c, m, p: m.get_choice_display('content_type', 'CONTENT_TYPE') 28 | ) 29 | form_choices = {'content_type': Topic.CONTENT_TYPE} 30 | form_widget_args = {'content': {'rows': 10}} 31 | form_excluded_columns = ('replies', 'collects', 'followers') 32 | form_ajax_refs = {'tags': {'fields': ('name', ), 'page_size': 10}} 33 | 34 | 35 | class CollectView(BaseView): 36 | pass 37 | 38 | 39 | class ReplyView(BaseView): 40 | column_searchable_list = ['topic.title', 'content'] 41 | column_filters = ['author.username', 'created_at'] 42 | form_excluded_columns = ['likers'] 43 | form_widget_args = {'content': {'rows': 10}} 44 | 45 | 46 | def init_admin(admin): 47 | admin.add_view( 48 | TopicView( 49 | Topic, 50 | db.session, 51 | name='管理问题', 52 | endpoint='admin_topic', 53 | category='管理主题')) 54 | admin.add_view( 55 | ReplyView( 56 | Reply, 57 | db.session, 58 | name='管理回复', 59 | endpoint='admin_reply', 60 | category='管理主题')) 61 | admin.add_view( 62 | CollectView( 63 | Collect, 64 | db.session, 65 | name='管理收藏', 66 | endpoint='admin_collect', 67 | category='管理主题')) 68 | -------------------------------------------------------------------------------- /forums/admin/user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: user.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-12-17 11:57:11 (CST) 9 | # Last Update:星期五 2017-11-10 11:05:56 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from .views import BaseView 14 | from forums.extension import db 15 | from forums.api.user.db import User, UserInfo, UserSetting 16 | from wtforms import PasswordField 17 | from wtforms.validators import DataRequired 18 | 19 | STATUS = UserSetting.STATUS 20 | 21 | 22 | def display_status(column): 23 | return lambda v, c, m, p: m.get_choice_display(column, 'STATUS') 24 | 25 | 26 | class UserView(BaseView): 27 | column_exclude_list = ['password', 'info', 'setting'] 28 | column_searchable_list = ['username', 'email'] 29 | column_filters = ['email', 'is_superuser', 'is_confirmed', 'register_time'] 30 | column_editable_list = ['is_confirmed', 'is_superuser'] 31 | form_columns = ('username', 'email', 'password', 'is_confirmed', 32 | 'is_superuser') 33 | # inline_db = (UserInfo, UserSetting) 34 | # form_extra_fields = { 35 | # 'password': PasswordField('Password', [DataRequired()]) 36 | # } 37 | 38 | 39 | class UserInfoView(BaseView): 40 | pass 41 | 42 | 43 | class UserSettingView(BaseView): 44 | column_formatters = dict( 45 | online_status=display_status('online_status'), 46 | topic_list=display_status('topic_list'), 47 | rep_list=display_status('rep_list'), 48 | ntb_list=display_status('ntb_list'), 49 | collect_list=display_status('collect_list'), 50 | locale=lambda v, c, m, p: m.get_choice_display('locale', 'LOCALE'), 51 | timezone=lambda v, c, m, p: m.get_choice_display('timezone', 'TIMEZONE'), 52 | ) 53 | column_editable_list = column_formatters.keys() 54 | form_choices = { 55 | 'online_status': UserSetting.STATUS, 56 | 'topic_list': UserSetting.STATUS, 57 | 'rep_list': UserSetting.STATUS, 58 | 'ntb_list': UserSetting.STATUS, 59 | 'collect_list': UserSetting.STATUS, 60 | 'locale': UserSetting.LOCALE, 61 | 'timezone': UserSetting.TIMEZONE 62 | } 63 | 64 | 65 | def init_admin(admin): 66 | admin.add_view( 67 | UserView( 68 | User, 69 | db.session, 70 | name='管理用户', 71 | endpoint='admin_user', 72 | category='管理用户')) 73 | admin.add_view( 74 | UserInfoView( 75 | UserInfo, 76 | db.session, 77 | name='管理用户信息', 78 | endpoint='admin_userinfo', 79 | category='管理用户')) 80 | admin.add_view( 81 | UserSettingView( 82 | UserSetting, 83 | db.session, 84 | name='管理用户设置', 85 | endpoint='admin_usersetting', 86 | category='管理用户')) 87 | -------------------------------------------------------------------------------- /forums/admin/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: views.py 6 | # Author: jianglin 7 | # Email: mail@honmaple.com 8 | # Created: 2016-12-17 13:15:10 (CST) 9 | # Last Update: Monday 2022-12-12 16:51:58 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import abort 14 | from flask_admin.contrib.sqla import ModelView 15 | from flask_wtf import Form 16 | from forums.permission import super_permission 17 | 18 | 19 | class BaseForm(Form): 20 | def __init__(self, formdata=None, obj=None, prefix=u'', **kwargs): 21 | self._obj = obj 22 | super(BaseForm, self).__init__( 23 | formdata=formdata, obj=obj, prefix=prefix, **kwargs) 24 | 25 | 26 | class BaseView(ModelView): 27 | 28 | page_size = 10 29 | can_view_details = True 30 | form_base_class = BaseForm 31 | 32 | def is_accessible(self): 33 | return super_permission.can() 34 | 35 | def inaccessible_callback(self, name, **kwargs): 36 | abort(404) 37 | -------------------------------------------------------------------------------- /forums/api/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: __init__.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-11-20 15:49:33 (CST) 9 | # Last Update: Wednesday 2019-05-08 15:43:20 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from werkzeug import import_string 14 | 15 | 16 | def init_app(app): 17 | blueprints = [ 18 | "forums.api.forums", 19 | "forums.api.topic", 20 | "forums.api.tag", 21 | "forums.api.user", 22 | "forums.api.collect", 23 | "forums.api.message", 24 | "forums.api.follow", 25 | "forums.api.upload", 26 | "forums.api.setting", 27 | "forums.api.search", 28 | ] 29 | for blueprint in blueprints: 30 | import_string(blueprint).init_app(app) 31 | -------------------------------------------------------------------------------- /forums/api/collect/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2017 jianglin 5 | # File Name: __init__.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2017-03-28 16:23:58 (CST) 9 | # Last Update: Wednesday 2019-05-08 15:09:17 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import Blueprint 14 | from .views import CollectListView, CollectView, AddToCollectView 15 | 16 | site = Blueprint('collect', __name__) 17 | 18 | 19 | def init_app(app): 20 | site.add_url_rule('/collect', view_func=CollectListView.as_view('list')) 21 | site.add_url_rule( 22 | '/collect/', view_func=CollectView.as_view('collect')) 23 | site.add_url_rule( 24 | '/topic//collect', 25 | view_func=AddToCollectView.as_view('add_to_collect')) 26 | app.register_blueprint(site) 27 | -------------------------------------------------------------------------------- /forums/api/collect/db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ******************************************************************************** 4 | # Copyright © 2019 jianglin 5 | # File Name: db.py 6 | # Author: jianglin 7 | # Email: mail@honmaple.com 8 | # Created: 2019-05-07 00:41:33 (CST) 9 | # Last Update: Wednesday 2019-05-08 13:21:02 (CST) 10 | # By: 11 | # Description: 12 | # ******************************************************************************** 13 | from datetime import datetime 14 | from flask_maple.models import ModelMixin 15 | from flask_login import current_user 16 | from forums.api.user.db import User 17 | from forums.extension import db 18 | 19 | topic_collect = db.Table( 20 | 'topic_collect', 21 | db.Column('topic_id', db.Integer, db.ForeignKey('topics.id')), 22 | db.Column('collect_id', db.Integer, db.ForeignKey('collects.id'))) 23 | 24 | collect_follower = db.Table( 25 | 'collect_follower', 26 | db.Column('collect_id', db.Integer, db.ForeignKey('collects.id')), 27 | db.Column('follower_id', db.Integer, db.ForeignKey('user.id'))) 28 | 29 | 30 | class Collect(db.Model, ModelMixin): 31 | __tablename__ = 'collects' 32 | id = db.Column(db.Integer, primary_key=True) 33 | name = db.Column(db.String(32), nullable=False) 34 | description = db.Column(db.String(256), nullable=True) 35 | is_hidden = db.Column(db.Boolean, default=False) 36 | created_at = db.Column( 37 | db.DateTime, default=datetime.utcnow(), nullable=False) 38 | updated_at = db.Column( 39 | db.DateTime, default=datetime.utcnow(), onupdate=datetime.utcnow()) 40 | author_id = db.Column(db.Integer, 41 | db.ForeignKey('user.id', ondelete="CASCADE")) 42 | author = db.relationship( 43 | User, 44 | backref=db.backref( 45 | 'collects', cascade='all,delete-orphan', lazy='dynamic'), 46 | lazy='joined') 47 | 48 | topics = db.relationship( 49 | 'Topic', 50 | secondary=topic_collect, 51 | backref=db.backref('collects', lazy='dynamic'), 52 | lazy='dynamic') 53 | 54 | followers = db.relationship( 55 | 'User', 56 | secondary=collect_follower, 57 | backref=db.backref('following_collects', lazy='dynamic'), 58 | lazy='dynamic') 59 | 60 | def is_followed(self, user=None): 61 | if user is None: 62 | user = current_user 63 | return db.session.query(collect_follower).filter( 64 | collect_follower.c.collect_id == self.id, 65 | collect_follower.c.follower_id == user.id).exists() 66 | 67 | def __str__(self): 68 | return self.name 69 | 70 | def __repr__(self): 71 | return "" % self.name 72 | -------------------------------------------------------------------------------- /forums/api/follow/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: __init__.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-12-22 21:49:01 (CST) 9 | # Last Update: Monday 2019-05-06 23:00:44 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import Blueprint 14 | from .views import (FollowingTagsView, FollowingUsersView, FollowingTopicsView, 15 | FollowingCollectsView) 16 | 17 | site = Blueprint('follow', __name__, url_prefix='/following') 18 | 19 | topic_view = FollowingTopicsView.as_view('topic') 20 | tag_view = FollowingTagsView.as_view('tag') 21 | user_view = FollowingUsersView.as_view('user') 22 | collect_view = FollowingCollectsView.as_view('collect') 23 | 24 | site.add_url_rule('', view_func=topic_view) 25 | site.add_url_rule('/topics', view_func=topic_view) 26 | site.add_url_rule('/tags', view_func=tag_view) 27 | site.add_url_rule('/collects', view_func=collect_view) 28 | site.add_url_rule('/users', view_func=user_view) 29 | 30 | 31 | def init_app(app): 32 | app.register_blueprint(site) 33 | -------------------------------------------------------------------------------- /forums/api/forums/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: __init__.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-12-17 20:44:07 (CST) 9 | # Last Update: Wednesday 2019-05-08 13:18:36 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import Blueprint 14 | 15 | site = Blueprint('forums', __name__) 16 | 17 | 18 | def init_app(app): 19 | from .views import (IndexView, AboutView, HelpView, ContactView, 20 | BoardListView, BoardView) 21 | forums_view = BoardListView.as_view('forums') 22 | 23 | site.add_url_rule('/', view_func=IndexView.as_view('index')) 24 | site.add_url_rule('/about', view_func=AboutView.as_view('about')) 25 | site.add_url_rule('/help', view_func=HelpView.as_view('help')) 26 | site.add_url_rule('/contact', view_func=ContactView.as_view('contact')) 27 | site.add_url_rule('/index', view_func=forums_view) 28 | site.add_url_rule('/forums', view_func=forums_view) 29 | site.add_url_rule('/forums/', view_func=BoardView.as_view('board')) 30 | 31 | app.register_blueprint(site) 32 | -------------------------------------------------------------------------------- /forums/api/forums/db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2017 jianglin 5 | # File Name: db.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2017-03-25 18:48:33 (CST) 9 | # Last Update: Wednesday 2019-05-08 14:39:58 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask_maple.models import ModelMixin 14 | from forums.extension import db 15 | from forums.count import Count 16 | 17 | 18 | class Board(db.Model, ModelMixin): 19 | __tablename__ = 'boards' 20 | id = db.Column(db.Integer, primary_key=True) 21 | name = db.Column(db.String(81), nullable=False) 22 | description = db.Column(db.String(128), nullable=False) 23 | parent_id = db.Column(db.Integer, 24 | db.ForeignKey('boards.id', ondelete="CASCADE")) 25 | parent = db.relationship( 26 | 'Board', 27 | remote_side=[id], 28 | backref=db.backref( 29 | 'children', 30 | remote_side=[parent_id], 31 | cascade='all,delete-orphan', 32 | lazy='dynamic'), 33 | lazy='joined', 34 | uselist=False) 35 | 36 | @property 37 | def parent_board(self): 38 | return self.parent 39 | 40 | @property 41 | def child_boards(self): 42 | return self.children 43 | 44 | @property 45 | def newest_topic(self): 46 | return self.topics.order_by('-id').first() 47 | 48 | @property 49 | def topic_count(self): 50 | # return self.topics.count() 51 | return Count.board_topic_count(self.id) 52 | 53 | @topic_count.setter 54 | def topic_count(self, value): 55 | return Count.board_topic_count(self.id, value) 56 | 57 | @property 58 | def post_count(self): 59 | return Count.board_post_count(self.id) 60 | 61 | @post_count.setter 62 | def post_count(self, value): 63 | return Count.board_post_count(self.id, value) 64 | 65 | def __str__(self): 66 | return self.name 67 | 68 | def __repr__(self): 69 | return '' % self.name 70 | -------------------------------------------------------------------------------- /forums/api/forums/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: views.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-12-17 20:45:08 (CST) 9 | # Last Update: Wednesday 2019-05-08 14:37:48 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import render_template, request 14 | 15 | from forums.api.topic.db import Topic 16 | from forums.common.views import BaseMethodView as MethodView 17 | from forums.common.utils import (gen_filter_dict, gen_order_by) 18 | from forums.api.utils import gen_topic_filter, gen_topic_orderby 19 | 20 | from .db import Board 21 | 22 | 23 | class IndexView(MethodView): 24 | def get(self): 25 | topics = Topic.query.filter_by( 26 | is_good=True, is_top=False).paginate(1, 10) 27 | top_topics = Topic.query.filter_by(is_top=True).limit(5) 28 | if not topics.items: 29 | topics = Topic.query.filter_by(is_top=False).paginate(1, 10) 30 | 31 | data = {'topics': topics, 'top_topics': top_topics} 32 | return render_template('forums/index.html', **data) 33 | 34 | 35 | class AboutView(MethodView): 36 | def get(self): 37 | return self.render_template('forums/about.html') 38 | 39 | 40 | class HelpView(MethodView): 41 | def get(self): 42 | return self.render_template('forums/help.html') 43 | 44 | 45 | class ContactView(MethodView): 46 | def get(self): 47 | return self.render_template('forums/contact.html') 48 | 49 | 50 | class BoardListView(MethodView): 51 | def get(self): 52 | boards = Board.query.filter_by(parent_id=None).order_by("-name").all() 53 | data = {'boards': boards} 54 | return render_template('board/board_list.html', **data) 55 | 56 | 57 | class BoardView(MethodView): 58 | def get(self, pk): 59 | board = Board.query.filter_by(id=pk).first_or_404() 60 | has_children = board.child_boards.exists() 61 | topics = self.topics(pk, has_children) 62 | data = {'board': board, 'topics': topics} 63 | return render_template('board/board.html', **data) 64 | 65 | def topics(self, pk, has_children): 66 | request_data = request.data 67 | page, number = self.pageinfo 68 | keys = ['title'] 69 | # order_by = gen_order_by(request_data, keys) 70 | # filter_dict = gen_filter_dict(request_data, keys) 71 | order_by = gen_topic_orderby(request_data, keys) 72 | filter_dict = gen_topic_filter(request_data, keys) 73 | if has_children: 74 | o = [] 75 | for i in order_by: 76 | if i.startswith('-'): 77 | o.append(getattr(Topic, i.split('-')[1]).desc()) 78 | else: 79 | o.append(getattr(Topic, i)) 80 | topics = Topic.query.filter_by(**filter_dict).outerjoin(Board).or_( 81 | Board.parent_id == pk, Board.id == pk).order_by(*o).paginate( 82 | page, number, True) 83 | return topics 84 | filter_dict.update(board_id=pk) 85 | topics = Topic.query.filter_by( 86 | **filter_dict).order_by(*order_by).paginate(page, number, True) 87 | return topics 88 | -------------------------------------------------------------------------------- /forums/api/message/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2017 jianglin 5 | # File Name: __init__.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2017-04-01 18:33:33 (CST) 9 | # Last Update: Monday 2019-05-06 23:00:30 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import Blueprint 14 | from .views import MessageListView 15 | 16 | site = Blueprint('message', __name__, url_prefix='/message') 17 | 18 | message_list = MessageListView.as_view('list') 19 | 20 | site.add_url_rule('', view_func=message_list) 21 | 22 | 23 | def init_app(app): 24 | app.register_blueprint(site) 25 | -------------------------------------------------------------------------------- /forums/api/message/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2017 jianglin 5 | # File Name: views.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2017-04-01 18:34:07 (CST) 9 | # Last Update: Wednesday 2019-05-08 13:16:02 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import render_template, request 14 | 15 | from forums.common.views import IsAuthMethodView as MethodView 16 | from forums.count import Count 17 | 18 | from .db import Message, MessageText 19 | 20 | 21 | class MessageListView(MethodView): 22 | def get(self): 23 | query_dict = request.data 24 | user = request.user 25 | status = query_dict.pop('status', '0') 26 | page, number = self.pageinfo 27 | messages = Message.query.filter_by( 28 | receiver_id=user.id, 29 | status=status).order_by('-created_at').paginate( 30 | page, number, True) 31 | data = {'title': 'Notice', 'messages': messages} 32 | Count.user_message_count(user.id, clear=True) 33 | return render_template('forums/message.html', **data) 34 | -------------------------------------------------------------------------------- /forums/api/search/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2017 jianglin 5 | # File Name: __init__.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2017-03-31 17:25:57 (CST) 9 | # Last Update: Monday 2019-05-06 23:00:20 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import Blueprint 14 | from .views import SearchView 15 | 16 | site = Blueprint('search', __name__) 17 | 18 | site.add_url_rule('/search', view_func=SearchView.as_view('search')) 19 | 20 | def init_app(app): 21 | app.register_blueprint(site) 22 | -------------------------------------------------------------------------------- /forums/api/search/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2017 jianglin 5 | # File Name: views.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2017-03-31 17:26:28 (CST) 9 | # Last Update: Thursday 2018-07-26 10:45:40 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import request, render_template 14 | from forums.common.views import BaseMethodView as MethodView 15 | from forums.api.topic.db import Topic 16 | 17 | 18 | class SearchView(MethodView): 19 | def get(self): 20 | query_dict = request.data 21 | page, number = self.pageinfo 22 | keyword = query_dict.pop('keyword', None) 23 | include = query_dict.pop('include', '0') 24 | if keyword and len(keyword) >= 2: 25 | fields = None 26 | if include == '0': 27 | fields = ['title', 'content'] 28 | elif include == '1': 29 | fields = ['title'] 30 | elif include == '2': 31 | fields = ['content'] 32 | results = Topic.query.msearch( 33 | keyword, fields=fields).paginate(page, number, True) 34 | data = {'title': 'Search', 'results': results, 'keyword': keyword} 35 | return render_template('search/result.html', **data) 36 | data = {'title': 'Search'} 37 | return render_template('search/search.html', **data) 38 | -------------------------------------------------------------------------------- /forums/api/setting/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: __init__.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-12-20 22:12:01 (CST) 9 | # Last Update: Monday 2019-05-06 23:00:11 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import Blueprint 14 | from .views import ProfileView, PasswordView, PrivacyView, BabelView 15 | 16 | site = Blueprint('setting', __name__, url_prefix='/setting') 17 | setting_view = ProfileView.as_view('setting') 18 | password_view = PasswordView.as_view('password') 19 | privacy_view = PrivacyView.as_view('privacy') 20 | babel_view = BabelView.as_view('babel') 21 | 22 | site.add_url_rule('', view_func=setting_view) 23 | site.add_url_rule('/profile', view_func=setting_view) 24 | site.add_url_rule('/password', view_func=password_view) 25 | site.add_url_rule('/privacy', view_func=privacy_view) 26 | site.add_url_rule('/babel', view_func=babel_view) 27 | 28 | 29 | def init_app(app): 30 | app.register_blueprint(site) 31 | -------------------------------------------------------------------------------- /forums/api/tag/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: __init__.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-12-15 20:46:19 (CST) 9 | # Last Update: Monday 2019-05-06 23:23:45 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import Blueprint 14 | 15 | site = Blueprint('tag', __name__, url_prefix='/tags') 16 | 17 | 18 | def init_app(app): 19 | from .views import TagsListView, TagsView, TagFeedView 20 | site.add_url_rule('', view_func=TagsListView.as_view('list')) 21 | site.add_url_rule('/', view_func=TagsView.as_view('tag')) 22 | site.add_url_rule('//feed', view_func=TagFeedView.as_view('feed')) 23 | 24 | app.register_blueprint(site) 25 | -------------------------------------------------------------------------------- /forums/api/tag/db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: models.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-12-15 20:46:13 (CST) 9 | # Last Update: Monday 2019-05-06 23:37:21 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask_login import current_user 14 | from flask_maple.models import ModelMixin 15 | from forums.extension import db 16 | 17 | tag_follower = db.Table( 18 | 'tag_follower', db.Column('tag_id', db.Integer, db.ForeignKey('tags.id')), 19 | db.Column('follower_id', db.Integer, db.ForeignKey('user.id'))) 20 | 21 | tag_topic = db.Table( 22 | 'tag_topic', db.Column('tag_id', db.Integer, db.ForeignKey('tags.id')), 23 | db.Column('topic_id', db.Integer, db.ForeignKey('topics.id'))) 24 | 25 | 26 | class Tags(db.Model, ModelMixin): 27 | __tablename__ = 'tags' 28 | id = db.Column(db.Integer, primary_key=True) 29 | name = db.Column(db.String(64), nullable=False) 30 | description = db.Column(db.String(128), nullable=False) 31 | parent_id = db.Column(db.Integer, 32 | db.ForeignKey('tags.id', ondelete="CASCADE")) 33 | parent = db.relationship( 34 | 'Tags', 35 | remote_side=[id], 36 | backref=db.backref( 37 | 'children', 38 | remote_side=[parent_id], 39 | cascade='all,delete-orphan', 40 | lazy='dynamic'), 41 | lazy='joined', 42 | uselist=False) 43 | topics = db.relationship( 44 | 'Topic', 45 | secondary=tag_topic, 46 | backref=db.backref('tags', lazy='dynamic'), 47 | lazy='dynamic') 48 | followers = db.relationship( 49 | 'User', 50 | secondary=tag_follower, 51 | backref=db.backref('following_tags', lazy='dynamic'), 52 | lazy='dynamic') 53 | 54 | def is_followed(self, user=None): 55 | if user is None: 56 | user = current_user 57 | return db.session.query(tag_follower).filter( 58 | tag_follower.c.tag_id == self.id, 59 | tag_follower.c.follower_id == user.id).exists() 60 | 61 | @property 62 | def parent_tag(self): 63 | return self.parent 64 | 65 | @property 66 | def child_tags(self): 67 | return self.children 68 | 69 | @property 70 | def related_tags(self): 71 | parent = self.parent 72 | if not parent: 73 | return [] 74 | relateds = parent.children.exclude_by(id=self.id).all() 75 | return relateds 76 | 77 | def __str__(self): 78 | return self.name 79 | 80 | def __repr__(self): 81 | return '' % self.name 82 | -------------------------------------------------------------------------------- /forums/api/tag/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: views.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-12-15 22:07:04 (CST) 9 | # Last Update: Wednesday 2019-05-08 16:27:50 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from urllib.parse import urljoin 14 | 15 | from flask import current_app, render_template, request, url_for 16 | from werkzeug.contrib.atom import AtomFeed 17 | 18 | from forums.api.topic.db import Topic 19 | from forums.api.utils import gen_topic_filter, gen_topic_orderby 20 | from forums.common.views import BaseMethodView as MethodView 21 | from forums.utils import filter_maybe, orderby_maybe 22 | from forums.default import SITE 23 | 24 | from .db import Tags 25 | 26 | 27 | class TagsListView(MethodView): 28 | per_page = 99 29 | 30 | def get(self): 31 | request_data = request.data 32 | page, number = self.pageinfo 33 | params = filter_maybe(request_data, ["name"]) 34 | orderby = orderby_maybe(request_data, ["name"]) 35 | tags = Tags.query.filter_by(**params).order_by(*orderby).paginate( 36 | page, number, True) 37 | data = {'tags': tags} 38 | return render_template('tag/tag_list.html', **data) 39 | 40 | 41 | class TagsView(MethodView): 42 | def get(self, name): 43 | request_data = request.data 44 | page, number = self.pageinfo 45 | tag = Tags.query.filter_by(name=name).first_or_404() 46 | 47 | keys = ['name'] 48 | params = gen_topic_filter(request_data, keys) 49 | params.update(tags__id=tag.id) 50 | orderby = gen_topic_orderby(request_data, keys) 51 | topics = Topic.query.filter_by(**params).order_by(*orderby).paginate( 52 | page, number, True) 53 | 54 | data = {'tag': tag, 'topics': topics} 55 | return render_template('tag/tag.html', **data) 56 | 57 | 58 | class TagFeedView(MethodView): 59 | def get(self, name): 60 | title = SITE['title'] 61 | subtitle = SITE['subtitle'] 62 | feed = AtomFeed( 63 | '%s·%s' % (name, title), 64 | feed_url=request.url, 65 | url=request.url_root, 66 | subtitle=subtitle) 67 | topics = Topic.query.filter_by(tags__name=name).limit(10) 68 | for topic in topics: 69 | feed.add( 70 | topic.title, 71 | topic.text, 72 | content_type='html', 73 | author=topic.author.username, 74 | url=urljoin(request.url_root, 75 | url_for('topic.topic', pk=topic.id)), 76 | updated=topic.updated_at, 77 | published=topic.created_at) 78 | return feed.get_response() 79 | -------------------------------------------------------------------------------- /forums/api/topic/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: __init__.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-12-13 23:22:48 (CST) 9 | # Last Update: Monday 2019-05-06 23:03:23 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import Blueprint 14 | 15 | site = Blueprint('topic', __name__) 16 | 17 | 18 | def init_app(app): 19 | from .views import (LikeView, ReplyListView, ReplyView, TopicAskView, 20 | TopicEditView, TopicListView, TopicView) 21 | topic_list = TopicListView.as_view('list') 22 | topic_good_list = TopicListView.as_view('good') 23 | topic_top_list = TopicListView.as_view('top') 24 | topic = TopicView.as_view('topic') 25 | ask_view = TopicAskView.as_view('ask') 26 | edit_view = TopicEditView.as_view('edit') 27 | 28 | reply_list = ReplyListView.as_view('reply_list') 29 | reply = ReplyView.as_view('reply') 30 | like_view = LikeView.as_view('reply_like') 31 | 32 | site.add_url_rule('/topic/ask', view_func=ask_view) 33 | site.add_url_rule('/topic', view_func=topic_list) 34 | site.add_url_rule('/topic/top', view_func=topic_top_list) 35 | site.add_url_rule('/topic/good', view_func=topic_good_list) 36 | site.add_url_rule('/topic/', view_func=topic) 37 | site.add_url_rule('/topic//edit', view_func=edit_view) 38 | site.add_url_rule('/topic//replies', view_func=reply_list) 39 | site.add_url_rule('/replies/', view_func=reply) 40 | site.add_url_rule('/replies//like', view_func=like_view) 41 | app.register_blueprint(site) 42 | -------------------------------------------------------------------------------- /forums/api/topic/permissions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2017 jianglin 5 | # File Name: permissions.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2017-03-29 15:53:37 (CST) 9 | # Last Update:星期四 2017-3-30 16:17:48 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import request 14 | from flask_login import login_required 15 | 16 | from forums.permission import (ReplyPermission, RestfulView, TopicPermission, 17 | is_confirmed) 18 | 19 | 20 | class Edit(RestfulView): 21 | def get(self, pk): 22 | permission = TopicPermission(pk) 23 | if not permission.can(): 24 | return self.callback() 25 | return True 26 | 27 | 28 | class TopicList(RestfulView): 29 | @is_confirmed 30 | def post(self): 31 | return True 32 | 33 | 34 | class Topic(RestfulView): 35 | @is_confirmed 36 | def put(self, pk): 37 | permission = TopicPermission(pk) 38 | if not permission.can(): 39 | return self.callback() 40 | return True 41 | 42 | @is_confirmed 43 | def delete(self, pk): 44 | permission = TopicPermission(pk) 45 | if not permission.can(): 46 | return self.callback() 47 | return True 48 | 49 | 50 | class ReplyList(RestfulView): 51 | @is_confirmed 52 | def post(self, pk): 53 | return True 54 | 55 | 56 | class Reply(RestfulView): 57 | @is_confirmed 58 | def put(self, pk): 59 | return True 60 | 61 | @is_confirmed 62 | def delete(self, pk): 63 | return True 64 | 65 | 66 | class Like(RestfulView): 67 | @is_confirmed 68 | def post(self, pk): 69 | return True 70 | 71 | @is_confirmed 72 | def delete(self, pk): 73 | return True 74 | 75 | 76 | topic_list_permission = TopicList() 77 | topic_permission = Topic() 78 | reply_list_permission = ReplyList() 79 | reply_permission = Reply() 80 | like_permission = Like() 81 | edit_permission = Edit() 82 | -------------------------------------------------------------------------------- /forums/api/upload/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: __init__.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-12-21 21:56:10 (CST) 9 | # Last Update: Monday 2019-05-06 22:59:34 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import Blueprint 14 | from .views import AvatarView, AvatarFileView 15 | 16 | site = Blueprint('upload', __name__) 17 | avatar_file_view = AvatarFileView.as_view('avatar_file') 18 | avatar_view = AvatarView.as_view('avatar') 19 | 20 | site.add_url_rule('/avatar', view_func=avatar_view) 21 | site.add_url_rule('/avatars/', view_func=avatar_file_view) 22 | 23 | 24 | def init_app(app): 25 | app.register_blueprint(site) 26 | -------------------------------------------------------------------------------- /forums/api/upload/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: views.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-12-21 21:56:41 (CST) 9 | # Last Update:星期三 2017-5-10 16:35:21 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import (url_for, redirect, send_from_directory, current_app, 14 | request) 15 | from flask.views import MethodView 16 | from flask_login import login_required, current_user 17 | from flask_maple.form import form_validate 18 | from forums.api.forms import AvatarForm 19 | from werkzeug import secure_filename 20 | from time import time 21 | from random import randint 22 | from PIL import Image 23 | import os 24 | 25 | 26 | class AvatarView(MethodView): 27 | decorators = [login_required] 28 | 29 | @form_validate( 30 | AvatarForm, error=lambda: redirect(url_for('setting.setting')), f='') 31 | def post(self): 32 | form = AvatarForm() 33 | user = request.user 34 | file = request.files[form.avatar.name] 35 | filename = user.username + '-' + str(int(time())) + str( 36 | randint(1000, 9999)) 37 | img = Image.open(file) 38 | size = 150, 150 39 | img.thumbnail(size, Image.ANTIALIAS) 40 | current_app.config.setdefault('AVATAR_FOLDER', os.path.join( 41 | current_app.static_folder, 'avatars')) 42 | avatar_path = current_app.config['AVATAR_FOLDER'] 43 | avatar = os.path.join(avatar_path, filename + '.png') 44 | if not os.path.exists(avatar_path): 45 | os.makedirs(avatar_path) 46 | img.save(avatar) 47 | img.close() 48 | info = user.info 49 | if info.avatar: 50 | ef = os.path.join(avatar_path, info.avatar) 51 | if os.path.exists(ef): 52 | os.remove(ef) 53 | # file.save(os.path.join(app.static_folder, filename + '.png')) 54 | info.avatar = filename + '.png' 55 | info.save() 56 | return redirect(url_for('setting.setting')) 57 | 58 | 59 | class AvatarFileView(MethodView): 60 | def get(self, filename): 61 | current_app.config.setdefault('AVATAR_FOLDER', os.path.join( 62 | current_app.static_folder, 'avatars')) 63 | avatar_path = current_app.config['AVATAR_FOLDER'] 64 | if not os.path.exists(os.path.join(avatar_path, filename)): 65 | filename = filename.split('-')[0] 66 | return redirect(url_for('avatar', text=filename)) 67 | return send_from_directory(avatar_path, filename) 68 | -------------------------------------------------------------------------------- /forums/api/user/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: __init__.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-12-15 20:58:31 (CST) 9 | # Last Update: Monday 2019-05-06 23:16:08 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import Blueprint 14 | from .views import (UserCollectListView, UserFollowingListView, 15 | UserFollowerListView, UserListView, UserReplyListView, 16 | UserView) 17 | 18 | site = Blueprint('user', __name__, url_prefix='/u') 19 | 20 | 21 | def init_app(app): 22 | 23 | user_list = UserListView.as_view('list') 24 | user = UserView.as_view('user') 25 | topics = UserView.as_view('topic') 26 | replies = UserReplyListView.as_view('reply') 27 | collects = UserCollectListView.as_view('collect') 28 | followers = UserFollowerListView.as_view('follower') 29 | followings = UserFollowingListView.as_view('following') 30 | 31 | site.add_url_rule('', view_func=user_list) 32 | site.add_url_rule('/', view_func=user) 33 | site.add_url_rule('//topics', view_func=topics) 34 | site.add_url_rule('//replies', view_func=replies) 35 | site.add_url_rule('//collects', view_func=collects) 36 | site.add_url_rule('//followers', view_func=followers) 37 | site.add_url_rule('//followings', view_func=followings) 38 | 39 | app.register_blueprint(site) 40 | -------------------------------------------------------------------------------- /forums/api/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2017 jianglin 5 | # File Name: utils.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2017-03-29 13:33:03 (CST) 9 | # Last Update:星期二 2017-5-2 12:21:37 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from datetime import datetime, timedelta 14 | 15 | one_day = datetime.now() + timedelta(days=-1) 16 | one_week = datetime.now() + timedelta(days=-7) 17 | one_month = datetime.now() + timedelta(days=-30) 18 | one_year = datetime.now() + timedelta(days=-365) 19 | 20 | 21 | def gen_topic_filter(query_dict=dict(), keys=[], equal_key=[], user=None): 22 | filter_dict = {} 23 | keys = list(set(keys) & set(query_dict.keys())) 24 | within = query_dict.pop('within', None) 25 | if within == '1': 26 | filter_dict.update(created_at__gte=one_day) 27 | elif within == '2': 28 | filter_dict.update(created_at__gte=one_week) 29 | elif within == '3': 30 | filter_dict.update(created_at__gte=one_month) 31 | elif within == '4': 32 | filter_dict.update(created_at__gte=one_year) 33 | for k in keys: 34 | if k in equal_key: 35 | filter_dict.update(**{k: query_dict[k]}) 36 | else: 37 | new_k = '%s__contains' % k 38 | filter_dict.update(**{new_k: query_dict[k]}) 39 | if user is not None and user.is_authenticated: 40 | filter_dict.update(user__id=user.id) 41 | return filter_dict 42 | 43 | 44 | def gen_topic_orderby(query_dict=dict(), keys=[], date_key=True): 45 | keys.append('id') 46 | order_by = ['-id'] 47 | # order_by = ['-is_top', '-id'] 48 | orderby = query_dict.pop('orderby', None) 49 | desc = query_dict.pop('desc', None) 50 | if orderby == '0': 51 | order_by = ['created_at'] 52 | elif orderby == '1': 53 | order_by = ['author_id'] 54 | if desc == '0': 55 | order_by = ['-%s' % i for i in order_by] 56 | order_by = ['-is_top'] + order_by 57 | return tuple(order_by) 58 | -------------------------------------------------------------------------------- /forums/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2017 jianglin 5 | # File Name: app.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2017-03-28 16:11:07 (CST) 9 | # Last Update:星期二 2017-9-19 12:49:24 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask_principal import RoleNeed, UserNeed, identity_loaded 14 | from flask_login import current_user 15 | from .permission import TopicNeed, ReplyNeed, CollectNeed 16 | 17 | 18 | def init_app(app): 19 | @identity_loaded.connect_via(app) 20 | def on_identity_loaded(sender, identity): 21 | '''基础权限''' 22 | identity.user = current_user 23 | 24 | if hasattr(current_user, 'id'): 25 | identity.provides.add(UserNeed(current_user.id)) 26 | 27 | if hasattr(current_user, 'is_superuser'): 28 | if current_user.is_superuser: 29 | identity.provides.add(RoleNeed('super')) 30 | 31 | if hasattr(current_user, 'is_confirmed'): 32 | if current_user.is_confirmed: 33 | identity.provides.add(RoleNeed('confirmed')) 34 | 35 | if hasattr(current_user, 'is_authenticated'): 36 | if current_user.is_authenticated: 37 | identity.provides.add(RoleNeed('auth')) 38 | else: 39 | identity.provides.add(RoleNeed('guest')) 40 | 41 | if hasattr(current_user, 'topics'): 42 | for topic in current_user.topics: 43 | identity.provides.add(TopicNeed(topic.id)) 44 | 45 | if hasattr(current_user, 'replies'): 46 | for reply in current_user.replies: 47 | identity.provides.add(ReplyNeed(reply.id)) 48 | 49 | if hasattr(current_user, 'collects'): 50 | for collect in current_user.collects: 51 | identity.provides.add(CollectNeed(collect.id)) 52 | -------------------------------------------------------------------------------- /forums/common/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: __init__.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-11-08 21:23:01 (CST) 9 | # Last Update:星期四 2016-12-29 21:53:54 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | -------------------------------------------------------------------------------- /forums/common/helper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding=UTF-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: helpers.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-05-20 13:56:43 (CST) 9 | # Last Update:星期六 2017-4-1 23:43:49 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import current_app 14 | from sqlalchemy import create_engine 15 | from sqlalchemy.orm import sessionmaker 16 | from logging.handlers import SMTPHandler 17 | from threading import Thread 18 | 19 | 20 | def db_session(): 21 | url = current_app.config['SQLALCHEMY_DATABASE_URI'] 22 | engine = create_engine(url) 23 | session = sessionmaker(bind=engine) 24 | return session 25 | 26 | 27 | class ThreadedSMTPHandler(SMTPHandler): 28 | def emit(self, record): 29 | thread = Thread(target=SMTPHandler.emit, args=(self, record)) 30 | thread.start() 31 | -------------------------------------------------------------------------------- /forums/common/middleware.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: middleware.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-11-12 13:29:17 (CST) 9 | # Last Update: Sunday 2018-03-04 22:38:33 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import g, request, abort 14 | from flask_login import current_user 15 | from forums.api.forms import SortForm 16 | from .records import mark_online, load_online_users 17 | 18 | 19 | def set_form(form): 20 | within = request.args.get('within', 0, type=int) 21 | orderby = request.args.get('orderby', 0, type=int) 22 | desc = request.args.get('desc', 0, type=int) 23 | form.within.data = within 24 | form.orderby.data = orderby 25 | form.desc.data = desc 26 | return form 27 | 28 | 29 | class GlobalMiddleware(object): 30 | def preprocess_request(self): 31 | g.user = current_user 32 | g.sort_form = SortForm() 33 | g.sort_form = set_form(g.sort_form) 34 | request.user = current_user._get_current_object() 35 | if request.method == 'GET': 36 | request.data = request.args.to_dict() 37 | else: 38 | request.data = request.json 39 | if request.data is None: 40 | request.data = request.form.to_dict() 41 | 42 | 43 | class OnlineMiddleware(object): 44 | def preprocess_request(self): 45 | if g.user.is_authenticated: 46 | mark_online(g.user.username) 47 | else: 48 | mark_online(request.remote_addr) 49 | g.get_online = get_online() 50 | # g.get_online = (1, 2, 3, 4, 5) 51 | 52 | 53 | def get_online(): 54 | return (load_online_users(1), load_online_users(2), load_online_users(3), 55 | load_online_users(4), load_online_users(5)) 56 | -------------------------------------------------------------------------------- /forums/common/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2017 jianglin 5 | # File Name: db.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2017-01-25 21:33:09 (CST) 9 | # Last Update:星期三 2017-1-25 21:33:42 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from forums.extension import db 14 | from sqlalchemy.ext.declarative import declared_attr 15 | from datetime import datetime 16 | from flask_maple.models import ModelMixin 17 | 18 | 19 | class CommonMixin(ModelMixin): 20 | @declared_attr 21 | def id(cls): 22 | return db.Column(db.Integer, primary_key=True) 23 | 24 | 25 | class CommonTimeMixin(CommonMixin): 26 | @declared_attr 27 | def created_at(cls): 28 | return db.Column(db.DateTime, default=datetime.utcnow()) 29 | 30 | @declared_attr 31 | def updated_at(cls): 32 | return db.Column( 33 | db.DateTime, default=datetime.utcnow(), onupdate=datetime.utcnow()) 34 | 35 | 36 | class CommonUserMixin(CommonTimeMixin): 37 | @declared_attr 38 | def user_id(cls): 39 | return db.Column( 40 | db.Integer, db.ForeignKey( 41 | 'user.id', ondelete="CASCADE")) 42 | 43 | @declared_attr 44 | def user(cls): 45 | name = cls.__name__.lower() 46 | if not name.endswith('s'): 47 | name = name + 's' 48 | if hasattr(cls, 'user_related_name'): 49 | name = cls.user_related_name 50 | return db.relationship( 51 | 'User', 52 | backref=db.backref( 53 | name, cascade='all,delete', lazy='dynamic'), 54 | uselist=False, 55 | lazy='joined') 56 | -------------------------------------------------------------------------------- /forums/common/response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: response.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-10-25 21:07:00 (CST) 9 | # Last Update: Wednesday 2018-07-25 18:54:54 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import jsonify 14 | from flask_babel import gettext as _ 15 | 16 | 17 | class HTTPResponse(object): 18 | NORMAL_STATUS = '200' 19 | AUTH_USER_OR_PASSWORD_ERROR = '301' 20 | AUTH_CAPTCHA_ERROR = '302' 21 | AUTH_USERNAME_UNIQUE = '303' 22 | AUTH_EMAIL_UNIQUE = '303' 23 | AUTH_EMAIL_NOT_REGISTER = '304' 24 | AUTH_USER_IS_CONFIRMED = '305', 25 | FORM_VALIDATE_ERROR = '305' 26 | AUTH_TOKEN_VERIFY_FAIL = '306' 27 | 28 | FORBIDDEN = '403' 29 | 30 | OTHER_ERROR = '500' 31 | 32 | STATUS_DESCRIPTION = { 33 | NORMAL_STATUS: 'normal', 34 | AUTH_USER_OR_PASSWORD_ERROR: _('Username or Password Error'), 35 | AUTH_CAPTCHA_ERROR: _('Captcha Error'), 36 | AUTH_EMAIL_UNIQUE: _('The email has been registered!'), 37 | AUTH_USERNAME_UNIQUE: _('The username has been registered!'), 38 | AUTH_EMAIL_NOT_REGISTER: _('The email is error'), 39 | AUTH_USER_IS_CONFIRMED: 40 | _('Your account has been confirmed,don\'t need again!'), 41 | AUTH_TOKEN_VERIFY_FAIL: 42 | _('Token is out of time,please get token again!'), 43 | FORM_VALIDATE_ERROR: _('Form validate error'), 44 | FORBIDDEN: _('You have no permission!'), 45 | OTHER_ERROR: _('Other error') 46 | } 47 | 48 | def __init__(self, 49 | status='200', 50 | message='', 51 | data=None, 52 | description='', 53 | pageinfo=None): 54 | self.status = status 55 | self.message = message or self.STATUS_DESCRIPTION.get(status) 56 | self.data = data 57 | self.description = description 58 | self.pageinfo = pageinfo 59 | 60 | def to_dict(self): 61 | response = { 62 | 'status': self.status, 63 | 'message': self.message, 64 | 'data': self.data, 65 | 'description': self.description, 66 | } 67 | if self.pageinfo is not None: 68 | response.update(pageinfo=self.pageinfo.as_dict()) 69 | return response 70 | 71 | def to_response(self): 72 | response = self.to_dict() 73 | return jsonify(response) 74 | -------------------------------------------------------------------------------- /forums/common/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2017 jianglin 5 | # File Name: views.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2017-03-13 13:29:37 (CST) 9 | # Last Update: Wednesday 2019-05-08 14:24:25 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import (request, flash, redirect, url_for, render_template, 14 | current_app) 15 | from flask_login import login_required, current_user 16 | from flask_maple.views import MethodView as _MethodView 17 | from forums.permission import confirm_permission 18 | from forums.extension import cache 19 | 20 | 21 | def cache_key(): 22 | if current_user.is_authenticated: 23 | return 'view:{}:{}'.format(current_user.id, request.url) 24 | return 'view:{}'.format(request.url) 25 | 26 | 27 | def is_confirmed(func): 28 | def _is_confirmed(*args, **kwargs): 29 | if confirm_permission.can(): 30 | ret = func(*args, **kwargs) 31 | return ret 32 | flash('请验证你的帐号', 'warning') 33 | return redirect(url_for('user.user', username=current_user.username)) 34 | 35 | return _is_confirmed 36 | 37 | 38 | class MethodView(_MethodView): 39 | @property 40 | def pageinfo(self): 41 | page = request.args.get('page', 1, type=int) 42 | if hasattr(self, 'per_page'): 43 | per_page = getattr(self, 'per_page') 44 | else: 45 | per_page = current_app.config.setdefault('PER_PAGE', 20) 46 | 47 | number = request.args.get('number', per_page, type=int) 48 | if number < -1: 49 | number = per_page 50 | if number > 100 or number == -1: 51 | number = 100 52 | return page, number 53 | 54 | def dispatch_request(self, *args, **kwargs): 55 | method = request.method 56 | meth = getattr(self, method.lower(), None) 57 | 58 | if meth is None and method == 'HEAD': 59 | meth = getattr(self, 'get', None) 60 | 61 | assert meth is not None, 'Unimplemented method %r' % request.method 62 | return meth(*args, **kwargs) 63 | 64 | def render_template(self, template, **kwargs): 65 | return render_template(template, **kwargs) 66 | 67 | 68 | class BaseMethodView(_MethodView): 69 | @cache.cached(timeout=180, key_prefix=cache_key) 70 | def dispatch_request(self, *args, **kwargs): 71 | return super(BaseMethodView, self).dispatch_request(*args, **kwargs) 72 | 73 | def render_template(self, template, **kwargs): 74 | return render_template(template, **kwargs) 75 | 76 | def render(self, template, **kwargs): 77 | return render_template(template, **kwargs) 78 | 79 | 80 | class IsAuthMethodView(BaseMethodView): 81 | decorators = [login_required] 82 | 83 | 84 | class IsConfirmedMethodView(BaseMethodView): 85 | decorators = [is_confirmed, login_required] 86 | -------------------------------------------------------------------------------- /forums/default.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ******************************************************************************** 4 | # Copyright © 2018 jianglin 5 | # File Name: default.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2018-07-26 10:00:54 (CST) 9 | # Last Update: Wednesday 2019-05-08 16:24:22 (CST) 10 | # By: 11 | # Description: 12 | # ******************************************************************************** 13 | SITE = {'title': '紅楓林', 'subtitle': '爱生活,更爱自由', 'avatar': ''} 14 | SUBDOMAIN = {"forums": True, "docs": True} 15 | LANGUAGES = {'en': 'English', 'zh': 'Chinese'} 16 | INDEX = [{ 17 | "name": "Forums", 18 | "url": "forums.forums", 19 | "icon": "fa-comments", 20 | "color": "#F86334" 21 | }, { 22 | "name": "Wiki", 23 | "url": "docs.list", 24 | "icon": "fa-book", 25 | "color": "#eeccaa" 26 | }, { 27 | "name": "Blog", 28 | "url": "docs.list", 29 | "icon": "fa-tasks", 30 | "color": "#337ab7" 31 | }, { 32 | "name": "Good", 33 | "url": "topic.good", 34 | "icon": "fa-globe", 35 | "color": "#017e66" 36 | }] 37 | HEADER = [{ 38 | "name": "Forums", 39 | "url": "forums.forums" 40 | }, { 41 | "name": "Wiki", 42 | "url": "docs.list" 43 | }, { 44 | "name": "Blog", 45 | "url": "forums.forums" 46 | }, { 47 | "name": "TagList", 48 | "url": "tag.list" 49 | }, { 50 | "name": "Good", 51 | "url": "topic.good" 52 | }] 53 | -------------------------------------------------------------------------------- /forums/docs/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: __init__.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-11-09 21:06:11 (CST) 9 | # Last Update: Thursday 2018-07-26 10:03:18 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from forums import default 14 | from .views import site as docs_site 15 | 16 | 17 | def init_app(app): 18 | app.config.setdefault("SUBDOMAIN", default.SUBDOMAIN) 19 | if app.config['SUBDOMAIN']['docs']: 20 | app.register_blueprint(docs_site, subdomain='docs') 21 | else: 22 | app.register_blueprint(docs_site, url_prefix='/docs') 23 | -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: 3c0c46891304c0047216ffaf1e2a5a0b 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/_sources/index.txt: -------------------------------------------------------------------------------- 1 | .. Flask-Avatar documentation master file, created by 2 | sphinx-quickstart on Sat Jul 2 20:55:02 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Flask-Avatar's documentation! 7 | ======================================== 8 | 9 | It's easy to generate avatar for flask. 10 | 11 | 12 | 1.1 Installation 13 | ~~~~~~~~~~~~~~~~ 14 | 15 | To install Flask-Avatar: 16 | 17 | .. code-block:: shell 18 | 19 | pip install flask-avatar 20 | 21 | Or alternatively, you can download the repository and install manually by doing: 22 | 23 | .. code-block:: sehll 24 | 25 | git clone git@github.com:honmaple/flask-avatar.git 26 | cd flask-avatar 27 | python setup.py install 28 | 29 | 1.2 Usage 30 | ~~~~~~~~~ 31 | 32 | .. code-block:: python 33 | 34 | from flask_avatar import Avatar 35 | [...] 36 | Avatar(app) 37 | 38 | Templates: 39 | 40 | .. code-block:: html 41 | 42 | {{ url_for('avatar',text = user.username )}} 43 | 44 | 1.3 Config 45 | ~~~~~~~~~~ 46 | 47 | AVATAR_URL = "The avatar url,default 'avatar'" 48 | 49 | .. toctree:: 50 | :maxdepth: 2 51 | 52 | 53 | 54 | Indices and tables 55 | ================== 56 | 57 | * :ref:`genindex` 58 | * :ref:`modindex` 59 | * :ref:`search` 60 | 61 | -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/_sources/introduction.txt: -------------------------------------------------------------------------------- 1 | 1 flask-avatar 2 | -------------- 3 | 4 | 1.1 Installation 5 | ~~~~~~~~~~~~~~~~ 6 | 7 | To install Flask-Avatar: 8 | 9 | .. code-block:: shell 10 | 11 | pip install flask-avatar 12 | 13 | Or alternatively, you can download the repository and install manually by doing: 14 | 15 | .. code-block:: sehll 16 | 17 | git clone git@github.com:honmaple/flask-avatar.git 18 | cd flask-avatar 19 | python setup.py install 20 | 21 | 1.2 Usage 22 | ~~~~~~~~~ 23 | 24 | .. code-block:: python 25 | 26 | from flask_avatar import Avatar 27 | [...] 28 | Avatar(app) 29 | 30 | Templates: 31 | 32 | .. code-block:: html 33 | 34 | {{ url_for('avatar',text = user.username )}} 35 | 36 | 1.3 Config 37 | ~~~~~~~~~~ 38 | 39 | AVATAR_URL = "The avatar url,default 'avatar'" 40 | -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/_static/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-avatar/_static/ajax-loader.gif -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/_static/comment-bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-avatar/_static/comment-bright.png -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/_static/comment-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-avatar/_static/comment-close.png -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/_static/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-avatar/_static/comment.png -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* This file intentionally left blank. */ 2 | -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/_static/down-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-avatar/_static/down-pressed.png -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/_static/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-avatar/_static/down.png -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-avatar/_static/file.png -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-avatar/_static/minus.png -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-avatar/_static/plus.png -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/_static/up-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-avatar/_static/up-pressed.png -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/_static/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-avatar/_static/up.png -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/genindex.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Index — Flask-Avatar 0.1.0 documentation 11 | 12 | 13 | 14 | 15 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 |
40 |
41 | 42 | 43 |

Index

44 | 45 |
46 | 47 |
48 | 49 | 50 |
51 |
52 |
53 | 76 |
77 |
78 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-avatar/objects.inv -------------------------------------------------------------------------------- /forums/docs/static/flask-avatar/searchindex.js: -------------------------------------------------------------------------------- 1 | Search.setIndex({envversion:48,filenames:["index","introduction"],objects:{},objnames:{},objtypes:{},terms:{"default":[0,1],"import":[0,1],altern:[0,1],app:[0,1],author:[],avatar_url:[0,1],can:[0,1],clone:[0,1],com:[0,1],download:[0,1],easi:0,flask_avatar:[0,1],from:[0,1],gener:0,git:[0,1],github:[0,1],honmapl:[0,1],index:0,jianglin:[],manual:[0,1],modul:0,page:0,pip:[0,1],python:[0,1],repositori:[0,1],search:0,setup:[0,1],templat:[0,1],text:[0,1],url:[0,1],url_for:[0,1],user:[0,1],usernam:[0,1],you:[0,1]},titles:["Welcome to Flask-Avatar’s documentation!","1 flask-avatar"],titleterms:{avatar:[0,1],config:[0,1],content:[],document:0,flask:[0,1],indic:0,instal:[0,1],jianglin:[],tabl:0,usag:[0,1],welcom:0}}) -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: c54f70026c9ac7d2a79d74c9e05cc3a9 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_sources/auth.txt: -------------------------------------------------------------------------------- 1 | 1 Auth 2 | ------ 3 | 4 | 1.1 Custom model 5 | ~~~~~~~~~~~~~~~~ 6 | 7 | You custom model if you need more when register or confirm email 8 | 9 | 1.1.1 register_models 10 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 11 | 12 | .. code-block:: python 13 | 14 | from flask_maple import Auth 15 | class MyAuth(Auth): 16 | def register_models(self, form): 17 | user = self.User() 18 | user.username = form.username.data 19 | user.password = user.set_password(form.password.data) 20 | user.email = form.email.data 21 | self.db.session.add(user) 22 | self.db.session.commit() 23 | return user 24 | 25 | 1.1.2 confirm_models 26 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 27 | 28 | .. code-block:: python 29 | 30 | class MyAuth(Auth): 31 | def confirm_models(self, user): 32 | user.is_confirmed = True 33 | user.confirmed_time = datetime.now() 34 | self.db.session.commit() 35 | 36 | 1.1.3 email_models 37 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 38 | 39 | .. code-block:: python 40 | 41 | class MyAuth(Auth): 42 | def email_models(self): 43 | current_user.send_email_time = datetime.now() 44 | self.db.session.commit() 45 | 46 | 1.2 Custom form 47 | ~~~~~~~~~~~~~~~ 48 | 49 | You can add custom form when register Auth 50 | 51 | .. code-block:: python 52 | 53 | Auth(app, db=db, mail=mail, user_model=User, 54 | login_form=loginform, 55 | register_form=registerform, 56 | forget_form=forgetpasswordform) 57 | 58 | **template** 59 | 60 | .. code-block:: python 61 | 62 | templates/auth/login.html 63 | templates/auth/register.html 64 | templates/auth/forget.html 65 | -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_sources/bootstrap.txt: -------------------------------------------------------------------------------- 1 | 1 Bootstrap 2 | ----------- 3 | 4 | 1.1 Add css or js file 5 | ~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | You can add you .js file or .css file with flask-assets 8 | 9 | .. code-block:: python 10 | 11 | maple = Bootstrap(css=('style/xxx.css',),js=('style/xxx.js',)) 12 | maple.init_app(app) 13 | 14 | Or you can add js or css in **templates** 15 | 16 | .. code-block:: html 17 | 18 | {% block style -%} 19 | {{super()}} 20 | You css file 21 | {% endblock -%} 22 | {% block script -%} 23 | {{super()}} 24 | You js file 25 | {% endblock -%} 26 | 27 | 1.2 use auth extension 28 | ~~~~~~~~~~~~~~~~~~~~~~ 29 | 30 | If you want use maple auth extension,you need set 31 | 32 | .. code-block:: python 33 | 34 | Bootstrap(app,use_auth=True) 35 | 36 | But before it,you need register csrf,beacuse ajax need csrf. 37 | 38 | .. code-block:: python 39 | 40 | from flask_wtf.csrf import CsrfProtect 41 | csrf = CsrfProtect() 42 | csrf.init_app(app) 43 | 44 | 1.3 Other block 45 | ~~~~~~~~~~~~~~~ 46 | 47 | .. code-block:: html 48 | 49 | {% block title -%} 50 | {% endblock -%} 51 | 52 | 1.4 Custom footer 53 | ~~~~~~~~~~~~~~~~~ 54 | 55 | The footer file in **templates/maple/footer.html**,you can custom it. 56 | 57 | .. code-block:: html 58 | 59 | 62 | -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_sources/error.txt: -------------------------------------------------------------------------------- 1 | 1 Error 2 | ------- 3 | 4 | 1.1 Custom templates 5 | ~~~~~~~~~~~~~~~~~~~~ 6 | 7 | You can custom you template for error 8 | 9 | .. code-block:: html 10 | 11 | templates/templet/error_404.html 12 | templates/templet/error_403.html 13 | templates/templet/error_500.html 14 | -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_sources/index.txt: -------------------------------------------------------------------------------- 1 | .. Flask-Maple documentation master file, created by 2 | sphinx-quickstart on Wed May 18 22:11:28 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Flask-Maple's documentation! 7 | ======================================= 8 | 9 | It's easy to use bootstrap,captcha,login and some error 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | introduction 15 | bootstrap 16 | error 17 | auth 18 | 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | 28 | -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_sources/insroduction.txt: -------------------------------------------------------------------------------- 1 | :tocdepth: 2 2 | 3 | Introduction To Flask-Admin 4 | ########################### 5 | 6 | Getting Started 7 | =============== 8 | 9 | **** 10 | 11 | Initialization 12 | -------------- 13 | 14 | The first step is to initialize an empty admin interface for your Flask app:: 15 | 16 | from flask import Flask 17 | from flask_admin import Admin 18 | 19 | app = Flask(__name__) 20 | 21 | admin = Admin(app, name='microblog', template_mode='bootstrap3') 22 | # Add administrative views here 23 | 24 | app.run() 25 | 26 | Here, both the *name* and *template_mode* parameters are optional. Alternatively, 27 | you could use the :meth:`~flask_admin.base.Admin.init_app` method. 28 | 29 | If you start this application and navigate to `http://localhost:5000/admin/ `_, 30 | you should see an empty page with a navigation bar on top. 31 | 32 | Adding Model Views 33 | ------------------ 34 | 35 | Model views allow you to add a dedicated set of admin pages for managing any model in your database. Do this by creating 36 | instances of the *ModelView* class, which you can import from one of Flask-Admin's built-in ORM backends. An example 37 | is the SQLAlchemy backend, which you can use as follows:: 38 | 39 | from flask_admin.contrib.sqla import ModelView 40 | 41 | # Flask and Flask-SQLAlchemy initialization here 42 | 43 | admin = Admin(app, name=' 44 | -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_static/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-maple/_static/ajax-loader.gif -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_static/comment-bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-maple/_static/comment-bright.png -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_static/comment-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-maple/_static/comment-close.png -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_static/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-maple/_static/comment.png -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* This file intentionally left blank. */ 2 | -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_static/down-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-maple/_static/down-pressed.png -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_static/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-maple/_static/down.png -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-maple/_static/file.png -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-maple/_static/minus.png -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-maple/_static/plus.png -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_static/up-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-maple/_static/up-pressed.png -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/_static/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-maple/_static/up.png -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/genindex.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Index — Flask-Maple 0.8 documentation 11 | 12 | 13 | 14 | 15 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 |
40 |
41 | 42 | 43 |

Index

44 | 45 |
46 | 47 |
48 | 49 | 50 |
51 |
52 |
53 | 76 |
77 |
78 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/forums/docs/static/flask-maple/objects.inv -------------------------------------------------------------------------------- /forums/docs/static/flask-maple/searchindex.js: -------------------------------------------------------------------------------- 1 | Search.setIndex({envversion:48,filenames:["advanced","auth","bootstrap","error","index","introduction"],objects:{},objnames:{},objtypes:{},terms:{"__main__":[],"__name__":0,"__repr__":[0,5],"class":[0,1,2,5],"default":5,"import":[0,1,2,5],"return":[0,1,5],"super":[0,2],"true":[0,1,2,5],add:0,administr:[],ajax:2,allow:[],altern:5,ani:[],api:0,app:[0,1,2,5],applic:[],aria:5,asd:[],asset:[0,2],auth:[],author:[],author_nam:5,babel:0,backend:[],bar:[],base:5,beacus:2,befor:[2,5],block:0,boostrap:5,bootstrap3:[],bootstrap:[],both:[],btn:5,built:[],button:5,can:[0,1,2,3,5],captcha:[],captcha_url:5,center:2,check_password:[0,5],check_password_hash:[0,5],clone:5,column:[0,5],com:5,commit:[0,1],config:[0,5],confirm:[],confirmed_tim:[0,1],content:[],contrib:[],could:[],creat:[],csrf:2,csrfprotect:2,css:0,current_us:[0,1],custom:0,data:[0,1],databas:[],datetim:[0,1],dedic:[],def:[0,1,5],div:2,don:5,download:5,easi:[4,5],email:0,empti:[],endblock:[0,2,5],error:[],error_403:[0,3],error_404:[0,3],error_500:[0,3],errorhandl:5,exampl:[],extend:5,extens:[],fals:0,file:0,first:[],flask_admin:[],flask_mapl:[0,1,5],flask_wtf:2,follow:[],footer:[],forget:1,forget_form:[0,1],forgetpasswordform:[0,1],form:0,from:[0,1,2,5],from_object:0,generate_password_hash:[0,5],git:5,github:5,glyphicon:5,guess:[],hard:[],here:[],hidden:5,honmapl:5,html:[0,1,2,3,5],http:5,index:4,init_app:[0,2,5],instanc:[],integ:[0,5],interfac:[],is_confirm:[0,1],localhost:[],login:[],login_form:[0,1],loginform:[0,1],mail:[0,1,5],main:5,manag:[],manual:5,mapleb:0,mapleboostrap:[],maplebootstrap:0,maplec:0,maplecaptcha:0,method:[],microblog:[],modelview:[],modul:4,more:1,myauth:[0,1],name:[0,5],navig:[],need:[1,2],none:0,now:[0,1],option:[],orm:[],other:0,page:4,paramet:[],password:[0,1,5],pillow:5,pip:5,pleas:5,primari:5,primary_kei:[0,5],princip:[0,5],print:[],provid:5,pw_hash:[0,5],python:5,regist:2,register_form:[0,1],registerform:[0,1],render_templ:[],repositori:5,role:0,rout:[],run:[],sampl:5,script:[0,2],search:[4,5],secret_kei:[],see:[],self:[0,1,5],send_email_tim:[0,1],session:[0,1],set:[0,2,5],set_password:[0,1,5],setup:5,should:[],show:5,simpl:5,some:[4,5],span:5,sqla:[],sqlalchemi:0,staticmethod:[0,5],step:[],string:[0,5],style:[0,2],sub:[],submit:5,templat:[0,2],template_mod:[],templet:[0,3],text:2,thi:5,titl:[0,2],top:[],uniqu:[0,5],url:5,url_map:[],usag:5,use_auth:2,use_princip:[0,5],user:[0,1,5],user_model:[0,1,5],usermixin:[0,5],usernam:[0,1,5],veri:5,visit:5,want:2,when:1,which:[],writer:0,xxx:[0,2],you:[0,1,2,3,5],your:[]},titles:["1 Advanced","1 Auth","1 Bootstrap","1 Error","Welcome to Flask-Maple’s documentation!","1 Introduction To Flask-Maple"],titleterms:{add:2,admin:[],advanc:0,auth:[0,1,2],block:2,bootstrap:[0,2,5],captcha:5,confirm:[],confirm_model:[0,1],confirmmodel:[],content:[],css:2,custom:[1,2,3],document:4,email:[],email_model:[0,1],emailmodel:[],error:[0,3,5],extens:2,file:2,flask:[4,5],footer:2,form:1,get:[],indic:4,initi:[],instal:5,introduct:5,jianglin:[],login:5,mapl:[4,5],model:1,other:2,quickstart:[],regist:[],register_model:[0,1],registermodel:[],start:[],tabl:4,templat:3,view:[],welcom:4}}) 2 | -------------------------------------------------------------------------------- /forums/docs/templates/docs/doc_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block content %} 3 | 7 | {% macro item(name,url,icon,color="#337ab7") %} 8 | 23 | {%- endmacro %} 24 | {% set icons = ['fa-book','fa-commenting-o','fa-cubes','fa-comments-o','fa-globe'] %} 25 |
26 | {% for doc in docs %} 27 | {{ item(doc,url_for('docs.doc',path=doc + '/index.html'),icons | random,"#F86334")}} 28 | {% endfor %} 29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /forums/docs/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: views.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2016-11-09 21:06:32 (CST) 9 | # Last Update:星期五 2017-4-21 19:17:46 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from flask import (Blueprint, render_template, send_from_directory) 14 | from flask.views import MethodView 15 | import os 16 | 17 | site = Blueprint( 18 | 'docs', __name__, template_folder='templates', static_folder='static') 19 | 20 | 21 | class DocListView(MethodView): 22 | def get(self): 23 | docs = [i for i in os.listdir(site.static_folder)] 24 | return render_template('docs/doc_list.html', docs=docs) 25 | 26 | 27 | class DocView(MethodView): 28 | def get(self, path): 29 | return send_from_directory(site.static_folder, path) 30 | 31 | 32 | doclist_view = DocListView.as_view('list') 33 | doc_view = DocView.as_view('doc') 34 | site.add_url_rule('/', view_func=doclist_view) 35 | site.add_url_rule('/', view_func=doc_view) 36 | -------------------------------------------------------------------------------- /forums/extension/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ******************************************************************************** 4 | # Copyright © 2018 jianglin 5 | # File Name: __init__.py 6 | # Author: jianglin 7 | # Email: mail@honmaple.com 8 | # Created: 2018-02-11 14:52:12 (CST) 9 | # Last Update: Tuesday 2019-05-07 01:05:29 (CST) 10 | # By: 11 | # Description: 12 | # ******************************************************************************** 13 | from flask import request 14 | from flask_wtf.csrf import CSRFProtect 15 | from flask_avatar import Avatar 16 | from flask_maple.models import db 17 | from flask_maple.redis import Redis 18 | from flask_maple.mail import Mail 19 | from flask_principal import Principal 20 | from flask_msearch import Search 21 | from flask_caching import Cache 22 | from . import babel, login, maple 23 | 24 | db = db 25 | csrf = CSRFProtect() 26 | redis_data = Redis() 27 | cache = Cache() 28 | mail = Mail() 29 | principal = Principal() 30 | search = Search(db=db) 31 | avatar = Avatar( 32 | cache=cache.cached( 33 | timeout=259200, key_prefix=lambda: "avatar:{}".format(request.url))) 34 | 35 | 36 | def init_app(app): 37 | db.init_app(app) 38 | cache.init_app(app) 39 | avatar.init_app(app) 40 | csrf.init_app(app) 41 | principal.init_app(app) 42 | redis_data.init_app(app) 43 | mail.init_app(app) 44 | search.init_app(app) 45 | 46 | babel.init_app(app) 47 | login.init_app(app) 48 | maple.init_app(app) 49 | -------------------------------------------------------------------------------- /forums/extension/babel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ******************************************************************************** 4 | # Copyright © 2018 jianglin 5 | # File Name: babel.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2018-02-11 14:52:25 (CST) 9 | # Last Update: Wednesday 2019-05-08 16:25:29 (CST) 10 | # By: 11 | # Description: 12 | # ******************************************************************************** 13 | from flask import request, g 14 | from flask_babel import Babel 15 | from forums.default import LANGUAGES 16 | 17 | babel = Babel() 18 | 19 | 20 | @babel.localeselector 21 | def locale(): 22 | user = getattr(g, 'user', None) 23 | if user is not None: 24 | if request.path.startswith('/admin'): 25 | return 'zh_Hans_CN' 26 | if g.user.is_authenticated: 27 | return user.setting.locale or 'zh' 28 | return request.accept_languages.best_match(LANGUAGES.keys()) 29 | 30 | 31 | @babel.timezoneselector 32 | def timezone(): 33 | user = getattr(g, 'user', None) 34 | if user is not None: 35 | if g.user.is_authenticated: 36 | return user.setting.timezone or 'UTC' 37 | return 'UTC' 38 | 39 | 40 | def init_app(app): 41 | babel.init_app(app) 42 | -------------------------------------------------------------------------------- /forums/extension/login.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ******************************************************************************** 4 | # Copyright © 2018 jianglin 5 | # File Name: login.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2018-02-11 14:54:38 (CST) 9 | # Last Update: Wednesday 2018-07-25 18:54:54 (CST) 10 | # By: 11 | # Description: 12 | # ******************************************************************************** 13 | from flask_login import LoginManager 14 | from flask_babel import lazy_gettext as _ 15 | 16 | login_manager = LoginManager() 17 | 18 | 19 | @login_manager.user_loader 20 | def user_loader(id): 21 | from forums.api.user.db import User 22 | user = User.query.get(int(id)) 23 | return user 24 | 25 | 26 | def init_app(app): 27 | login_manager.login_view = "auth.login" 28 | # remember me only work with `basic` rathar than `strong` 29 | login_manager.session_protection = "basic" 30 | login_manager.login_message = _("Please login to access this page.") 31 | # login_manager.anonymous_user = Anonymous 32 | login_manager.init_app(app) 33 | -------------------------------------------------------------------------------- /forums/extension/maple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ******************************************************************************** 4 | # Copyright © 2018 jianglin 5 | # File Name: maple.py 6 | # Author: jianglin 7 | # Email: mail@honmaple.com 8 | # Created: 2018-02-11 14:56:08 (CST) 9 | # Last Update: Monday 2022-12-12 16:38:21 (CST) 10 | # By: 11 | # Description: 12 | # ******************************************************************************** 13 | from flask_maple.bootstrap import Bootstrap 14 | from flask_maple.captcha import Captcha 15 | from flask_maple.error import Error 16 | from flask_maple.app import App 17 | from flask_maple.json import CustomJSONEncoder 18 | from flask_maple.middleware import Middleware 19 | from flask_maple.log import Logging 20 | from PIL import ImageFont 21 | 22 | bootstrap = Bootstrap(css=('styles/monokai.css', 'styles/mine.css'), 23 | js=('styles/upload.js', 'styles/forums.js', 24 | 'styles/following.js', 'styles/topic.js'), 25 | use_auth=True) 26 | 27 | 28 | def init_app(app): 29 | bootstrap.init_app(app) 30 | Captcha(app, font=ImageFont.load_default()) 31 | Error(app) 32 | App(app, json=CustomJSONEncoder) 33 | Middleware(app) 34 | Logging(app) 35 | -------------------------------------------------------------------------------- /forums/jinja.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2016 jianglin 5 | # File Name: jinja.py 6 | # Author: jianglin 7 | # Email: mail@honmaple.com 8 | # Created: 2016-11-07 21:00:32 (CST) 9 | # Last Update: Monday 2022-12-12 16:11:32 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from datetime import datetime 14 | from forums import default 15 | 16 | from bleach import clean 17 | from flask import Markup, g 18 | from flask_babel import format_datetime 19 | from markdown import markdown as m 20 | 21 | 22 | def safe_clean(text): 23 | tags = ['b', 'i', 'font', 'br', 'blockquote', 'div', 'h2', 'a', 'p'] 24 | attrs = {'*': ['style', 'id', 'class'], 'font': ['color'], 'a': ['href']} 25 | styles = ['color'] 26 | return Markup(clean(text, tags=tags, attributes=attrs, styles=styles)) 27 | 28 | 29 | def markdown(text, clean=True): 30 | html = m(text, extensions=['markdown.extensions.fenced_code']) 31 | if clean: 32 | return Markup(safe_clean(html)) 33 | return Markup(html) 34 | 35 | 36 | def timesince(dt, default="just now"): 37 | now = datetime.utcnow() 38 | diff = now - dt 39 | if diff.days > 10: 40 | return format_datetime(dt, 'Y-M-d H:m') 41 | elif diff.days <= 10 and diff.days > 0: 42 | periods = ((diff.days, "day", "days"), ) 43 | elif diff.days <= 0 and diff.seconds > 3600: 44 | periods = ((diff.seconds / 3600, "hour", "hours"), ) 45 | elif diff.seconds <= 3600 and diff.seconds > 90: 46 | periods = ((diff.seconds / 60, "minute", "minutes"), ) 47 | else: 48 | return default 49 | 50 | for period, singular, plural in periods: 51 | 52 | if period: 53 | return "%d %s ago" % (period, singular if period == 1 else plural) 54 | 55 | return default 56 | 57 | 58 | def show_time(): 59 | if g.user.is_authenticated: 60 | return 'LOCALE:' + format_datetime(datetime.utcnow()) 61 | return 'UTC:' + format_datetime(datetime.utcnow()) 62 | 63 | 64 | def hot_tags(): 65 | from forums.api.tag.db import Tags 66 | tags = Tags.query.limit(9).all() 67 | return tags 68 | 69 | 70 | def recent_tags(): 71 | from forums.api.tag.db import Tags 72 | tags = Tags.query.limit(12).all() 73 | return tags 74 | 75 | 76 | def forums_count(): 77 | from forums.extension import redis_data 78 | key = 'count:forums' 79 | return redis_data.hgetall(key) 80 | 81 | 82 | def init_app(app): 83 | 84 | app.jinja_env.globals['DEFAULT'] = default 85 | app.jinja_env.globals['hot_tags'] = hot_tags 86 | app.jinja_env.globals['recent_tags'] = recent_tags 87 | app.jinja_env.globals['show_time'] = show_time 88 | app.jinja_env.globals['forums_count'] = forums_count 89 | app.jinja_env.filters['timesince'] = timesince 90 | app.jinja_env.filters['safe_clean'] = safe_clean 91 | -------------------------------------------------------------------------------- /forums/subdomain.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2017 jianglin 5 | # File Name: subdomain.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2017-11-10 10:52:47 (CST) 9 | # Last Update: Thursday 2018-07-26 10:02:02 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from forums import default 14 | 15 | 16 | def init_app(app): 17 | app.config.setdefault("SUBDOMAIN", default.SUBDOMAIN) 18 | if app.config['SUBDOMAIN']['forums']: 19 | app.url_map._rules.clear() 20 | app.url_map._rules_by_endpoint.clear() 21 | app.url_map.default_subdomain = 'forums' 22 | app.add_url_rule( 23 | app.static_url_path + '/', 24 | endpoint='static', 25 | view_func=app.send_static_file, 26 | subdomain='forums') 27 | -------------------------------------------------------------------------------- /gunicorn.conf: -------------------------------------------------------------------------------- 1 | debug = True 2 | loglevel = 'debug' 3 | workers = 2 4 | bind = "127.0.0.1:8000" 5 | worker_class = 'gevent' 6 | threads = 4 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bleach==2.1.2 2 | cffi==1.11.4 3 | cssmin==0.2.0 4 | jsmin==2.2.0 5 | Flask==0.12.2 6 | Flask-Admin==1.4.2 7 | Flask-Assets==0.12 8 | Flask-Avatar==0.1.3 9 | Flask-Caching==1.3.2 10 | Flask-Babel==0.11.2 11 | Flask-Login==0.4.1 12 | Flask-Mail==0.9.1 13 | Flask-Maple==0.5.3 14 | Flask-Migrate==1.8.0 15 | flask-msearch==0.1.5 16 | Flask-Principal==0.4.0 17 | Flask-SQLAlchemy==2.2 18 | Flask-WTF==0.14.2 19 | speaklater==1.3 20 | Pillow==5.4.1 21 | Pygments==2.1 22 | pytz==2018.3 23 | redis==2.10.6 24 | psycopg2-binary==2.7.5 25 | Whoosh==2.7.4 26 | gunicorn==19.9.0 27 | jinja2==3.0.3 28 | itsdangerous==2.0.1 29 | Werkzeug==0.16.1 30 | wtforms==2.3.1 31 | SQLAlchemy==1.3.5 32 | Markdown==3.3.7 33 | email-validator==1.3.0 34 | -------------------------------------------------------------------------------- /screenshooter/ask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/screenshooter/ask.png -------------------------------------------------------------------------------- /screenshooter/board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/screenshooter/board.png -------------------------------------------------------------------------------- /screenshooter/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/screenshooter/index.png -------------------------------------------------------------------------------- /script/upgrade_count.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ************************************************************************** 4 | # Copyright © 2017 jianglin 5 | # File Name: upgrade_coount.py 6 | # Author: jianglin 7 | # Email: xiyang0807@gmail.com 8 | # Created: 2017-04-02 13:00:02 (CST) 9 | # Last Update:星期日 2017-4-2 15:52:41 (CST) 10 | # By: 11 | # Description: 12 | # ************************************************************************** 13 | from forums.api.topic.db import Topic, Reply 14 | from forums.api.forums.db import Board 15 | from forums.api.user.db import User 16 | from forums.extension import redis_data 17 | from runserver import app 18 | 19 | 20 | def forums(): 21 | key = 'count:forums' 22 | redis_data.hset(key, 'user', User.query.count()) 23 | redis_data.hset(key, 'topic', Topic.query.count()) 24 | redis_data.hset(key, 'post', Reply.query.count() + Topic.query.count()) 25 | 26 | 27 | def topic(): 28 | topics = Topic.query.all() 29 | for t in topics: 30 | print('topic', t.title) 31 | key = 'count:topic:{}'.format(t.id) 32 | reply_count = t.replies.count() 33 | read_key = 'topic:{}'.format(t.id) 34 | read_count = redis_data.hget(read_key, 'read') 35 | if reply_count: 36 | redis_data.hset(key, 'replies', reply_count) 37 | if read_count: 38 | redis_data.hset(key, 'read', read_count) 39 | # 删除旧key 40 | redis_data.delete(read_key) 41 | 42 | 43 | def reply(): 44 | replies = Reply.query.all() 45 | for t in replies: 46 | print('reply', t.id) 47 | key = 'count:reply:{}'.format(t.id) 48 | liker_count = t.likers.count() 49 | if liker_count: 50 | redis_data.hset(key, 'liker', liker_count) 51 | 52 | 53 | def user(): 54 | users = User.query.all() 55 | for t in users: 56 | print('user', t.username) 57 | key = 'count:user:{}'.format(t.id) 58 | topic_count = t.topics.count() 59 | reply_count = t.replies.count() 60 | if topic_count: 61 | redis_data.hset(key, 'topic', topic_count) 62 | if reply_count: 63 | redis_data.hset(key, 'replies', topic_count) 64 | # 删除旧key 65 | old_user_key = 'user:{}'.format(t.id) 66 | redis_data.delete(old_user_key) 67 | 68 | 69 | def board(): 70 | boards = Board.query.all() 71 | for b in boards: 72 | print('board', b.name) 73 | key = 'count:board:{}'.format(b.id) 74 | topic_count = Topic.query.filter_by(board_id=b.id).count() 75 | reply_count = Reply.query.filter_by(topic__board_id=b.id).count() 76 | post_count = topic_count + reply_count 77 | if topic_count: 78 | redis_data.hset(key, 'topic', topic_count) 79 | if post_count: 80 | redis_data.hset(key, 'post', topic_count) 81 | 82 | 83 | def main(): 84 | with app.app_context(): 85 | forums() 86 | topic() 87 | board() 88 | user() 89 | reply() 90 | 91 | 92 | if __name__ == '__main__': 93 | main() 94 | -------------------------------------------------------------------------------- /static/avatars/honmaple-14670891852944.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/static/avatars/honmaple-14670891852944.png -------------------------------------------------------------------------------- /static/avatars/honmaple-14685857008552.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/static/avatars/honmaple-14685857008552.png -------------------------------------------------------------------------------- /static/avatars/qwert-14823290629699.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/static/avatars/qwert-14823290629699.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/static/favicon.ico -------------------------------------------------------------------------------- /static/images/Moo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/static/images/Moo.png -------------------------------------------------------------------------------- /static/images/bg1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/static/images/bg1.jpg -------------------------------------------------------------------------------- /static/images/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/static/images/header.png -------------------------------------------------------------------------------- /static/images/snow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/static/images/snow.jpg -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /login 3 | Disallow: /logout 4 | Disallow: /register 5 | Disallow: /index 6 | Disallow: /t 7 | -------------------------------------------------------------------------------- /static/socketio/chat.css: -------------------------------------------------------------------------------- 1 | .chatroom-msg { 2 | background:#bbb; 3 | border-radius:5px; 4 | font-size:8px; 5 | padding:3px 5px; 6 | color:#fff; 7 | } 8 | .chatroom-left-infor { 9 | margin:10px 10px 10px 30px; 10 | width:80%; 11 | } 12 | .chatroom-right-infor { 13 | margin:10px 30px 10px 10px; 14 | } 15 | .chatroom-infor { 16 | padding:8px; 17 | border-radius:4px; 18 | display:inline-block; 19 | text-align:left; 20 | } 21 | .chatroom { 22 | font-size: 12px; 23 | position: fixed; 24 | right: -3px; 25 | bottom: -3px; 26 | width: 150px; 27 | height: 36px; 28 | z-index: 998; 29 | overflow: hidden; 30 | background: #FFF; 31 | box-shadow: -3px -2px 8px -1px rgba(0,0,0,0.2); 32 | border-radius: 4px 0px 0px 4px; 33 | transition: all 0.3s; 34 | } 35 | .chatroom-heading { 36 | margin:7px; 37 | background-color:#a5eeee; 38 | } 39 | .chatroom-feedback { 40 | display:block; 41 | width:100%; 42 | text-align:right; 43 | } 44 | .chatroom-panel { 45 | bottom:0px; 46 | } 47 | .chatroom-send { 48 | margin-bottom:2px; 49 | position: fixed; 50 | bottom:0px; 51 | } 52 | .chatroom-open { 53 | color:#f40; 54 | font-size:12px; 55 | cursor: pointer; 56 | } 57 | .chatroom .panel-body { 58 | padding:0; 59 | } 60 | .chatroom-close { 61 | color:#fff; 62 | font-size:12px; 63 | cursor: pointer; 64 | } 65 | .chatroom-open:after { 66 | content:'[展开聊天]'; 67 | } 68 | .chatroom-close:after { 69 | content:'[关闭聊天]'; 70 | } 71 | .form-inline .chatroom-input { 72 | display: inline-block; 73 | width: auto; 74 | vertical-align: middle; 75 | } 76 | .chatroom-input { 77 | resize: none; 78 | height:62px; 79 | width:1200px; 80 | bottom:0; 81 | outline: none; 82 | font-size: 12px; 83 | overflow-y: auto; 84 | border:none; 85 | border-right:1px solid #f2f2f5; 86 | } -------------------------------------------------------------------------------- /static/socketio/chat.js: -------------------------------------------------------------------------------- 1 | var socket; 2 | var namespace; 3 | $(document).ready(function(){ 4 | namespace = '/chat'; 5 | socket = io.connect('http://' + document.domain + ':' + location.port + namespace); 6 | socket.on('connect', function() { 7 | socket.emit('joined', {room:'tags'}); 8 | }); 9 | socket.on('status', function(data) { 10 | var exdata = $('#chat').html(); 11 | var addata = exdata + '
' + data.msg + '
'; 12 | $('#chat').html(addata); 13 | $('#chat').scrollTop($('#chat')[0].scrollHeight); 14 | }); 15 | socket.on('message', function(data) { 16 | var exdata = $('#chat').html(); 17 | var addata = exdata + data.html; 18 | $('#chat').html(addata); 19 | $('#chat').scrollTop($('#chat')[0].scrollHeight); 20 | }); 21 | $('#text').keypress(function(e) { 22 | if (e.ctrlKey && e.which == 13 || e.which == 10) { 23 | text = $('#text').val(); 24 | if (text === ''){ 25 | alert('输入不能为空!'); 26 | return false; 27 | }else{ 28 | $('#text').val(''); 29 | socket.emit('text', {msg: text}); 30 | } 31 | } 32 | }); 33 | $('.send-msg').click(function() { 34 | text = $('#text').val(); 35 | if (text === ''){ 36 | alert('输入不能为空!'); 37 | return false; 38 | }else{ 39 | $('#text').val(''); 40 | socket.emit('text', {msg: text}); 41 | } 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /static/styles/forums.js: -------------------------------------------------------------------------------- 1 | function getQueryParams(k){ 2 | var p={}; 3 | location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(s,k,v){p[k]=v;}); 4 | return k?p[k]:p; 5 | } 6 | function SortFuntion(){ 7 | var within = $('select#within').val(); 8 | var orderby = $('select#orderby').val(); 9 | var desc = $('select#desc').val(); 10 | var params = getQueryParams(); 11 | params.within = within; 12 | params.orderby = orderby; 13 | params.desc = desc; 14 | window.location.href = window.location.pathname + '?' + $.param(params); 15 | } 16 | $(document).ready(function(){ 17 | $('select#within').change(function() { 18 | SortFuntion(); 19 | }); 20 | $('select#orderby').change(function() { 21 | SortFuntion(); 22 | }); 23 | $('select#desc').change(function() { 24 | SortFuntion(); 25 | }); 26 | $('span#email-confirm').click(function(){ 27 | $.ajax ({ 28 | type : "POST", 29 | url : "/confirm", 30 | data:JSON.stringify({ 31 | }), 32 | contentType: 'application/json;charset=UTF-8', 33 | success: function(response) { 34 | if (response.status === '200') 35 | { 36 | alert(response.message); 37 | } else 38 | { 39 | alert(response.message); 40 | }} 41 | }); 42 | }); 43 | }); 44 | function dispatch() { 45 | var q = document.getElementById("search"); 46 | if (q.value !== "") { 47 | var url = 'https://www.google.com/search?q=site:forums.honmaple.org%20' + q.value; 48 | if (navigator.userAgent.indexOf('iPad') > -1 || navigator.userAgent.indexOf('iPod') > -1 || navigator.userAgent.indexOf('iPhone') > -1) { 49 | location.href = url; 50 | } else { 51 | window.open(url, "_blank"); 52 | } 53 | return false; 54 | } else { 55 | return false; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /static/styles/mine.css: -------------------------------------------------------------------------------- 1 | body { 2 | background:#f5f5f5; 3 | } 4 | .online { 5 | color: #fff; 6 | font-size: 10px; 7 | line-height: 10px; 8 | font-weight: 500; 9 | padding: 2px 5px 2px 5px; 10 | -moz-border-radius: 10px; 11 | -webkit-border-radius: 10px; 12 | border-radius: 10px; 13 | display: inline-block; 14 | background: #52bf1c; 15 | background: -moz-linear-gradient(top, #52bf1c 0%, #438906 100%); 16 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#52bf1c), color-stop(100%,#438906)); 17 | background: -webkit-linear-gradient(top, #52bf1c 0%,#438906 100%); 18 | background: -o-linear-gradient(top, #52bf1c 0%,#438906 100%); 19 | background: -ms-linear-gradient(top, #52bf1c 0%,#438906 100%); 20 | background: linear-gradient(top, #52bf1c 0%,#438906 100%); 21 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#52bf1c', endColorstr='#438906',GradientType=0 ); 22 | } 23 | a.tag { 24 | background:rgba(1,126,102,.08); 25 | font-size: 10px; 26 | line-height: 10px; 27 | display: inline-block; 28 | padding: 4px 4px 4px 4px; 29 | -moz-border-radius: 2px; 30 | -webkit-border-radius: 2px; 31 | border-radius: 2px; 32 | text-decoration: none; 33 | } 34 | a.item_node { 35 | font-size: 12px; 36 | line-height: 12px; 37 | padding: 4px 10px 4px 10px; 38 | margin: 0px 5px 5px 0px; 39 | border-radius: 16px; 40 | display: inline-block; 41 | border: 1px solid #e5e5e5; 42 | text-decoration: none; 43 | } 44 | .panel-heading h3{ 45 | color:#555; 46 | font-size:14px; 47 | margin:0 48 | } 49 | .topic-content h1{ 50 | font-size:30px; 51 | font-weight:bold; 52 | } 53 | .topic-content h2{ 54 | font-size:26px; 55 | font-weight:bold; 56 | } 57 | .topic-content h3{ 58 | font-size:22px; 59 | font-weight:bold; 60 | } 61 | .topic-content h4{ 62 | font-size:18px; 63 | font-weight:bold; 64 | } 65 | .topic-content img { 66 | max-width: 100%; 67 | height: auto; 68 | display: block; 69 | } 70 | .vote { 71 | font-size: 10px; 72 | line-height: 1; 73 | padding: 2px 8px; 74 | border: 1px solid #C4C4C4; 75 | border-radius: 3px; 76 | color: #778; 77 | display: inline-block; 78 | vertical-align: baseline; 79 | text-align: center; 80 | background-color: #fff; 81 | text-decoration: none; 82 | } 83 | .rss { 84 | display:block; 85 | margin-bottom:20px; 86 | } 87 | .follow-panel { 88 | background-color:#fff; 89 | border-bottom:None; 90 | } 91 | select { 92 | background-color: #fff; 93 | border-color: #c6c6c6; 94 | color: #141414; 95 | } 96 | div.alert ul li { 97 | list-style-type:none 98 | } 99 | .like-active { 100 | color:#a40004; 101 | } 102 | .like-no-active { 103 | color:#ccc; 104 | } 105 | .reply-order a{ 106 | padding: 1px 2px; 107 | color: #778087; 108 | text-decoration: none; 109 | font-size:13px; 110 | } 111 | .reply-count { 112 | font-size:12px; 113 | color:#999; 114 | } 115 | .reply-content a { 116 | color:#778087; 117 | } 118 | -------------------------------------------------------------------------------- /static/styles/upload.js: -------------------------------------------------------------------------------- 1 | function loadFile(event) { 2 | var _file=document.getElementById("avatar"); 3 | var i=_file.value.lastIndexOf('.'); 4 | var len=_file.value.length; 5 | var extEndName=_file.value.substring(i+1,len); 6 | var extName="JPG,PNG"; 7 | if(extName.indexOf(extEndName.toUpperCase())==-1){ 8 | alert("您只能上传"+extName+"格式的文件"); 9 | $('#avatar').val(''); 10 | }else{ 11 | var reader = new FileReader(); 12 | reader.onload = function(){ 13 | var icon = '' + '\n'; 14 | var img = ''; 15 | $("#show-avatar").html(icon + img); 16 | }; 17 | reader.readAsDataURL(event.target.files[0]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /templates/auth/forget.html: -------------------------------------------------------------------------------- 1 | {% set title = _('Forget Password - ') %} 2 | {% extends "base/base.html" %} 3 | {% block content %} 4 | {% import 'maple/auth.html' as auth %} 5 | {{ breadcrumb(active=_('Forget Password'))}} 6 |
7 | 10 |
11 | {{ auth.forget()}} 12 |
13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {% set title = _('Login - ') %} 2 | {% extends "base/base.html" %} 3 | {%- block content -%} 4 | {% import 'maple/auth.html' as auth %} 5 | {{ breadcrumb(active=_('Login'))}} 6 |
7 |
8 | {{ _('Login')}} 9 |
10 |
11 | {{ auth.login()}} 12 |
13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /templates/auth/register.html: -------------------------------------------------------------------------------- 1 | {% set title = _('Register - ') %} 2 | {% extends "base/base.html" %} 3 | {% block content %} 4 | {% import 'maple/auth.html' as auth %} 5 | {{ breadcrumb(active=_('Register'))}} 6 |
7 | 10 |
11 | {{ auth.register()}} 12 |
13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /templates/base/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'maple/base.html' %} 2 | {% from 'base/paginate.html' import paginate%} 3 | {% from 'base/paginate.html' import footer as p_footer %} 4 | {% import 'base/link.html' as link %} 5 | {% import 'base/panel.html' as panel_base %} 6 | {% from 'base/head.html' import breadcrumb %} 7 | 8 | {% block title -%}{{ DEFAULT.SITE['title'] }} - {{ _(DEFAULT.SITE['subtitle']) }}{%- endblock title %} 9 | 10 | {% block style -%} 11 | 12 | 13 | 14 | {{ super() }} 15 | {%- endblock style %} 16 | 17 | {% block script -%} 18 | {{ super() }} 19 | 20 | 21 | {%- endblock script %} 22 | 23 | {% block main %} 24 | {% include "base/header.html" %} 25 |
26 | {% with messages = get_flashed_messages(with_categories=true) %} 27 | {% if messages %} 28 | {% for category,message in messages %} 29 | {% if category == 'message' -%} 30 | {% set category = 'info' %} 31 | {%- endif %} 32 |
33 | 36 |
    37 |
  • {{ message }}
  • 38 |
39 |
40 | {% endfor %} 41 | {% endif %} 42 | {% endwith %} 43 | {% block content %}{% endblock %} 44 |
45 | {% endblock %} 46 | 47 | {% macro dropdown() -%} 48 | {% if g.user.is_authenticated %} 49 |
50 | 53 | 59 |
60 | 61 | {{ _('NoticeList') }} 62 | {%- set n = current_user.message_count -%} 63 | {%- if n and n != '0' -%} 64 | 65 | {{ n }} 66 | 67 | {%- endif -%} 68 | 69 | {% else %} 70 | {{ _('Register') }} 71 | {{ _('Login') }} 72 | {% endif %} 73 | {%- endmacro %} 74 | -------------------------------------------------------------------------------- /templates/base/form.html: -------------------------------------------------------------------------------- 1 | {% macro forms(form) %} 2 |
3 | {{ form.hidden_tag() }} 4 | {% for field in form if field.widget.input_type != 'hidden' %} 5 |
6 | 7 |
8 | {{ field(class="form-control")}} 9 |
10 |
11 | {% endfor %} 12 |
13 |
14 | 15 |
16 |
17 |
18 | {%- endmacro %} 19 | -------------------------------------------------------------------------------- /templates/base/head.html: -------------------------------------------------------------------------------- 1 | {% macro breadcrumb(hrefs=None,active=None) -%} 2 | 11 | {%- endmacro %} 12 | 13 | {% macro breadcrumb_list(hrefs) -%} 14 | {% if hrefs %} 15 | {% for k,v in hrefs.items() %} 16 |
  • {{ k }}
  • 17 | {% endfor %} 18 | {% endif %} 19 | {%- endmacro %} 20 | -------------------------------------------------------------------------------- /templates/base/link.html: -------------------------------------------------------------------------------- 1 | {% macro userlist() -%} 2 | {{ _('UserList') }} 3 | {%- endmacro %} 4 | 5 | {% macro user(user) -%} 6 | {{ user.username }} 7 | {%- endmacro %} 8 | 9 | {% macro taglist() -%} 10 | {{ _('TagList') }} 11 | {%- endmacro %} 12 | 13 | {% macro tag(tag) -%} 14 | {{ tag.name }} 15 | {%- endmacro %} 16 | 17 | {% macro topiclist() -%} 18 | {{ _('TopicList') }} 19 | {%- endmacro %} 20 | 21 | {% macro topic(topic) -%} 22 | {{ topic.title }} 23 | {%- endmacro %} 24 | 25 | 26 | {% macro boardlist() -%} 27 | {{ _('BoardList') }} 28 | {%- endmacro %} 29 | 30 | {% macro board(board) -%} 31 | {{ board.name }} 32 | {%- endmacro %} 33 | 34 | {% macro avatar(text,width=64) -%} 35 | 36 | {%- endmacro %} 37 | 38 | {% macro user_avatar(user,width=64) -%} 39 | {% set info = user.info %} 40 | {% if not info.avatar -%} 41 | 42 | 43 | 44 | {% else %} 45 | 46 | 47 | 48 | {%- endif %} 49 | {%- endmacro %} 50 | 51 | {% macro following_tag() -%} 52 | {{ _('Following Tags') }} 53 | {%- endmacro %} 54 | {% macro following_topic() -%} 55 | {{ _('Following Topics')}} 56 | {%- endmacro %} 57 | {% macro following_user() -%} 58 | {{ _('Following Users')}} 59 | {%- endmacro %} 60 | {% macro following_collect() -%} 61 | {{ _('Following Collects')}} 62 | {%- endmacro %} 63 | 64 | 65 | {% macro collectlist() -%} 66 | {{ _('CollectList')}} 67 | {%- endmacro %} 68 | 69 | {% macro collect(c) -%} 70 | {{ c.name }} 71 | {%- endmacro %} 72 | -------------------------------------------------------------------------------- /templates/base/paginate.html: -------------------------------------------------------------------------------- 1 | {% macro paginate(pagination, endpoint,kw=None) %} 2 |
      3 | {% if pagination.items and pagination.pages > 1 -%} 4 | {%- if pagination.has_prev -%} 5 | {%- if not kw -%} 6 |
    • 7 | {%- else -%} 8 |
    • 9 | {% endif -%} 10 | {%- else %} 11 |
    • 12 | {% endif -%} 13 | {%- for page in pagination.iter_pages(left_edge=1, left_current=2, right_current=3, right_edge=1) %} 14 | {%- if page -%} 15 | {%- if page == pagination.page -%} 16 |
    • {{ page}}
    • 17 | {%- else -%} 18 | {%- if not kw %} 19 |
    • {{ page }}
    • 20 | {%- else -%} 21 |
    • {{ page }}
    • 22 | {%- endif -%} 23 | {%- endif -%} 24 | {% else -%} 25 |
    • ·····
    • 26 | {%- endif %} 27 | {%- endfor %} 28 | {%- if pagination.has_next -%} 29 | {%- if not kw %} 30 |
    • 31 | {%- else %} 32 |
    • 33 | {% endif -%} 34 | {%- else -%} 35 |
    • 36 | {% endif -%} 37 | {%- endif -%} 38 |
    39 | {%- endmacro %} 40 | 41 | {% macro footer(pagination, endpoint,kw=None) %} 42 | {% if pagination.pages > 1 %} 43 | 46 | {% endif %} 47 | {%- endmacro %} 48 | -------------------------------------------------------------------------------- /templates/base/panel.html: -------------------------------------------------------------------------------- 1 | {% macro base() -%} 2 | {% if g.user.is_authenticated %} 3 | 8 | 16 | {% else %} 17 |
    18 |
    19 | honmaple 20 |
    21 | 27 | 30 |
    31 | {% endif %} 32 | {%- endmacro %} 33 | 34 | {% macro board_list() -%} 35 | {{ base() }} 36 | {{ count() }} 37 | {%- endmacro %} 38 | 39 | {% macro board() -%} 40 | {{ base() }} 41 | {{ count() }} 42 | {%- endmacro %} 43 | 44 | {% macro collect() -%} 45 | {{ base() }} 46 | {{ count() }} 47 | {%- endmacro %} 48 | 49 | {% macro follow() -%} 50 | {{ base() }} 51 | {{ count() }} 52 | {%- endmacro %} 53 | 54 | {% macro tag_list() -%} 55 | {{ base() }} 56 | {{ count() }} 57 | {%- endmacro %} 58 | 59 | {% macro tag_panel() -%} 60 | {{ base() }} 61 | {{ count() }} 62 | {%- endmacro %} 63 | 64 | {% macro topic_list() -%} 65 | {{ base() }} 66 | {{ count() }} 67 | {%- endmacro %} 68 | 69 | {% macro index() -%} 70 | {{ base() }} 71 | {{ tags_panel(_('Hot Tags'),hot_tags) }} 72 | {{ tags_panel(_('Recent Tags'),recent_tags) }} 73 | {{ count() }} 74 | {%- endmacro %} 75 | {% macro tags_panel(text,tags) -%} 76 |
    77 |
    78 |

    {{ text }}

    79 |
    80 |
    81 | {% set tags = tags() %} 82 | {% for tag in tags -%} 83 | {{ tag.name }} 84 | {%- endfor %} 85 |
    86 |
    87 | {%- endmacro %} 88 | {% macro count() -%} 89 |
    90 |
    91 | {{ _('Forums Count') }} 92 |
    93 |
      94 | {% set count = forums_count() %} 95 |
    • {{ _('Total number of registered users:')}} {{ count['user'] }}
    • 96 |
    • {{ _('Total number of topics:') }} {{ count['topic'] }}
    • 97 |
    • {{ _('Total number of posts:') }} {{ count['post'] }}
    • 98 |
    99 |
    100 | {%- endmacro %} 101 | 102 | {% macro panel(heading,body) -%} 103 |
    104 |
    105 | {{ heading }} 106 |
    107 |
    108 | {{ body }} 109 |
    110 |
    111 | {%- endmacro %} 112 | -------------------------------------------------------------------------------- /templates/board/_macro.html: -------------------------------------------------------------------------------- 1 | {% macro board_body(board) -%} 2 | {% import 'base/link.html' as link %} 3 |
    4 |
    5 |
    6 |
    7 |
    {{ link.board(board) }}
    8 |
    9 | {{_('Topics:')}} 10 | {{ board.topic_count }} 11 | {{ _('Posts:')}} 12 | {{ board.post_count }} 13 |
    14 |
    15 |
    16 |
    17 | {% set newest_topic = board.newest_topic %} 18 | {% if newest_topic %} 19 |
    20 |
    {{ link.topic(newest_topic)}}
    21 |
    {{ _('published by %(author)s',author=link.user(newest_topic.author)) }}
    22 |
    {{ newest_topic.created_at | timesince }}
    23 |
    24 | {% else %} 25 | {{_('No Topic')}} 26 | {% endif %} 27 |
    28 |
    29 |
    30 | {%- endmacro %} 31 | -------------------------------------------------------------------------------- /templates/board/board.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | 3 | {% block title -%}{{ board.name }} · {{ super() }}{%- endblock title %} 4 | 5 | {% block content %} 6 | {% if board.parent_id %} 7 | {{ breadcrumb(hrefs={board.parent.name:url_for('forums.board',pk=board.parent_id)},active=board.name) }} 8 | {% else %} 9 | {{ breadcrumb(active=board.name) }} 10 | {% endif %} 11 | {% from 'board/_macro.html' import board_body %} 12 |
    13 |
    14 |
    15 | 18 | {% for child in board.children -%} 19 | {{ board_body(child) }} 20 | {% else %} 21 | {{ board_body(board) }} 22 | {%- endfor %} 23 |
    24 |
    25 | {% include "topic/_topic.html" %} 26 | {{ p_footer(topics,'forums.board',dict(pk=board.id))}} 27 |
    28 |
    29 |
    30 | {{ panel_base.board() }} 31 |
    32 |
    33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /templates/board/board_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | 3 | {% block title -%}{{ _("Board") }} · {{ super() }}{%- endblock title %} 4 | 5 | {% block content %} 6 | {% from 'board/_macro.html' import board_body %} 7 | {{ breadcrumb(active=_('Board')) }} 8 | {% for board in boards %} 9 | {% if not board.parents %} 10 |
    11 | 14 | {% for child in board.children -%} 15 | {{ board_body(child) }} 16 | {% else %} 17 | {{ board_body(board) }} 18 | {%- endfor %} 19 |
    20 | {% endif %} 21 | {% endfor %} 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /templates/board/chat.html: -------------------------------------------------------------------------------- 1 | {% if g.user.is_authenticated %} 2 | 3 | 4 | 32 | 33 |
    34 |
    35 | 36 |
    37 |
    38 |
    39 |
    40 |
    41 |
    42 | 43 |
    44 | 45 |
    46 |
    47 |
    48 | {%- endif %} 49 | -------------------------------------------------------------------------------- /templates/collect/collect.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block content %} 3 | {{ breadcrumb(hrefs={_('My Collects'):url_for('collect.list')},active=collect.name)}} 4 | 7 |
    8 |
    9 |
    10 |
    11 | 12 | {{ _('edit') }} 13 | {{ _('delete')}} 14 | 15 |

    {{ collect.name }}

    16 |
    17 | {% for topic in topics.items %} 18 |
    19 | {{ topic.title}} 20 | {{_('delete')}} 21 |
    22 | {% else %} 23 |
    24 | 25 | {{_('No Collect')}} 26 | 27 |
    28 | {% endfor %} 29 |
    30 |
    31 |
    32 | {{ panel_base.collect() }} 33 |
    34 |
    35 | {% include "collect/edit.html" %} 36 | {% include "collect/delete.html" %} 37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /templates/collect/collect_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block content %} 3 | {{ breadcrumb(active=_('My Collects'))}} 4 |
    5 |
    6 | 10 |
    11 |
    12 | 13 |
    14 | {% for collect in collects.items %} 15 |
    16 | {{ collect.name}} 17 | {% if collect.is_hidden %} 18 | {{_('Privacy')}} 19 | {% else %} 20 | {{_('Public')}} 21 | {% endif %} 22 |
    23 | {% endfor %} 24 | {{ p_footer(collects,'collect.list')}} 25 |
    26 |
    27 |
    28 | {{ panel_base.collect() }} 29 |
    30 |
    31 | {% include "collect/create.html" %} 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /templates/collect/create.html: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /templates/collect/delete.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /templates/collect/edit.html: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /templates/follow/_macro.html: -------------------------------------------------------------------------------- 1 | {% import 'base/link.html' as link %} 2 | 3 | {% macro nav_title(type) -%} 4 | 10 | {%- endmacro %} 11 | -------------------------------------------------------------------------------- /templates/follow/following_collects.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block content %} 3 | {% from 'follow/_macro.html' import nav_title %} 4 | {{ breadcrumb(active=_('Following Collects'))}} 5 |
    6 |
    7 | {{ nav_title('collect') }} 8 |
    9 | {% for collect in collects.items %} 10 |
    11 | 12 | {{ link.collect(collect)}} 13 |
    14 | {%- else %} 15 | {% include 'follow/none.html' %} 16 | {% endfor %} 17 | {{ p_footer(collects,'follow.collect')}} 18 |
    19 |
    20 |
    21 | {{ panel_base.follow() }} 22 |
    23 |
    24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/follow/following_tags.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block content %} 3 | {% from 'follow/_macro.html' import nav_title %} 4 | {{ breadcrumb(active=_('Following Tags'))}} 5 |
    6 |
    7 | {{ nav_title('tag') }} 8 |
    9 | {% for tag in tags.items %} 10 |
    11 |

    {{ link.tag(tag)}}

    12 | 13 | {{ tag.description }} 14 |
    15 | {% else %} 16 | {% include 'follow/none.html' %} 17 | {% endfor %} 18 | {{ p_footer(tags,'follow.tag')}} 19 |
    20 |
    21 |
    22 | {{ panel_base.follow() }} 23 |
    24 |
    25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /templates/follow/following_topics.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block content %} 3 | {% from 'follow/_macro.html' import nav_title %} 4 | {{ breadcrumb(active=_('Following Topics'))}} 5 |
    6 |
    7 | {{ nav_title('topic') }} 8 |
    9 | {% for topic in topics.items %} 10 |
    11 |

    {{ link.topic(topic)}}

    12 | 13 | {% for tag in topic.tags %} 14 | {{ link.tag(tag)}} 15 | {% endfor %} 16 |
    17 | {% else %} 18 | {% include 'follow/none.html' %} 19 | {% endfor %} 20 | {{ p_footer(topics,'follow.topic')}} 21 |
    22 |
    23 |
    24 | {{ panel_base.follow() }} 25 |
    26 |
    27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /templates/follow/following_users.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block content %} 3 | {% from 'follow/_macro.html' import nav_title %} 4 | {{ breadcrumb(active=_('Following Users'))}} 5 |
    6 |
    7 | {{ nav_title('user') }} 8 |
    9 | {% for user in users.items %} 10 |
    11 | {{ link.user(user)}} 12 | 13 |
    14 | {% else %} 15 | {% include 'follow/none.html' %} 16 | {% endfor %} 17 | {{ p_footer(users,'follow.user')}} 18 |
    19 |
    20 |
    21 | {{ panel_base.follow() }} 22 |
    23 |
    24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/follow/none.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | {{_('No Followings')}} 4 | 5 |
    6 | -------------------------------------------------------------------------------- /templates/forums/_macro.html: -------------------------------------------------------------------------------- 1 | {% macro forums_title() -%} 2 | 17 | {%- endmacro %} 18 | 19 | {% macro forums_item() %} 20 | 39 | {%- endmacro %} 40 | 41 | {% macro topic_form() -%} 42 |
    43 |
    44 |
    45 | {{_('Choice:')}} 46 | {{ g.sort_form.display() }} 47 | {{ _('Order:')}} 48 | {{ g.sort_form.sort() }} 49 | {{ g.sort_form.st() }} 50 |
    51 | 54 | 57 | 60 |
    61 |
    62 | {%- endmacro %} 63 | -------------------------------------------------------------------------------- /templates/forums/about.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | 3 | {% block title -%}{{ _("About") }} · {{ super() }}{%- endblock title %} 4 | 5 | {% block content %} 6 | {{ breadcrumb(active=_('About')) }} 7 | {% cache 300 %} 8 |
    9 |
    10 | {{ _('About')}} 11 |
    12 |
    13 | about 14 |
    15 |
    16 | {% endcache %} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /templates/forums/contact.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | 3 | {% block title -%}{{ _("Contact") }} · {{ super() }}{%- endblock title %} 4 | 5 | {% block content %} 6 | {{ breadcrumb(active=_('Contact me')) }} 7 | {% cache 300 %} 8 |
    9 |
    10 | {{ _('Contact me') }} 11 |
    12 |
    13 | root@honmaple.com 14 |
    15 |
    16 | {% endcache %} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /templates/forums/help.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | 3 | {% block title -%}{{ _("Help") }} · {{ super() }}{%- endblock title %} 4 | 5 | {% block content %} 6 | {{ breadcrumb(active=_('Help')) }} 7 | {% cache 300 %} 8 |
    9 |
    10 | {{ _('Help') }} 11 |
    12 |
    13 | help 14 |
    15 |
    16 | {% endcache %} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /templates/forums/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | 3 | {% block content %} 4 | {% from 'forums/_macro.html' import forums_title,forums_item %} 5 | {% from 'topic/_list_macro.html' import form as topic_form %} 6 | {% from 'topic/_list_macro.html' import body as topic_body %} 7 | {% from 'topic/_list_macro.html' import no_topics %} 8 | {{ breadcrumb() }} 9 | {{ forums_title() }} 10 | {{ forums_item() }} 11 |
    12 |
    13 |
    14 | {{ topic_form() }} 15 | {% if topics.items %} 16 | {% for topic in topics.items %} 17 | {{ topic_body(topic) }} 18 | {% endfor %} 19 | {% else %} 20 | {{ no_topics() }} 21 | {% endif %} 22 | 28 |
    29 |
    30 |
    31 | {{ panel_base.index() }} 32 |
    33 |
    34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /templates/forums/message.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block content %} 3 | {{ breadcrumb(active=_('Notices'))}} 4 |
    5 |
    6 |
    7 |
    8 | 9 | {{ _('Notices') }} 10 |
    11 | {% if messages.items %} 12 | {% for message in messages.items %} 13 |
    14 | {{ message.created_at | timesince }} 15 | {{ message.title }} 16 |

    17 | {{ message.content }} 18 |

    19 |
    20 | {% endfor %} 21 | {{ p_footer(messages, 'message.list')}} 22 | {% else %} 23 |
    24 | 25 | {{ _('No Notices')}} 26 | 27 |
    28 | {% endif %} 29 |
    30 |
    31 |
    32 | {{ panel_base.index() }} 33 |
    34 |
    35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /templates/forums/userlist.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block content %} 3 | {{ breadcrumb(active=_('Userlist'))}} 4 |
    5 |
    6 |
    7 |
    8 | {{ _('Userlist')}} 9 |
    10 | {% for user in users.items %} 11 |
    12 | {{ link_base.user(user)}} 13 |
    14 | {% endfor %} 15 | {{ p_footer(users, 'forums.userlist')}} 16 |
    17 |
    18 |
    19 | {{ panel_base.index() }} 20 |
    21 |
    22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /templates/maple/footer.html: -------------------------------------------------------------------------------- 1 | 36 | 64 | -------------------------------------------------------------------------------- /templates/search/form.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    关键字:
    4 | 5 |
    6 | 11 |
    12 |
    13 | 14 |
    15 | -------------------------------------------------------------------------------- /templates/search/result.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block content %} 3 | {{ breadcrumb(hrefs={_('Search'):url_for('search.search')},active=keyword)}} 4 |
    5 |
    6 | {{ _('Search') }} 7 |
    8 |
    9 | {% include "search/form.html" %} 10 |
    11 | {% for result in results.items %} 12 |
    13 | {{ result.title }} 14 |
    15 | {% else %} 16 |
    17 | 您的搜索没有返回结果。 18 |
    19 | {% endfor %} 20 | {% set params = request.args.to_dict() %} 21 | {% set page = params.pop('page',None) %} 22 | {{ p_footer(results,'search.search',params)}} 23 |
    24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/search/search.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block content %} 3 | {{ breadcrumb(active=_('Search')) }} 4 | {% cache 300 %} 5 |
    6 |
    7 | {{ _('Search') }} 8 |
    9 |
    10 | {% include "search/form.html" %} 11 |
    12 |
    13 | {% endcache %} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /templates/setting/_macro.html: -------------------------------------------------------------------------------- 1 | {% import 'base/link.html' as link %} 2 | {% macro title(type) -%} 3 | 9 | {%- endmacro %} 10 | 11 | {% macro avatar(form) -%} 12 | {{ form.hidden_tag() }} 13 |
    14 |
    15 | 16 |
    17 | 18 | {{ link.user_avatar(g.user,100) }} 19 | 20 | 21 | {{ form.avatar(class="form-control",onchange="loadFile(event)")}} 22 |
    23 |
    24 |
    25 |
    26 | 27 |
    28 |
    29 |
    30 | {%- endmacro %} 31 | -------------------------------------------------------------------------------- /templates/setting/babel.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% import 'base/link.html' as link %} 3 | {% block content %} 4 | {% from 'setting/_macro.html' import title %} 5 | {{ breadcrumb(active=_('Timezone and Locale'))}} 6 | {% block script -%} 7 | {{ super() }} 8 | 14 | {%- endblock script %} 15 |
    16 |
    17 | {{ title('babel') }} 18 |
    19 |
    20 |
    21 |
    22 | {{_('Timezone and Locale')}} 23 |
    24 |
    25 |
    26 | {% from 'base/form.html' import forms %} 27 | {{ forms(form) }} 28 |
    29 |
    30 |
    31 |
    32 |
    33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /templates/setting/password.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% import 'base/link.html' as link %} 3 | {% block content %} 4 | {% from 'setting/_macro.html' import title %} 5 | {{ breadcrumb(active=_('Password '))}} 6 |
    7 |
    8 | {{ title('password') }} 9 |
    10 |
    11 |
    12 |
    13 | {{_('Account')}} 14 |
    15 |
    16 |
    17 | {% from 'base/form.html' import forms %} 18 | {{ forms(form) }} 19 |
    20 |
    21 |
    22 |
    23 |
    24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/setting/privacy.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% import 'base/link.html' as link %} 3 | {% block content %} 4 | {% from 'setting/_macro.html' import title %} 5 | {{ breadcrumb(active= _('Privacy '))}} 6 |
    7 |
    8 | {{ title('privacy') }} 9 |
    10 |
    11 |
    12 |
    13 | {{_('Privacy ')}} 14 |
    15 |
    16 |
    17 | {% from 'base/form.html' import forms %} 18 | {{ forms(form) }} 19 |
    20 |
    21 |
    22 |
    23 |
    24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/setting/setting.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% import 'base/link.html' as link %} 3 | {% block content %} 4 | {% from 'setting/_macro.html' import title,avatar %} 5 | {{ breadcrumb(active=_('Profile '))}} 6 | 12 |
    13 |
    14 | {{ title('setting') }} 15 |
    16 |
    17 |
    18 |
    19 | {{ _('Profile ')}} 20 |
    21 |
    22 |
    23 | {{ avatar(avatarform) }} 24 |
    25 |
    26 | {% from 'base/form.html' import forms %} 27 | {{ forms(form) }} 28 |
    29 |
    30 |
    31 |
    32 |
    33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /templates/tag/_macro.html: -------------------------------------------------------------------------------- 1 | {% macro title(tag) -%} 2 |
    3 |
    4 | 5 | avatar 6 | 7 |
    8 |
    9 |

    {{ tag.name }}

    10 | {% set description = tag.description %} 11 | {% if description -%} 12 | {{ description }} 13 | {%- endif %} 14 |
    15 |
    16 | 17 | 18 | Rss 19 | 20 | 21 | {% if g.user.is_authenticated %} 22 | {% if tag.is_followed() %} 23 | 24 | {% else %} 25 | 26 | {% endif %} 27 | {% endif %} 28 |
    29 |
    30 | {%- endmacro %} 31 | -------------------------------------------------------------------------------- /templates/tag/panel.html: -------------------------------------------------------------------------------- 1 | {% from 'base/panel.html' import base %} 2 | {% from 'base/panel.html' import count %} 3 | {% macro tag_list() -%} 4 | {{ base() }} 5 | {%- endmacro %} 6 | 7 | {% macro tag(tag) -%} 8 | {{ base() }} 9 | 14 | {{ count() }} 15 | {%- endmacro %} 16 | 17 | {% macro parents(tag) -%} 18 |
    19 | 父节点 20 |
    21 | {% if tag.parent_id -%} 22 | {{ link(tag.parent.name) }} 23 | {% else %} 24 | {{ link('honmaple') }} 25 | {%- endif %} 26 |
    27 | {%- endmacro %} 28 | 29 | {% macro relation(tag) -%} 30 | {% set relates = tag.related_tags %} 31 | {% if relates -%} 32 |
    33 | 相关节点 34 |
    35 | {% for child in relates -%} 36 | {{ link(child.name) }} 37 | {%- endfor %} 38 | {%- endif %} 39 | {%- endmacro %} 40 | 41 | {% macro children(tag) -%} 42 | {% set children = tag.children.all() %} 43 | {% if children -%} 44 |
    45 | 子节点 46 |
    47 | {% for child in children -%} 48 | {{ link(child.name) }} 49 | {%- endfor %} 50 | {%- endif %} 51 | {%- endmacro %} 52 | 53 | 54 | {% macro link(name) -%} 55 |
    56 | 57 | {{ name }} 58 |
    59 | {%- endmacro %} 60 | -------------------------------------------------------------------------------- /templates/tag/tag.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | 3 | {% block title -%}{{ tag.name }} · {{ super() }}{%- endblock title %} 4 | 5 | {% block content %} 6 | {% from 'tag/_macro.html' import title %} 7 | {{ breadcrumb(hrefs={_('All Tags'):url_for('tag.list')},active=tag.name)}} 8 |
    9 |
    10 |
    11 |
    12 | {{ title(tag) }} 13 |
    14 | {% include "topic/_topic.html" %} 15 | {{ p_footer(topics,'tag.tag',dict(name=tag.name))}} 16 |
    17 |
    18 |
    19 | {% from 'tag/panel.html' import tag as tag_panel %} 20 | {{ tag_panel(tag) }} 21 |
    22 |
    23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /templates/tag/tag_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | 3 | {% block title -%}{{ _("Tags") }} · {{ super() }}{%- endblock title %} 4 | 5 | {% block content %} 6 | {{ breadcrumb(active=_('All Tags'))}} 7 |
    8 |
    9 |
    10 |
    11 | {{ _('All Tags') }} 12 |
    13 |
    14 | {% for tag in tags.items %} 15 | {{ tag.name }} 16 | {% else %} 17 | 18 | {{ _('No Tags') }} 19 | 20 | {% endfor %} 21 |
    22 |
    23 |
    24 |
    25 | {{ panel_base.tag_list() }} 26 |
    27 |
    28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /templates/topic/_list_macro.html: -------------------------------------------------------------------------------- 1 | {% import 'base/link.html' as link %} 2 | {% macro form() -%} 3 |
    4 |
    5 |
    6 | {{ _('Choice:') }} 7 | {{ g.sort_form.within() }} 8 | {{ _('Order:') }} 9 | {{ g.sort_form.orderby() }} 10 | {{ g.sort_form.desc() }} 11 |
    12 | 15 | 18 | 21 |
    22 |
    23 | {%- endmacro %} 24 | 25 | {% macro body(topic) -%} 26 |
    27 |
    28 | {{ body_header(topic) }} 29 | {{ body_title(topic) }} 30 | {{ body_read(topic) }} 31 | {{ body_reply(topic) }} 32 |
    33 |
    34 | {%- endmacro %} 35 | 36 | {% macro body_header(topic) -%} 37 | {% set author = topic.author %} 38 | {% set tags = topic.tags %} 39 |
    40 |
    41 | {{ link.user_avatar(author,width=48) }} 42 |
    43 |
    44 |
    45 | {% if topic.is_top %} 46 |   47 | {% endif %} 48 | {{ topic.title }} 49 |
    50 | 51 | 由{{ link.user(author) }} 52 | 53 | {{ topic.created_at | timesince }}发布 54 | 55 | 56 | 61 |
    62 |
    63 | {%- endmacro %} 64 | 65 | {% macro body_title(topic) -%} 66 | 73 | {%- endmacro %} 74 | 75 | {% macro body_read(topic) -%} 76 | 80 | {%- endmacro %} 81 | 82 | {% macro body_reply(topic) -%} 83 | 95 | {%- endmacro %} 96 | 97 | {% macro no_topics() -%} 98 |
    99 | 100 | {{ _('No Topic')}} 101 | 102 |
    103 | {%- endmacro %} 104 | -------------------------------------------------------------------------------- /templates/topic/_topic.html: -------------------------------------------------------------------------------- 1 | {% from 'topic/_list_macro.html' import form,body,no_topics %} 2 | {{ form() }} 3 | {% for topic in topics.items %} 4 | {{ body(topic) }} 5 | {% else %} 6 | {{ no_topics() }} 7 | {% endfor %} 8 | -------------------------------------------------------------------------------- /templates/topic/ask.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | 3 | {%- block script %} 4 | {{ super() }} 5 | 6 | 7 | {%- endblock -%} 8 | 9 | {%- block style %} 10 | {{ super() }} 11 | 18 | {%- endblock -%} 19 | 20 | {% block content %} 21 | {{ breadcrumb(active=_('Ask'))}} 22 |
    23 |
    {{ _('Ask') }}
    24 |
    25 |
    26 |
    27 | {% include "topic/ask/form.html" %} 28 |
    29 | {{ form.content_type(class="input-sm pull-right",style="padding:2")}} 30 | 31 | 32 |
    33 |
    34 |
    35 |
    36 |
    37 |
    38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /templates/topic/ask/_macro.html: -------------------------------------------------------------------------------- 1 | {% macro title() -%} 2 | 3 | (请用简短的话语描述你的问题) 4 | 5 | {%- endmacro %} 6 | 7 | {% macro category() -%} 8 | 9 | (请输入分类) 10 | 11 | {%- endmacro %} 12 | 13 | {% macro tags() -%} 14 | 15 | (请输入节点) 16 | 17 | {%- endmacro %} 18 | 19 | {% macro content() -%} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 41 | 42 |
    默认语法支持部分html标签
    b:加粗
    i:倾斜
    br:换行
    font: 设置字体(目前仅允许设置字体颜色) 39 |

    注:字体颜色请勿设置成白色或与白色相近的颜色

    40 |
    43 | {%- endmacro %} 44 | -------------------------------------------------------------------------------- /templates/topic/ask/form.html: -------------------------------------------------------------------------------- 1 | {% import 'topic/ask/_macro.html' as place %} 2 |
    3 |
    4 | {{ form.hidden_tag() }} 5 | {{ form.title.label(class="control-label") }} 6 | {{ place.title() }} 7 |
    8 |
    9 | {{ form.title(class="form-control") }} 10 |
    11 |
    12 |
    13 |
    14 | {{ form.category.label(class="control-label") }} 15 | {{ place.category() }} 16 |
    17 |
    18 | {{ form.category(class="form-control") }} 19 |
    20 |
    21 |
    22 |
    23 | {{ form.tags.label(class="control-label") }} 24 | {{ place.tags() }} 25 |
    26 |
    27 | {{ form.tags(class="form-control",id="tokenfield",placeholder="节点请以英文逗号隔开.请勿输入超过4个节点") }} 28 |
    29 |
    30 |
    31 |
    32 | {{ form.content.label(class="control-label") }} 33 | {{ place.content() }} 34 |
    35 |
    36 | {{ form.content(class="form-control",rows="8",placeholder="请输入问题描述") }} 37 |
    38 |
    39 | -------------------------------------------------------------------------------- /templates/topic/collect.html: -------------------------------------------------------------------------------- 1 | 39 | -------------------------------------------------------------------------------- /templates/topic/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block content %} 3 | {{ breadcrumb(active='编辑')}} 4 | 11 |
    12 |
    13 | {{ _('Edit' )}} 14 |
    15 |
    16 |
    17 |
    18 | {% include "topic/ask/form.html" %} 19 |
    20 | {{ form.content_type(class="input-sm pull-right",style="padding:2")}} 21 | 22 | 23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /templates/topic/item/_macro.html: -------------------------------------------------------------------------------- 1 | {% import 'base/link.html' as link %} 2 | 3 | {% macro tags(topic) -%} 4 | {% for tag in topic.tags %} 5 | {{ link.tag(tag) }} 6 | {% endfor %} 7 | {%- endmacro %} 8 | 9 | 10 | {% macro last_reply(topic) -%} 11 | 12 | {% set last_reply = topic.replies.first() %} 13 | {{ topic.author.username }} 14 | {{ _('published at %(time)s',time = topic.created_at | timesince ) }} 15 | {% if last_reply %} 16 | · {{ _('The last reply published by %(author)s at %(time)s',author=link.user(last_reply.author.username),time=last_reply.created_at | timesince)}} 17 | {% endif %} 18 | 19 | {%- endmacro %} 20 | 21 | {% macro title(topic) -%} 22 | {{ tags(topic) }} 23 | {{ last_reply(topic) }} 24 | {%- endmacro %} 25 | -------------------------------------------------------------------------------- /templates/topic/item/body.html: -------------------------------------------------------------------------------- 1 |
    2 | {{ topic.text }} 3 |
    4 | -------------------------------------------------------------------------------- /templates/topic/item/heading.html: -------------------------------------------------------------------------------- 1 | {% import 'base/link.html' as link %} 2 | {% from 'topic/item/_macro.html' import title %} 3 | {% set author = topic.author %} 4 |
    5 |
    6 |

    {{ topic.title }}

    7 | 8 | {{ title(topic) }} 9 | 10 |
    11 |
    12 | {{ link.user_avatar(topic.author) }} 13 |
    14 |
    15 | -------------------------------------------------------------------------------- /templates/topic/itemlist/_macro.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/templates/topic/itemlist/_macro.html -------------------------------------------------------------------------------- /templates/topic/panel.html: -------------------------------------------------------------------------------- 1 | {% if g.user.is_authenticated %} 2 |
      3 | {% if topic.is_followed() %} 4 | 5 | {% else %} 6 | 7 | {% endif %} 8 | {% if g.user == topic.author %} 9 | 10 | 编辑问题 11 | 12 | {%- endif %} 13 | {% if topic.is_collected() %} 14 | 已收藏 15 | {% else %} 16 | 收藏问题 17 | {% include "topic/collect.html" %} 18 | {% endif %} 19 |
    20 | {% endif %} 21 | -------------------------------------------------------------------------------- /templates/topic/reply/_macro.html: -------------------------------------------------------------------------------- 1 | {% macro like(reply) -%} 2 | {% if reply.is_liked() %} 3 | 7 | {% else %} 8 | 12 | {% endif %} 13 | {%- endmacro %} 14 | 15 | {% macro no_replies() -%} 16 |
    17 | 18 | {{_('no reply')}} 19 | 20 |
    21 | {%- endmacro %} 22 | -------------------------------------------------------------------------------- /templates/topic/reply/form.html: -------------------------------------------------------------------------------- 1 |
    2 | {% if g.user.is_authenticated %} 3 |
    {{ _('Reply this topic') }}
    4 |
    5 |
    6 | {{ form.hidden_tag() }} 7 | {{ form.content(class='form-control',rows=4)}} 8 | 9 |
    10 |
    11 | {% else %} 12 |
    13 | 14 | 15 | {{_('You need')}} {{ _('Login')}} {{_('before you can reply.')}} 16 | 17 |
    18 | {% endif %} 19 |
    20 | -------------------------------------------------------------------------------- /templates/topic/reply/itemlist.html: -------------------------------------------------------------------------------- 1 | {% from 'topic/reply/_macro.html' import like,no_replies %} 2 | {% import 'base/link.html' as link %} 3 | {% from 'base/paginate.html' import footer as p_footer %} 4 |
    5 |
    6 | {{ _('Received %(total)s replies',total=replies.total) }} 7 | 11 |
    12 | {% if replies.items %} 13 | {% for reply in replies.items %} 14 | {% set user = reply.author %} 15 | {% set floor = loop.index + 12 * (replies.page - 1) %} 16 |
    17 |
    18 | 19 | avatar 20 | 21 |
    22 |
    23 | 24 | {{ link.user(reply.author)}} 25 | {{ reply.created_at | timesince }} 26 | 27 | 28 |
    29 | {{ reply.content | safe_clean }} 30 |
    31 |
    32 |
    33 | {{ like(reply) }} 34 |
    35 |
    36 | 37 | 38 | 39 |
    40 |
    41 | {% endfor %} 42 | {{ p_footer(replies,'topic.topic',dict(pk=topic.id))}} 43 | {% else %} 44 | {{ no_replies() }} 45 | {% endif %} 46 |
    47 | {% include "topic/reply/form.html" %} 48 | -------------------------------------------------------------------------------- /templates/topic/topic.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block content %} 3 | {% set board = topic.board %} 4 | {% if board.parent_id %} 5 | {{ breadcrumb(hrefs={board.parent.name:url_for('forums.board',pk=board.parent_id), 6 | board.name:url_for('forums.board',pk=board.id)},active=topic.title) }} 7 | {% else %} 8 | {{ breadcrumb(hrefs={board.name:url_for('forums.board',pk=board.id)},active=topic.title) }} 9 | {% endif %} 10 |
    11 |
    12 |
    13 | {% include "topic/item/heading.html" %} 14 | {% include "topic/item/body.html" %} 15 |
    16 |
    17 | {% include "topic/reply/itemlist.html" %} 18 |
    19 |
    20 |
    21 | {% include "topic/panel.html" %} 22 | {% set ask_url = url_for('topic.ask',pk=topic.board_id) %} 23 | {{ panel_base.board() }} 24 |
    25 |
    26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /templates/topic/topic_good.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block script %} 3 | 10 | {{ super()}} 11 | {% endblock %} 12 | {% block content %} 13 | {{ breadcrumb(hrefs={_('All Topics'):url_for('topic.topiclist')},active=_('Good Topics'))}} 14 |
    15 |
    16 |
    17 | {% from 'topic/topic_list.html' import view as board_view %} 18 | {{ board_view(topics)}} 19 | {{ p_footer(topics,'topic.topic')}} 20 |
    21 |
    22 |
    23 | {{ panel_base.topic_list() }} 24 |
    25 |
    26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /templates/topic/topic_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | 3 | {% block title -%}{{ title }} · {{ super() }}{%- endblock title %} 4 | 5 | {% block content %} 6 | {% if request.path.endswith('top') %} 7 | {{ breadcrumb(hrefs={_('All Topics'):url_for('topic.list')},active=_('Top Topics')) }} 8 | {% elif request.path.endswith('good') %} 9 | {{ breadcrumb(hrefs={_('All Topics'):url_for('topic.list')},active=_('Good Topics')) }} 10 | {% else %} 11 | {{ breadcrumb(active=_('All Topics')) }} 12 | {% endif %} 13 |
    14 |
    15 |
    16 | {% include "topic/_topic.html" %} 17 | {{ p_footer(topics,'topic.list')}} 18 |
    19 |
    20 |
    21 | {{ panel_base.topic_list() }} 22 |
    23 |
    24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/topic/topic_top.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block script %} 3 | 10 | {{ super()}} 11 | {% endblock %} 12 | {% block content %} 13 | {{ breadcrumb(hrefs={_('All Topics'):url_for('topic.topiclist')},active=_('Top Topics'))}} 14 |
    15 |
    16 |
    17 | {% from 'topic/topic_list.html' import view as board_view %} 18 | {{ board_view(topics)}} 19 | {{ p_footer(topics,'topic.topic')}} 20 |
    21 |
    22 |
    23 | {{ panel_base.topic_list() }} 24 |
    25 |
    26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /templates/user/_macro.html: -------------------------------------------------------------------------------- 1 | {% macro not_allowed() -%} 2 |
    3 | 4 | 5 | {{_("Due to user's setting,the list have been hidden.")}} 6 | 7 |
    8 | {%- endmacro %} 9 | -------------------------------------------------------------------------------- /templates/user/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block content %} 3 | {{ breadcrumb(active=user.username) }} 4 |
    5 |
    6 | {% set setting = user.setting %} 7 | {% set info = user.info %} 8 | {% include 'user/info.html' %} 9 | 18 |
    19 |
    20 | {% block mmm -%} 21 | {%- endblock mmm %} 22 |
    23 |
    24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/user/followers.html: -------------------------------------------------------------------------------- 1 | {% extends 'user/base.html' %} 2 | {% from 'user/_macro.html' import not_allowed %} 3 | {% block mmm -%} 4 | {% set orderby = request.args.get('orderby') %} 5 | {% set username = user.username %} 6 | {% set setting = user.setting %} 7 |
    8 |
    9 | 10 | {{_('Sort:')}} 11 | 15 | 16 |
    17 | {% for follower in followers.items %} 18 |
    19 | {{ link.user(follower) }} 20 |
    21 | {% else %} 22 |
    23 | {{_('No Follower')}} 24 |
    25 | {% endfor %} 26 | {{ p_footer(followers,'user.follower',dict(username=user.username)) }} 27 |
    28 | {%- endblock mmm %} 29 | -------------------------------------------------------------------------------- /templates/user/info.html: -------------------------------------------------------------------------------- 1 | {% macro other(user) -%} 2 | {% if current_user.is_authenticated and current_user.id != user.id %} 3 |
    4 | 5 | {% if user.is_followed() %} 6 | 7 | {% else %} 8 | 9 | {% endif %} 10 | 11 |
    12 | {% endif %} 13 | {%- endmacro %} 14 | 15 | {% macro mine(user) -%} 16 | {% if g.user.is_authenticated and user.is_not_confirmed %} 17 |
    18 | 21 | {{ _("You haven't confirm your account,Please confirmed") }} 22 | {{ _('Activate Account')}} 23 |
    24 | {% endif %} 25 | {%- endmacro %} 26 | 27 | {% macro online(user) -%} 28 | {% if user.is_online %} 29 | {{ _('ONLINE') }} 30 | {%- else %} 31 | {{ _('OUTLINE') }} 32 | {% endif -%} 33 | {%- endmacro %} 34 | 35 |
    36 |
    37 |
    38 | avatar 39 |
    40 |
    41 |

    42 | {{ user.username}} 43 | {{ online(user) }} 44 |

    45 | 46 | 第{{ user.id }}号会员/ 47 | {{user.register_time | timesince }} 48 |
    49 | {{ user.topic_count }}篇主题 | 50 | {{ user.reply_count }}条回复 51 |
    52 |
    53 |
    54 |

    {{ user.info.word }}

    55 |
    Someone famous in {{ user.info.school }}
    56 |
    57 | 58 | 59 | 60 | 61 | 62 | 63 |

    1

    积分

    {{ user.followers.count()}}

    粉丝

    {{ user.following_users.count() }}

    关注
    64 |
    65 | {{ other(user) }} 66 | {{ mine(user) }} 67 |
    68 | -------------------------------------------------------------------------------- /templates/user/replies.html: -------------------------------------------------------------------------------- 1 | {% extends 'user/base.html' %} 2 | {% from 'user/_macro.html' import not_allowed %} 3 | {% block mmm -%} 4 | {% set orderby = request.args.get('orderby') %} 5 | {% set username = user.username %} 6 | {% set setting = user.setting %} 7 |
    8 |
    9 | 10 | {{_("Sort:")}} 11 | 15 | 16 |
    17 | {% if replies_is_allowed %} 18 | {{ itemlist(replies,user) }} 19 | {% else %} 20 | {{ not_allowed() }} 21 | {% endif %} 22 |
    23 | {%- endblock mmm %} 24 | 25 | {% macro itemlist(replies,user) -%} 26 | {% for reply in replies.items %} 27 |
    28 | 29 | {{_('replied %(title)s created by %(author)s',title=link.topic(reply.topic),author = link.user(reply.author))}} 30 | 31 |

    {{ reply.content | safe_clean }}

    32 | 33 | {{_('replied time:')}}{{ reply.created_at | timesince }} 34 | 35 |
    36 | {% else %} 37 |
    38 | {{_('No Reply')}} 39 |
    40 | {% endfor %} 41 | {{ p_footer(replies,'user.reply',dict(username=user.username))}} 42 | {%- endmacro %} 43 | -------------------------------------------------------------------------------- /templates/user/user.html: -------------------------------------------------------------------------------- 1 | {% extends 'user/base.html' %} 2 | {% from 'user/_macro.html' import not_allowed %} 3 | {% block mmm -%} 4 | {% set orderby = request.args.get('orderby') %} 5 | {% set username = user.username %} 6 | {% set setting = user.setting %} 7 |
    8 |
    9 | 10 | {{_('Sort:')}} 11 |
    12 | {{_('time')}} 13 | {{_('vote')}} 14 |
    15 |
    16 |
    17 | {% if topic_is_allowed %} 18 | {{ itemlist(topics,user) }} 19 | {% else %} 20 | {{ not_allowed() }} 21 | {% endif %} 22 |
    23 | {%- endblock mmm %} 24 | 25 | {% macro itemlist(topics,user) -%} 26 | {% for topic in topics.items %} 27 |
    28 | {{ topic.board.board }} 29 | {{ link.topic(topic) }} 30 |
    31 | {{_('create time:')}}{{ topic.created_at | timesince }} 32 | · {{ _('the last reply published by %(author)s',author=link.user(topic.author))}} 33 |
    34 |
    35 | {% else %} 36 |
    37 | {{_('No Topic')}} 38 |
    39 | {% endfor %} 40 | {{ p_footer(topics,'user.topic',dict(username=user.username))}} 41 | {%- endmacro %} 42 | -------------------------------------------------------------------------------- /templates/user/user_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/base.html' %} 2 | {% block content %} 3 | {{ breadcrumb(active=_('Userlist'))}} 4 |
    5 |
    6 |
    7 |
    8 | {{ _('Userlist') }} 9 |
    10 | {% for user in users.items %} 11 |
    12 | {{ link.user(user) }} 13 |
    14 | {% endfor %} 15 | {{ p_footer(users, 'user.list')}} 16 |
    17 |
    18 |
    19 | {{ panel_base.board() }} 20 |
    21 |
    22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /translations/babel.cfg: -------------------------------------------------------------------------------- 1 | [ignore: **/static/**.html] 2 | [python: **.py] 3 | [jinja2: **.html] 4 | extensions=jinja2.ext.autoescape,jinja2.ext.with_ -------------------------------------------------------------------------------- /translations/zh/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/honmaple/maple-bbs/25a2282225b86993023b7199bcb019c1f277f66c/translations/zh/LC_MESSAGES/messages.mo --------------------------------------------------------------------------------