├── .gitignore ├── README.md ├── async_book ├── app.py ├── book.db ├── templates │ └── index.html └── test_website.py ├── database ├── .idea │ ├── .gitignore │ ├── database.iml │ ├── inspectionProfiles │ │ ├── Project_Default.xml │ │ └── profiles_settings.xml │ ├── misc.xml │ ├── modules.xml │ └── vcs.xml ├── __pycache__ │ └── app.cpython-39.pyc ├── app.py └── migrations │ ├── README │ ├── alembic.ini │ ├── env.py │ └── script.py.mako ├── demo01 ├── .idea │ ├── .gitignore │ ├── demo01.iml │ ├── inspectionProfiles │ │ ├── Project_Default.xml │ │ └── profiles_settings.xml │ ├── misc.xml │ └── modules.xml ├── __pycache__ │ └── app.cpython-39.pyc └── app.py ├── demo02 ├── .idea │ ├── .gitignore │ ├── demo02.iml │ ├── inspectionProfiles │ │ ├── Project_Default.xml │ │ └── profiles_settings.xml │ ├── misc.xml │ └── modules.xml ├── __pycache__ │ └── app.cpython-39.pyc └── app.py ├── demo03 ├── .idea │ ├── .gitignore │ ├── demo03.iml │ ├── inspectionProfiles │ │ ├── Project_Default.xml │ │ └── profiles_settings.xml │ ├── misc.xml │ └── modules.xml └── app.py ├── demo04 ├── .idea │ ├── .gitignore │ ├── demo04.iml │ ├── inspectionProfiles │ │ ├── Project_Default.xml │ │ └── profiles_settings.xml │ ├── misc.xml │ └── modules.xml └── app.py ├── formlearn ├── .idea │ ├── .gitignore │ ├── formlearn.iml │ ├── inspectionProfiles │ │ ├── Project_Default.xml │ │ └── profiles_settings.xml │ ├── misc.xml │ └── modules.xml ├── __pycache__ │ ├── app.cpython-39.pyc │ └── forms.cpython-39.pyc ├── app.py ├── forms.py └── templates │ ├── base.html │ ├── login.html │ └── register.html ├── im_demo ├── .idea │ ├── .gitignore │ ├── im_demo.iml │ ├── inspectionProfiles │ │ ├── Project_Default.xml │ │ └── profiles_settings.xml │ ├── misc.xml │ ├── modules.xml │ └── vcs.xml ├── __pycache__ │ └── app.cpython-39.pyc ├── app.py ├── static │ ├── bg.jpg │ ├── index.js │ └── login.js └── templates │ ├── index.html │ └── login.html └── pythonbbs ├── .gitignore ├── app.py ├── awebsite ├── static │ ├── cms │ │ └── css │ │ │ └── base.css │ ├── common │ │ ├── images │ │ │ └── logo.png │ │ ├── sweetalert │ │ │ └── sweetalert.min.js │ │ ├── zlajax.js │ │ ├── zlalert.js │ │ ├── zlparam.js │ │ └── zlqiniu.js │ └── front │ │ └── css │ │ ├── base.css │ │ ├── index.css │ │ ├── post_detail.css │ │ └── sign.css └── templates │ ├── cms │ ├── add_staff.html │ ├── base.html │ ├── boards.html │ ├── comments.html │ ├── edit_staff.html │ ├── index.html │ ├── posts.html │ ├── staff_list.html │ └── users.html │ ├── errors │ ├── 401.html │ ├── 404.html │ └── 500.html │ └── front │ ├── base.html │ ├── index.html │ ├── login.html │ ├── post_detail.html │ ├── profile.html │ ├── public_post.html │ └── register.html ├── bbs_celery.py ├── blueprints ├── __init__.py ├── cms.py ├── front.py ├── media.py └── user.py ├── commands.py ├── config.py ├── decorators.py ├── exts.py ├── filters.py ├── forms ├── __init__.py ├── baseform.py ├── cms.py ├── post.py └── user.py ├── hooks.py ├── media ├── 1_huangyong1314.jpg └── avatars │ └── 1_huangyong1314.jpg ├── migrations ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ ├── 47dde31b98c0_create_post_board_banner_comment_model.py │ ├── 510b7126b120_create_user_model.py │ ├── 5e1dacecfe8c_create_permission_and_role_model.py │ ├── 7600f5255f11_.py │ ├── 8c2dc98e3105_.py │ ├── c143fec1e7c7_.py │ └── cd7a9d3254a5_.py ├── models ├── __init__.py ├── post.py └── user.py ├── requirements.txt ├── static ├── cms │ ├── css │ │ └── base.css │ └── js │ │ ├── boards.js │ │ ├── comments.js │ │ ├── posts.js │ │ └── users.js ├── common │ ├── images │ │ └── logo.png │ ├── sweetalert │ │ └── sweetalert.min.js │ ├── zlajax.js │ ├── zlalert.js │ ├── zlparam.js │ └── zlqiniu.js └── front │ ├── css │ ├── base.css │ ├── index.css │ ├── post_detail.css │ └── sign.css │ └── js │ ├── public_post.js │ └── register.js ├── templates ├── cms │ ├── add_staff.html │ ├── base.html │ ├── boards.html │ ├── comments.html │ ├── edit_staff.html │ ├── index.html │ ├── posts.html │ ├── staff_list.html │ └── users.html ├── errors │ ├── 401.html │ ├── 404.html │ └── 500.html └── front │ ├── base.html │ ├── index.html │ ├── login.html │ ├── post_detail.html │ ├── profile.html │ ├── public_post.html │ └── register.html └── utils ├── __init__.py └── restful.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .idea 132 | __pycache__ 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flask_fullstack 2 | 《Flask Web全栈开发实战》图书配套代码 3 | -------------------------------------------------------------------------------- /async_book/app.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from flask import Flask, redirect, request, render_template 4 | from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession 5 | from sqlalchemy.orm import declarative_base, sessionmaker 6 | from sqlalchemy import Column, Integer, String, Float, select, create_engine 7 | import aiohttp 8 | import asyncio 9 | import time 10 | import requests 11 | from asgiref.wsgi import WsgiToAsgi 12 | 13 | 14 | ASYNC_DATABASE_URL = "sqlite+aiosqlite:///./book.db" 15 | async_engine = create_async_engine(ASYNC_DATABASE_URL,echo=False) 16 | async_session = sessionmaker(bind=async_engine, expire_on_commit=False, class_=AsyncSession) 17 | 18 | 19 | Base = declarative_base() 20 | 21 | app = Flask(__name__) 22 | app.jinja_env.is_async = True 23 | 24 | 25 | # @app.before_first_request 26 | # async def before_first_request(): 27 | # async with async_engine.begin() as conn: 28 | # await conn.run_sync(Base.metadata.drop_all) 29 | # await conn.run_sync(Base.metadata.create_all) 30 | # 31 | # fake = Faker(locale="zh_CN") 32 | # async with async_session() as session: 33 | # async with session.begin(): 34 | # for x in range(10000): 35 | # name = fake.text() 36 | # author = fake.name() 37 | # price = random.random() * 100 38 | # book = Book(name=name, author=author, price=price) 39 | # session.add(book) 40 | 41 | @app.teardown_appcontext 42 | async def teardown_appcontext(f): 43 | await async_engine.dispose() 44 | 45 | 46 | 47 | class Book(Base): 48 | __tablename__ = "books" 49 | id = Column(Integer, primary_key=True) 50 | name = Column(String(200), nullable=False) 51 | author = Column(String(200), nullable=False) 52 | price = Column(Float, default=0) 53 | 54 | 55 | async def get_all_books(): 56 | async with async_session() as session: 57 | stmt = select(Book) 58 | result = await session.execute(stmt) 59 | books = result.scalars().all() 60 | return books 61 | 62 | app.jinja_env.globals["books"] = get_all_books 63 | 64 | 65 | @app.route('/') 66 | def index(): 67 | return render_template("index.html") 68 | 69 | async def fetch_url(session,url): 70 | response = await session.get(url) 71 | return {'url': response.url, 'status': response.status} 72 | 73 | 74 | @app.route("/website/async") 75 | async def website_async(): 76 | start_time = time.time() 77 | urls = [ 78 | "https://www.python.org/", 79 | "https://www.php.net/", 80 | "https://www.java.com/", 81 | "https://dotnet.microsoft.com/", 82 | "https://www.javascript.com/" 83 | ] 84 | async with aiohttp.ClientSession() as session: 85 | tasks = [] 86 | for url in urls: 87 | tasks.append(fetch_url(session,url)) 88 | sites = await asyncio.gather(*tasks) 89 | 90 | response = '

URLs:

' 91 | for site in sites: 92 | response += f"

URL: {site['url']}, Status Code: {site['status']}

" 93 | 94 | end_time = time.time() 95 | print("time:%.2f"%(end_time-start_time)) 96 | return response 97 | 98 | 99 | @app.route("/website/sync") 100 | def website_sync(): 101 | start_time = time.time() 102 | urls = [ 103 | "https://www.python.org/", 104 | "https://www.php.net/", 105 | "https://www.java.com/", 106 | "https://dotnet.microsoft.com/", 107 | "https://www.javascript.com/" 108 | ] 109 | sites = [] 110 | for url in urls: 111 | response = requests.get(url) 112 | sites.append({'url': response.url, 'status': response.status_code}) 113 | 114 | response = '

URLs:

' 115 | for site in sites: 116 | response += f"

URL: {site['url']}, Status Code: {site['status']}

