├── .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 | {{ book.name }} |
20 | {{ book.author }} |
21 | {{ book.price }} |
22 |
23 | {% endfor %}
24 |
25 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
19 |
--------------------------------------------------------------------------------
/database/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/database/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/demo01/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/demo01/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/demo02/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/demo02/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/demo02/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/demo03/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/demo03/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/demo04/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/demo04/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/formlearn/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/formlearn/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
46 |
47 |
--------------------------------------------------------------------------------
/formlearn/templates/register.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 注册
6 |
7 |
8 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/im_demo/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/im_demo/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
144 |
145 |
聊天窗口
146 |
150 |
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 |
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 | {{ board.name }} |
30 | {{ board.posts|length }} |
31 | {{ board.create_time }} |
32 |
33 |
36 |
39 | |
40 |
41 | {% endfor %}
42 |
43 |
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 | {{ comment.content }} |
28 | {{ comment.create_time }} |
29 | {{ comment.post.title }} |
30 | {{ comment.author.username }} |
31 |
32 | {% if comment.is_active %}
33 |
34 | {% else %}
35 |
36 | {% endif %}
37 | |
38 |
39 | {% endfor %}
40 |
41 |
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 |
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 | {{ post.title }} |
28 | {{ post.create_time }} |
29 | {{ post.board.name }} |
30 | {{ post.author.username }} |
31 |
32 | {% if post.is_active %}
33 |
34 | {% else %}
35 |
36 | {% endif %}
37 | |
38 |
39 | {% endfor %}
40 |
41 |
42 |
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 | 1 |
29 | hynever@163.com |
30 | 张三 |
31 | 2021-10-10 |
32 | 运营 |
33 |
34 | 编辑
35 | |
36 |
37 |
38 |
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 | {{ loop.index }} |
28 | {{ user.email }} |
29 | {{ user.username }} |
30 | {{ user.join_time }} |
31 |
32 | {% if user.is_active %}
33 |
34 | {% else %}
35 |
36 | {% endif %}
37 | |
38 |
39 | {% endfor %}
40 |
41 |
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 |
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 |
13 | -
14 |
15 |
16 | 钢铁是怎样炼成的
17 |
18 |
19 | 作者: 张三
20 | 发表时间:2021-10-01
21 | 评论:0
22 |
23 |
24 |
25 |
26 |
27 |
28 |
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 |
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 |
16 | 发表时间:2021-10-01
17 | 作者:张三
18 | 所属板块:Python基础
19 | 阅读数:20
20 | 评论数:10
21 |
22 |
23 | 帖子内容
24 |
25 |
26 |
42 |
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 |
31 |
32 | |
33 |
34 |
35 | 签名: |
36 | 我就是我,不一样的烟火! |
37 |
38 |
39 |
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 |
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 |
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 |
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 | {{ board.name }} |
30 | {{ board.posts|length }} |
31 | {{ board.create_time }} |
32 |
33 |
36 |
39 | |
40 |
41 | {% endfor %}
42 |
43 |
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 | {{ comment.content }} |
28 | {{ comment.create_time }} |
29 | {{ comment.post.title }} |
30 | {{ comment.author.username }} |
31 |
32 | {% if comment.is_active %}
33 |
34 | {% else %}
35 |
36 | {% endif %}
37 | |
38 |
39 | {% endfor %}
40 |
41 |
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 |
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 | {{ post.title }} |
28 | {{ post.create_time }} |
29 | {{ post.board.name }} |
30 | {{ post.author.username }} |
31 |
32 | {% if post.is_active %}
33 |
34 | {% else %}
35 |
36 | {% endif %}
37 | |
38 |
39 | {% endfor %}
40 |
41 |
42 |
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 | {{ loop.index }} |
30 | {{ user.email }} |
31 | {{ user.username }} |
32 | {{ user.join_time }} |
33 | {{ user.role.name }} |
34 |
35 | {% if not user.has_permission(PermissionEnum.CMS_USER) %}
36 | 编辑
37 | {% endif %}
38 | |
39 |
40 | {% endfor %}
41 |
42 |
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 | {{ loop.index }} |
28 | {{ user.email }} |
29 | {{ user.username }} |
30 | {{ user.join_time }} |
31 |
32 | {% if user.is_active %}
33 |
34 | {% else %}
35 |
36 | {% endif %}
37 | |
38 |
39 | {% endfor %}
40 |
41 |
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 |
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 |
16 |
17 | {{ post.title }}
18 |
19 |
20 | 作者:{{ post.author.username }}
21 | 发表时间:{{ post.create_time }}
22 | 评论:{{ post.comments.all()|length }}
23 |
24 |
25 |
26 | {% endfor %}
27 |
28 | {{ pagination.links }}
29 |
30 |
31 |
32 |
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 |
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 |
16 | 发表时间:{{ post.create_time }}
17 | 作者:{{ post.author.username }}
18 | 所属板块:{{ post.board.name }}
19 | 阅读数:{{ post.read_count }}
20 | 评论数:{{ post.comments.count() }}
21 |
22 |
23 | {{ post.content|safe }}
24 |
25 |
26 |
44 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
32 | 张三 33 | 2021-10-01 34 |
35 |36 | 评论内容 37 |
38 |