" 117 | 118 | end_time = time.time() 119 | print("time:%.2f"%(end_time-start_time)) 120 | return response 121 | 122 | 123 | @app.post('/books/add') 124 | async def add_books(): 125 | name = request.form.get('name') 126 | author = request.form.get("author") 127 | price = request.form.get('price') 128 | async with async_session() as session: 129 | async with session.begin(): 130 | book = Book(name=name, author=author, price=price) 131 | session.add(book) 132 | await session.flush() 133 | return "success" 134 | 135 | 136 | wsgi_app = WsgiToAsgi(app) 137 | 138 | # if __name__ == '__main__': 139 | # app.run(debug=True, host="0.0.0.0") 140 | -------------------------------------------------------------------------------- /async_book/book.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/async_book/book.db -------------------------------------------------------------------------------- /async_book/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% for book in books() %} 18 | 19 | 20 | 21 | 22 | 23 | {% endfor %} 24 | 25 |
书名作者价格
{{ book.name }}{{ book.author }}{{ book.price }}
26 | 27 | -------------------------------------------------------------------------------- /async_book/test_website.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | data = { 4 | "name": "三国演义", 5 | "author": "罗贯中", 6 | "price": 99 7 | } 8 | 9 | # resp = requests.post("http://127.0.0.1:5000/books/add",data=data) 10 | # print(resp.text) 11 | 12 | resp = requests.get("http://127.0.0.1:5000/books/async") 13 | print(resp.text) -------------------------------------------------------------------------------- /database/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /database/.idea/database.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /database/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 57 | -------------------------------------------------------------------------------- /database/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /database/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /database/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /database/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /database/__pycache__/app.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/database/__pycache__/app.cpython-39.pyc -------------------------------------------------------------------------------- /database/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /database/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic,flask_migrate 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [logger_flask_migrate] 38 | level = INFO 39 | handlers = 40 | qualname = flask_migrate 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /database/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from flask import current_app 7 | 8 | from alembic import context 9 | 10 | # this is the Alembic Config object, which provides 11 | # access to the values within the .ini file in use. 12 | config = context.config 13 | 14 | # Interpret the config file for Python logging. 15 | # This line sets up loggers basically. 16 | fileConfig(config.config_file_name) 17 | logger = logging.getLogger('alembic.env') 18 | 19 | # add your model's MetaData object here 20 | # for 'autogenerate' support 21 | # from myapp import mymodel 22 | # target_metadata = mymodel.Base.metadata 23 | config.set_main_option( 24 | 'sqlalchemy.url', 25 | str(current_app.extensions['migrate'].db.get_engine().url).replace( 26 | '%', '%%')) 27 | target_metadata = current_app.extensions['migrate'].db.metadata 28 | 29 | # other values from the config, defined by the needs of env.py, 30 | # can be acquired: 31 | # my_important_option = config.get_main_option("my_important_option") 32 | # ... etc. 33 | 34 | 35 | def run_migrations_offline(): 36 | """Run migrations in 'offline' mode. 37 | 38 | This configures the context with just a URL 39 | and not an Engine, though an Engine is acceptable 40 | here as well. By skipping the Engine creation 41 | we don't even need a DBAPI to be available. 42 | 43 | Calls to context.execute() here emit the given string to the 44 | script output. 45 | 46 | """ 47 | url = config.get_main_option("sqlalchemy.url") 48 | context.configure( 49 | url=url, target_metadata=target_metadata, literal_binds=True 50 | ) 51 | 52 | with context.begin_transaction(): 53 | context.run_migrations() 54 | 55 | 56 | def run_migrations_online(): 57 | """Run migrations in 'online' mode. 58 | 59 | In this scenario we need to create an Engine 60 | and associate a connection with the context. 61 | 62 | """ 63 | 64 | # this callback is used to prevent an auto-migration from being generated 65 | # when there are no changes to the schema 66 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 67 | def process_revision_directives(context, revision, directives): 68 | if getattr(config.cmd_opts, 'autogenerate', False): 69 | script = directives[0] 70 | if script.upgrade_ops.is_empty(): 71 | directives[:] = [] 72 | logger.info('No changes in schema detected.') 73 | 74 | connectable = current_app.extensions['migrate'].db.get_engine() 75 | 76 | with connectable.connect() as connection: 77 | context.configure( 78 | connection=connection, 79 | target_metadata=target_metadata, 80 | process_revision_directives=process_revision_directives, 81 | **current_app.extensions['migrate'].configure_args 82 | ) 83 | 84 | with context.begin_transaction(): 85 | context.run_migrations() 86 | 87 | 88 | if context.is_offline_mode(): 89 | run_migrations_offline() 90 | else: 91 | run_migrations_online() 92 | -------------------------------------------------------------------------------- /database/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /demo01/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /demo01/.idea/demo01.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /demo01/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 57 | -------------------------------------------------------------------------------- /demo01/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /demo01/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /demo01/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demo01/__pycache__/app.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/demo01/__pycache__/app.cpython-39.pyc -------------------------------------------------------------------------------- /demo01/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__) 4 | 5 | 6 | @app.route('/') 7 | def hello_world(): 8 | return 'Hello World!' 9 | 10 | 11 | if __name__ == '__main__': 12 | app.run() 13 | -------------------------------------------------------------------------------- /demo02/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /demo02/.idea/demo02.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /demo02/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 57 | -------------------------------------------------------------------------------- /demo02/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /demo02/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /demo02/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demo02/__pycache__/app.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/demo02/__pycache__/app.cpython-39.pyc -------------------------------------------------------------------------------- /demo02/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__) 4 | 5 | 6 | @app.route('/') 7 | def hello_world(): 8 | return 'Hello Flask!' 9 | 10 | 11 | if __name__ == '__main__': 12 | app.run(debug=True, host="0.0.0.0", port=8000) 13 | -------------------------------------------------------------------------------- /demo03/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /demo03/.idea/demo03.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /demo03/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 57 | -------------------------------------------------------------------------------- /demo03/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /demo03/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /demo03/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demo03/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__) 4 | 5 | 6 | @app.route('/') 7 | def hello_world(): 8 | return 'Hello World!' 9 | 10 | 11 | if __name__ == '__main__': 12 | app.run() 13 | -------------------------------------------------------------------------------- /demo04/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /demo04/.idea/demo04.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /demo04/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 57 | -------------------------------------------------------------------------------- /demo04/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /demo04/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /demo04/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demo04/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__, template_folder=r"E:\flask_fullstack\demo04\mytemplates") 4 | 5 | 6 | @app.route('/') 7 | def hello_world(): 8 | return 'Hello World!' 9 | 10 | 11 | if __name__ == '__main__': 12 | app.run() 13 | -------------------------------------------------------------------------------- /formlearn/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /formlearn/.idea/formlearn.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /formlearn/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 57 | -------------------------------------------------------------------------------- /formlearn/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /formlearn/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /formlearn/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /formlearn/__pycache__/app.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/formlearn/__pycache__/app.cpython-39.pyc -------------------------------------------------------------------------------- /formlearn/__pycache__/forms.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/formlearn/__pycache__/forms.cpython-39.pyc -------------------------------------------------------------------------------- /formlearn/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask,request,render_template,redirect,url_for,flash 2 | from forms import RegisterForm,LoginForm 3 | from flask_wtf import CSRFProtect 4 | 5 | app = Flask(__name__) 6 | app.secret_key = "sfajksd" 7 | CSRFProtect(app) 8 | 9 | @app.route('/') 10 | def hello_world(): 11 | return 'Hello World!' 12 | 13 | 14 | @app.route("/register",methods=['GET','POST']) 15 | def register(): 16 | if request.method == 'GET': 17 | return render_template("register.html") 18 | else: 19 | # request.form是html模板提交上来的表单数据 20 | form = RegisterForm(request.form) 21 | # 如果表单验证通过 22 | if form.validate(): 23 | email = form.email.data 24 | username = form.username.data 25 | password = form.password.data 26 | 27 | # 以下可以把数据保存到数据库的操作 28 | print("email:",email) 29 | print("username:",username) 30 | print("password:",password) 31 | return "注册成功!" 32 | else: 33 | print(form.errors) 34 | for errors in form.errors.values(): 35 | for error in errors: 36 | flash(error) 37 | return redirect(url_for("register")) 38 | 39 | 40 | @app.route("/login",methods=['GET','POST']) 41 | def login(): 42 | form = LoginForm(meta={"csrf":False}) 43 | if form.validate_on_submit(): 44 | email = form.email.data 45 | password = form.password.data 46 | print("email:",email) 47 | print("password:",password) 48 | return redirect("/") 49 | print(form.errors) 50 | return render_template("login.html",form=form) 51 | 52 | 53 | 54 | if __name__ == '__main__': 55 | app.run() 56 | -------------------------------------------------------------------------------- /formlearn/forms.py: -------------------------------------------------------------------------------- 1 | from wtforms import Form, StringField,BooleanField,SubmitField, ValidationError,PasswordField 2 | from wtforms.validators import length, email, equal_to 3 | from flask_wtf import FlaskForm 4 | 5 | registed_email = ['aa@example.com', 'bb@example.com'] 6 | 7 | 8 | class RegisterForm(Form): 9 | username = StringField(validators=[length(min=3, max=20, message="请输入正确长度的用户名!")]) 10 | email = StringField(validators=[email(message="请输入正确格式的邮箱!")]) 11 | password = StringField(validators=[length(min=6, max=20, message="请输入正确长度的密码!")]) 12 | confirm_password = StringField(validators=[equal_to("password", message="两次密码不一致!")]) 13 | 14 | def validate_email(self, field): 15 | email = field.data 16 | if email in registed_email: 17 | raise ValidationError("邮箱已经被注册!") 18 | return True 19 | 20 | 21 | class LoginForm(FlaskForm): 22 | email = StringField(label="邮箱:",validators=[email(message="请输入正确格式的邮箱!")],render_kw={"placeholder":"请输入邮箱"}) 23 | password = PasswordField(label="密码:",validators=[length(min=6, max=20, message="请输入正确长度的密码!")],render_kw={"placeholder":"请输入密码"}) 24 | remember = BooleanField(label="记住我:") 25 | submit = SubmitField(label="提交") 26 | -------------------------------------------------------------------------------- /formlearn/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}{% endblock %} 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /formlearn/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 登录 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% for error in form.email.errors %} 20 | 21 | 22 | 23 | 24 | {% endfor %} 25 | 26 | 27 | 28 | 29 | {% for error in form.password.errors %} 30 | 31 | 32 | 33 | 34 | {% endfor %} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
{{ form.csrf_token }}
{{ form.email.label }}{{ form.email }}
{{ error }}
{{ form.password.label }}{{ form.password }}
{{ error }}
{{ form.remember.label }}{{ form.remember() }}
{{ form.submit }}
45 |
46 | 47 | -------------------------------------------------------------------------------- /formlearn/templates/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 注册 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
用户名:
邮箱:
密码:
确认密码:
35 | 40 |
41 | 42 | -------------------------------------------------------------------------------- /im_demo/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /im_demo/.idea/im_demo.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /im_demo/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64 | -------------------------------------------------------------------------------- /im_demo/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /im_demo/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /im_demo/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /im_demo/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /im_demo/__pycache__/app.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/im_demo/__pycache__/app.cpython-39.pyc -------------------------------------------------------------------------------- /im_demo/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, redirect, session 2 | from flask_socketio import SocketIO, emit, join_room, leave_room, send 3 | from functools import wraps 4 | 5 | app = Flask(__name__) 6 | app.config['SECRET_KEY'] = "fasdfsdfasdkj" 7 | socketio = SocketIO(app) 8 | 9 | 10 | class UserManager: 11 | # 单例实例对象 12 | __instance = None 13 | # 所有用户,里面存储的是字典类型,字典中分别为sid和username 14 | # 比如{"sid": "assdfsgsd", "username":"张三"} 15 | _users = [] 16 | 17 | # 设置单例设计模式 18 | def __new__(cls, *args, **kwargs): 19 | if not cls.__instance: 20 | cls.__instance = super(UserManager, cls).__new__(cls) 21 | return cls.__instance 22 | 23 | # 添加用户 24 | @classmethod 25 | def add_user(cls, username, sid): 26 | for user in cls._users: 27 | if user['sid'] == sid or user['username'] == username: 28 | return False 29 | cls._users.append({"sid": sid, "username": username}) 30 | 31 | # 移除用户 32 | @classmethod 33 | def remove_user(cls, username): 34 | for user in cls._users: 35 | if user['username'] == username: 36 | cls._users.remove(user) 37 | return True 38 | return False 39 | 40 | # 根据key获取用户,key可以为sid或username 41 | @classmethod 42 | def get_user(cls, key): 43 | for user in cls._users: 44 | if user['sid'] == key or user['username'] == key: 45 | return user 46 | return None 47 | 48 | # 根据key判断是否有某个用户,key可以为sid或username 49 | @classmethod 50 | def has_user(cls, key): 51 | if cls.get_user(key): 52 | return True 53 | else: 54 | return False 55 | 56 | # 获取当前用户 57 | @classmethod 58 | def get_current_user(cls): 59 | username = session.get("username") 60 | for user in cls._users: 61 | if user['username'] == username: 62 | return user 63 | return None 64 | 65 | # 获取所有用户的用户名 66 | @classmethod 67 | def all_username(cls): 68 | return [user['username'] for user in cls._users] 69 | 70 | 71 | rooms = ["Flask交流群"] 72 | 73 | 74 | def login_required(func): 75 | @wraps(func) 76 | def wrapper(*args, **kwargs): 77 | if not session.get("username"): 78 | return redirect("/login") 79 | else: 80 | return func(*args, **kwargs) 81 | return wrapper 82 | 83 | 84 | class ResultCode: 85 | OK = 200 86 | ERROR_PARAMS = 400 87 | ERROR_SERVER = 500 88 | 89 | 90 | def result(code=ResultCode.OK, data=None, message=""): 91 | return {"code": code, "data": data or {}, "message": message} 92 | 93 | 94 | def send_personal(uid, message): 95 | data = { 96 | "message": message, 97 | "from_user": session.get("username") 98 | } 99 | return data 100 | 101 | 102 | @app.route('/') 103 | @login_required 104 | def index(): 105 | return render_template("index.html") 106 | 107 | 108 | @app.route("/login", methods=['GET','POST']) 109 | def login(): 110 | if request.method == 'GET': 111 | return render_template("login.html") 112 | else: 113 | username = request.form.get('username') 114 | if not username: 115 | return result(ResultCode.ERROR_PARAMS, message="请输入用户名") 116 | elif UserManager.has_user(username): 117 | return result(ResultCode.ERROR_PARAMS, message="此用户名已存在") 118 | session['username'] = username 119 | return result() 120 | 121 | 122 | @socketio.on('connect') 123 | @login_required 124 | def connect(): 125 | print("连接成功") 126 | UserManager.add_user(session.get("username"), request.sid) 127 | emit("users", {"users": UserManager.all_username()}, broadcast=True) 128 | return result(message="连接成功!") 129 | 130 | 131 | @socketio.on("disconnect") 132 | @login_required 133 | def disconnect(): 134 | UserManager.remove_user(session.get('username')) 135 | emit("users", {"users": UserManager.all_username()}, broadcast=True) 136 | 137 | 138 | @socketio.on("personal") 139 | def send_personal(data): 140 | to_username = data.get('to_user') 141 | message = data.get('message') 142 | if not to_username or not UserManager.has_user(to_username): 143 | return result(ResultCode.ERROR_PARAMS, message="请输入正确的目标用户") 144 | to_user = UserManager.get_user(to_username) 145 | emit("personal" ,{"message": message, "from_user": session.get("username")}, room=[to_user.get("sid")]) 146 | 147 | 148 | @socketio.on("join") 149 | @login_required 150 | def join(data): 151 | room = data.get("room") 152 | join_room(room) 153 | username = session.get("username") 154 | print(username+"加入群聊") 155 | send(username+"加入群聊", to=room) 156 | # 如果房间是新建的,则发布广播 157 | if not room in rooms: 158 | emit("rooms", {"rooms": rooms}, broadcast=True) 159 | 160 | 161 | @socketio.on("leave") 162 | @login_required 163 | def leave(data): 164 | room = data.get("room") 165 | leave_room(room) 166 | username = session.get("username") 167 | send(username+"离开群聊", to=room) 168 | 169 | 170 | @socketio.on("room_chat") 171 | @login_required 172 | def room_chat(data): 173 | room = data.get("room") 174 | message = data.get("message") 175 | from_user = UserManager.get_current_user().get("username") 176 | send({"message": message, "from_user": from_user}, to=room) 177 | 178 | 179 | if __name__ == '__main__': 180 | socketio.run() 181 | -------------------------------------------------------------------------------- /im_demo/static/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/im_demo/static/bg.jpg -------------------------------------------------------------------------------- /im_demo/static/index.js: -------------------------------------------------------------------------------- 1 | const socket = io(); 2 | 3 | let current_oppsite = ""; 4 | 5 | 6 | function bindAllUserClickEvent(){ 7 | $(".list-group-item").click(function (event){ 8 | let user = $(this).text().replace("【私】","").replace("【群】",""); 9 | current_oppsite = user; 10 | }); 11 | 12 | $(".list-group-item").click(function (event){ 13 | let room = $(this).text().replace("【私】","").replace("【群】",""); 14 | current_oppsite = user; 15 | }); 16 | } 17 | 18 | $(window).bind("beforeunload", function (){ 19 | socket.on("disconnect"); 20 | }); 21 | 22 | $(function (){ 23 | socket.on("users", function (result){ 24 | let users = result.users; 25 | let chats = []; 26 | for (let index = 0; index < users.length; index++) { 27 | let user = users[index]; 28 | chats.push({"group": false, "name": user}); 29 | } 30 | let raw_source = $("#chat-list-template").html(); 31 | let source = template.render(raw_source, {"group": false, chats}); 32 | $("#chat-ul").html(source); 33 | 34 | bindAllUserClickEvent(); 35 | }); 36 | 37 | socket.on("personal", function (data){ 38 | let message = data.message; 39 | let from_user = data.from_user; 40 | current_oppsite = from_user; 41 | let raw_source = $("#chat-content-template").html(); 42 | let source = template.render(raw_source, {"from_user": current_oppsite, "message": message}); 43 | $("#chat-list-box").append(source); 44 | }); 45 | 46 | socket.on("connect", function(){ 47 | console.log("连接成功"); 48 | $("#send-button").click(function (event){ 49 | event.preventDefault(); 50 | if(!current_oppsite){ 51 | alert("请先选择联系人!"); 52 | return; 53 | } 54 | let textarea = $("#chat-textarea"); 55 | let content = textarea.val(); 56 | socket.emit("personal", {"to_user": current_oppsite, "message": content}, function (){ 57 | let raw_source = $("#chat-content-template").html(); 58 | let source = template.render(raw_source, {"from_user": "me", "message": content}); 59 | $("#chat-list-box").append(source); 60 | }); 61 | textarea.val(""); 62 | }); 63 | 64 | socket.emit("join", {"room": "Flask交流群"}); 65 | socket.emit("room_chat", {"room": "Flask交流群", "message": "大家好"}); 66 | socket.on("room_chat", function (result){ 67 | socket.emit("room_hat", {"room": "Flask交流群", "message": "大家好"}); 68 | }); 69 | }); 70 | }); -------------------------------------------------------------------------------- /im_demo/static/login.js: -------------------------------------------------------------------------------- 1 | $(function (){ 2 | $("#submit-button").click(function (event){ 3 | event.preventDefault(); 4 | let username = $("#username-input").val(); 5 | if(!username){ 6 | alert("请输入用户名!"); 7 | return; 8 | } 9 | $.post({ 10 | url: "/login", 11 | data: {"username": username}, 12 | }).done(function (data){ 13 | let code = data['code']; 14 | if(code != 200){ 15 | alert(data['message']); 16 | return; 17 | }else{ 18 | window.location = "/"; 19 | } 20 | }); 21 | }); 22 | }); -------------------------------------------------------------------------------- /im_demo/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 知了聊天室 6 | 7 | 8 | 9 | 10 | 107 | 108 | 115 | 121 | 136 | 137 |
138 |
139 |
140 |
聊天列表
141 |
    142 |
143 |
144 |
145 |
聊天窗口
146 |
147 |
    148 |
149 |
150 |
151 | 152 |
153 | 154 | 155 |
156 |
157 |
158 |
159 |
160 | 161 | -------------------------------------------------------------------------------- /im_demo/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 知了聊天室 6 | 7 | 8 | 9 | 65 | 66 | 67 |
68 |

知了即时聊天登录

69 |
70 | 71 |
72 |
73 | 74 |
75 |
76 | 77 | -------------------------------------------------------------------------------- /pythonbbs/.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | __pycache__ 3 | .idea/ 4 | -------------------------------------------------------------------------------- /pythonbbs/app.py: -------------------------------------------------------------------------------- 1 | import click 2 | from flask import Flask 3 | from exts import db, mail, cache, csrf, avatars 4 | import config 5 | from flask_migrate import Migrate 6 | from blueprints.cms import bp as cms_bp 7 | from blueprints.front import bp as front_bp 8 | from blueprints.user import bp as user_bp 9 | from blueprints.media import bp as media_bp 10 | import commands 11 | from bbs_celery import make_celery 12 | import hooks 13 | import filters 14 | import logging 15 | 16 | 17 | app = Flask(__name__) 18 | app.config.from_object(config.DevelopmentConfig) 19 | db.init_app(app) 20 | mail.init_app(app) 21 | cache.init_app(app) 22 | avatars.init_app(app) 23 | 24 | 25 | # 设置日志级别 26 | app.logger.setLevel(logging.INFO) 27 | 28 | 29 | # CSRF保护 30 | csrf.init_app(app) 31 | 32 | migrate = Migrate(app, db) 33 | 34 | # 注册蓝图 35 | app.register_blueprint(cms_bp) 36 | app.register_blueprint(front_bp) 37 | app.register_blueprint(user_bp) 38 | app.register_blueprint(media_bp) 39 | 40 | # 添加命令 41 | app.cli.command("create-permission")(commands.create_permission) 42 | app.cli.command("create-role")(commands.create_role) 43 | app.cli.command("create-test-front")(commands.create_test_user) 44 | app.cli.command("create-board")(commands.create_board) 45 | app.cli.command("create-test-post")(commands.create_test_post) 46 | app.cli.command("create-admin")(commands.create_admin) 47 | 48 | # 构建celery 49 | celery = make_celery(app) 50 | 51 | # 添加钩子函数 52 | app.before_request(hooks.bbs_before_request) 53 | app.errorhandler(401)(hooks.bbs_401_error) 54 | app.errorhandler(404)(hooks.bbs_404_error) 55 | app.errorhandler(500)(hooks.bbs_500_error) 56 | 57 | # 添加模板过滤器 58 | app.template_filter("email_hash")(filters.email_hash) 59 | 60 | 61 | 62 | if __name__ == '__main__': 63 | app.run() 64 | -------------------------------------------------------------------------------- /pythonbbs/awebsite/static/cms/css/base.css: -------------------------------------------------------------------------------- 1 | .main-body{ 2 | width: 100%; 3 | display: flex; 4 | } 5 | 6 | .left-body{ 7 | width: 200px; 8 | position: relative; 9 | } 10 | 11 | .right-body{ 12 | flex: 1; 13 | } 14 | 15 | .sub-header { 16 | padding-bottom: 10px; 17 | border-bottom: 1px solid #eee; 18 | } 19 | 20 | /* 21 | * Top navigation 22 | * Hide default border to remove 1px line. 23 | */ 24 | .navbar-fixed-top { 25 | border: 0; 26 | } 27 | 28 | /* 29 | * Sidebar 30 | */ 31 | 32 | /* Hide for mobile, show later */ 33 | .sidebar { 34 | display: none; 35 | } 36 | @media (min-width: 768px) { 37 | .sidebar { 38 | box-sizing: border-box; 39 | width: 200px; 40 | position: fixed; 41 | top: 56px; 42 | bottom: 0; 43 | left: 0; 44 | z-index: 1000; 45 | display: block; 46 | padding: 20px; 47 | background-color: #363a47; 48 | border-right: 1px solid #eee; 49 | } 50 | } 51 | 52 | .nav-sidebar{ 53 | padding: 5px 0; 54 | margin-left: -20px; 55 | margin-right: -20px; 56 | } 57 | 58 | .nav-sidebar > li{ 59 | background: #494f60; 60 | border-bottom: 1px solid #363a47; 61 | border-top: 1px solid #666; 62 | line-height: 35px; 63 | } 64 | 65 | .nav-sidebar > li > a { 66 | background: #494f60; 67 | color: #9b9fb1; 68 | margin-left: 25px; 69 | display: block; 70 | } 71 | 72 | .nav-sidebar > li a span{ 73 | float: right; 74 | width: 10px; 75 | height:10px; 76 | border-style: solid; 77 | border-color: #9b9fb1 #9b9fb1 transparent transparent; 78 | border-width: 1px; 79 | transform: rotate(45deg); 80 | position: relative; 81 | top: 10px; 82 | margin-right: 10px; 83 | } 84 | 85 | .nav-sidebar > li > a:hover{ 86 | color: #fff; 87 | background: #494f60; 88 | text-decoration: none; 89 | } 90 | 91 | .nav-sidebar > li > .subnav{ 92 | display: none; 93 | } 94 | 95 | .nav-sidebar > li.unfold{ 96 | background: #494f60; 97 | } 98 | 99 | .nav-sidebar > li.unfold > .subnav{ 100 | display: block; 101 | } 102 | 103 | .nav-sidebar > li.unfold > a{ 104 | color: #db4055; 105 | } 106 | 107 | .nav-sidebar > li.unfold > a span{ 108 | transform: rotate(135deg); 109 | top: 5px; 110 | border-color: #db4055 #db4055 transparent transparent; 111 | } 112 | 113 | .subnav{ 114 | padding-left: 10px; 115 | padding-right: 10px; 116 | background: #363a47; 117 | overflow: hidden; 118 | } 119 | 120 | .subnav li{ 121 | overflow: hidden; 122 | margin-top: 10px; 123 | line-height: 25px; 124 | height: 25px; 125 | } 126 | 127 | .subnav li.active{ 128 | background: #db4055; 129 | } 130 | 131 | .subnav li a{ 132 | /*display: block;*/ 133 | color: #9b9fb1; 134 | padding-left: 30px; 135 | height:25px; 136 | line-height: 25px; 137 | } 138 | 139 | .subnav li a:hover{ 140 | color: #fff; 141 | } 142 | 143 | .nav-group{ 144 | margin-top: 10px; 145 | } 146 | 147 | 148 | .main { 149 | padding: 20px; 150 | } 151 | @media (min-width: 768px) { 152 | .main { 153 | padding-right: 40px; 154 | padding-left: 40px; 155 | } 156 | } 157 | .main .page-header { 158 | margin-top: 0; 159 | } 160 | 161 | 162 | /* 163 | * Placeholder dashboard ideas 164 | */ 165 | 166 | .placeholders { 167 | margin-bottom: 30px; 168 | text-align: center; 169 | } 170 | .placeholders h4 { 171 | margin-bottom: 0; 172 | } 173 | .placeholder { 174 | margin-bottom: 20px; 175 | } 176 | .placeholder img { 177 | display: inline-block; 178 | border-radius: 50%; 179 | } 180 | 181 | .main_content{ 182 | margin-top: 20px; 183 | } 184 | 185 | .top-group{ 186 | padding: 5px 10px; 187 | border-radius: 2px; 188 | background: #ecedf0; 189 | overflow: hidden; 190 | } 191 | 192 | .form-container{ 193 | width: 300px; 194 | } 195 | 196 | .top-box{ 197 | overflow: hidden; 198 | background: #ecedf0; 199 | padding: 10px; 200 | } -------------------------------------------------------------------------------- /pythonbbs/awebsite/static/common/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/pythonbbs/awebsite/static/common/images/logo.png -------------------------------------------------------------------------------- /pythonbbs/awebsite/static/common/zlajax.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var zlajax = { 3 | 'get': function (args) { 4 | args['method'] = "get" 5 | return this.ajax(args); 6 | }, 7 | 'post': function (args) { 8 | args['method'] = "post" 9 | return this.ajax(args); 10 | }, 11 | 'put': function(args){ 12 | args['method'] = "put" 13 | return this.ajax(args) 14 | }, 15 | 'delete': function(args){ 16 | args['method'] = 'delete' 17 | return this.ajax(args) 18 | }, 19 | 'ajax': function (args) { 20 | // 设置csrftoken 21 | this._ajaxSetup(); 22 | return $.ajax(args); 23 | }, 24 | '_ajaxSetup': function () { 25 | $.ajaxSetup({ 26 | 'beforeSend': function (xhr, settings) { 27 | if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) { 28 | var csrftoken = $('meta[name=csrf-token]').attr('content'); 29 | xhr.setRequestHeader("X-CSRFToken", csrftoken) 30 | } 31 | } 32 | }); 33 | } 34 | }; -------------------------------------------------------------------------------- /pythonbbs/awebsite/static/common/zlalert.js: -------------------------------------------------------------------------------- 1 | var ZLAlert = function () { 2 | this.alert = Swal.mixin({}); 3 | 4 | this.toast = Swal.mixin({ 5 | toast: true, 6 | position: "top", 7 | showConfirmButton: false, 8 | timer: 3000, 9 | timerProgressBar: false, 10 | didOpen: (toast) => { 11 | toast.addEventListener('mouseenter', Swal.stopTimer) 12 | toast.addEventListener('mouseleave', Swal.resumeTimer) 13 | } 14 | }); 15 | } 16 | 17 | ZLAlert.prototype.successToast = function (title){ 18 | this.toast.fire({ 19 | icon: "success", 20 | title 21 | }) 22 | } 23 | 24 | ZLAlert.prototype.infoToast = function (title){ 25 | this.toast.fire({ 26 | icon: "info", 27 | title 28 | }) 29 | } 30 | 31 | 32 | var zlalert = new ZLAlert(); -------------------------------------------------------------------------------- /pythonbbs/awebsite/static/common/zlparam.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Administrator on 2017/3/24. 3 | */ 4 | 5 | var zlparam = { 6 | setParam: function (href,key,value) { 7 | // 重新加载整个页面 8 | var isReplaced = false; 9 | var urlArray = href.split('?'); 10 | if(urlArray.length > 1){ 11 | var queryArray = urlArray[1].split('&'); 12 | for(var i=0; i < queryArray.length; i++){ 13 | var paramsArray = queryArray[i].split('='); 14 | if(paramsArray[0] == key){ 15 | paramsArray[1] = value; 16 | queryArray[i] = paramsArray.join('='); 17 | isReplaced = true; 18 | break; 19 | } 20 | } 21 | 22 | if(!isReplaced){ 23 | var params = {}; 24 | params[key] = value; 25 | if(urlArray.length > 1){ 26 | href = href + '&' + $.param(params); 27 | }else{ 28 | href = href + '?' + $.param(params); 29 | } 30 | }else{ 31 | var params = queryArray.join('&'); 32 | urlArray[1] = params; 33 | href = urlArray.join('?'); 34 | } 35 | }else{ 36 | var param = {}; 37 | param[key] = value; 38 | if(urlArray.length > 1){ 39 | href = href + '&' + $.param(param); 40 | }else{ 41 | href = href + '?' + $.param(param); 42 | } 43 | } 44 | return href; 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /pythonbbs/awebsite/static/common/zlqiniu.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var zlqiniu = { 5 | 'setUp': function(args) { 6 | var domain = args['domain']; 7 | var params = { 8 | browse_button:args['browse_btn'], 9 | runtimes: 'html5,flash,html4', //上传模式,依次退化 10 | max_file_size: '500mb', //文件最大允许的尺寸 11 | dragdrop: false, //是否开启拖拽上传 12 | chunk_size: '4mb', //分块上传时,每片的大小 13 | uptoken_url: args['uptoken_url'], //ajax请求token的url 14 | domain: domain, //图片下载时候的域名 15 | get_new_uptoken: false, //是否每次上传文件都要从业务服务器获取token 16 | auto_start: true, //如果设置了true,只要选择了图片,就会自动上传 17 | unique_names: true, 18 | multi_selection: false, 19 | filters: { 20 | mime_types :[ 21 | {title:'Image files',extensions: 'jpg,gif,png'}, 22 | {title:'Video files',extensions: 'flv,mpg,mpeg,avi,wmv,mov,asf,rm,rmvb,mkv,m4v,mp4'} 23 | ] 24 | }, 25 | log_level: 5, //log级别 26 | init: { 27 | 'FileUploaded': function(up,file,info) { 28 | if(args['success']){ 29 | var success = args['success']; 30 | file.name = domain + file.target_name; 31 | success(up,file,info); 32 | } 33 | }, 34 | 'Error': function(up,err,errTip) { 35 | if(args['error']){ 36 | var error = args['error']; 37 | error(up,err,errTip); 38 | } 39 | }, 40 | 'UploadProgress': function (up,file) { 41 | if(args['progress']){ 42 | args['progress'](up,file); 43 | } 44 | }, 45 | 'FilesAdded': function (up,files) { 46 | if(args['fileadded']){ 47 | args['fileadded'](up,files); 48 | } 49 | }, 50 | 'UploadComplete': function () { 51 | if(args['complete']){ 52 | args['complete'](); 53 | } 54 | } 55 | } 56 | }; 57 | 58 | // 把args中的参数放到params中去 59 | for(var key in args){ 60 | params[key] = args[key]; 61 | } 62 | var uploader = Qiniu.uploader(params); 63 | return uploader; 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /pythonbbs/awebsite/static/front/css/base.css: -------------------------------------------------------------------------------- 1 | a, abbr, acronym, address, applet, article, aside, audio, b, big, blockquote, body, canvas, caption, center, cite, code, dd, del, details, dfn, div, dl, dt, em, embed, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, html, i, iframe, img, ins, kbd, label, legend, li, mark, menu, nav, object, ol, output, p, pre, q, ruby, s, samp, section, small, span, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, thead, time, tr, tt, u, ul, var, video { 2 | margin: 0; 3 | padding: 0; 4 | border: 0; 5 | vertical-align: baseline; 6 | list-style: none; 7 | } 8 | 9 | button{ 10 | outline: none; 11 | border: none; 12 | } 13 | 14 | .main-container{ 15 | width: 990px; 16 | margin: 0 auto; 17 | overflow: hidden; 18 | } 19 | 20 | .lg-container{ 21 | width: 730px; 22 | float: left; 23 | } 24 | 25 | .sm-container{ 26 | width: 250px; 27 | float: right; 28 | } -------------------------------------------------------------------------------- /pythonbbs/awebsite/static/front/css/index.css: -------------------------------------------------------------------------------- 1 | .index-banner{ 2 | border-radius: 10px; 3 | overflow: hidden; 4 | height: 200px; 5 | } 6 | 7 | .index-banner img{ 8 | height: 200px; 9 | } 10 | 11 | .carousel-control-prev, .carousel-control-next{ 12 | background-color: rgba(0,0,0,0); 13 | } 14 | 15 | .carousel-control-prev:hover, .carousel-control-next:hover{ 16 | background-color: rgba(1,1,1,0.1); 17 | } 18 | 19 | .post-group{ 20 | border: 1px solid #ddd; 21 | margin-top: 20px; 22 | overflow: hidden; 23 | border-radius: 5px; 24 | padding: 10px; 25 | } 26 | 27 | .post-group-head{ 28 | overflow: hidden; 29 | list-style: none; 30 | } 31 | 32 | .post-group-head li{ 33 | float: left; 34 | padding: 5px 10px; 35 | } 36 | 37 | .post-group-head li a{ 38 | color:#333; 39 | } 40 | 41 | .post-group-head li.active{ 42 | background: #ccc; 43 | } 44 | 45 | .post-list-group{ 46 | margin-top: 20px; 47 | } 48 | 49 | .post-list-group li{ 50 | overflow: hidden; 51 | padding-bottom: 20px; 52 | } 53 | 54 | .author-avatar-group{ 55 | float: left; 56 | } 57 | 58 | .author-avatar-group img{ 59 | width: 50px; 60 | height: 50px; 61 | border-radius: 50%; 62 | } 63 | 64 | .post-info-group{ 65 | float: left; 66 | border-bottom: 1px solid #e6e6e6; 67 | width: 85%; 68 | padding-bottom: 10px; 69 | } 70 | 71 | .post-info-group .post-info{ 72 | margin-top: 10px; 73 | font-size: 12px; 74 | color: #8c8c8c; 75 | } 76 | 77 | .post-info span{ 78 | margin-right: 10px; 79 | } -------------------------------------------------------------------------------- /pythonbbs/awebsite/static/front/css/post_detail.css: -------------------------------------------------------------------------------- 1 | .post-container{ 2 | border: 1px solid #e6e6e6; 3 | padding: 10px; 4 | } 5 | 6 | .post-info-group{ 7 | font-size: 12px; 8 | color: #8c8c8c; 9 | border-bottom: 1px solid #e6e6e6; 10 | margin-top: 20px; 11 | padding-bottom: 10px; 12 | } 13 | 14 | .post-info-group span{ 15 | margin-right: 20px; 16 | } 17 | 18 | .post-content{ 19 | margin-top: 20px; 20 | } 21 | 22 | .post-content img{ 23 | max-width: 100%; 24 | } 25 | 26 | .comment-group{ 27 | margin-top: 20px; 28 | border: 1px solid #e8e8e8; 29 | padding: 10px; 30 | } 31 | 32 | .add-comment-group{ 33 | margin-top: 20px; 34 | padding: 10px; 35 | border: 1px solid #e8e8e8; 36 | } 37 | 38 | .add-comment-group h3{ 39 | margin-bottom: 10px; 40 | } 41 | 42 | .comment-btn-group{ 43 | margin-top: 10px; 44 | text-align:right; 45 | } 46 | 47 | .comment-list-group li{ 48 | overflow: hidden; 49 | padding: 10px 0; 50 | border-bottom: 1px solid #e8e8e8; 51 | } 52 | 53 | .avatar-group{ 54 | float: left; 55 | } 56 | 57 | .avatar-group img{ 58 | width: 50px; 59 | height: 50px; 60 | border-radius: 50%; 61 | } 62 | 63 | .comment-content{ 64 | float: left; 65 | margin-left:10px; 66 | } 67 | 68 | .comment-content .author-info{ 69 | font-size: 12px; 70 | color: #8c8c8c; 71 | } 72 | 73 | .author-info span{ 74 | margin-right: 10px; 75 | } 76 | 77 | .comment-content .comment-txt{ 78 | margin-top: 10px; 79 | } 80 | 81 | -------------------------------------------------------------------------------- /pythonbbs/awebsite/static/front/css/sign.css: -------------------------------------------------------------------------------- 1 | body{ 2 | background: #f3f3f3; 3 | } 4 | .outer-box{ 5 | width: 854px; 6 | background: #fff; 7 | margin: 0 auto; 8 | overflow: hidden; 9 | } 10 | .page-title{ 11 | padding-top: 50px; 12 | text-align: center; 13 | } 14 | .sign-box{ 15 | width: 300px; 16 | margin: 0 auto; 17 | padding-top: 20px; 18 | } 19 | .captcha-addon{ 20 | padding: 0; 21 | overflow: hidden; 22 | } 23 | .captcha-img{ 24 | width: 94px; 25 | height: 32px; 26 | cursor: pointer; 27 | } -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/cms/add_staff.html: -------------------------------------------------------------------------------- 1 | {% extends "cms/base.html" %} 2 | 3 | {% block title -%} 4 | 添加员工 5 | {%- endblock %} 6 | 7 | {% block head %} 8 | {% endblock %} 9 | 10 | {% block page_title -%} 11 | {{ self.title() }} 12 | {%- endblock %} 13 | 14 | {% block main_content %} 15 |
16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/cms/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block title %}{% endblock %} 11 | 12 | {% block head %}{% endblock %} 13 | 14 | 15 | 32 | 33 |
34 |
35 | 47 |
48 |
49 |
50 |

{% block page_title %}{% endblock %}

51 |
52 | {% block main_content %}{% endblock %} 53 |
54 |
55 |
56 |
57 | 58 | -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/cms/boards.html: -------------------------------------------------------------------------------- 1 | {% extends 'cms/base.html' %} 2 | 3 | {% block title %}板块管理{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block page_title %} 10 | {{ self.title() }} 11 | {% endblock %} 12 | 13 | {% block main_content %} 14 |
15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% for board in boards %} 28 | 29 | 30 | 31 | 32 | 40 | 41 | {% endfor %} 42 | 43 |
板块名称帖子数量创建时间操作
{{ board.name }}{{ board.posts|length }}{{ board.create_time }} 33 | 36 | 39 |
44 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/cms/comments.html: -------------------------------------------------------------------------------- 1 | {% extends 'cms/base.html' %} 2 | 3 | {% block title %}评论管理{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block page_title %} 10 | {{ self.title() }} 11 | {% endblock %} 12 | 13 | {% block main_content %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for comment in comments %} 26 | 27 | 28 | 29 | 30 | 31 | 38 | 39 | {% endfor %} 40 | 41 |
内容发布时间所属帖子作者操作
{{ comment.content }}{{ comment.create_time }}{{ comment.post.title }}{{ comment.author.username }} 32 | {% if comment.is_active %} 33 | 34 | {% else %} 35 | 36 | {% endif %} 37 |
42 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/cms/edit_staff.html: -------------------------------------------------------------------------------- 1 | {% extends "cms/base.html" %} 2 | 3 | {% block title -%} 4 | 编辑员工 5 | {%- endblock %} 6 | 7 | {% block head %} 8 | {% endblock %} 9 | 10 | {% block page_title -%} 11 | {{ self.title() }} 12 | {%- endblock %} 13 | 14 | {% block main_content %} 15 |
16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 | {% if user.is_staff %} 25 | 26 | {% else %} 27 | 28 | {% endif %} 29 | 30 |
31 |
32 | 33 | 34 |
35 |
36 |
37 | 38 | {% for role in roles %} 39 |
40 | {% if user.role.id == role.id %} 41 | 42 | {% else %} 43 | 44 | {% endif %} 45 | 46 |
47 | {% endfor %} 48 |
49 |
50 | 51 |
52 |
53 |
54 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/cms/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'cms/base.html' %} 2 | 3 | {% block title %} 4 | 知了CMS管理系统 5 | {% endblock %} 6 | 7 | {% block page_title %} 8 | 欢迎来到知了CMS管理系统 9 | {% endblock %} 10 | 11 | -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/cms/posts.html: -------------------------------------------------------------------------------- 1 | {% extends 'cms/base.html' %} 2 | 3 | {% block title %}帖子管理{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block page_title %} 10 | {{ self.title() }} 11 | {% endblock %} 12 | 13 | {% block main_content %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for post in posts %} 26 | 27 | 28 | 29 | 30 | 31 | 38 | 39 | {% endfor %} 40 | 41 | 42 |
标题发布时间板块作者操作
{{ post.title }}{{ post.create_time }}{{ post.board.name }}{{ post.author.username }} 32 | {% if post.is_active %} 33 | 34 | {% else %} 35 | 36 | {% endif %} 37 |
43 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/cms/staff_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'cms/base.html' %} 2 | 3 | {% block title %}CMS用户管理{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block page_title %} 10 | {{ self.title() }} 11 | {% endblock %} 12 | 13 | {% block main_content %} 14 | 添加员工 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 |
#邮箱用户名加入时间角色操作
1hynever@163.com张三2021-10-10运营 34 | 编辑 35 |
39 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/cms/users.html: -------------------------------------------------------------------------------- 1 | {% extends 'cms/base.html' %} 2 | 3 | {% block title %}用户管理{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block page_title %} 10 | {{ self.title() }} 11 | {% endblock %} 12 | 13 | {% block main_content %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for user in users %} 26 | 27 | 28 | 29 | 30 | 31 | 38 | 39 | {% endfor %} 40 | 41 |
#邮箱用户名加入时间操作
{{ loop.index }}{{ user.email }}{{ user.username }}{{ user.join_time }} 32 | {% if user.is_active %} 33 | 34 | {% else %} 35 | 36 | {% endif %} 37 |
42 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/errors/401.html: -------------------------------------------------------------------------------- 1 | {% extends "front/base.html" %} 2 | 3 | {% block body %} 4 |
5 |

401

6 |

您没有权限访问此页面!

7 |
8 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/errors/404.html: -------------------------------------------------------------------------------- 1 | {% extends "front/base.html" %} 2 | 3 | {% block body %} 4 |
5 |

404

6 |

您找的页面到火星去啦~

7 |
8 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/errors/500.html: -------------------------------------------------------------------------------- 1 | {% extends "front/base.html" %} 2 | 3 | {% block body %} 4 |
5 |

500

6 |

服务器开小差啦!

7 |
8 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/front/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% block title %}{% endblock %} 10 | {% block head %}{% endblock %} 11 | 12 | 13 | 40 |
41 | {% block body %}{% endblock %} 42 |
43 | 44 | -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/front/index.html: -------------------------------------------------------------------------------- 1 | {% extends "front/base.html" %} 2 | 3 | {% block title %}知了Python论坛{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block body %} 10 |
11 |
12 | 26 |
27 |
28 |
29 |
30 | 发布帖子 31 |
32 |
33 | 所有板块 34 |
35 |
36 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/front/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'front/base.html' %} 2 | 3 | {% block title %} 4 | 登录 5 | {% endblock %} 6 | 7 | {% block head %} 8 | 9 | {% endblock %} 10 | 11 | {% block body %} 12 |

登录

13 |
14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | 25 |
26 |
27 | 28 |
29 | 33 |
34 |
35 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/front/post_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'front/base.html' %} 2 | 3 | {% block title %} 4 | 帖子标题 5 | {% endblock %} 6 | 7 | {% block head %} 8 | 9 | {% endblock %} 10 | 11 | {% block body %} 12 |
13 |
14 |

钢铁是怎样炼成的

15 | 22 |
23 | 帖子内容 24 |
25 |
26 |
27 |

评论列表

28 | 41 |
42 |
43 |

发表评论

44 |
45 | 46 |
47 | 48 |
49 |
50 |
51 |
52 |
53 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/front/profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'front/base.html' %} 2 | 3 | {% block title %} 4 | 张三个人中心 5 | {% endblock %} 6 | 7 | {% block head %} 8 | 17 | {% endblock %} 18 | 19 | {% block body %} 20 |
21 |

张三个人中心

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
用户名:张三
头像: 31 | 32 |
签名:我就是我,不一样的烟火!
40 |
41 | 42 |
43 |
44 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/front/public_post.html: -------------------------------------------------------------------------------- 1 | {% extends "front/base.html" %} 2 | 3 | {% block title %} 4 | 发布帖子 5 | {% endblock %} 6 | 7 | {% block head %} 8 | {% endblock %} 9 | 10 | {% block body %} 11 |

发布帖子

12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/awebsite/templates/front/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'front/base.html' %} 2 | 3 | {% block title %} 4 | 知了课堂注册 5 | {% endblock %} 6 | 7 | {% block head %} 8 | 9 | {% endblock %} 10 | 11 | {% block body %} 12 |

注册

13 |
14 |
15 |
16 |
17 | 18 |
19 | 20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 | 37 |
38 |
39 | 40 | 找回密码 41 |
42 |
43 |
44 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/bbs_celery.py: -------------------------------------------------------------------------------- 1 | from flask_mail import Message 2 | from exts import mail 3 | from celery import Celery 4 | 5 | # 定义任务函数 6 | def send_mail(recipient,subject,body): 7 | message = Message(subject=subject,recipients=[recipient],body=body) 8 | mail.send(message) 9 | print("发送成功!") 10 | 11 | 12 | # 创建celery对象 13 | def make_celery(app): 14 | celery = Celery(app.import_name, backend=app.config['CELERY_RESULT_BACKEND'], 15 | broker=app.config['CELERY_BROKER_URL']) 16 | TaskBase = celery.Task 17 | 18 | class ContextTask(TaskBase): 19 | abstract = True 20 | 21 | def __call__(self, *args, **kwargs): 22 | with app.app_context(): 23 | return TaskBase.__call__(self, *args, **kwargs) 24 | 25 | celery.Task = ContextTask 26 | app.celery = celery 27 | 28 | # 添加任务 29 | celery.task(name="send_mail")(send_mail) 30 | 31 | return celery -------------------------------------------------------------------------------- /pythonbbs/blueprints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/pythonbbs/blueprints/__init__.py -------------------------------------------------------------------------------- /pythonbbs/blueprints/cms.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, g, redirect, render_template, request, flash, url_for 2 | from models.user import PermissionEnum 3 | from models.user import UserModel, RoleModel 4 | from models.post import PostModel, CommentModel, BoardModel 5 | from forms.cms import AddStaffForm, EditStaffForm, EditBoardForm 6 | from exts import db 7 | from decorators import permission_required 8 | from utils import restful 9 | 10 | 11 | bp = Blueprint("cms",__name__,url_prefix="/cms") 12 | 13 | @bp.before_request 14 | def cms_before_request(): 15 | if not hasattr(g,"user") or g.user.is_staff == False: 16 | return redirect("/") 17 | 18 | 19 | @bp.context_processor 20 | def cms_context_processor(): 21 | return {"PermissionEnum": PermissionEnum} 22 | 23 | 24 | @bp.get("") 25 | def index(): 26 | return render_template("cms/index.html") 27 | 28 | 29 | @bp.get("/staff/list") 30 | @permission_required(PermissionEnum.CMS_USER) 31 | def staff_list(): 32 | users = UserModel.query.filter_by(is_staff=True).all() 33 | return render_template("cms/staff_list.html", users=users) 34 | 35 | 36 | @bp.route("/staff/add",methods=['GET','POST']) 37 | @permission_required(PermissionEnum.CMS_USER) 38 | def add_staff(): 39 | if request.method == "GET": 40 | roles = RoleModel.query.all() 41 | return render_template("cms/add_staff.html",roles=roles) 42 | else: 43 | form = AddStaffForm(request.form) 44 | if form.validate(): 45 | email = form.email.data 46 | role_id = form.role.data 47 | user = UserModel.query.filter_by(email=email).first() 48 | if not user: 49 | flash("没有此用户!") 50 | return redirect(url_for("cms.add_staff")) 51 | user.is_staff = True 52 | user.role = RoleModel.query.get(role_id) 53 | db.session.commit() 54 | return redirect(url_for("cms.staff_list")) 55 | 56 | 57 | @bp.route("/staff/edit/",methods=['GET','POST']) 58 | @permission_required(PermissionEnum.CMS_USER) 59 | def edit_staff(user_id): 60 | user = UserModel.query.get(user_id) 61 | if request.method == 'GET': 62 | roles = RoleModel.query.all() 63 | return render_template("cms/edit_staff.html",user=user,roles=roles) 64 | else: 65 | form = EditStaffForm(request.form) 66 | if form.validate(): 67 | is_staff = form.is_staff.data 68 | role_id = form.role.data 69 | 70 | user.is_staff = is_staff 71 | if user.role.id != role_id: 72 | user.role = RoleModel.query.get(role_id) 73 | db.session.commit() 74 | return redirect(url_for("cms.edit_staff",user_id=user_id)) 75 | else: 76 | for message in form.messages: 77 | flash(message) 78 | return redirect(url_for("cms.edit_staff",user_id=user_id)) 79 | 80 | 81 | @bp.route("/users") 82 | @permission_required(PermissionEnum.FRONT_USER) 83 | def user_list(): 84 | users = UserModel.query.filter_by(is_staff=False).all() 85 | return render_template("cms/users.html",users=users) 86 | 87 | 88 | @bp.post("/users/active/") 89 | @permission_required(PermissionEnum.FRONT_USER) 90 | def active_user(user_id): 91 | is_active = request.form.get("is_active",type=int) 92 | if is_active == None: 93 | return restful.params_error(message="请传入is_active参数!") 94 | user = UserModel.query.get(user_id) 95 | user.is_active = bool(is_active) 96 | db.session.commit() 97 | return restful.ok() 98 | 99 | 100 | @bp.get('/posts') 101 | @permission_required(PermissionEnum.POST) 102 | def post_list(): 103 | posts = PostModel.query.all() 104 | return render_template("cms/posts.html",posts=posts) 105 | 106 | 107 | @bp.post('/posts/active/') 108 | @permission_required(PermissionEnum.POST) 109 | def active_post(post_id): 110 | is_active = request.form.get("is_active", type=int) 111 | if is_active == None: 112 | return restful.params_error(message="请传入is_active参数!") 113 | post = PostModel.query.get(post_id) 114 | post.is_active = bool(is_active) 115 | db.session.commit() 116 | return restful.ok() 117 | 118 | 119 | @bp.get('/comments') 120 | @permission_required(PermissionEnum.COMMENT) 121 | def comment_list(): 122 | comments = CommentModel.query.all() 123 | return render_template("cms/comments.html",comments=comments) 124 | 125 | 126 | @bp.post('/comments/active/') 127 | @permission_required(PermissionEnum.COMMENT) 128 | def active_comment(comment_id): 129 | is_active = request.form.get("is_active", type=int) 130 | if is_active == None: 131 | return restful.params_error(message="请传入is_active参数!") 132 | comment = CommentModel.query.get(comment_id) 133 | comment.is_active = bool(is_active) 134 | db.session.commit() 135 | return restful.ok() 136 | 137 | 138 | @bp.get("/boards") 139 | @permission_required(PermissionEnum.BOARD) 140 | def board_list(): 141 | boards = BoardModel.query.all() 142 | return render_template("cms/boards.html",boards=boards) 143 | 144 | 145 | @bp.post("/boards/edit") 146 | @permission_required(PermissionEnum.BOARD) 147 | def edit_board(): 148 | form = EditBoardForm(request.form) 149 | if form.validate(): 150 | board_id = form.board_id.data 151 | name = form.name.data 152 | board = BoardModel.query.get(board_id) 153 | board.name = name 154 | db.session.commit() 155 | return restful.ok() 156 | else: 157 | return restful.params_error(form.messages[0]) 158 | 159 | 160 | @bp.delete("/boards/active/") 161 | @permission_required(PermissionEnum.BOARD) 162 | def active_board(board_id): 163 | is_active = request.form.get("is_active", int) 164 | if is_active == None: 165 | return restful.params_error("请传入is_active参数!") 166 | board = BoardModel.query.get(board_id) 167 | board.is_active = bool(is_active) 168 | db.session.commit() 169 | return restful.ok() 170 | -------------------------------------------------------------------------------- /pythonbbs/blueprints/front.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, render_template, jsonify, current_app, url_for, send_from_directory, g, abort, redirect,flash 2 | from werkzeug.utils import secure_filename 3 | import os 4 | from models.post import PostModel, BoardModel, CommentModel 5 | from exts import csrf, db 6 | from decorators import login_required 7 | from forms.post import PublicPostForm, PublicCommentForm 8 | from utils import restful 9 | from flask_paginate import Pagination 10 | 11 | bp = Blueprint("front", __name__, url_prefix="") 12 | 13 | 14 | @bp.route("/") 15 | def index(): 16 | boards = BoardModel.query.filter_by(is_active=True).all() 17 | 18 | # 获取页码参数 19 | page = request.args.get("page", type=int, default=1) 20 | # 获取板块参数 21 | board_id = request.args.get("board_id",type=int,default=0) 22 | 23 | # 当前page下的起始位置 24 | start = (page - 1) * current_app.config.get("PER_PAGE_COUNT") 25 | # 当前page下的结束位置 26 | end = start + current_app.config.get("PER_PAGE_COUNT") 27 | 28 | # 查询对象 29 | query_obj = PostModel.query.filter_by(is_active=True).order_by(PostModel.create_time.desc()) 30 | # 过滤帖子 31 | if board_id: 32 | query_obj = query_obj.filter_by(board_id=board_id) 33 | # 总共有多少帖子 34 | total = query_obj.count() 35 | 36 | # 当前page下的帖子列表 37 | posts = query_obj.slice(start, end) 38 | 39 | # 分页对象 40 | pagination = Pagination(bs_version=4, page=page, total=total, outer_window=0, inner_window=2, alignment="center") 41 | 42 | context = { 43 | "posts": posts, 44 | "boards": boards, 45 | "pagination": pagination, 46 | "current_board": board_id 47 | } 48 | current_app.logger.info("index页面被请求了") 49 | return render_template("front/index.html", **context) 50 | 51 | 52 | @bp.route("/post/public", methods=['GET', 'POST']) 53 | @login_required 54 | def public_post(): 55 | if request.method == 'GET': 56 | boards = BoardModel.query.all() 57 | return render_template("front/public_post.html", boards=boards) 58 | else: 59 | form = PublicPostForm(request.form) 60 | if form.validate(): 61 | title = form.title.data 62 | content = form.content.data 63 | board_id = form.board_id.data 64 | post = PostModel(title=title, content=content, board_id=board_id, author=g.user) 65 | db.session.add(post) 66 | db.session.commit() 67 | return restful.ok() 68 | else: 69 | message = form.messages[0] 70 | return restful.params_error(message=message) 71 | 72 | 73 | @bp.route('/image/') 74 | def uploaded_image(filename): 75 | path = current_app.config.get("UPLOAD_IMAGE_PATH") 76 | return send_from_directory(path, filename) 77 | 78 | 79 | @bp.post("/upload/image") 80 | @csrf.exempt 81 | @login_required 82 | def upload_image(): 83 | f = request.files.get('image') 84 | extension = f.filename.split('.')[-1].lower() 85 | if extension not in ['jpg', 'gif', 'png', 'jpeg']: 86 | return jsonify({ 87 | "errno": 400, 88 | "data": [] 89 | }) 90 | filename = secure_filename(f.filename) 91 | f.save(os.path.join(current_app.config.get("UPLOAD_IMAGE_PATH"), filename)) 92 | url = url_for('front.uploaded_image', filename=filename) 93 | return jsonify({ 94 | "errno": 0, 95 | "data": [{ 96 | "url": url, 97 | "alt": "", 98 | "href": "" 99 | }] 100 | }) 101 | 102 | 103 | @bp.get("/post/detail/") 104 | def post_detail(post_id): 105 | post = PostModel.query.get(post_id) 106 | if not post.is_active: 107 | return abort(404) 108 | post.read_count += 1 109 | db.session.commit() 110 | return render_template("front/post_detail.html",post=post) 111 | 112 | 113 | @bp.post("/post//comment") 114 | @login_required 115 | def public_comment(post_id): 116 | form = PublicCommentForm(request.form) 117 | if form.validate(): 118 | content = form.content.data 119 | comment = CommentModel(content=content, post_id=post_id, author=g.user) 120 | db.session.add(comment) 121 | db.session.commit() 122 | else: 123 | for message in form.messages: 124 | flash(message) 125 | 126 | return redirect(url_for("front.post_detail", post_id=post_id)) -------------------------------------------------------------------------------- /pythonbbs/blueprints/media.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, current_app 2 | import os 3 | 4 | bp = Blueprint("media",__name__,url_prefix="/media") 5 | 6 | 7 | @bp.get("/") 8 | def media_file(filename): 9 | return os.path.join(current_app.config.get("UPLOAD_IMAGE_PATH"),filename) -------------------------------------------------------------------------------- /pythonbbs/blueprints/user.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, request, current_app, redirect, url_for, flash, session, g, send_from_directory 2 | from exts import cache,db 3 | import random 4 | from utils import restful 5 | from forms.user import RegisterForm, LoginForm, EditProfileForm 6 | from models.user import UserModel 7 | from decorators import login_required 8 | from werkzeug.datastructures import CombinedMultiDict 9 | from werkzeug.utils import secure_filename 10 | import os 11 | 12 | bp = Blueprint("user",__name__,url_prefix="/user") 13 | 14 | @bp.route("/register",methods=['GET','POST']) 15 | def register(): 16 | if request.method == 'GET': 17 | return render_template("front/register.html") 18 | else: 19 | form = RegisterForm(request.form) 20 | if form.validate(): 21 | email = form.email.data 22 | username = form.username.data 23 | password = form.password.data 24 | user = UserModel(email=email,username=username,password=password) 25 | db.session.add(user) 26 | db.session.commit() 27 | return redirect(url_for("user.login")) 28 | else: 29 | for message in form.messages: 30 | flash(message) 31 | return redirect(url_for("user.register")) 32 | 33 | 34 | @bp.route("/mail/captcha") 35 | def mail_captcha(): 36 | try: 37 | email = request.args.get("mail") 38 | digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] 39 | captcha = "".join(random.sample(digits, 4)) 40 | subject="【知了Python论坛】注册验证码" 41 | body = f"【知了Python论坛】您的注册验证码是:{captcha},请勿告诉别人!" 42 | current_app.celery.send_task("send_mail",(email,subject,body)) 43 | cache.set(email, captcha, timeout=100) 44 | return restful.ok() 45 | except Exception as e: 46 | print(e) 47 | return restful.server_error() 48 | 49 | 50 | @bp.route('/login',methods=['GET','POST']) 51 | def login(): 52 | if request.method == 'GET': 53 | return render_template("front/login.html") 54 | else: 55 | form = LoginForm(request.form) 56 | if form.validate(): 57 | email = form.email.data 58 | password = form.password.data 59 | remember = form.remember.data 60 | user = UserModel.query.filter_by(email=email).first() 61 | if user and user.check_password(password): 62 | if not user.is_active: 63 | flash("该用户已被禁用!") 64 | return redirect(url_for("user.login")) 65 | session['user_id'] = user.id 66 | if remember: 67 | session.permanent = True 68 | return redirect("/") 69 | else: 70 | flash("邮箱或者密码错误!") 71 | return redirect(url_for("login")) 72 | else: 73 | for message in form.messages: 74 | flash(message) 75 | return render_template("front/login.html") 76 | 77 | 78 | @bp.get('/logout') 79 | def logout(): 80 | session.clear() 81 | return redirect("/") 82 | 83 | 84 | @bp.get("/profile/") 85 | def profile(user_id): 86 | user = UserModel.query.get(user_id) 87 | is_mine = False 88 | if hasattr(g,"user") and g.user.id == user_id: 89 | is_mine = True 90 | context = { 91 | "user": user, 92 | "is_mine": is_mine 93 | } 94 | print(user) 95 | return render_template("front/profile.html",**context) 96 | 97 | 98 | @bp.post("/profile/edit") 99 | @login_required 100 | def edit_profile(): 101 | form = EditProfileForm(CombinedMultiDict([request.form,request.files])) 102 | if form.validate(): 103 | username = form.username.data 104 | avatar = form.avatar.data 105 | signature = form.signature.data 106 | 107 | # 如果上传了头像 108 | if avatar: 109 | # 生成安全的文件名 110 | filename = secure_filename(avatar.filename) 111 | # 拼接头像存储路径 112 | avatar_path = os.path.join(current_app.config.get("AVATARS_SAVE_PATH"), filename) 113 | # 保存文件 114 | avatar.save(avatar_path) 115 | # 设置头像的url 116 | g.user.avatar = url_for("media.media_file",filename=os.path.join("avatars",filename)) 117 | 118 | g.user.username = username 119 | g.user.signature = signature 120 | db.session.commit() 121 | return redirect(url_for("user.profile",user_id=g.user.id)) 122 | else: 123 | for message in form.messages: 124 | flash(message) 125 | return redirect(url_for("user.profile",user_id=g.user.id)) -------------------------------------------------------------------------------- /pythonbbs/commands.py: -------------------------------------------------------------------------------- 1 | from models.user import PermissionModel,RoleModel,PermissionEnum,UserModel 2 | from models.post import BoardModel,PostModel 3 | import click 4 | from exts import db 5 | from faker import Faker 6 | import random 7 | 8 | 9 | def create_permission(): 10 | for permission_name in dir(PermissionEnum): 11 | if permission_name.startswith("__"): 12 | continue 13 | permission = PermissionModel(name=getattr(PermissionEnum,permission_name)) 14 | db.session.add(permission) 15 | db.session.commit() 16 | click.echo("权限添加成功!") 17 | 18 | 19 | def create_role(): 20 | # 稽查员 21 | inspector = RoleModel(name="稽查",desc="负责审核帖子和评论是否合法合规!") 22 | inspector.permissions = PermissionModel.query.filter(PermissionModel.name.in_([PermissionEnum.POST,PermissionEnum.COMMENT])).all() 23 | 24 | # 运营 25 | operator = RoleModel(name="运营",desc="负责网站持续正常运营!") 26 | operator.permissions = PermissionModel.query.filter(PermissionModel.name.in_([ 27 | PermissionEnum.POST, 28 | PermissionEnum.COMMENT, 29 | PermissionEnum.BOARD, 30 | PermissionEnum.FRONT_USER 31 | ])).all() 32 | 33 | # 管理员 34 | administrator = RoleModel(name="管理员",desc="负责整个网站所有工作!") 35 | administrator.permissions = PermissionModel.query.all() 36 | 37 | db.session.add_all([inspector,operator,administrator]) 38 | db.session.commit() 39 | click.echo("角色添加成功!") 40 | 41 | 42 | def create_test_user(): 43 | admin_role = RoleModel.query.filter_by(name="管理员").first() 44 | zhangsan = UserModel(username="张三",email="zhangsan@zlkt.net",password="111111",is_staff=True,role=admin_role) 45 | 46 | operator_role = RoleModel.query.filter_by(name="运营").first() 47 | lisi = UserModel(username="李四",email="lisi@zlkt.net",password="111111",is_staff=True,role=operator_role) 48 | 49 | inspector_role = RoleModel.query.filter_by(name="稽查").first() 50 | wangwu = UserModel(username="王五",email="wangwu@zlkt.net",password="111111",is_staff=True,role=inspector_role) 51 | 52 | db.session.add_all([zhangsan,lisi,wangwu]) 53 | db.session.commit() 54 | click.echo("测试用户添加成功!") 55 | 56 | 57 | @click.option("--username",'-u') 58 | @click.option("--email",'-e') 59 | @click.option("--password",'-p') 60 | def create_admin(username,email,password): 61 | admin_role = RoleModel.query.filter_by(name="管理员").first() 62 | admin_user = UserModel(username=username, email=email, password=password, is_staff=True, role=admin_role) 63 | db.session.add(admin_user) 64 | db.session.commit() 65 | click.echo("管理员创建成功!") 66 | 67 | 68 | def create_board(): 69 | board_names = ['Python语法', 'web开发', '数据分析', '测试开发', '运维开发'] 70 | for board_name in board_names: 71 | board = BoardModel(name=board_name) 72 | db.session.add(board) 73 | db.session.commit() 74 | click.echo("板块添加成功!") 75 | 76 | 77 | def create_test_post(): 78 | fake = Faker(locale="zh_CN") 79 | author = UserModel.query.first() 80 | boards = BoardModel.query.all() 81 | 82 | click.echo("开始生成测试帖子...") 83 | for x in range(98): 84 | title = fake.sentence() 85 | content = fake.paragraph(nb_sentences=10) 86 | random_index = random.randint(0,4) 87 | board = boards[random_index] 88 | post = PostModel(title=title, content=content, board=board, author=author) 89 | db.session.add(post) 90 | db.session.commit() 91 | click.echo("测试帖子生成成功!") 92 | 93 | -------------------------------------------------------------------------------- /pythonbbs/config.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | import os 3 | 4 | class BaseConfig: 5 | SECRET_KEY = "your secret key" 6 | SQLALCHEMY_TRACK_MODIFICATIONS = False 7 | 8 | PERMANENT_SESSION_LIFETIME = timedelta(days=7) 9 | 10 | UPLOAD_IMAGE_PATH = os.path.join(os.path.dirname(__file__),"media") 11 | 12 | PER_PAGE_COUNT = 10 13 | 14 | 15 | class DevelopmentConfig(BaseConfig): 16 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://root:root@127.0.0.1:3306/pythonbbs?charset=utf8mb4" 17 | 18 | # 邮箱配置 19 | MAIL_SERVER = "smtp.163.com" 20 | MAIL_USE_SSL = True 21 | MAIL_PORT = 465 22 | MAIL_USERNAME = "hynever@163.com" 23 | MAIL_PASSWORD = "1111111111111" 24 | MAIL_DEFAULT_SENDER = "hynever@163.com" 25 | 26 | # 缓存配置 27 | CACHE_TYPE = "RedisCache" 28 | CACHE_REDIS_HOST = "127.0.0.1" 29 | CACHE_REDIS_PORT = 6379 30 | 31 | # Celery配置 32 | # 格式:redis://:password@hostname:port/db_number 33 | CELERY_BROKER_URL = "redis://127.0.0.1:6379/0" 34 | CELERY_RESULT_BACKEND = "redis://127.0.0.1:6379/0" 35 | 36 | AVATARS_SAVE_PATH = os.path.join(BaseConfig.UPLOAD_IMAGE_PATH,"avatars") 37 | 38 | 39 | 40 | class TestingConfig(BaseConfig): 41 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://[测试服务器MySQL用户名]:[测试服务器MySQL密码]@[测试服务器MySQL域名]:[测试服务器MySQL端口号]/pythonbbs?charset=utf8mb4" 42 | 43 | 44 | class ProductionConfig(BaseConfig): 45 | SQLALCHEMY_DATABASE_URI = "mysql+pymysql://[生产环境服务器MySQL用户名]:[生产环境服务器MySQL密码]@[生产环境服务器MySQL域名]:[生产环境服务器MySQL端口号]/pythonbbs?charset=utf8mb4" 46 | -------------------------------------------------------------------------------- /pythonbbs/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from flask import redirect, url_for, g, abort, flash 3 | 4 | 5 | def login_required(func): 6 | @wraps(func) 7 | def inner(*args, **kwargs): 8 | if not hasattr(g, "user"): 9 | return redirect(url_for("user.login")) 10 | elif not g.user.is_active: 11 | flash("该用户已被禁用!") 12 | return redirect(url_for("user.login")) 13 | else: 14 | return func(*args, **kwargs) 15 | 16 | return inner 17 | 18 | 19 | def permission_required(permission): 20 | def outer(func): 21 | @wraps(func) 22 | def inner(*args, **kwargs): 23 | if hasattr(g,"user") and g.user.has_permission(permission): 24 | return func(*args, **kwargs) 25 | else: 26 | return abort(403) 27 | return inner 28 | return outer -------------------------------------------------------------------------------- /pythonbbs/exts.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from flask_mail import Mail 3 | from flask_caching import Cache 4 | from flask_wtf import CSRFProtect 5 | from flask_avatars import Avatars 6 | 7 | 8 | db = SQLAlchemy() 9 | mail = Mail() 10 | cache = Cache() 11 | csrf = CSRFProtect() 12 | avatars = Avatars() 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /pythonbbs/filters.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | def email_hash(email): 4 | return hashlib.md5(email.lower().encode("utf-8")).hexdigest() -------------------------------------------------------------------------------- /pythonbbs/forms/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/pythonbbs/forms/__init__.py -------------------------------------------------------------------------------- /pythonbbs/forms/baseform.py: -------------------------------------------------------------------------------- 1 | from wtforms import Form 2 | 3 | class BaseForm(Form): 4 | @property 5 | def messages(self): 6 | message_list = [] 7 | if self.errors: 8 | for errors in self.errors.values(): 9 | message_list.extend(errors) 10 | return message_list -------------------------------------------------------------------------------- /pythonbbs/forms/cms.py: -------------------------------------------------------------------------------- 1 | from .baseform import BaseForm 2 | from wtforms import StringField, IntegerField, BooleanField 3 | from wtforms.validators import Email, InputRequired, Length 4 | 5 | 6 | class AddStaffForm(BaseForm): 7 | email = StringField(validators=[Email(message="请输入正确格式的邮箱!")]) 8 | role = IntegerField(validators=[InputRequired(message="请选择角色!")]) 9 | 10 | 11 | class EditStaffForm(BaseForm): 12 | is_staff = BooleanField(validators=[InputRequired(message="请选择是否为员工!")]) 13 | role = IntegerField(validators=[InputRequired(message="请选择分组!")]) 14 | 15 | 16 | class EditBoardForm(BaseForm): 17 | board_id = IntegerField(validators=[InputRequired(message="请输入板块ID!")]) 18 | name = StringField(validators=[Length(min=1,max=20,message="请输入1-20位长度!")]) -------------------------------------------------------------------------------- /pythonbbs/forms/post.py: -------------------------------------------------------------------------------- 1 | from .baseform import BaseForm 2 | from wtforms import StringField,IntegerField 3 | from wtforms.validators import InputRequired,Length 4 | 5 | 6 | class PublicPostForm(BaseForm): 7 | title = StringField(validators=[Length(min=2,max=100,message='请输入正确长度的标题!')]) 8 | content = StringField(validators=[Length(min=2,message="请输入正确长度的内容!")]) 9 | board_id = IntegerField(validators=[InputRequired(message='请输入板块id!')]) 10 | 11 | 12 | class PublicCommentForm(BaseForm): 13 | content = StringField(validators=[Length(min=2,max=200,message="请输入正确长度的评论!")]) -------------------------------------------------------------------------------- /pythonbbs/forms/user.py: -------------------------------------------------------------------------------- 1 | from wtforms import Form,StringField,ValidationError,BooleanField,FileField 2 | from wtforms.validators import Email,EqualTo,Length 3 | from flask_wtf.file import FileAllowed 4 | from exts import cache 5 | from models.user import UserModel 6 | from .baseform import BaseForm 7 | 8 | class RegisterForm(BaseForm): 9 | email = StringField(validators=[Email(message="请输入正确格式的邮箱!")]) 10 | captcha = StringField(validators=[Length(min=4,max=4,message="请输入正确格式的验证码!")]) 11 | username = StringField(validators=[Length(min=2,max=20,message="请输入正确长度的用户名!")]) 12 | password = StringField(validators=[Length(min=6,max=20,message="请输入正确长度的密码!")]) 13 | confirm_password = StringField(validators=[EqualTo("password",message="两次密码不一致!")]) 14 | 15 | def validate_email(self,field): 16 | email = field.data 17 | user = UserModel.query.filter_by(email=email).first() 18 | if user: 19 | raise ValidationError(message="邮箱已经存在") 20 | 21 | 22 | def validate_captcha(self,field): 23 | captcha = field.data 24 | email = self.email.data 25 | cache_captcha = cache.get(email) 26 | if not cache_captcha or captcha != cache_captcha: 27 | raise ValidationError(message="验证码错误!") 28 | 29 | 30 | class LoginForm(BaseForm): 31 | email = StringField(validators=[Email(message="请输入正确格式的邮箱!")]) 32 | password = StringField(validators=[Length(min=6, max=20, message="请输入正确长度的密码!")]) 33 | remember = BooleanField() 34 | 35 | 36 | class EditProfileForm(BaseForm): 37 | username = StringField(validators=[Length(min=2,max=20,message="请输入正确格式的用户名!")]) 38 | avatar = FileField(validators=[FileAllowed(['jpg','jpeg','png'],message="文件类型错误!")]) 39 | signature = StringField() 40 | 41 | def validate_signature(self,field): 42 | signature = field.data 43 | if signature and len(signature) > 100: 44 | raise ValidationError(message="签名不能超过100个字符") -------------------------------------------------------------------------------- /pythonbbs/hooks.py: -------------------------------------------------------------------------------- 1 | from flask import session, g, render_template 2 | from models.user import UserModel 3 | 4 | 5 | def bbs_before_request(): 6 | if "user_id" in session: 7 | user_id = session.get("user_id") 8 | try: 9 | user = UserModel.query.get(user_id) 10 | setattr(g,"user",user) 11 | except Exception: 12 | pass 13 | 14 | 15 | def bbs_404_error(error): 16 | return render_template("errors/404.html"), 404 17 | 18 | 19 | def bbs_401_error(error): 20 | return render_template("errors/401.html"), 401 21 | 22 | 23 | def bbs_500_error(error): 24 | return render_template("errors/500.html"), 500 -------------------------------------------------------------------------------- /pythonbbs/media/1_huangyong1314.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/pythonbbs/media/1_huangyong1314.jpg -------------------------------------------------------------------------------- /pythonbbs/media/avatars/1_huangyong1314.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/pythonbbs/media/avatars/1_huangyong1314.jpg -------------------------------------------------------------------------------- /pythonbbs/migrations/README: -------------------------------------------------------------------------------- 1 | Single-database configuration for Flask. 2 | -------------------------------------------------------------------------------- /pythonbbs/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic,flask_migrate 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [logger_flask_migrate] 38 | level = INFO 39 | handlers = 40 | qualname = flask_migrate 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /pythonbbs/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from flask import current_app 7 | 8 | from alembic import context 9 | 10 | # this is the Alembic Config object, which provides 11 | # access to the values within the .ini file in use. 12 | config = context.config 13 | 14 | # Interpret the config file for Python logging. 15 | # This line sets up loggers basically. 16 | fileConfig(config.config_file_name) 17 | logger = logging.getLogger('alembic.env') 18 | 19 | # add your model's MetaData object here 20 | # for 'autogenerate' support 21 | # from myapp import mymodel 22 | # target_metadata = mymodel.Base.metadata 23 | config.set_main_option( 24 | 'sqlalchemy.url', 25 | str(current_app.extensions['migrate'].db.get_engine().url).replace( 26 | '%', '%%')) 27 | target_metadata = current_app.extensions['migrate'].db.metadata 28 | 29 | # other values from the config, defined by the needs of env.py, 30 | # can be acquired: 31 | # my_important_option = config.get_main_option("my_important_option") 32 | # ... etc. 33 | 34 | 35 | def run_migrations_offline(): 36 | """Run migrations in 'offline' mode. 37 | 38 | This configures the context with just a URL 39 | and not an Engine, though an Engine is acceptable 40 | here as well. By skipping the Engine creation 41 | we don't even need a DBAPI to be available. 42 | 43 | Calls to context.execute() here emit the given string to the 44 | script output. 45 | 46 | """ 47 | url = config.get_main_option("sqlalchemy.url") 48 | context.configure( 49 | url=url, target_metadata=target_metadata, literal_binds=True 50 | ) 51 | 52 | with context.begin_transaction(): 53 | context.run_migrations() 54 | 55 | 56 | def run_migrations_online(): 57 | """Run migrations in 'online' mode. 58 | 59 | In this scenario we need to create an Engine 60 | and associate a connection with the context. 61 | 62 | """ 63 | 64 | # this callback is used to prevent an auto-migration from being generated 65 | # when there are no changes to the schema 66 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 67 | def process_revision_directives(context, revision, directives): 68 | if getattr(config.cmd_opts, 'autogenerate', False): 69 | script = directives[0] 70 | if script.upgrade_ops.is_empty(): 71 | directives[:] = [] 72 | logger.info('No changes in schema detected.') 73 | 74 | connectable = current_app.extensions['migrate'].db.get_engine() 75 | 76 | with connectable.connect() as connection: 77 | context.configure( 78 | connection=connection, 79 | target_metadata=target_metadata, 80 | process_revision_directives=process_revision_directives, 81 | **current_app.extensions['migrate'].configure_args 82 | ) 83 | 84 | with context.begin_transaction(): 85 | context.run_migrations() 86 | 87 | 88 | if context.is_offline_mode(): 89 | run_migrations_offline() 90 | else: 91 | run_migrations_online() 92 | -------------------------------------------------------------------------------- /pythonbbs/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /pythonbbs/migrations/versions/47dde31b98c0_create_post_board_banner_comment_model.py: -------------------------------------------------------------------------------- 1 | """create post board banner comment model 2 | 3 | Revision ID: 47dde31b98c0 4 | Revises: 510b7126b120 5 | Create Date: 2021-08-21 13:26:31.582475 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '47dde31b98c0' 14 | down_revision = '510b7126b120' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('banner', 22 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 23 | sa.Column('name', sa.String(length=255), nullable=False), 24 | sa.Column('image_url', sa.String(length=255), nullable=False), 25 | sa.Column('link_url', sa.String(length=255), nullable=False), 26 | sa.Column('priority', sa.Integer(), nullable=True), 27 | sa.Column('create_time', sa.DateTime(), nullable=True), 28 | sa.PrimaryKeyConstraint('id') 29 | ) 30 | op.create_table('board', 31 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 32 | sa.Column('name', sa.String(length=20), nullable=False), 33 | sa.Column('create_time', sa.DateTime(), nullable=True), 34 | sa.PrimaryKeyConstraint('id') 35 | ) 36 | op.create_table('post', 37 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 38 | sa.Column('title', sa.String(length=200), nullable=False), 39 | sa.Column('content', sa.Text(), nullable=False), 40 | sa.Column('create_time', sa.DateTime(), nullable=True), 41 | sa.Column('board_id', sa.Integer(), nullable=True), 42 | sa.Column('author_id', sa.String(length=100), nullable=False), 43 | sa.ForeignKeyConstraint(['author_id'], ['user.id'], ), 44 | sa.ForeignKeyConstraint(['board_id'], ['board.id'], ), 45 | sa.PrimaryKeyConstraint('id') 46 | ) 47 | op.create_table('comment', 48 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 49 | sa.Column('content', sa.Text(), nullable=False), 50 | sa.Column('create_time', sa.DateTime(), nullable=True), 51 | sa.Column('post_id', sa.Integer(), nullable=True), 52 | sa.Column('author_id', sa.String(length=100), nullable=False), 53 | sa.ForeignKeyConstraint(['author_id'], ['user.id'], ), 54 | sa.ForeignKeyConstraint(['post_id'], ['post.id'], ), 55 | sa.PrimaryKeyConstraint('id') 56 | ) 57 | # ### end Alembic commands ### 58 | 59 | 60 | def downgrade(): 61 | # ### commands auto generated by Alembic - please adjust! ### 62 | op.drop_table('comment') 63 | op.drop_table('post') 64 | op.drop_table('board') 65 | op.drop_table('banner') 66 | # ### end Alembic commands ### 67 | -------------------------------------------------------------------------------- /pythonbbs/migrations/versions/510b7126b120_create_user_model.py: -------------------------------------------------------------------------------- 1 | """create front model 2 | 3 | Revision ID: 510b7126b120 4 | Revises: 5e1dacecfe8c 5 | Create Date: 2021-08-17 20:00:45.026424 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '510b7126b120' 14 | down_revision = '5e1dacecfe8c' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('front', 22 | sa.Column('id', sa.String(length=100), nullable=False), 23 | sa.Column('username', sa.String(length=50), nullable=False), 24 | sa.Column('_password', sa.String(length=200), nullable=False), 25 | sa.Column('email', sa.String(length=50), nullable=False), 26 | sa.Column('avatar', sa.String(length=100), nullable=True), 27 | sa.Column('signature', sa.String(length=100), nullable=True), 28 | sa.Column('join_time', sa.DateTime(), nullable=True), 29 | sa.Column('is_staff', sa.Boolean(), nullable=True), 30 | sa.Column('role_id', sa.Integer(), nullable=True), 31 | sa.ForeignKeyConstraint(['role_id'], ['role.id'], ), 32 | sa.PrimaryKeyConstraint('id'), 33 | sa.UniqueConstraint('email'), 34 | sa.UniqueConstraint('username') 35 | ) 36 | # ### end Alembic commands ### 37 | 38 | 39 | def downgrade(): 40 | # ### commands auto generated by Alembic - please adjust! ### 41 | op.drop_table('front') 42 | # ### end Alembic commands ### 43 | -------------------------------------------------------------------------------- /pythonbbs/migrations/versions/5e1dacecfe8c_create_permission_and_role_model.py: -------------------------------------------------------------------------------- 1 | """create permission and role model 2 | 3 | Revision ID: 5e1dacecfe8c 4 | Revises: 5 | Create Date: 2021-08-17 16:56:26.145003 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '5e1dacecfe8c' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('permission', 22 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 23 | sa.Column('name', sa.Enum('BOARD', 'POST', 'COMMENT', 'FRONT_USER', 'CMS_USER', name='permissionenum'), nullable=False), 24 | sa.PrimaryKeyConstraint('id'), 25 | sa.UniqueConstraint('name') 26 | ) 27 | op.create_table('role', 28 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 29 | sa.Column('name', sa.String(length=50), nullable=False), 30 | sa.Column('desc', sa.String(length=200), nullable=True), 31 | sa.Column('create_time', sa.DateTime(), nullable=True), 32 | sa.PrimaryKeyConstraint('id') 33 | ) 34 | op.create_table('role_permission_table', 35 | sa.Column('role_id', sa.Integer(), nullable=True), 36 | sa.Column('permission_id', sa.Integer(), nullable=True), 37 | sa.ForeignKeyConstraint(['permission_id'], ['permission.id'], ), 38 | sa.ForeignKeyConstraint(['role_id'], ['role.id'], ) 39 | ) 40 | # ### end Alembic commands ### 41 | 42 | 43 | def downgrade(): 44 | # ### commands auto generated by Alembic - please adjust! ### 45 | op.drop_table('role_permission_table') 46 | op.drop_table('role') 47 | op.drop_table('permission') 48 | # ### end Alembic commands ### 49 | -------------------------------------------------------------------------------- /pythonbbs/migrations/versions/7600f5255f11_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 7600f5255f11 4 | Revises: cd7a9d3254a5 5 | Create Date: 2021-08-25 20:40:59.559498 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '7600f5255f11' 14 | down_revision = 'cd7a9d3254a5' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('user', sa.Column('is_active', sa.Boolean(), server_default=sa.text('1'), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('user', 'is_active') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /pythonbbs/migrations/versions/8c2dc98e3105_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 8c2dc98e3105 4 | Revises: 7600f5255f11 5 | Create Date: 2021-08-25 21:55:24.380715 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import mysql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '8c2dc98e3105' 14 | down_revision = '7600f5255f11' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.drop_table('banner') 22 | op.add_column('comment', sa.Column('is_active', sa.Boolean(), server_default=sa.text('1'), nullable=True)) 23 | op.add_column('post', sa.Column('is_active', sa.Boolean(), server_default=sa.text('1'), nullable=True)) 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | op.drop_column('post', 'is_active') 30 | op.drop_column('comment', 'is_active') 31 | op.create_table('banner', 32 | sa.Column('id', mysql.INTEGER(), autoincrement=True, nullable=False), 33 | sa.Column('name', mysql.VARCHAR(length=255), nullable=False), 34 | sa.Column('image_url', mysql.VARCHAR(length=255), nullable=False), 35 | sa.Column('link_url', mysql.VARCHAR(length=255), nullable=False), 36 | sa.Column('priority', mysql.INTEGER(), autoincrement=False, nullable=True), 37 | sa.Column('create_time', mysql.DATETIME(), nullable=True), 38 | sa.PrimaryKeyConstraint('id'), 39 | mysql_collate='utf8mb4_0900_ai_ci', 40 | mysql_default_charset='utf8mb4', 41 | mysql_engine='InnoDB' 42 | ) 43 | # ### end Alembic commands ### 44 | -------------------------------------------------------------------------------- /pythonbbs/migrations/versions/c143fec1e7c7_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: c143fec1e7c7 4 | Revises: 8c2dc98e3105 5 | Create Date: 2021-08-26 12:04:25.002106 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'c143fec1e7c7' 14 | down_revision = '8c2dc98e3105' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('board', sa.Column('is_active', sa.Boolean(), server_default=sa.text('1'), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('board', 'is_active') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /pythonbbs/migrations/versions/cd7a9d3254a5_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: cd7a9d3254a5 4 | Revises: 47dde31b98c0 5 | Create Date: 2021-08-23 17:55:10.915387 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'cd7a9d3254a5' 14 | down_revision = '47dde31b98c0' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('post', sa.Column('read_count', sa.Integer(), nullable=True)) 22 | # ### end Alembic commands ### 23 | 24 | 25 | def downgrade(): 26 | # ### commands auto generated by Alembic - please adjust! ### 27 | op.drop_column('post', 'read_count') 28 | # ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /pythonbbs/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/pythonbbs/models/__init__.py -------------------------------------------------------------------------------- /pythonbbs/models/post.py: -------------------------------------------------------------------------------- 1 | from exts import db 2 | from datetime import datetime 3 | from sqlalchemy import text 4 | 5 | 6 | # 板块模型 7 | class BoardModel(db.Model): 8 | __tablename__ = 'board' 9 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 10 | name = db.Column(db.String(20), nullable=False) 11 | create_time = db.Column(db.DateTime, default=datetime.now) 12 | is_active = db.Column(db.Boolean, default=True) 13 | 14 | 15 | # 帖子模型 16 | class PostModel(db.Model): 17 | __tablename__ = 'post' 18 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 19 | title = db.Column(db.String(200), nullable=False) 20 | content = db.Column(db.Text, nullable=False) 21 | create_time = db.Column(db.DateTime, default=datetime.now) 22 | read_count = db.Column(db.Integer,default=0) 23 | is_active = db.Column(db.Boolean, default=True) 24 | board_id = db.Column(db.Integer, db.ForeignKey("board.id")) 25 | author_id = db.Column(db.String(100), db.ForeignKey("user.id"), nullable=False) 26 | 27 | board = db.relationship("BoardModel", backref="posts") 28 | author = db.relationship("UserModel", backref='posts') 29 | 30 | 31 | # 评论模型 32 | class CommentModel(db.Model): 33 | __tablename__ = 'comment' 34 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 35 | content = db.Column(db.Text, nullable=False) 36 | create_time = db.Column(db.DateTime, default=datetime.now) 37 | post_id = db.Column(db.Integer, db.ForeignKey("post.id")) 38 | author_id = db.Column(db.String(100), db.ForeignKey("user.id"), nullable=False) 39 | is_active = db.Column(db.Boolean, default=True) 40 | 41 | post = db.relationship("PostModel", backref=db.backref('comments',order_by=create_time.desc(),lazy="dynamic")) 42 | author = db.relationship("UserModel", backref='comments') 43 | -------------------------------------------------------------------------------- /pythonbbs/models/user.py: -------------------------------------------------------------------------------- 1 | from exts import db 2 | from datetime import datetime 3 | from shortuuid import uuid 4 | from enum import Enum 5 | from werkzeug.security import generate_password_hash,check_password_hash 6 | 7 | 8 | class PermissionEnum(Enum): 9 | BOARD = "板块" 10 | POST = "帖子" 11 | COMMENT = "评论" 12 | FRONT_USER = "前台用户" 13 | CMS_USER = "后台用户" 14 | 15 | 16 | class PermissionModel(db.Model): 17 | __tablename__ = "permission" 18 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 19 | name = db.Column(db.Enum(PermissionEnum), nullable=False, unique=True) 20 | 21 | 22 | role_permission_table = db.Table( 23 | "role_permission_table", 24 | db.Column("role_id", db.Integer, db.ForeignKey("role.id")), 25 | db.Column("permission_id", db.Integer, db.ForeignKey("permission.id")) 26 | ) 27 | 28 | 29 | class RoleModel(db.Model): 30 | __tablename__ = 'role' 31 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 32 | name = db.Column(db.String(50), nullable=False) 33 | desc = db.Column(db.String(200), nullable=True) 34 | create_time = db.Column(db.DateTime, default=datetime.now) 35 | 36 | permissions = db.relationship("PermissionModel", secondary=role_permission_table, backref="roles") 37 | 38 | 39 | class UserModel(db.Model): 40 | __tablename__ = 'user' 41 | id = db.Column(db.String(100), primary_key=True, default=uuid) 42 | username = db.Column(db.String(50), nullable=False,unique=True) 43 | _password = db.Column(db.String(200), nullable=False) 44 | email = db.Column(db.String(50), nullable=False, unique=True) 45 | avatar = db.Column(db.String(100)) 46 | signature = db.Column(db.String(100)) 47 | join_time = db.Column(db.DateTime, default=datetime.now) 48 | is_staff = db.Column(db.Boolean, default=False) 49 | is_active = db.Column(db.Boolean,default=True) 50 | 51 | # 外键 52 | role_id = db.Column(db.Integer, db.ForeignKey("role.id")) 53 | role = db.relationship("RoleModel", backref="users") 54 | 55 | def __init__(self, *args, **kwargs): 56 | if "password" in kwargs: 57 | self.password = kwargs.get('password') 58 | kwargs.pop("password") 59 | super(UserModel, self).__init__(*args, **kwargs) 60 | 61 | @property 62 | def password(self): 63 | return self._password 64 | 65 | @password.setter 66 | def password(self, raw_password): 67 | self._password = generate_password_hash(raw_password) 68 | 69 | def check_password(self, raw_password): 70 | result = check_password_hash(self.password, raw_password) 71 | return result 72 | 73 | def has_permission(self, permission): 74 | return permission in [permission.name for permission in self.role.permissions] -------------------------------------------------------------------------------- /pythonbbs/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.6.5 2 | amqp==5.0.6 3 | billiard==3.6.4.0 4 | blinker==1.4 5 | celery==5.1.2 6 | cffi==1.14.6 7 | click==7.1.2 8 | click-didyoumean==0.0.3 9 | click-plugins==1.1.1 10 | click-repl==0.2.0 11 | colorama==0.4.4 12 | cryptography==3.4.7 13 | dnspython==1.16.0 14 | email-validator==1.1.3 15 | enum-compat==0.0.3 16 | eventlet==0.31.1 17 | Faker==8.12.0 18 | Flask==2.0.1 19 | Flask-Avatars==0.2.2 20 | Flask-Caching==1.10.1 21 | Flask-Mail==0.9.1 22 | Flask-Migrate==3.1.0 23 | flask-paginate==0.8.1 24 | Flask-SQLAlchemy==2.5.1 25 | Flask-WTF==0.15.1 26 | gevent==21.8.0 27 | greenlet==1.1.1 28 | idna==3.2 29 | itsdangerous==2.0.1 30 | Jinja2==3.0.1 31 | kombu==5.1.0 32 | Mako==1.1.4 33 | MarkupSafe==2.0.1 34 | Pillow==8.3.1 35 | prompt-toolkit==3.0.19 36 | pycparser==2.20 37 | PyMySQL==1.0.2 38 | python-dateutil==2.8.2 39 | python-editor==1.0.4 40 | pytz==2021.1 41 | redis==3.5.3 42 | shortuuid==1.0.1 43 | six==1.16.0 44 | SQLAlchemy==1.4.22 45 | text-unidecode==1.3 46 | vine==5.0.0 47 | wcwidth==0.2.5 48 | Werkzeug==2.0.1 49 | WTForms==2.3.3 50 | zope.event==4.5.0 51 | zope.interface==5.4.0 52 | -------------------------------------------------------------------------------- /pythonbbs/static/cms/css/base.css: -------------------------------------------------------------------------------- 1 | .main-body{ 2 | width: 100%; 3 | display: flex; 4 | } 5 | 6 | .left-body{ 7 | width: 200px; 8 | position: relative; 9 | } 10 | 11 | .right-body{ 12 | flex: 1; 13 | } 14 | 15 | .sub-header { 16 | padding-bottom: 10px; 17 | border-bottom: 1px solid #eee; 18 | } 19 | 20 | /* 21 | * Top navigation 22 | * Hide default border to remove 1px line. 23 | */ 24 | .navbar-fixed-top { 25 | border: 0; 26 | } 27 | 28 | /* 29 | * Sidebar 30 | */ 31 | 32 | /* Hide for mobile, show later */ 33 | .sidebar { 34 | display: none; 35 | } 36 | @media (min-width: 768px) { 37 | .sidebar { 38 | box-sizing: border-box; 39 | width: 200px; 40 | position: fixed; 41 | top: 56px; 42 | bottom: 0; 43 | left: 0; 44 | z-index: 1000; 45 | display: block; 46 | padding: 20px; 47 | background-color: #363a47; 48 | border-right: 1px solid #eee; 49 | } 50 | } 51 | 52 | .nav-sidebar{ 53 | padding: 5px 0; 54 | margin-left: -20px; 55 | margin-right: -20px; 56 | } 57 | 58 | .nav-sidebar > li{ 59 | background: #494f60; 60 | border-bottom: 1px solid #363a47; 61 | border-top: 1px solid #666; 62 | line-height: 35px; 63 | } 64 | 65 | .nav-sidebar > li > a { 66 | background: #494f60; 67 | color: #9b9fb1; 68 | margin-left: 25px; 69 | display: block; 70 | } 71 | 72 | .nav-sidebar > li a span{ 73 | float: right; 74 | width: 10px; 75 | height:10px; 76 | border-style: solid; 77 | border-color: #9b9fb1 #9b9fb1 transparent transparent; 78 | border-width: 1px; 79 | transform: rotate(45deg); 80 | position: relative; 81 | top: 10px; 82 | margin-right: 10px; 83 | } 84 | 85 | .nav-sidebar > li > a:hover{ 86 | color: #fff; 87 | background: #494f60; 88 | text-decoration: none; 89 | } 90 | 91 | .nav-sidebar > li > .subnav{ 92 | display: none; 93 | } 94 | 95 | .nav-sidebar > li.unfold{ 96 | background: #494f60; 97 | } 98 | 99 | .nav-sidebar > li.unfold > .subnav{ 100 | display: block; 101 | } 102 | 103 | .nav-sidebar > li.unfold > a{ 104 | color: #db4055; 105 | } 106 | 107 | .nav-sidebar > li.unfold > a span{ 108 | transform: rotate(135deg); 109 | top: 5px; 110 | border-color: #db4055 #db4055 transparent transparent; 111 | } 112 | 113 | .subnav{ 114 | padding-left: 10px; 115 | padding-right: 10px; 116 | background: #363a47; 117 | overflow: hidden; 118 | } 119 | 120 | .subnav li{ 121 | overflow: hidden; 122 | margin-top: 10px; 123 | line-height: 25px; 124 | height: 25px; 125 | } 126 | 127 | .subnav li.active{ 128 | background: #db4055; 129 | } 130 | 131 | .subnav li a{ 132 | /*display: block;*/ 133 | color: #9b9fb1; 134 | padding-left: 30px; 135 | height:25px; 136 | line-height: 25px; 137 | } 138 | 139 | .subnav li a:hover{ 140 | color: #fff; 141 | } 142 | 143 | .nav-group{ 144 | margin-top: 10px; 145 | } 146 | 147 | 148 | .main { 149 | padding: 20px; 150 | } 151 | @media (min-width: 768px) { 152 | .main { 153 | padding-right: 40px; 154 | padding-left: 40px; 155 | } 156 | } 157 | .main .page-header { 158 | margin-top: 0; 159 | } 160 | 161 | 162 | /* 163 | * Placeholder dashboard ideas 164 | */ 165 | 166 | .placeholders { 167 | margin-bottom: 30px; 168 | text-align: center; 169 | } 170 | .placeholders h4 { 171 | margin-bottom: 0; 172 | } 173 | .placeholder { 174 | margin-bottom: 20px; 175 | } 176 | .placeholder img { 177 | display: inline-block; 178 | border-radius: 50%; 179 | } 180 | 181 | .main_content{ 182 | margin-top: 20px; 183 | } 184 | 185 | .top-group{ 186 | padding: 5px 10px; 187 | border-radius: 2px; 188 | background: #ecedf0; 189 | overflow: hidden; 190 | } 191 | 192 | .form-container{ 193 | width: 300px; 194 | } 195 | 196 | .top-box{ 197 | overflow: hidden; 198 | background: #ecedf0; 199 | padding: 10px; 200 | } -------------------------------------------------------------------------------- /pythonbbs/static/cms/js/boards.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/pythonbbs/static/cms/js/boards.js -------------------------------------------------------------------------------- /pythonbbs/static/cms/js/comments.js: -------------------------------------------------------------------------------- 1 | $(function (){ 2 | $(".active-btn").click(function (event){ 3 | event.preventDefault(); 4 | var $this = $(this); 5 | var is_active = parseInt($this.attr("data-active")); 6 | var message = is_active?"您确定要禁用此评论吗?":"您确定要取消禁用此评论吗?"; 7 | var comment_id = $this.attr("data-comment-id"); 8 | var result = confirm(message); 9 | if(!result){ 10 | return; 11 | } 12 | var data = { 13 | is_active: is_active?0:1 14 | } 15 | zlajax.post({ 16 | url: "/cms/comments/active/" + comment_id, 17 | data: data 18 | }).done(function (){ 19 | window.location.reload(); 20 | }).fail(function (error){ 21 | alert(error.message); 22 | }) 23 | }); 24 | }); -------------------------------------------------------------------------------- /pythonbbs/static/cms/js/posts.js: -------------------------------------------------------------------------------- 1 | $(function (){ 2 | $(".active-btn").click(function (event){ 3 | event.preventDefault(); 4 | var $this = $(this); 5 | var is_active = parseInt($this.attr("data-active")); 6 | var message = is_active?"您确定要禁用此帖子吗?":"您确定要取消禁用此帖子吗?"; 7 | var post_id = $this.attr("data-post-id"); 8 | var result = confirm(message); 9 | if(!result){ 10 | return; 11 | } 12 | var data = { 13 | is_active: is_active?0:1 14 | } 15 | console.log(data); 16 | zlajax.post({ 17 | url: "/cms/posts/active/" + post_id, 18 | data: data 19 | }).done(function (){ 20 | window.location.reload(); 21 | }).fail(function (error){ 22 | alert(error.message); 23 | }) 24 | }); 25 | }); -------------------------------------------------------------------------------- /pythonbbs/static/cms/js/users.js: -------------------------------------------------------------------------------- 1 | $(function (){ 2 | $(".active-btn").click(function (event){ 3 | event.preventDefault(); 4 | var $this = $(this); 5 | var is_active = parseInt($this.attr("data-active")); 6 | var message = is_active?"您确定要禁用此用户吗?":"您确定要取消禁用此用户吗?"; 7 | var user_id = $this.attr("data-user-id"); 8 | var result = confirm(message); 9 | if(!result){ 10 | return; 11 | } 12 | var data = { 13 | is_active: is_active?0:1 14 | } 15 | console.log(data); 16 | zlajax.post({ 17 | url: "/cms/users/active/" + user_id, 18 | data: data 19 | }).done(function (){ 20 | window.location.reload(); 21 | }).fail(function (error){ 22 | alert(error.message); 23 | }) 24 | }); 25 | }); -------------------------------------------------------------------------------- /pythonbbs/static/common/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/pythonbbs/static/common/images/logo.png -------------------------------------------------------------------------------- /pythonbbs/static/common/zlajax.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var zlajax = { 3 | 'get': function (args) { 4 | args['method'] = "get" 5 | return this.ajax(args); 6 | }, 7 | 'post': function (args) { 8 | args['method'] = "post" 9 | return this.ajax(args); 10 | }, 11 | 'put': function(args){ 12 | args['method'] = "put" 13 | return this.ajax(args) 14 | }, 15 | 'delete': function(args){ 16 | args['method'] = 'delete' 17 | return this.ajax(args) 18 | }, 19 | 'ajax': function (args) { 20 | // 设置csrftoken 21 | this._ajaxSetup(); 22 | return $.ajax(args); 23 | }, 24 | '_ajaxSetup': function () { 25 | $.ajaxSetup({ 26 | 'beforeSend': function (xhr, settings) { 27 | if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) { 28 | var csrftoken = $('meta[name=csrf-token]').attr('content'); 29 | xhr.setRequestHeader("X-CSRFToken", csrftoken) 30 | } 31 | } 32 | }); 33 | } 34 | }; -------------------------------------------------------------------------------- /pythonbbs/static/common/zlalert.js: -------------------------------------------------------------------------------- 1 | var ZLAlert = function () { 2 | this.alert = Swal.mixin({}); 3 | 4 | this.toast = Swal.mixin({ 5 | toast: true, 6 | position: "top", 7 | showConfirmButton: false, 8 | timer: 3000, 9 | timerProgressBar: false, 10 | didOpen: (toast) => { 11 | toast.addEventListener('mouseenter', Swal.stopTimer) 12 | toast.addEventListener('mouseleave', Swal.resumeTimer) 13 | } 14 | }); 15 | } 16 | 17 | ZLAlert.prototype.successToast = function (title){ 18 | this.toast.fire({ 19 | icon: "success", 20 | title 21 | }) 22 | } 23 | 24 | ZLAlert.prototype.infoToast = function (title){ 25 | this.toast.fire({ 26 | icon: "info", 27 | title 28 | }) 29 | } 30 | 31 | 32 | var zlalert = new ZLAlert(); -------------------------------------------------------------------------------- /pythonbbs/static/common/zlparam.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Administrator on 2017/3/24. 3 | */ 4 | 5 | var zlparam = { 6 | setParam: function (href,key,value) { 7 | // 重新加载整个页面 8 | var isReplaced = false; 9 | var urlArray = href.split('?'); 10 | if(urlArray.length > 1){ 11 | var queryArray = urlArray[1].split('&'); 12 | for(var i=0; i < queryArray.length; i++){ 13 | var paramsArray = queryArray[i].split('='); 14 | if(paramsArray[0] == key){ 15 | paramsArray[1] = value; 16 | queryArray[i] = paramsArray.join('='); 17 | isReplaced = true; 18 | break; 19 | } 20 | } 21 | 22 | if(!isReplaced){ 23 | var params = {}; 24 | params[key] = value; 25 | if(urlArray.length > 1){ 26 | href = href + '&' + $.param(params); 27 | }else{ 28 | href = href + '?' + $.param(params); 29 | } 30 | }else{ 31 | var params = queryArray.join('&'); 32 | urlArray[1] = params; 33 | href = urlArray.join('?'); 34 | } 35 | }else{ 36 | var param = {}; 37 | param[key] = value; 38 | if(urlArray.length > 1){ 39 | href = href + '&' + $.param(param); 40 | }else{ 41 | href = href + '?' + $.param(param); 42 | } 43 | } 44 | return href; 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /pythonbbs/static/common/zlqiniu.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var zlqiniu = { 5 | 'setUp': function(args) { 6 | var domain = args['domain']; 7 | var params = { 8 | browse_button:args['browse_btn'], 9 | runtimes: 'html5,flash,html4', //上传模式,依次退化 10 | max_file_size: '500mb', //文件最大允许的尺寸 11 | dragdrop: false, //是否开启拖拽上传 12 | chunk_size: '4mb', //分块上传时,每片的大小 13 | uptoken_url: args['uptoken_url'], //ajax请求token的url 14 | domain: domain, //图片下载时候的域名 15 | get_new_uptoken: false, //是否每次上传文件都要从业务服务器获取token 16 | auto_start: true, //如果设置了true,只要选择了图片,就会自动上传 17 | unique_names: true, 18 | multi_selection: false, 19 | filters: { 20 | mime_types :[ 21 | {title:'Image files',extensions: 'jpg,gif,png'}, 22 | {title:'Video files',extensions: 'flv,mpg,mpeg,avi,wmv,mov,asf,rm,rmvb,mkv,m4v,mp4'} 23 | ] 24 | }, 25 | log_level: 5, //log级别 26 | init: { 27 | 'FileUploaded': function(up,file,info) { 28 | if(args['success']){ 29 | var success = args['success']; 30 | file.name = domain + file.target_name; 31 | success(up,file,info); 32 | } 33 | }, 34 | 'Error': function(up,err,errTip) { 35 | if(args['error']){ 36 | var error = args['error']; 37 | error(up,err,errTip); 38 | } 39 | }, 40 | 'UploadProgress': function (up,file) { 41 | if(args['progress']){ 42 | args['progress'](up,file); 43 | } 44 | }, 45 | 'FilesAdded': function (up,files) { 46 | if(args['fileadded']){ 47 | args['fileadded'](up,files); 48 | } 49 | }, 50 | 'UploadComplete': function () { 51 | if(args['complete']){ 52 | args['complete'](); 53 | } 54 | } 55 | } 56 | }; 57 | 58 | // 把args中的参数放到params中去 59 | for(var key in args){ 60 | params[key] = args[key]; 61 | } 62 | var uploader = Qiniu.uploader(params); 63 | return uploader; 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /pythonbbs/static/front/css/base.css: -------------------------------------------------------------------------------- 1 | a, abbr, acronym, address, applet, article, aside, audio, b, big, blockquote, body, canvas, caption, center, cite, code, dd, del, details, dfn, div, dl, dt, em, embed, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, html, i, iframe, img, ins, kbd, label, legend, li, mark, menu, nav, object, ol, output, p, pre, q, ruby, s, samp, section, small, span, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, thead, time, tr, tt, u, ul, var, video { 2 | margin: 0; 3 | padding: 0; 4 | border: 0; 5 | vertical-align: baseline; 6 | list-style: none; 7 | } 8 | 9 | button{ 10 | outline: none; 11 | border: none; 12 | } 13 | 14 | .main-container{ 15 | width: 990px; 16 | margin: 0 auto; 17 | padding-top: 20px; 18 | overflow: hidden; 19 | } 20 | 21 | .lg-container{ 22 | width: 730px; 23 | float: left; 24 | } 25 | 26 | .sm-container{ 27 | width: 250px; 28 | float: right; 29 | } -------------------------------------------------------------------------------- /pythonbbs/static/front/css/index.css: -------------------------------------------------------------------------------- 1 | .post-group{ 2 | border: 1px solid #ddd; 3 | overflow: hidden; 4 | border-radius: 5px; 5 | padding: 10px; 6 | } 7 | 8 | .post-group-head{ 9 | overflow: hidden; 10 | list-style: none; 11 | } 12 | 13 | .post-group-head li{ 14 | float: left; 15 | padding: 5px 10px; 16 | } 17 | 18 | .post-group-head li a{ 19 | color:#333; 20 | } 21 | 22 | .post-group-head li.active{ 23 | background: #ccc; 24 | } 25 | 26 | .post-list-group li{ 27 | overflow: hidden; 28 | padding-bottom: 20px; 29 | } 30 | 31 | .author-avatar-group{ 32 | float: left; 33 | } 34 | 35 | .author-avatar-group img{ 36 | width: 50px; 37 | height: 50px; 38 | border-radius: 50%; 39 | } 40 | 41 | .post-info-group{ 42 | float: left; 43 | border-bottom: 1px solid #e6e6e6; 44 | width: 85%; 45 | padding-bottom: 10px; 46 | } 47 | 48 | .post-info-group .post-info{ 49 | margin-top: 10px; 50 | font-size: 12px; 51 | color: #8c8c8c; 52 | } 53 | 54 | .post-info span{ 55 | margin-right: 10px; 56 | } -------------------------------------------------------------------------------- /pythonbbs/static/front/css/post_detail.css: -------------------------------------------------------------------------------- 1 | .post-container{ 2 | border: 1px solid #e6e6e6; 3 | padding: 10px; 4 | } 5 | 6 | .post-info-group{ 7 | font-size: 12px; 8 | color: #8c8c8c; 9 | border-bottom: 1px solid #e6e6e6; 10 | margin-top: 20px; 11 | padding-bottom: 10px; 12 | } 13 | 14 | .post-info-group span{ 15 | margin-right: 20px; 16 | } 17 | 18 | .post-content{ 19 | margin-top: 20px; 20 | } 21 | 22 | .post-content img{ 23 | max-width: 100%; 24 | } 25 | 26 | .comment-group{ 27 | margin-top: 20px; 28 | border: 1px solid #e8e8e8; 29 | padding: 10px; 30 | } 31 | 32 | .add-comment-group{ 33 | margin-top: 20px; 34 | padding: 10px; 35 | border: 1px solid #e8e8e8; 36 | } 37 | 38 | .add-comment-group h3{ 39 | margin-bottom: 10px; 40 | } 41 | 42 | .comment-btn-group{ 43 | margin-top: 10px; 44 | text-align:right; 45 | } 46 | 47 | .comment-list-group li{ 48 | overflow: hidden; 49 | padding: 10px 0; 50 | border-bottom: 1px solid #e8e8e8; 51 | } 52 | 53 | .avatar-group{ 54 | float: left; 55 | } 56 | 57 | .avatar-group img{ 58 | width: 50px; 59 | height: 50px; 60 | border-radius: 50%; 61 | } 62 | 63 | .comment-content{ 64 | float: left; 65 | margin-left:10px; 66 | } 67 | 68 | .comment-content .author-info{ 69 | font-size: 12px; 70 | color: #8c8c8c; 71 | } 72 | 73 | .author-info span{ 74 | margin-right: 10px; 75 | } 76 | 77 | .comment-content .comment-txt{ 78 | margin-top: 10px; 79 | } 80 | 81 | -------------------------------------------------------------------------------- /pythonbbs/static/front/css/sign.css: -------------------------------------------------------------------------------- 1 | body{ 2 | background: #f3f3f3; 3 | } 4 | .outer-box{ 5 | width: 854px; 6 | background: #fff; 7 | margin: 0 auto; 8 | overflow: hidden; 9 | } 10 | .page-title{ 11 | padding-top: 50px; 12 | text-align: center; 13 | } 14 | .sign-box{ 15 | width: 300px; 16 | margin: 0 auto; 17 | padding-top: 20px; 18 | } 19 | .captcha-addon{ 20 | padding: 0; 21 | overflow: hidden; 22 | } 23 | .captcha-img{ 24 | width: 94px; 25 | height: 32px; 26 | cursor: pointer; 27 | } -------------------------------------------------------------------------------- /pythonbbs/static/front/js/public_post.js: -------------------------------------------------------------------------------- 1 | $(function (){ 2 | var editor = new window.wangEditor("#editor"); 3 | editor.config.uploadImgServer = "/upload/image"; 4 | editor.config.uploadFileName = "image"; 5 | editor.create(); 6 | 7 | 8 | // 提交按钮点击事件 9 | $("#submit-btn").click(function (event) { 10 | event.preventDefault(); 11 | 12 | var title = $("input[name='title']").val(); 13 | var board_id = $("select[name='board_id']").val(); 14 | var content = editor.txt.html(); 15 | 16 | zlajax.post({ 17 | url: "/post/public", 18 | data: {title,board_id,content} 19 | }).done(function(data){ 20 | setTimeout(function (){ 21 | window.location = "/"; 22 | },2000); 23 | }).fail(function(error){ 24 | alert(error.message); 25 | }); 26 | }); 27 | }); -------------------------------------------------------------------------------- /pythonbbs/static/front/js/register.js: -------------------------------------------------------------------------------- 1 | function captchaBtnClickEvent(event) { 2 | event.preventDefault(); 3 | var $this = $(this); 4 | 5 | // 获取邮箱 6 | var email = $("input[name='email']").val(); 7 | var reg = /^\w+((.\w+)|(-\w+))@[A-Za-z0-9]+((.|-)[A-Za-z0-9]+).[A-Za-z0-9]+$/; 8 | if (!email || !reg.test(email)) { 9 | alert("请输入正确格式的邮箱!"); 10 | return; 11 | } 12 | 13 | zlajax.get({ 14 | url: "/user/mail/captcha?mail=" + email 15 | }).done(function (result) { 16 | alert("验证码发送成功!"); 17 | }).fail(function (error) { 18 | alert(error.message); 19 | }) 20 | } 21 | 22 | $(function () { 23 | $('#captcha-btn').on("click",function(event) { 24 | event.preventDefault(); 25 | // 获取邮箱 26 | var email = $("input[name='email']").val(); 27 | 28 | zlajax.get({ 29 | url: "/user/mail/captcha?mail=" + email 30 | }).done(function (result) { 31 | alert("验证码发送成功!"); 32 | }).fail(function (error) { 33 | alert(error.message); 34 | }) 35 | }); 36 | }); -------------------------------------------------------------------------------- /pythonbbs/templates/cms/add_staff.html: -------------------------------------------------------------------------------- 1 | {% extends "cms/base.html" %} 2 | 3 | {% block title -%} 4 | 添加员工 5 | {%- endblock %} 6 | 7 | {% block head %} 8 | {% endblock %} 9 | 10 | {% block page_title -%} 11 | {{ self.title() }} 12 | {%- endblock %} 13 | 14 | {% block main_content %} 15 |
16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 | {% for role in roles %} 24 |
25 | 26 | 27 |
28 | {% endfor %} 29 |
30 |
31 | 32 |
33 |
34 |
35 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/templates/cms/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block title %}{% endblock %} 11 | 12 | {% block head %}{% endblock %} 13 | 14 | 15 | 35 | 36 |
37 |
38 | 59 |
60 |
61 |
62 |

{% block page_title %}{% endblock %}

63 |
64 | {% block main_content %}{% endblock %} 65 |
66 |
67 |
68 |
69 | 70 | -------------------------------------------------------------------------------- /pythonbbs/templates/cms/boards.html: -------------------------------------------------------------------------------- 1 | {% extends 'cms/base.html' %} 2 | 3 | {% block title %}板块管理{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block page_title %} 10 | {{ self.title() }} 11 | {% endblock %} 12 | 13 | {% block main_content %} 14 |
15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% for board in boards %} 28 | 29 | 30 | 31 | 32 | 40 | 41 | {% endfor %} 42 | 43 |
板块名称帖子数量创建时间操作
{{ board.name }}{{ board.posts|length }}{{ board.create_time }} 33 | 36 | 39 |
44 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/templates/cms/comments.html: -------------------------------------------------------------------------------- 1 | {% extends 'cms/base.html' %} 2 | 3 | {% block title %}评论管理{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block page_title %} 10 | {{ self.title() }} 11 | {% endblock %} 12 | 13 | {% block main_content %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for comment in comments %} 26 | 27 | 28 | 29 | 30 | 31 | 38 | 39 | {% endfor %} 40 | 41 |
内容发布时间所属帖子作者操作
{{ comment.content }}{{ comment.create_time }}{{ comment.post.title }}{{ comment.author.username }} 32 | {% if comment.is_active %} 33 | 34 | {% else %} 35 | 36 | {% endif %} 37 |
42 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/templates/cms/edit_staff.html: -------------------------------------------------------------------------------- 1 | {% extends "cms/base.html" %} 2 | 3 | {% block title -%} 4 | 编辑员工 5 | {%- endblock %} 6 | 7 | {% block head %} 8 | {% endblock %} 9 | 10 | {% block page_title -%} 11 | {{ self.title() }} 12 | {%- endblock %} 13 | 14 | {% block main_content %} 15 |
16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 | {% if user.is_staff %} 25 | 26 | {% else %} 27 | 28 | {% endif %} 29 | 30 |
31 |
32 | 33 | 34 |
35 |
36 |
37 | 38 | {% for role in roles %} 39 |
40 | {% if user.role.id == role.id %} 41 | 42 | {% else %} 43 | 44 | {% endif %} 45 | 46 |
47 | {% endfor %} 48 |
49 |
50 | 51 |
52 |
53 |
54 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/templates/cms/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'cms/base.html' %} 2 | 3 | {% block title %} 4 | 知了CMS管理系统 5 | {% endblock %} 6 | 7 | {% block page_title %} 8 | 欢迎来到知了CMS管理系统 9 | {% endblock %} 10 | 11 | -------------------------------------------------------------------------------- /pythonbbs/templates/cms/posts.html: -------------------------------------------------------------------------------- 1 | {% extends 'cms/base.html' %} 2 | 3 | {% block title %}帖子管理{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block page_title %} 10 | {{ self.title() }} 11 | {% endblock %} 12 | 13 | {% block main_content %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for post in posts %} 26 | 27 | 28 | 29 | 30 | 31 | 38 | 39 | {% endfor %} 40 | 41 | 42 |
标题发布时间板块作者操作
{{ post.title }}{{ post.create_time }}{{ post.board.name }}{{ post.author.username }} 32 | {% if post.is_active %} 33 | 34 | {% else %} 35 | 36 | {% endif %} 37 |
43 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/templates/cms/staff_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'cms/base.html' %} 2 | 3 | {% block title %}员工管理{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block page_title %} 10 | {{ self.title() }} 11 | {% endblock %} 12 | 13 | {% block main_content %} 14 | 添加员工 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% for user in users %} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 40 | {% endfor %} 41 | 42 |
#邮箱用户名加入时间角色操作
{{ loop.index }}{{ user.email }}{{ user.username }}{{ user.join_time }}{{ user.role.name }} 35 | {% if not user.has_permission(PermissionEnum.CMS_USER) %} 36 | 编辑 37 | {% endif %} 38 |
43 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/templates/cms/users.html: -------------------------------------------------------------------------------- 1 | {% extends 'cms/base.html' %} 2 | 3 | {% block title %}用户管理{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block page_title %} 10 | {{ self.title() }} 11 | {% endblock %} 12 | 13 | {% block main_content %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for user in users %} 26 | 27 | 28 | 29 | 30 | 31 | 38 | 39 | {% endfor %} 40 | 41 |
#邮箱用户名加入时间操作
{{ loop.index }}{{ user.email }}{{ user.username }}{{ user.join_time }} 32 | {% if user.is_active %} 33 | 34 | {% else %} 35 | 36 | {% endif %} 37 |
42 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/templates/errors/401.html: -------------------------------------------------------------------------------- 1 | {% extends "front/base.html" %} 2 | 3 | {% block body %} 4 |
5 |

401

6 |

您没有权限访问此页面!

7 |
8 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/templates/errors/404.html: -------------------------------------------------------------------------------- 1 | {% extends "front/base.html" %} 2 | 3 | {% block body %} 4 |
5 |

404

6 |

您找的页面到火星去啦~

7 |
8 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/templates/errors/500.html: -------------------------------------------------------------------------------- 1 | {% extends "front/base.html" %} 2 | 3 | {% block body %} 4 |
5 |

500

6 |

服务器开小差啦!

7 |
8 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/templates/front/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% block title %}{% endblock %} 12 | {% block head %}{% endblock %} 13 | 14 | 15 | 56 |
57 | {% block body %}{% endblock %} 58 |
59 | 60 | -------------------------------------------------------------------------------- /pythonbbs/templates/front/index.html: -------------------------------------------------------------------------------- 1 | {% extends "front/base.html" %} 2 | 3 | {% block title %}知了Python论坛{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block body %} 10 |
11 |
12 |
    13 | {% for post in posts %} 14 |
  • 15 | 25 |
  • 26 | {% endfor %} 27 |
28 | {{ pagination.links }} 29 |
30 |
31 |
32 |
33 | 发布帖子 34 |
35 |
36 | {% if current_board %} 37 | 所有板块 38 | {% else %} 39 | 所有板块 40 | {% endif %} 41 | {% for board in boards %} 42 | {% if board.id == current_board %} 43 | {{ board.name }} 44 | {% else %} 45 | {{ board.name }} 46 | {% endif %} 47 | {% endfor %} 48 |
49 |
50 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/templates/front/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'front/base.html' %} 2 | 3 | {% block title %} 4 | 登录 5 | {% endblock %} 6 | 7 | {% block head %} 8 | 9 | {% endblock %} 10 | 11 | {% block body %} 12 |

登录

13 |
14 |
15 | 16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | 26 |
27 | {% with messages = get_flashed_messages() %} 28 | {% if messages %} 29 |
30 |
    31 | {% for message in messages %} 32 |
  • {{ message }}
  • 33 | {% endfor %} 34 |
35 |
36 | {% endif %} 37 | {% endwith %} 38 |
39 | 40 |
41 | 45 |
46 |
47 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/templates/front/post_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'front/base.html' %} 2 | 3 | {% block title %} 4 | {{ post.title }} 5 | {% endblock %} 6 | 7 | {% block head %} 8 | 9 | {% endblock %} 10 | 11 | {% block body %} 12 |
13 |
14 |

{{ post.title }}

15 | 22 |
23 | {{ post.content|safe }} 24 |
25 |
26 |
27 |

评论列表

28 |
    29 | {% for comment in post.comments.filter_by(is_active=True) %} 30 |
  • 31 |
    32 |

    33 | {{ comment.author.username }} 34 | {{ comment.create_time }} 35 |

    36 |

    37 | {{ comment.content }} 38 |

    39 |
    40 |
  • 41 | {% endfor %} 42 |
43 |
44 |
45 |

发表评论

46 |
47 | 48 | 49 | {% with messages = get_flashed_messages() %} 50 | {% if messages %} 51 | {% for message in messages %} 52 |
{{ message }}
53 | {% endfor %} 54 | {% endif %} 55 | {% endwith %} 56 |
57 | 58 |
59 |
60 |
61 |
62 |
63 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/templates/front/profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'front/base.html' %} 2 | 3 | {% block title %} 4 | {{ user.username }}个人中心 5 | {% endblock %} 6 | 7 | {% block head %} 8 | 17 | {% endblock %} 18 | 19 | {% block body %} 20 |
21 |

{{ user.username }}个人中心

22 |
23 | 24 | 25 | 26 | 27 | 28 | 35 | 36 | 37 | 38 | 48 | 49 | 50 | 51 | 58 | 59 | 60 |
用户名: 29 | {% if is_mine %} 30 | 31 | {% else %} 32 | {{ user.username }} 33 | {% endif %} 34 |
头像: 39 | {% if user.avatar %} 40 | 41 | {% else %} 42 | 43 | {% endif %} 44 | {% if is_mine %} 45 | 46 | {% endif %} 47 |
签名: 52 | {% if is_mine %} 53 | 54 | {% else %} 55 | {{ user.signature or "" }} 56 | {% endif %} 57 |
61 | {% if is_mine %} 62 |
63 | 64 |
65 | {% endif %} 66 |
67 |
68 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/templates/front/public_post.html: -------------------------------------------------------------------------------- 1 | {% extends "front/base.html" %} 2 | 3 | {% block title %} 4 | 发布帖子 5 | {% endblock %} 6 | 7 | {% block head %} 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block body %} 13 |

发布帖子

14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | 27 |
28 |
29 | 30 |
31 |
32 |
33 | 34 |
35 |
36 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/templates/front/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'front/base.html' %} 2 | 3 | {% block title %} 4 | 知了课堂注册 5 | {% endblock %} 6 | 7 | {% block head %} 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block body %} 13 |

注册

14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 |
23 |
24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 | {% with messages = get_flashed_messages() %} 38 | {% if messages %} 39 |
40 |
    41 | {% for message in messages %} 42 |
  • {{ message }}
  • 43 | {% endfor %} 44 |
45 |
46 | {% endif %} 47 | {% endwith %} 48 |
49 | 50 |
51 |
52 | 53 | 找回密码 54 |
55 |
56 |
57 | {% endblock %} -------------------------------------------------------------------------------- /pythonbbs/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynever/flask_fullstack/5413bf4cf558169d58db9dd7993a152de4d4741c/pythonbbs/utils/__init__.py -------------------------------------------------------------------------------- /pythonbbs/utils/restful.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | 3 | 4 | class HttpCode(object): 5 | # 响应正常 6 | ok = 200 7 | # 没有登陆错误 8 | unloginerror = 401 9 | # 没有权限错误 10 | permissionerror = 403 11 | # 客户端参数错误 12 | paramserror = 400 13 | # 服务器错误 14 | servererror = 500 15 | 16 | 17 | def _restful_result(code, message, data): 18 | return jsonify({"message": message or "", "data": data or {}}), code 19 | 20 | 21 | def ok(message=None, data=None): 22 | return _restful_result(code=HttpCode.ok, message=message, data=data) 23 | 24 | 25 | def unlogin_error(message="没有登录!"): 26 | return _restful_result(code=HttpCode.unloginerror, message=message, data=None) 27 | 28 | 29 | def permission_error(message="没有权限访问!"): 30 | return _restful_result(code=HttpCode.paramserror, message=message, data=None) 31 | 32 | 33 | def params_error(message="参数错误!"): 34 | return _restful_result(code=HttpCode.paramserror, message=message, data=None) 35 | 36 | 37 | def server_error(message="服务器开小差啦!"): 38 | return _restful_result(code=HttpCode.servererror, message=message or '服务器内部错误', data=None) 39 | --------------------------------------------------------------------------------