Compare commits
13 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
e6f14a0521 | 1 month ago |
|
|
7bb5ae5142 | 1 month ago |
|
|
bf21a18e60 | 1 month ago |
|
|
7966b91f71 | 1 month ago |
|
|
a5de835956 | 1 month ago |
|
|
bdec3f0495 | 1 month ago |
|
|
f342f5881f | 1 month ago |
|
|
c97427c3d0 | 1 month ago |
|
|
f05c3d853d | 1 month ago |
|
|
6a57df2240 | 1 month ago |
|
|
c09ae84295 | 3 months ago |
|
|
8041305900 | 3 months ago |
|
|
92db6f54a2 | 3 months ago |
@ -0,0 +1,207 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
#poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||
#pdm.lock
|
||||
#pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# pixi
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||
#pixi.lock
|
||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||
.pixi
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Cursor
|
||||
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
||||
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||
# refer to https://docs.cursor.com/context/ignore-files
|
||||
.cursorignore
|
||||
.cursorindexingignore
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 flj666
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2025 车亮亮
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,10 @@
|
||||
[run]
|
||||
source = .
|
||||
include = *.py
|
||||
omit =
|
||||
*migrations*
|
||||
*tests*
|
||||
*.html
|
||||
*whoosh_cn_backend*
|
||||
*settings.py*
|
||||
*venv*
|
||||
@ -0,0 +1,11 @@
|
||||
bin/data/
|
||||
# virtualenv
|
||||
venv/
|
||||
collectedstatic/
|
||||
djangoblog/whoosh_index/
|
||||
uploads/
|
||||
settings_production.py
|
||||
*.md
|
||||
docs/
|
||||
logs/
|
||||
static/
|
||||
@ -0,0 +1,6 @@
|
||||
blog/static/* linguist-vendored
|
||||
*.js linguist-vendored
|
||||
*.css linguist-vendored
|
||||
* text=auto
|
||||
*.sh text eol=lf
|
||||
*.conf text eol=lf
|
||||
@ -0,0 +1,18 @@
|
||||
<!--
|
||||
如果你不认真勾选下面的内容,我可能会直接关闭你的 Issue。
|
||||
提问之前,建议先阅读 https://github.com/ruby-china/How-To-Ask-Questions-The-Smart-Way
|
||||
-->
|
||||
|
||||
**我确定我已经查看了** (标注`[ ]`为`[x]`)
|
||||
|
||||
- [ ] [DjangoBlog的readme](https://github.com/liangliangyy/DjangoBlog/blob/master/README.md)
|
||||
- [ ] [配置说明](https://github.com/liangliangyy/DjangoBlog/blob/master/bin/config.md)
|
||||
- [ ] [其他 Issues](https://github.com/liangliangyy/DjangoBlog/issues)
|
||||
|
||||
----
|
||||
|
||||
**我要申请** (标注`[ ]`为`[x]`)
|
||||
|
||||
- [ ] BUG 反馈
|
||||
- [ ] 添加新的特性或者功能
|
||||
- [ ] 请求技术支持
|
||||
@ -0,0 +1,47 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.css'
|
||||
- '**/*.js'
|
||||
- '**/*.yml'
|
||||
- '**/*.txt'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.css'
|
||||
- '**/*.js'
|
||||
- '**/*.yml'
|
||||
- '**/*.txt'
|
||||
schedule:
|
||||
- cron: '30 1 * * 0'
|
||||
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
@ -0,0 +1,136 @@
|
||||
name: Django CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.css'
|
||||
- '**/*.js'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.css'
|
||||
- '**/*.js'
|
||||
|
||||
jobs:
|
||||
build-normal:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
python-version: ["3.10","3.11" ]
|
||||
|
||||
steps:
|
||||
- name: Start MySQL
|
||||
uses: samin/mysql-action@v1.3
|
||||
with:
|
||||
host port: 3306
|
||||
container port: 3306
|
||||
character set server: utf8mb4
|
||||
collation server: utf8mb4_general_ci
|
||||
mysql version: latest
|
||||
mysql root password: root
|
||||
mysql database: djangoblog
|
||||
mysql user: root
|
||||
mysql password: root
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: Run Tests
|
||||
env:
|
||||
DJANGO_MYSQL_PASSWORD: root
|
||||
DJANGO_MYSQL_HOST: 127.0.0.1
|
||||
run: |
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
python manage.py test
|
||||
|
||||
build-with-es:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
python-version: ["3.10","3.11" ]
|
||||
|
||||
steps:
|
||||
- name: Start MySQL
|
||||
uses: samin/mysql-action@v1.3
|
||||
with:
|
||||
host port: 3306
|
||||
container port: 3306
|
||||
character set server: utf8mb4
|
||||
collation server: utf8mb4_general_ci
|
||||
mysql version: latest
|
||||
mysql root password: root
|
||||
mysql database: djangoblog
|
||||
mysql user: root
|
||||
mysql password: root
|
||||
|
||||
- name: Configure sysctl limits
|
||||
run: |
|
||||
sudo swapoff -a
|
||||
sudo sysctl -w vm.swappiness=1
|
||||
sudo sysctl -w fs.file-max=262144
|
||||
sudo sysctl -w vm.max_map_count=262144
|
||||
|
||||
- uses: miyataka/elasticsearch-github-actions@1
|
||||
|
||||
with:
|
||||
stack-version: '7.12.1'
|
||||
plugins: 'https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.12.1.zip'
|
||||
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: Run Tests
|
||||
env:
|
||||
DJANGO_MYSQL_PASSWORD: root
|
||||
DJANGO_MYSQL_HOST: 127.0.0.1
|
||||
DJANGO_ELASTICSEARCH_HOST: 127.0.0.1:9200
|
||||
run: |
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
coverage run manage.py test
|
||||
coverage xml
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: djangoblog/djangoblog:dev
|
||||
@ -0,0 +1,43 @@
|
||||
name: docker
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.yml'
|
||||
branches:
|
||||
- 'master'
|
||||
- 'dev'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set env to docker dev tag
|
||||
if: endsWith(github.ref, '/dev')
|
||||
run: |
|
||||
echo "DOCKER_TAG=test" >> $GITHUB_ENV
|
||||
- name: Set env to docker latest tag
|
||||
if: endsWith(github.ref, '/master')
|
||||
run: |
|
||||
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{env.DOCKER_TAG}}
|
||||
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
name: publish release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: name/app
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
linux/arm/v7
|
||||
linux/arm/v6
|
||||
linux/386
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{ github.event.release.tag_name }}
|
||||
@ -0,0 +1,226 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
logs/
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
#poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||
#pdm.lock
|
||||
#pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# pixi
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||
#pixi.lock
|
||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||
.pixi
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
.idea
|
||||
.iml
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Cursor
|
||||
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
||||
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||
# refer to https://docs.cursor.com/context/ignore-files
|
||||
.cursorignore
|
||||
.cursorindexingignore
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
# 本地项目特有忽略规则
|
||||
static/
|
||||
venv/
|
||||
collectedstatic/
|
||||
djangoblog/whoosh_index/
|
||||
google93fd32dbd906620a.html
|
||||
baidu_verify_FlHL7cUyC9.html
|
||||
BingSiteAuth.xml
|
||||
cb9339dbe2ff86a5aa169d28dba5f615.txt
|
||||
werobot_session.*
|
||||
django.jpg
|
||||
uploads/
|
||||
settings_production.py
|
||||
werobot_session.db
|
||||
bin/datas/
|
||||
@ -0,0 +1,15 @@
|
||||
FROM python:3.11
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
WORKDIR /code/djangoblog/
|
||||
RUN apt-get update && \
|
||||
apt-get install default-libmysqlclient-dev gettext -y && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN pip install --upgrade pip && \
|
||||
pip install --no-cache-dir -r requirements.txt && \
|
||||
pip install --no-cache-dir gunicorn[gevent] && \
|
||||
pip cache purge
|
||||
|
||||
COPY . .
|
||||
RUN chmod +x /code/djangoblog/deploy/entrypoint.sh
|
||||
ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"]
|
||||
@ -0,0 +1,42 @@
|
||||
<<<<<<< HEAD
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2025 车亮亮
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
=======
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 flj666
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
>>>>>>> 7baa91fead559a664046db2f89d0d51813f1ea38
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
<<<<<<< HEAD
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
=======
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
>>>>>>> 7baa91fead559a664046db2f89d0d51813f1ea38
|
||||
@ -0,0 +1,78 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.contrib.auth.forms import UserChangeForm
|
||||
from django.contrib.auth.forms import UsernameField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# Register your models here.
|
||||
from .models import BlogUser
|
||||
|
||||
|
||||
class BlogUserCreationForm(forms.ModelForm):
|
||||
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
|
||||
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
|
||||
|
||||
class Meta:
|
||||
model = BlogUser
|
||||
fields = ('email',)
|
||||
|
||||
def clean_password2(self):
|
||||
# Check that the two password entries match
|
||||
password1 = self.cleaned_data.get("password1")
|
||||
password2 = self.cleaned_data.get("password2")
|
||||
if password1 and password2 and password1 != password2:
|
||||
raise forms.ValidationError(_("passwords do not match"))
|
||||
return password2
|
||||
|
||||
def save(self, commit=True):
|
||||
# Save the provided password in hashed format
|
||||
user = super().save(commit=False)
|
||||
user.set_password(self.cleaned_data["password1"])
|
||||
if commit:
|
||||
user.source = 'adminsite'
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
class BlogUserChangeForm(UserChangeForm):
|
||||
class Meta:
|
||||
model = BlogUser
|
||||
fields = '__all__'
|
||||
field_classes = {'username': UsernameField}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class BlogUserAdmin(UserAdmin):
|
||||
form = BlogUserChangeForm
|
||||
add_form = BlogUserCreationForm
|
||||
list_display = (
|
||||
'id',
|
||||
'nickname',
|
||||
'username',
|
||||
'email',
|
||||
'last_login',
|
||||
'date_joined',
|
||||
'source')
|
||||
list_display_links = ('id', 'username')
|
||||
ordering = ('-id',)
|
||||
|
||||
# 添加 fieldsets 配置,解决 usable_password 字段错误
|
||||
fieldsets = (
|
||||
(None, {'fields': ('username', 'password')}),
|
||||
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email', 'nickname')}),
|
||||
(_('Permissions'), {
|
||||
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'),
|
||||
}),
|
||||
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
|
||||
(_('Other info'), {'fields': ('source', 'creation_time', 'last_modify_time')}),
|
||||
)
|
||||
|
||||
# 添加 add_fieldsets 配置
|
||||
add_fieldsets = (
|
||||
(None, {
|
||||
'classes': ('wide',),
|
||||
'fields': ('email', 'password1', 'password2'),
|
||||
}),
|
||||
)
|
||||
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
name = 'accounts'
|
||||
@ -0,0 +1,133 @@
|
||||
# 用户账户相关的表单定义
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model, password_validation
|
||||
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.forms import widgets
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from . import utils
|
||||
from .models import BlogUser
|
||||
|
||||
|
||||
class LoginForm(AuthenticationForm):
|
||||
"""用户登录表单"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoginForm, self).__init__(*args, **kwargs)
|
||||
# 为用户名输入框添加样式和占位符
|
||||
self.fields['username'].widget = widgets.TextInput(
|
||||
attrs={'placeholder': "username", "class": "form-control"})
|
||||
# 为密码输入框添加样式和占位符
|
||||
self.fields['password'].widget = widgets.PasswordInput(
|
||||
attrs={'placeholder': "password", "class": "form-control"})
|
||||
|
||||
|
||||
class RegisterForm(UserCreationForm):
|
||||
"""用户注册表单"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RegisterForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# 为所有输入框添加Bootstrap样式和占位符
|
||||
self.fields['username'].widget = widgets.TextInput(
|
||||
attrs={'placeholder': "username", "class": "form-control"})
|
||||
self.fields['email'].widget = widgets.EmailInput(
|
||||
attrs={'placeholder': "email", "class": "form-control"})
|
||||
self.fields['password1'].widget = widgets.PasswordInput(
|
||||
attrs={'placeholder': "password", "class": "form-control"})
|
||||
self.fields['password2'].widget = widgets.PasswordInput(
|
||||
attrs={'placeholder': "repeat password", "class": "form-control"})
|
||||
|
||||
def clean_email(self):
|
||||
"""验证邮箱是否已存在"""
|
||||
email = self.cleaned_data['email']
|
||||
if get_user_model().objects.filter(email=email).exists():
|
||||
raise ValidationError(_("email already exists"))
|
||||
return email
|
||||
|
||||
class Meta:
|
||||
model = get_user_model() # 使用自定义用户模型
|
||||
fields = ("username", "email") # 只包含用户名和邮箱字段
|
||||
|
||||
|
||||
class ForgetPasswordForm(forms.Form):
|
||||
"""忘记密码重置表单"""
|
||||
# 新密码字段
|
||||
new_password1 = forms.CharField(
|
||||
label=_("New password"),
|
||||
widget=forms.PasswordInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
'placeholder': _("New password")
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
# 确认新密码字段
|
||||
new_password2 = forms.CharField(
|
||||
label="确认密码",
|
||||
widget=forms.PasswordInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
'placeholder': _("Confirm password")
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
# 邮箱字段
|
||||
email = forms.EmailField(
|
||||
label='邮箱',
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': _("Email")
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
# 验证码字段
|
||||
code = forms.CharField(
|
||||
label=_('Code'),
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': _("Code")
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
def clean_new_password2(self):
|
||||
"""验证两次输入的密码是否一致"""
|
||||
password1 = self.data.get("new_password1")
|
||||
password2 = self.data.get("new_password2")
|
||||
if password1 and password2 and password1 != password2:
|
||||
raise ValidationError(_("passwords do not match"))
|
||||
# 使用Django内置的密码验证器
|
||||
password_validation.validate_password(password2)
|
||||
return password2
|
||||
|
||||
def clean_email(self):
|
||||
"""验证邮箱是否已注册"""
|
||||
user_email = self.cleaned_data.get("email")
|
||||
if not BlogUser.objects.filter(
|
||||
email=user_email
|
||||
).exists():
|
||||
# 注意:这里的报错提示可以判断一个邮箱是否注册过,如果不想暴露可以修改
|
||||
raise ValidationError(_("email does not exist"))
|
||||
return user_email
|
||||
|
||||
def clean_code(self):
|
||||
"""验证邮箱验证码是否正确"""
|
||||
code = self.cleaned_data.get("code")
|
||||
error = utils.verify(
|
||||
email=self.cleaned_data.get("email"),
|
||||
code=code,
|
||||
)
|
||||
if error:
|
||||
raise ValidationError(error)
|
||||
return code
|
||||
|
||||
|
||||
class ForgetPasswordCodeForm(forms.Form):
|
||||
"""发送忘记密码验证码的表单"""
|
||||
email = forms.EmailField(
|
||||
label=_('Email'), # 邮箱字段,用于发送验证码
|
||||
)
|
||||
@ -0,0 +1,49 @@
|
||||
# Generated by Django 4.1.7 on 2023-03-02 07:14
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BlogUser',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
|
||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户',
|
||||
'verbose_name_plural': '用户',
|
||||
'ordering': ['-id'],
|
||||
'get_latest_by': 'id',
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,46 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-06 13:13
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='bloguser',
|
||||
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='bloguser',
|
||||
name='created_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='bloguser',
|
||||
name='last_mod_time',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='bloguser',
|
||||
name='creation_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='bloguser',
|
||||
name='last_modify_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='bloguser',
|
||||
name='nickname',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='bloguser',
|
||||
name='source',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,207 @@
|
||||
from django.test import Client, RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.models import BlogUser
|
||||
from blog.models import Article, Category
|
||||
from djangoblog.utils import *
|
||||
from . import utils
|
||||
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
class AccountTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.factory = RequestFactory()
|
||||
self.blog_user = BlogUser.objects.create_user(
|
||||
username="test",
|
||||
email="admin@admin.com",
|
||||
password="12345678"
|
||||
)
|
||||
self.new_test = "xxx123--="
|
||||
|
||||
def test_validate_account(self):
|
||||
site = get_current_site().domain
|
||||
user = BlogUser.objects.create_superuser(
|
||||
email="liangliangyy1@gmail.com",
|
||||
username="liangliangyy1",
|
||||
password="qwer!@#$ggg")
|
||||
testuser = BlogUser.objects.get(username='liangliangyy1')
|
||||
|
||||
loginresult = self.client.login(
|
||||
username='liangliangyy1',
|
||||
password='qwer!@#$ggg')
|
||||
self.assertEqual(loginresult, True)
|
||||
response = self.client.get('/admin/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
category = Category()
|
||||
category.name = "categoryaaa"
|
||||
category.creation_time = timezone.now()
|
||||
category.last_modify_time = timezone.now()
|
||||
category.save()
|
||||
|
||||
article = Article()
|
||||
article.title = "nicetitleaaa"
|
||||
article.body = "nicecontentaaa"
|
||||
article.author = user
|
||||
article.category = category
|
||||
article.article_type = 'a'
|
||||
article.status = 'p'
|
||||
article.save()
|
||||
|
||||
response = self.client.get(article.get_admin_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_validate_register(self):
|
||||
self.assertEquals(
|
||||
0, len(
|
||||
BlogUser.objects.filter(
|
||||
email='user123@user.com')))
|
||||
response = self.client.post(reverse('account:register'), {
|
||||
'username': 'user1233',
|
||||
'email': 'user123@user.com',
|
||||
'password1': 'password123!q@wE#R$T',
|
||||
'password2': 'password123!q@wE#R$T',
|
||||
})
|
||||
self.assertEquals(
|
||||
1, len(
|
||||
BlogUser.objects.filter(
|
||||
email='user123@user.com')))
|
||||
user = BlogUser.objects.filter(email='user123@user.com')[0]
|
||||
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
|
||||
path = reverse('accounts:result')
|
||||
url = '{path}?type=validation&id={id}&sign={sign}'.format(
|
||||
path=path, id=user.id, sign=sign)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.client.login(username='user1233', password='password123!q@wE#R$T')
|
||||
user = BlogUser.objects.filter(email='user123@user.com')[0]
|
||||
user.is_superuser = True
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
delete_sidebar_cache()
|
||||
category = Category()
|
||||
category.name = "categoryaaa"
|
||||
category.creation_time = timezone.now()
|
||||
category.last_modify_time = timezone.now()
|
||||
category.save()
|
||||
|
||||
article = Article()
|
||||
article.category = category
|
||||
article.title = "nicetitle333"
|
||||
article.body = "nicecontentttt"
|
||||
article.author = user
|
||||
|
||||
article.article_type = 'a'
|
||||
article.status = 'p'
|
||||
article.save()
|
||||
|
||||
response = self.client.get(article.get_admin_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse('account:logout'))
|
||||
self.assertIn(response.status_code, [301, 302, 200])
|
||||
|
||||
response = self.client.get(article.get_admin_url())
|
||||
self.assertIn(response.status_code, [301, 302, 200])
|
||||
|
||||
response = self.client.post(reverse('account:login'), {
|
||||
'username': 'user1233',
|
||||
'password': 'password123'
|
||||
})
|
||||
self.assertIn(response.status_code, [301, 302, 200])
|
||||
|
||||
response = self.client.get(article.get_admin_url())
|
||||
self.assertIn(response.status_code, [301, 302, 200])
|
||||
|
||||
def test_verify_email_code(self):
|
||||
to_email = "admin@admin.com"
|
||||
code = generate_code()
|
||||
utils.set_code(to_email, code)
|
||||
utils.send_verify_email(to_email, code)
|
||||
|
||||
err = utils.verify("admin@admin.com", code)
|
||||
self.assertEqual(err, None)
|
||||
|
||||
err = utils.verify("admin@123.com", code)
|
||||
self.assertEqual(type(err), str)
|
||||
|
||||
def test_forget_password_email_code_success(self):
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password_code"),
|
||||
data=dict(email="admin@admin.com")
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.content.decode("utf-8"), "ok")
|
||||
|
||||
def test_forget_password_email_code_fail(self):
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password_code"),
|
||||
data=dict()
|
||||
)
|
||||
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
|
||||
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password_code"),
|
||||
data=dict(email="admin@com")
|
||||
)
|
||||
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
|
||||
|
||||
def test_forget_password_email_success(self):
|
||||
code = generate_code()
|
||||
utils.set_code(self.blog_user.email, code)
|
||||
data = dict(
|
||||
new_password1=self.new_test,
|
||||
new_password2=self.new_test,
|
||||
email=self.blog_user.email,
|
||||
code=code,
|
||||
)
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password"),
|
||||
data=data
|
||||
)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
# 验证用户密码是否修改成功
|
||||
blog_user = BlogUser.objects.filter(
|
||||
email=self.blog_user.email,
|
||||
).first() # type: BlogUser
|
||||
self.assertNotEqual(blog_user, None)
|
||||
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
|
||||
|
||||
def test_forget_password_email_not_user(self):
|
||||
data = dict(
|
||||
new_password1=self.new_test,
|
||||
new_password2=self.new_test,
|
||||
email="123@123.com",
|
||||
code="123456",
|
||||
)
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password"),
|
||||
data=data
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
|
||||
def test_forget_password_email_code_error(self):
|
||||
code = generate_code()
|
||||
utils.set_code(self.blog_user.email, code)
|
||||
data = dict(
|
||||
new_password1=self.new_test,
|
||||
new_password2=self.new_test,
|
||||
email=self.blog_user.email,
|
||||
code="111111",
|
||||
)
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password"),
|
||||
data=data
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
# 用户账户应用的URL配置文件
|
||||
from django.urls import path
|
||||
from django.urls import re_path
|
||||
|
||||
from . import views
|
||||
from .forms import LoginForm
|
||||
|
||||
app_name = "accounts" # 应用命名空间
|
||||
|
||||
urlpatterns = [
|
||||
# 用户登录
|
||||
re_path(r'^login/$',
|
||||
views.LoginView.as_view(success_url='/'),
|
||||
name='login',
|
||||
kwargs={'authentication_form': LoginForm}), # 登录页面
|
||||
|
||||
# 用户注册
|
||||
re_path(r'^register/$',
|
||||
views.RegisterView.as_view(success_url="/"),
|
||||
name='register'), # 注册页面
|
||||
|
||||
# 用户登出
|
||||
re_path(r'^logout/$',
|
||||
views.LogoutView.as_view(),
|
||||
name='logout'), # 登出页面
|
||||
|
||||
# 账户操作结果页面
|
||||
path(r'account/result.html',
|
||||
views.account_result,
|
||||
name='result'), # 注册/验证结果页面
|
||||
|
||||
# 忘记密码
|
||||
re_path(r'^forget_password/$',
|
||||
views.ForgetPasswordView.as_view(),
|
||||
name='forget_password'), # 忘记密码页面
|
||||
|
||||
# 忘记密码验证码
|
||||
re_path(r'^forget_password_code/$',
|
||||
views.ForgetPasswordEmailCode.as_view(),
|
||||
name='forget_password_code'), # 发送验证码接口
|
||||
]
|
||||
@ -0,0 +1,26 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
|
||||
class EmailOrUsernameModelBackend(ModelBackend):
|
||||
"""
|
||||
允许使用用户名或邮箱登录
|
||||
"""
|
||||
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
if '@' in username:
|
||||
kwargs = {'email': username}
|
||||
else:
|
||||
kwargs = {'username': username}
|
||||
try:
|
||||
user = get_user_model().objects.get(**kwargs)
|
||||
if user.check_password(password):
|
||||
return user
|
||||
except get_user_model().DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_user(self, username):
|
||||
try:
|
||||
return get_user_model().objects.get(pk=username)
|
||||
except get_user_model().DoesNotExist:
|
||||
return None
|
||||
@ -0,0 +1,112 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# Register your models here.
|
||||
from .models import Article
|
||||
|
||||
|
||||
class ArticleForm(forms.ModelForm):
|
||||
# body = forms.CharField(widget=AdminPagedownWidget())
|
||||
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
def makr_article_publish(modeladmin, request, queryset):
|
||||
queryset.update(status='p')
|
||||
|
||||
|
||||
def draft_article(modeladmin, request, queryset):
|
||||
queryset.update(status='d')
|
||||
|
||||
|
||||
def close_article_commentstatus(modeladmin, request, queryset):
|
||||
queryset.update(comment_status='c')
|
||||
|
||||
|
||||
def open_article_commentstatus(modeladmin, request, queryset):
|
||||
queryset.update(comment_status='o')
|
||||
|
||||
|
||||
makr_article_publish.short_description = _('Publish selected articles')
|
||||
draft_article.short_description = _('Draft selected articles')
|
||||
close_article_commentstatus.short_description = _('Close article comments')
|
||||
open_article_commentstatus.short_description = _('Open article comments')
|
||||
|
||||
|
||||
class ArticlelAdmin(admin.ModelAdmin):
|
||||
list_per_page = 20
|
||||
search_fields = ('body', 'title')
|
||||
form = ArticleForm
|
||||
list_display = (
|
||||
'id',
|
||||
'title',
|
||||
'author',
|
||||
'link_to_category',
|
||||
'creation_time',
|
||||
'views',
|
||||
'status',
|
||||
'article_type',
|
||||
'article_order')
|
||||
list_display_links = ('id', 'title')
|
||||
list_filter = ('status', 'article_type', 'category')
|
||||
filter_horizontal = ('tags',)
|
||||
exclude = ('creation_time', 'last_modify_time')
|
||||
view_on_site = True
|
||||
actions = [
|
||||
makr_article_publish,
|
||||
draft_article,
|
||||
close_article_commentstatus,
|
||||
open_article_commentstatus]
|
||||
|
||||
def link_to_category(self, obj):
|
||||
info = (obj.category._meta.app_label, obj.category._meta.model_name)
|
||||
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
|
||||
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
|
||||
|
||||
link_to_category.short_description = _('category')
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
|
||||
form.base_fields['author'].queryset = get_user_model(
|
||||
).objects.filter(is_superuser=True)
|
||||
return form
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super(ArticlelAdmin, self).save_model(request, obj, form, change)
|
||||
|
||||
def get_view_on_site_url(self, obj=None):
|
||||
if obj:
|
||||
url = obj.get_full_url()
|
||||
return url
|
||||
else:
|
||||
from djangoblog.utils import get_current_site
|
||||
site = get_current_site().domain
|
||||
return site
|
||||
|
||||
|
||||
class TagAdmin(admin.ModelAdmin):
|
||||
exclude = ('slug', 'last_mod_time', 'creation_time')
|
||||
|
||||
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'parent_category', 'index')
|
||||
exclude = ('slug', 'last_mod_time', 'creation_time')
|
||||
|
||||
|
||||
class LinksAdmin(admin.ModelAdmin):
|
||||
exclude = ('last_mod_time', 'creation_time')
|
||||
|
||||
|
||||
class SideBarAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'content', 'is_enable', 'sequence')
|
||||
exclude = ('last_mod_time', 'creation_time')
|
||||
|
||||
|
||||
class BlogSettingsAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BlogConfig(AppConfig):
|
||||
name = 'blog'
|
||||
@ -0,0 +1,43 @@
|
||||
import logging
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from djangoblog.utils import cache, get_blog_setting
|
||||
from .models import Category, Article
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def seo_processor(requests):
|
||||
key = 'seo_processor'
|
||||
value = cache.get(key)
|
||||
if value:
|
||||
return value
|
||||
else:
|
||||
logger.info('set processor cache.')
|
||||
setting = get_blog_setting()
|
||||
value = {
|
||||
'SITE_NAME': setting.site_name,
|
||||
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
|
||||
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
|
||||
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
|
||||
'SITE_DESCRIPTION': setting.site_description,
|
||||
'SITE_KEYWORDS': setting.site_keywords,
|
||||
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
|
||||
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
|
||||
'nav_category_list': Category.objects.all(),
|
||||
'nav_pages': Article.objects.filter(
|
||||
article_type='p',
|
||||
status='p'),
|
||||
'OPEN_SITE_COMMENT': setting.open_site_comment,
|
||||
'BEIAN_CODE': setting.beian_code,
|
||||
'ANALYTICS_CODE': setting.analytics_code,
|
||||
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,
|
||||
"SHOW_GONGAN_CODE": setting.show_gongan_code,
|
||||
"CURRENT_YEAR": timezone.now().year,
|
||||
"GLOBAL_HEADER": setting.global_header,
|
||||
"GLOBAL_FOOTER": setting.global_footer,
|
||||
"COMMENT_NEED_REVIEW": setting.comment_need_review,
|
||||
}
|
||||
cache.set(key, value, 60 * 60 * 10)
|
||||
return value
|
||||
@ -0,0 +1,213 @@
|
||||
import time
|
||||
|
||||
import elasticsearch.client
|
||||
from django.conf import settings
|
||||
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
|
||||
from elasticsearch_dsl.connections import connections
|
||||
|
||||
from blog.models import Article
|
||||
|
||||
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
|
||||
|
||||
if ELASTICSEARCH_ENABLED:
|
||||
connections.create_connection(
|
||||
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
|
||||
from elasticsearch import Elasticsearch
|
||||
|
||||
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
|
||||
from elasticsearch.client import IngestClient
|
||||
|
||||
c = IngestClient(es)
|
||||
try:
|
||||
c.get_pipeline('geoip')
|
||||
except elasticsearch.exceptions.NotFoundError:
|
||||
c.put_pipeline('geoip', body='''{
|
||||
"description" : "Add geoip info",
|
||||
"processors" : [
|
||||
{
|
||||
"geoip" : {
|
||||
"field" : "ip"
|
||||
}
|
||||
}
|
||||
]
|
||||
}''')
|
||||
|
||||
|
||||
class GeoIp(InnerDoc):
|
||||
continent_name = Keyword()
|
||||
country_iso_code = Keyword()
|
||||
country_name = Keyword()
|
||||
location = GeoPoint()
|
||||
|
||||
|
||||
class UserAgentBrowser(InnerDoc):
|
||||
Family = Keyword()
|
||||
Version = Keyword()
|
||||
|
||||
|
||||
class UserAgentOS(UserAgentBrowser):
|
||||
pass
|
||||
|
||||
|
||||
class UserAgentDevice(InnerDoc):
|
||||
Family = Keyword()
|
||||
Brand = Keyword()
|
||||
Model = Keyword()
|
||||
|
||||
|
||||
class UserAgent(InnerDoc):
|
||||
browser = Object(UserAgentBrowser, required=False)
|
||||
os = Object(UserAgentOS, required=False)
|
||||
device = Object(UserAgentDevice, required=False)
|
||||
string = Text()
|
||||
is_bot = Boolean()
|
||||
|
||||
|
||||
class ElapsedTimeDocument(Document):
|
||||
url = Keyword()
|
||||
time_taken = Long()
|
||||
log_datetime = Date()
|
||||
ip = Keyword()
|
||||
geoip = Object(GeoIp, required=False)
|
||||
useragent = Object(UserAgent, required=False)
|
||||
|
||||
class Index:
|
||||
name = 'performance'
|
||||
settings = {
|
||||
"number_of_shards": 1,
|
||||
"number_of_replicas": 0
|
||||
}
|
||||
|
||||
class Meta:
|
||||
doc_type = 'ElapsedTime'
|
||||
|
||||
|
||||
class ElaspedTimeDocumentManager:
|
||||
@staticmethod
|
||||
def build_index():
|
||||
from elasticsearch import Elasticsearch
|
||||
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
|
||||
res = client.indices.exists(index="performance")
|
||||
if not res:
|
||||
ElapsedTimeDocument.init()
|
||||
|
||||
@staticmethod
|
||||
def delete_index():
|
||||
from elasticsearch import Elasticsearch
|
||||
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
|
||||
es.indices.delete(index='performance', ignore=[400, 404])
|
||||
|
||||
@staticmethod
|
||||
def create(url, time_taken, log_datetime, useragent, ip):
|
||||
ElaspedTimeDocumentManager.build_index()
|
||||
ua = UserAgent()
|
||||
ua.browser = UserAgentBrowser()
|
||||
ua.browser.Family = useragent.browser.family
|
||||
ua.browser.Version = useragent.browser.version_string
|
||||
|
||||
ua.os = UserAgentOS()
|
||||
ua.os.Family = useragent.os.family
|
||||
ua.os.Version = useragent.os.version_string
|
||||
|
||||
ua.device = UserAgentDevice()
|
||||
ua.device.Family = useragent.device.family
|
||||
ua.device.Brand = useragent.device.brand
|
||||
ua.device.Model = useragent.device.model
|
||||
ua.string = useragent.ua_string
|
||||
ua.is_bot = useragent.is_bot
|
||||
|
||||
doc = ElapsedTimeDocument(
|
||||
meta={
|
||||
'id': int(
|
||||
round(
|
||||
time.time() *
|
||||
1000))
|
||||
},
|
||||
url=url,
|
||||
time_taken=time_taken,
|
||||
log_datetime=log_datetime,
|
||||
useragent=ua, ip=ip)
|
||||
doc.save(pipeline="geoip")
|
||||
|
||||
|
||||
class ArticleDocument(Document):
|
||||
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
|
||||
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
|
||||
author = Object(properties={
|
||||
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
|
||||
'id': Integer()
|
||||
})
|
||||
category = Object(properties={
|
||||
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
|
||||
'id': Integer()
|
||||
})
|
||||
tags = Object(properties={
|
||||
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
|
||||
'id': Integer()
|
||||
})
|
||||
|
||||
pub_time = Date()
|
||||
status = Text()
|
||||
comment_status = Text()
|
||||
type = Text()
|
||||
views = Integer()
|
||||
article_order = Integer()
|
||||
|
||||
class Index:
|
||||
name = 'blog'
|
||||
settings = {
|
||||
"number_of_shards": 1,
|
||||
"number_of_replicas": 0
|
||||
}
|
||||
|
||||
class Meta:
|
||||
doc_type = 'Article'
|
||||
|
||||
|
||||
class ArticleDocumentManager():
|
||||
|
||||
def __init__(self):
|
||||
self.create_index()
|
||||
|
||||
def create_index(self):
|
||||
ArticleDocument.init()
|
||||
|
||||
def delete_index(self):
|
||||
from elasticsearch import Elasticsearch
|
||||
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
|
||||
es.indices.delete(index='blog', ignore=[400, 404])
|
||||
|
||||
def convert_to_doc(self, articles):
|
||||
return [
|
||||
ArticleDocument(
|
||||
meta={
|
||||
'id': article.id},
|
||||
body=article.body,
|
||||
title=article.title,
|
||||
author={
|
||||
'nickname': article.author.username,
|
||||
'id': article.author.id},
|
||||
category={
|
||||
'name': article.category.name,
|
||||
'id': article.category.id},
|
||||
tags=[
|
||||
{
|
||||
'name': t.name,
|
||||
'id': t.id} for t in article.tags.all()],
|
||||
pub_time=article.pub_time,
|
||||
status=article.status,
|
||||
comment_status=article.comment_status,
|
||||
type=article.article_type,
|
||||
views=article.views,
|
||||
article_order=article.article_order) for article in articles]
|
||||
|
||||
def rebuild(self, articles=None):
|
||||
ArticleDocument.init()
|
||||
articles = articles if articles else Article.objects.all()
|
||||
docs = self.convert_to_doc(articles)
|
||||
for doc in docs:
|
||||
doc.save()
|
||||
|
||||
def update_docs(self, docs):
|
||||
for doc in docs:
|
||||
doc.save()
|
||||
@ -0,0 +1,23 @@
|
||||
# 博客相关的表单定义
|
||||
import logging
|
||||
|
||||
from django import forms
|
||||
from haystack.forms import SearchForm # Haystack搜索表单基类
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BlogSearchForm(SearchForm):
|
||||
"""博客搜索表单"""
|
||||
querydata = forms.CharField(required=True) # 搜索关键词字段
|
||||
|
||||
def search(self):
|
||||
"""执行搜索操作"""
|
||||
datas = super(BlogSearchForm, self).search()
|
||||
if not self.is_valid():
|
||||
return self.no_query_found()
|
||||
|
||||
# 记录搜索关键词到日志
|
||||
if self.cleaned_data['querydata']:
|
||||
logger.info(self.cleaned_data['querydata'])
|
||||
return datas
|
||||
@ -0,0 +1,18 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
|
||||
ELASTICSEARCH_ENABLED
|
||||
|
||||
|
||||
# TODO 参数化
|
||||
class Command(BaseCommand):
|
||||
help = 'build search index'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if ELASTICSEARCH_ENABLED:
|
||||
ElaspedTimeDocumentManager.build_index()
|
||||
manager = ElapsedTimeDocument()
|
||||
manager.init()
|
||||
manager = ArticleDocumentManager()
|
||||
manager.delete_index()
|
||||
manager.rebuild()
|
||||
@ -0,0 +1,13 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from blog.models import Tag, Category
|
||||
|
||||
|
||||
# TODO 参数化
|
||||
class Command(BaseCommand):
|
||||
help = 'build search words'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
datas = set([t.name for t in Tag.objects.all()] +
|
||||
[t.name for t in Category.objects.all()])
|
||||
print('\n'.join(datas))
|
||||
@ -0,0 +1,11 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from djangoblog.utils import cache
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'clear the whole cache'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
cache.clear()
|
||||
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))
|
||||
@ -0,0 +1,40 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from blog.models import Article, Tag, Category
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'create test datas'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
user = get_user_model().objects.get_or_create(
|
||||
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
|
||||
|
||||
pcategory = Category.objects.get_or_create(
|
||||
name='我是父类目', parent_category=None)[0]
|
||||
|
||||
category = Category.objects.get_or_create(
|
||||
name='子类目', parent_category=pcategory)[0]
|
||||
|
||||
category.save()
|
||||
basetag = Tag()
|
||||
basetag.name = "标签"
|
||||
basetag.save()
|
||||
for i in range(1, 20):
|
||||
article = Article.objects.get_or_create(
|
||||
category=category,
|
||||
title='nice title ' + str(i),
|
||||
body='nice content ' + str(i),
|
||||
author=user)[0]
|
||||
tag = Tag()
|
||||
tag.name = "标签" + str(i)
|
||||
tag.save()
|
||||
article.tags.add(tag)
|
||||
article.tags.add(basetag)
|
||||
article.save()
|
||||
|
||||
from djangoblog.utils import cache
|
||||
cache.clear()
|
||||
self.stdout.write(self.style.SUCCESS('created test datas \n'))
|
||||
@ -0,0 +1,50 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from djangoblog.spider_notify import SpiderNotify
|
||||
from djangoblog.utils import get_current_site
|
||||
from blog.models import Article, Tag, Category
|
||||
|
||||
site = get_current_site().domain
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'notify baidu url'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'data_type',
|
||||
type=str,
|
||||
choices=[
|
||||
'all',
|
||||
'article',
|
||||
'tag',
|
||||
'category'],
|
||||
help='article : all article,tag : all tag,category: all category,all: All of these')
|
||||
|
||||
def get_full_url(self, path):
|
||||
url = "https://{site}{path}".format(site=site, path=path)
|
||||
return url
|
||||
|
||||
def handle(self, *args, **options):
|
||||
type = options['data_type']
|
||||
self.stdout.write('start get %s' % type)
|
||||
|
||||
urls = []
|
||||
if type == 'article' or type == 'all':
|
||||
for article in Article.objects.filter(status='p'):
|
||||
urls.append(article.get_full_url())
|
||||
if type == 'tag' or type == 'all':
|
||||
for tag in Tag.objects.all():
|
||||
url = tag.get_absolute_url()
|
||||
urls.append(self.get_full_url(url))
|
||||
if type == 'category' or type == 'all':
|
||||
for category in Category.objects.all():
|
||||
url = category.get_absolute_url()
|
||||
urls.append(self.get_full_url(url))
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
'start notify %d urls' %
|
||||
len(urls)))
|
||||
SpiderNotify.baidu_notify(urls)
|
||||
self.stdout.write(self.style.SUCCESS('finish notify'))
|
||||
@ -0,0 +1,47 @@
|
||||
import requests
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.templatetags.static import static
|
||||
|
||||
from djangoblog.utils import save_user_avatar
|
||||
from oauth.models import OAuthUser
|
||||
from oauth.oauthmanager import get_manager_by_type
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'sync user avatar'
|
||||
|
||||
def test_picture(self, url):
|
||||
try:
|
||||
if requests.get(url, timeout=2).status_code == 200:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
def handle(self, *args, **options):
|
||||
static_url = static("../")
|
||||
users = OAuthUser.objects.all()
|
||||
self.stdout.write(f'开始同步{len(users)}个用户头像')
|
||||
for u in users:
|
||||
self.stdout.write(f'开始同步:{u.nickname}')
|
||||
url = u.picture
|
||||
if url:
|
||||
if url.startswith(static_url):
|
||||
if self.test_picture(url):
|
||||
continue
|
||||
else:
|
||||
if u.metadata:
|
||||
manage = get_manager_by_type(u.type)
|
||||
url = manage.get_picture(u.metadata)
|
||||
url = save_user_avatar(url)
|
||||
else:
|
||||
url = static('blog/img/avatar.png')
|
||||
else:
|
||||
url = save_user_avatar(url)
|
||||
else:
|
||||
url = static('blog/img/avatar.png')
|
||||
if url:
|
||||
self.stdout.write(
|
||||
f'结束同步:{u.nickname}.url:{url}')
|
||||
u.picture = url
|
||||
u.save()
|
||||
self.stdout.write('结束同步')
|
||||
@ -0,0 +1,42 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from ipware import get_client_ip
|
||||
from user_agents import parse
|
||||
|
||||
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OnlineMiddleware(object):
|
||||
def __init__(self, get_response=None):
|
||||
self.get_response = get_response
|
||||
super().__init__()
|
||||
|
||||
def __call__(self, request):
|
||||
''' page render time '''
|
||||
start_time = time.time()
|
||||
response = self.get_response(request)
|
||||
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
|
||||
ip, _ = get_client_ip(request)
|
||||
user_agent = parse(http_user_agent)
|
||||
if not response.streaming:
|
||||
try:
|
||||
cast_time = time.time() - start_time
|
||||
if ELASTICSEARCH_ENABLED:
|
||||
time_taken = round((cast_time) * 1000, 2)
|
||||
url = request.path
|
||||
from django.utils import timezone
|
||||
ElaspedTimeDocumentManager.create(
|
||||
url=url,
|
||||
time_taken=time_taken,
|
||||
log_datetime=timezone.now(),
|
||||
useragent=user_agent,
|
||||
ip=ip)
|
||||
response.content = response.content.replace(
|
||||
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
|
||||
except Exception as e:
|
||||
logger.error("Error OnlineMiddleware: %s" % e)
|
||||
|
||||
return response
|
||||
@ -0,0 +1,137 @@
|
||||
# Generated by Django 4.1.7 on 2023-03-02 07:14
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import mdeditor.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BlogSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
|
||||
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
|
||||
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
|
||||
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
|
||||
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
|
||||
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
|
||||
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
|
||||
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
|
||||
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
|
||||
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
|
||||
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
|
||||
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
|
||||
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
|
||||
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
|
||||
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '网站配置',
|
||||
'verbose_name_plural': '网站配置',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Links',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
|
||||
('link', models.URLField(verbose_name='链接地址')),
|
||||
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
|
||||
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
|
||||
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
|
||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '友情链接',
|
||||
'verbose_name_plural': '友情链接',
|
||||
'ordering': ['sequence'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SideBar',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='标题')),
|
||||
('content', models.TextField(verbose_name='内容')),
|
||||
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
|
||||
('is_enable', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '侧边栏',
|
||||
'verbose_name_plural': '侧边栏',
|
||||
'ordering': ['sequence'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tag',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
|
||||
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '标签',
|
||||
'verbose_name_plural': '标签',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
|
||||
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
|
||||
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
|
||||
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '分类',
|
||||
'verbose_name_plural': '分类',
|
||||
'ordering': ['-index'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Article',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
|
||||
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
|
||||
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
|
||||
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
|
||||
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
|
||||
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
|
||||
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
|
||||
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
|
||||
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
|
||||
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '文章',
|
||||
'verbose_name_plural': '文章',
|
||||
'ordering': ['-article_order', '-pub_time'],
|
||||
'get_latest_by': 'id',
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.1.7 on 2023-03-29 06:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='blogsettings',
|
||||
name='global_footer',
|
||||
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='blogsettings',
|
||||
name='global_header',
|
||||
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.1 on 2023-05-09 07:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('blog', '0002_blogsettings_global_footer_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='blogsettings',
|
||||
name='comment_need_review',
|
||||
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,27 @@
|
||||
# Generated by Django 4.2.1 on 2023-05-09 07:51
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('blog', '0003_blogsettings_comment_need_review'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='blogsettings',
|
||||
old_name='analyticscode',
|
||||
new_name='analytics_code',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='blogsettings',
|
||||
old_name='beiancode',
|
||||
new_name='beian_code',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='blogsettings',
|
||||
old_name='sitename',
|
||||
new_name='site_name',
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,300 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-06 13:13
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import mdeditor.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='article',
|
||||
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='category',
|
||||
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='links',
|
||||
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='sidebar',
|
||||
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='tag',
|
||||
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='article',
|
||||
name='created_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='article',
|
||||
name='last_mod_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='category',
|
||||
name='created_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='category',
|
||||
name='last_mod_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='links',
|
||||
name='created_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='sidebar',
|
||||
name='created_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tag',
|
||||
name='created_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tag',
|
||||
name='last_mod_time',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='article',
|
||||
name='creation_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='article',
|
||||
name='last_modify_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='creation_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='category',
|
||||
name='last_modify_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='links',
|
||||
name='creation_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sidebar',
|
||||
name='creation_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='creation_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tag',
|
||||
name='last_modify_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='article_order',
|
||||
field=models.IntegerField(default=0, verbose_name='order'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='author',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='body',
|
||||
field=mdeditor.fields.MDTextField(verbose_name='body'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='category',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='comment_status',
|
||||
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='pub_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='show_toc',
|
||||
field=models.BooleanField(default=False, verbose_name='show toc'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='tags',
|
||||
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='title',
|
||||
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='views',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='views'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogsettings',
|
||||
name='article_comment_count',
|
||||
field=models.IntegerField(default=5, verbose_name='article comment count'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogsettings',
|
||||
name='article_sub_length',
|
||||
field=models.IntegerField(default=300, verbose_name='article sub length'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogsettings',
|
||||
name='google_adsense_codes',
|
||||
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogsettings',
|
||||
name='open_site_comment',
|
||||
field=models.BooleanField(default=True, verbose_name='open site comment'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogsettings',
|
||||
name='show_google_adsense',
|
||||
field=models.BooleanField(default=False, verbose_name='show adsense'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogsettings',
|
||||
name='sidebar_article_count',
|
||||
field=models.IntegerField(default=10, verbose_name='sidebar article count'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogsettings',
|
||||
name='sidebar_comment_count',
|
||||
field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogsettings',
|
||||
name='site_description',
|
||||
field=models.TextField(default='', max_length=1000, verbose_name='site description'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogsettings',
|
||||
name='site_keywords',
|
||||
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogsettings',
|
||||
name='site_name',
|
||||
field=models.CharField(default='', max_length=200, verbose_name='site name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogsettings',
|
||||
name='site_seo_description',
|
||||
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='index',
|
||||
field=models.IntegerField(default=0, verbose_name='index'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='name',
|
||||
field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='parent_category',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='links',
|
||||
name='is_enable',
|
||||
field=models.BooleanField(default=True, verbose_name='is show'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='links',
|
||||
name='last_mod_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='links',
|
||||
name='link',
|
||||
field=models.URLField(verbose_name='link'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='links',
|
||||
name='name',
|
||||
field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='links',
|
||||
name='sequence',
|
||||
field=models.IntegerField(unique=True, verbose_name='order'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='links',
|
||||
name='show_type',
|
||||
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sidebar',
|
||||
name='content',
|
||||
field=models.TextField(verbose_name='content'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sidebar',
|
||||
name='is_enable',
|
||||
field=models.BooleanField(default=True, verbose_name='is enable'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sidebar',
|
||||
name='last_mod_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sidebar',
|
||||
name='name',
|
||||
field=models.CharField(max_length=100, verbose_name='title'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sidebar',
|
||||
name='sequence',
|
||||
field=models.IntegerField(unique=True, verbose_name='order'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tag',
|
||||
name='name',
|
||||
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2.7 on 2024-01-26 02:41
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0005_alter_article_options_alter_category_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='blogsettings',
|
||||
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,90 @@
|
||||
# Generated by Django 5.2.4 on 2025-11-23 18:24
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0006_alter_blogsettings_options'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='article',
|
||||
name='type',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='links',
|
||||
name='last_mod_time',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='article',
|
||||
name='article_type',
|
||||
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='links',
|
||||
name='last_modify_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogsettings',
|
||||
name='beian_code',
|
||||
field=models.CharField(blank=True, default='', max_length=2000, verbose_name='备案号'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogsettings',
|
||||
name='global_footer',
|
||||
field=models.TextField(blank=True, default='', verbose_name='公共尾部'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogsettings',
|
||||
name='global_header',
|
||||
field=models.TextField(blank=True, default='', verbose_name='公共头部'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogsettings',
|
||||
name='gongan_beiancode',
|
||||
field=models.TextField(blank=True, default='', max_length=2000, verbose_name='公安备案号'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogsettings',
|
||||
name='google_adsense_codes',
|
||||
field=models.TextField(blank=True, default='', max_length=2000, verbose_name='adsense code'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Favorite',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('creation_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time')),
|
||||
('last_modify_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time')),
|
||||
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'favorite',
|
||||
'verbose_name_plural': 'favorite',
|
||||
'unique_together': {('user', 'article')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Like',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('creation_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time')),
|
||||
('last_modify_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time')),
|
||||
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'like',
|
||||
'verbose_name_plural': 'like',
|
||||
'unique_together': {('user', 'article')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.4 on 2025-11-23 19:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0007_remove_article_type_remove_links_last_mod_time_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='article',
|
||||
name='slug',
|
||||
field=models.SlugField(blank=True, max_length=200, null=True, verbose_name='slug'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,13 @@
|
||||
from haystack import indexes
|
||||
|
||||
from blog.models import Article
|
||||
|
||||
|
||||
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
|
||||
text = indexes.CharField(document=True, use_template=True)
|
||||
|
||||
def get_model(self):
|
||||
return Article
|
||||
|
||||
def index_queryset(self, using=None):
|
||||
return self.get_model().objects.filter(status='p')
|
||||
@ -0,0 +1,64 @@
|
||||
{% extends "blog/base.html" %}
|
||||
{% load staticfiles %}
|
||||
{% load custom_markdown %}
|
||||
{% load likes_tags %}
|
||||
{% load static %}
|
||||
{% block title %}{{ title }} - {{ site_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="well well-lg">
|
||||
<h2 class="text-center">我的收藏</h2>
|
||||
{% if article_list %}
|
||||
<div class="article-list">
|
||||
{% for article in article_list %}
|
||||
<div class="article">
|
||||
<div class="title">
|
||||
<h3><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h3>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="author"><a href="{% url 'blog:author' article.author.username %}">{{ article.author.username }}</a></span>
|
||||
<span class="views">{{ article.views }} 阅读</span>
|
||||
<span class="likes">{{ article.total_likes }} 点赞</span>
|
||||
<span class="favorites">{{ article.total_favorites }} 收藏</span>
|
||||
<span class="time">{{ article.created_time }}</span>
|
||||
</div>
|
||||
<div class="entry-content">
|
||||
<p>{{ article.body | custom_markdown | truncatechars:200 }}</p>
|
||||
</div>
|
||||
<div class="article-footer">
|
||||
<a href="{{ article.get_absolute_url }}" class="btn btn-primary btn-sm">阅读全文</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info text-center">
|
||||
<p>您还没有收藏任何文章</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if article_list %}
|
||||
<div class="pagination">
|
||||
{% if article_list.has_previous %}
|
||||
<a href="?page={{ article_list.previous_page_number }}" class="page-link">上一页</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="current-page">{{ article_list.number }} / {{ article_list.paginator.num_pages }}</span>
|
||||
|
||||
{% if article_list.has_next %}
|
||||
<a href="?page={{ article_list.next_page_number }}" class="page-link">下一页</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
{% include "blog/right_sidebar.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -0,0 +1,232 @@
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.management import call_command
|
||||
from django.core.paginator import Paginator
|
||||
from django.templatetags.static import static
|
||||
from django.test import Client, RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from accounts.models import BlogUser
|
||||
from blog.forms import BlogSearchForm
|
||||
from blog.models import Article, Category, Tag, SideBar, Links
|
||||
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
|
||||
from djangoblog.utils import get_current_site, get_sha256
|
||||
from oauth.models import OAuthUser, OAuthConfig
|
||||
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
class ArticleTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_validate_article(self):
|
||||
site = get_current_site().domain
|
||||
user = BlogUser.objects.get_or_create(
|
||||
email="liangliangyy@gmail.com",
|
||||
username="liangliangyy")[0]
|
||||
user.set_password("liangliangyy")
|
||||
user.is_staff = True
|
||||
user.is_superuser = True
|
||||
user.save()
|
||||
response = self.client.get(user.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get('/admin/servermanager/emailsendlog/')
|
||||
response = self.client.get('admin/admin/logentry/')
|
||||
s = SideBar()
|
||||
s.sequence = 1
|
||||
s.name = 'test'
|
||||
s.content = 'test content'
|
||||
s.is_enable = True
|
||||
s.save()
|
||||
|
||||
category = Category()
|
||||
category.name = "category"
|
||||
category.creation_time = timezone.now()
|
||||
category.last_mod_time = timezone.now()
|
||||
category.save()
|
||||
|
||||
tag = Tag()
|
||||
tag.name = "nicetag"
|
||||
tag.save()
|
||||
|
||||
article = Article()
|
||||
article.title = "nicetitle"
|
||||
article.body = "nicecontent"
|
||||
article.author = user
|
||||
article.category = category
|
||||
article.article_type = 'a'
|
||||
article.status = 'p'
|
||||
|
||||
article.save()
|
||||
self.assertEqual(0, article.tags.count())
|
||||
article.tags.add(tag)
|
||||
article.save()
|
||||
self.assertEqual(1, article.tags.count())
|
||||
|
||||
for i in range(20):
|
||||
article = Article()
|
||||
article.title = "nicetitle" + str(i)
|
||||
article.body = "nicetitle" + str(i)
|
||||
article.author = user
|
||||
article.category = category
|
||||
article.article_type = 'a'
|
||||
article.status = 'p'
|
||||
article.save()
|
||||
article.tags.add(tag)
|
||||
article.save()
|
||||
from blog.documents import ELASTICSEARCH_ENABLED
|
||||
if ELASTICSEARCH_ENABLED:
|
||||
call_command("build_index")
|
||||
response = self.client.get('/search', {'q': 'nicetitle'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(article.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
from djangoblog.spider_notify import SpiderNotify
|
||||
SpiderNotify.notify(article.get_absolute_url())
|
||||
response = self.client.get(tag.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(category.get_absolute_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get('/search', {'q': 'django'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
s = load_articletags(article)
|
||||
self.assertIsNotNone(s)
|
||||
|
||||
self.client.login(username='liangliangyy', password='liangliangyy')
|
||||
|
||||
response = self.client.get(reverse('blog:archives'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
|
||||
self.check_pagination(p, '', '')
|
||||
|
||||
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
|
||||
self.check_pagination(p, '分类标签归档', tag.slug)
|
||||
|
||||
p = Paginator(
|
||||
Article.objects.filter(
|
||||
author__username='liangliangyy'), settings.PAGINATE_BY)
|
||||
self.check_pagination(p, '作者文章归档', 'liangliangyy')
|
||||
|
||||
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
|
||||
self.check_pagination(p, '分类目录归档', category.slug)
|
||||
|
||||
f = BlogSearchForm()
|
||||
f.search()
|
||||
# self.client.login(username='liangliangyy', password='liangliangyy')
|
||||
from djangoblog.spider_notify import SpiderNotify
|
||||
SpiderNotify.baidu_notify([article.get_full_url()])
|
||||
|
||||
from blog.templatetags.blog_tags import gravatar_url, gravatar
|
||||
u = gravatar_url('liangliangyy@gmail.com')
|
||||
u = gravatar('liangliangyy@gmail.com')
|
||||
|
||||
link = Links(
|
||||
sequence=1,
|
||||
name="lylinux",
|
||||
link='https://wwww.lylinux.net')
|
||||
link.save()
|
||||
response = self.client.get('/links.html')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get('/feed/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get('/sitemap.xml')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.client.get("/admin/blog/article/1/delete/")
|
||||
self.client.get('/admin/servermanager/emailsendlog/')
|
||||
self.client.get('/admin/admin/logentry/')
|
||||
self.client.get('/admin/admin/logentry/1/change/')
|
||||
|
||||
def check_pagination(self, p, type, value):
|
||||
for page in range(1, p.num_pages + 1):
|
||||
s = load_pagination_info(p.page(page), type, value)
|
||||
self.assertIsNotNone(s)
|
||||
if s['previous_url']:
|
||||
response = self.client.get(s['previous_url'])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
if s['next_url']:
|
||||
response = self.client.get(s['next_url'])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_image(self):
|
||||
import requests
|
||||
rsp = requests.get(
|
||||
'https://www.python.org/static/img/python-logo.png')
|
||||
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
|
||||
with open(imagepath, 'wb') as file:
|
||||
file.write(rsp.content)
|
||||
rsp = self.client.post('/upload')
|
||||
self.assertEqual(rsp.status_code, 403)
|
||||
sign = get_sha256(get_sha256(settings.SECRET_KEY))
|
||||
with open(imagepath, 'rb') as file:
|
||||
imgfile = SimpleUploadedFile(
|
||||
'python.png', file.read(), content_type='image/jpg')
|
||||
form_data = {'python.png': imgfile}
|
||||
rsp = self.client.post(
|
||||
'/upload?sign=' + sign, form_data, follow=True)
|
||||
self.assertEqual(rsp.status_code, 200)
|
||||
os.remove(imagepath)
|
||||
from djangoblog.utils import save_user_avatar, send_email
|
||||
send_email(['qq@qq.com'], 'testTitle', 'testContent')
|
||||
save_user_avatar(
|
||||
'https://www.python.org/static/img/python-logo.png')
|
||||
|
||||
def test_errorpage(self):
|
||||
rsp = self.client.get('/eee')
|
||||
self.assertEqual(rsp.status_code, 404)
|
||||
|
||||
def test_commands(self):
|
||||
user = BlogUser.objects.get_or_create(
|
||||
email="liangliangyy@gmail.com",
|
||||
username="liangliangyy")[0]
|
||||
user.set_password("liangliangyy")
|
||||
user.is_staff = True
|
||||
user.is_superuser = True
|
||||
user.save()
|
||||
|
||||
c = OAuthConfig()
|
||||
c.type = 'qq'
|
||||
c.appkey = 'appkey'
|
||||
c.appsecret = 'appsecret'
|
||||
c.save()
|
||||
|
||||
u = OAuthUser()
|
||||
u.type = 'qq'
|
||||
u.openid = 'openid'
|
||||
u.user = user
|
||||
u.picture = static("/blog/img/avatar.png")
|
||||
u.metadata = '''
|
||||
{
|
||||
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
|
||||
}'''
|
||||
u.save()
|
||||
|
||||
u = OAuthUser()
|
||||
u.type = 'qq'
|
||||
u.openid = 'openid1'
|
||||
u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30'
|
||||
u.metadata = '''
|
||||
{
|
||||
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
|
||||
}'''
|
||||
u.save()
|
||||
|
||||
from blog.documents import ELASTICSEARCH_ENABLED
|
||||
if ELASTICSEARCH_ENABLED:
|
||||
call_command("build_index")
|
||||
call_command("ping_baidu", "all")
|
||||
call_command("create_testdata")
|
||||
call_command("clear_cache")
|
||||
call_command("sync_user_avatar")
|
||||
call_command("build_search_words")
|
||||
@ -0,0 +1,47 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
def disable_commentstatus(modeladmin, request, queryset):
|
||||
queryset.update(is_enable=False)
|
||||
|
||||
|
||||
def enable_commentstatus(modeladmin, request, queryset):
|
||||
queryset.update(is_enable=True)
|
||||
|
||||
|
||||
disable_commentstatus.short_description = _('Disable comments')
|
||||
enable_commentstatus.short_description = _('Enable comments')
|
||||
|
||||
|
||||
class CommentAdmin(admin.ModelAdmin):
|
||||
list_per_page = 20
|
||||
list_display = (
|
||||
'id',
|
||||
'body',
|
||||
'link_to_userinfo',
|
||||
'link_to_article',
|
||||
'is_enable',
|
||||
'creation_time')
|
||||
list_display_links = ('id', 'body', 'is_enable')
|
||||
list_filter = ('is_enable',)
|
||||
exclude = ('creation_time', 'last_modify_time')
|
||||
actions = [disable_commentstatus, enable_commentstatus]
|
||||
|
||||
def link_to_userinfo(self, obj):
|
||||
info = (obj.author._meta.app_label, obj.author._meta.model_name)
|
||||
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
|
||||
return format_html(
|
||||
u'<a href="%s">%s</a>' %
|
||||
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
|
||||
|
||||
def link_to_article(self, obj):
|
||||
info = (obj.article._meta.app_label, obj.article._meta.model_name)
|
||||
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
|
||||
return format_html(
|
||||
u'<a href="%s">%s</a>' % (link, obj.article.title))
|
||||
|
||||
link_to_userinfo.short_description = _('User')
|
||||
link_to_article.short_description = _('Article')
|
||||
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CommentsConfig(AppConfig):
|
||||
name = 'comments'
|
||||
@ -0,0 +1,13 @@
|
||||
from django import forms
|
||||
from django.forms import ModelForm
|
||||
|
||||
from .models import Comment
|
||||
|
||||
|
||||
class CommentForm(ModelForm):
|
||||
parent_comment_id = forms.IntegerField(
|
||||
widget=forms.HiddenInput, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = ['body']
|
||||
@ -0,0 +1,38 @@
|
||||
# Generated by Django 4.1.7 on 2023-03-02 07:14
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('blog', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Comment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('body', models.TextField(max_length=300, verbose_name='正文')),
|
||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
|
||||
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
|
||||
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '评论',
|
||||
'verbose_name_plural': '评论',
|
||||
'ordering': ['-id'],
|
||||
'get_latest_by': 'id',
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.7 on 2023-04-24 13:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('comments', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='is_enable',
|
||||
field=models.BooleanField(default=False, verbose_name='是否显示'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,60 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-06 13:13
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('blog', '0005_alter_article_options_alter_category_options_and_more'),
|
||||
('comments', '0002_alter_comment_is_enable'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='comment',
|
||||
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='comment',
|
||||
name='created_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='comment',
|
||||
name='last_mod_time',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='comment',
|
||||
name='creation_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='comment',
|
||||
name='last_modify_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='article',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='author',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='is_enable',
|
||||
field=models.BooleanField(default=False, verbose_name='enable'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='parent_comment',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,49 @@
|
||||
#zy 评论相关模型
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from blog.models import Article
|
||||
|
||||
|
||||
# Create your models here.
|
||||
|
||||
#zy 评论模型类
|
||||
class Comment(models.Model):
|
||||
#zy 评论正文
|
||||
body = models.TextField('正文', max_length=300)
|
||||
#zy 创建时间
|
||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
||||
#zy 最后修改时间
|
||||
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
|
||||
#zy 评论作者(外键关联用户)
|
||||
author = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name=_('author'),
|
||||
on_delete=models.CASCADE)
|
||||
#zy 评论的文章(外键关联文章)
|
||||
article = models.ForeignKey(
|
||||
Article,
|
||||
verbose_name=_('article'),
|
||||
on_delete=models.CASCADE)
|
||||
#zy 父评论(支持回复功能)
|
||||
parent_comment = models.ForeignKey(
|
||||
'self',
|
||||
verbose_name=_('parent comment'),
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.CASCADE)
|
||||
#zy 是否启用(审核功能)
|
||||
is_enable = models.BooleanField(_('enable'),
|
||||
default=False, blank=False, null=False)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-id'] #zy 按ID倒序排列
|
||||
verbose_name = _('comment') #zy 在管理后台显示的名称
|
||||
verbose_name_plural = verbose_name #zy 复数形式
|
||||
get_latest_by = 'id' #zy 获取最新记录的依据
|
||||
|
||||
#zy 返回评论正文作为字符串表示
|
||||
def __str__(self):
|
||||
return self.body
|
||||
@ -0,0 +1,30 @@
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def parse_commenttree(commentlist, comment):
|
||||
"""获得当前评论子评论的列表
|
||||
用法: {% parse_commenttree article_comments comment as childcomments %}
|
||||
"""
|
||||
datas = []
|
||||
|
||||
def parse(c):
|
||||
childs = commentlist.filter(parent_comment=c, is_enable=True)
|
||||
for child in childs:
|
||||
datas.append(child)
|
||||
parse(child)
|
||||
|
||||
parse(comment)
|
||||
return datas
|
||||
|
||||
|
||||
@register.inclusion_tag('comments/tags/comment_item.html')
|
||||
def show_comment_item(comment, ischild):
|
||||
"""评论"""
|
||||
depth = 1 if ischild else 2
|
||||
return {
|
||||
'comment_item': comment,
|
||||
'depth': depth
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
from django.test import Client, RequestFactory, TransactionTestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from accounts.models import BlogUser
|
||||
from blog.models import Category, Article
|
||||
from comments.models import Comment
|
||||
from comments.templatetags.comments_tags import *
|
||||
from djangoblog.utils import get_max_articleid_commentid
|
||||
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
class CommentsTest(TransactionTestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.factory = RequestFactory()
|
||||
from blog.models import BlogSettings
|
||||
value = BlogSettings()
|
||||
value.comment_need_review = True
|
||||
value.save()
|
||||
|
||||
self.user = BlogUser.objects.create_superuser(
|
||||
email="liangliangyy1@gmail.com",
|
||||
username="liangliangyy1",
|
||||
password="liangliangyy1")
|
||||
|
||||
def update_article_comment_status(self, article):
|
||||
comments = article.comment_set.all()
|
||||
for comment in comments:
|
||||
comment.is_enable = True
|
||||
comment.save()
|
||||
|
||||
def test_validate_comment(self):
|
||||
self.client.login(username='liangliangyy1', password='liangliangyy1')
|
||||
|
||||
category = Category()
|
||||
category.name = "categoryccc"
|
||||
category.save()
|
||||
|
||||
article = Article()
|
||||
article.title = "nicetitleccc"
|
||||
article.body = "nicecontentccc"
|
||||
article.author = self.user
|
||||
article.category = category
|
||||
article.article_type = 'a'
|
||||
article.status = 'p'
|
||||
article.save()
|
||||
|
||||
comment_url = reverse(
|
||||
'comments:postcomment', kwargs={
|
||||
'article_id': article.id})
|
||||
|
||||
response = self.client.post(comment_url,
|
||||
{
|
||||
'body': '123ffffffffff'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
article = Article.objects.get(pk=article.pk)
|
||||
self.assertEqual(len(article.comment_list()), 0)
|
||||
self.update_article_comment_status(article)
|
||||
|
||||
self.assertEqual(len(article.comment_list()), 1)
|
||||
|
||||
response = self.client.post(comment_url,
|
||||
{
|
||||
'body': '123ffffffffff',
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
article = Article.objects.get(pk=article.pk)
|
||||
self.update_article_comment_status(article)
|
||||
self.assertEqual(len(article.comment_list()), 2)
|
||||
parent_comment_id = article.comment_list()[0].id
|
||||
|
||||
response = self.client.post(comment_url,
|
||||
{
|
||||
'body': '''
|
||||
# Title1
|
||||
|
||||
```python
|
||||
import os
|
||||
```
|
||||
|
||||
[url](https://www.lylinux.net/)
|
||||
|
||||
[ddd](http://www.baidu.com)
|
||||
|
||||
|
||||
''',
|
||||
'parent_comment_id': parent_comment_id
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.update_article_comment_status(article)
|
||||
article = Article.objects.get(pk=article.pk)
|
||||
self.assertEqual(len(article.comment_list()), 3)
|
||||
comment = Comment.objects.get(id=parent_comment_id)
|
||||
tree = parse_commenttree(article.comment_list(), comment)
|
||||
self.assertEqual(len(tree), 1)
|
||||
data = show_comment_item(comment, True)
|
||||
self.assertIsNotNone(data)
|
||||
s = get_max_articleid_commentid()
|
||||
self.assertIsNotNone(s)
|
||||
|
||||
from comments.utils import send_comment_email
|
||||
send_comment_email(comment)
|
||||
@ -0,0 +1,11 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "comments"
|
||||
urlpatterns = [
|
||||
path(
|
||||
'article/<int:article_id>/postcomment',
|
||||
views.CommentPostView.as_view(),
|
||||
name='postcomment'),
|
||||
]
|
||||
@ -0,0 +1,38 @@
|
||||
import logging
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from djangoblog.utils import get_current_site
|
||||
from djangoblog.utils import send_email
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_comment_email(comment):
|
||||
site = get_current_site().domain
|
||||
subject = _('Thanks for your comment')
|
||||
article_url = f"https://{site}{comment.article.get_absolute_url()}"
|
||||
html_content = _("""<p>Thank you very much for your comments on this site</p>
|
||||
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
|
||||
to review your comments,
|
||||
Thank you again!
|
||||
<br />
|
||||
If the link above cannot be opened, please copy this link to your browser.
|
||||
%(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
|
||||
tomail = comment.author.email
|
||||
send_email([tomail], subject, html_content)
|
||||
try:
|
||||
if comment.parent_comment:
|
||||
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
|
||||
received a reply. <br/> %(comment_body)s
|
||||
<br/>
|
||||
go check it out!
|
||||
<br/>
|
||||
If the link above cannot be opened, please copy this link to your browser.
|
||||
%(article_url)s
|
||||
""") % {'article_url': article_url, 'article_title': comment.article.title,
|
||||
'comment_body': comment.parent_comment.body}
|
||||
tomail = comment.parent_comment.author.email
|
||||
send_email([tomail], subject, html_content)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
@ -0,0 +1,70 @@
|
||||
# Create your views here.
|
||||
#zxt 评论相关视图
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_protect
|
||||
from django.views.generic.edit import FormView
|
||||
|
||||
from accounts.models import BlogUser
|
||||
from blog.models import Article
|
||||
from .forms import CommentForm
|
||||
from .models import Comment
|
||||
|
||||
|
||||
#zzr 评论提交视图类
|
||||
class CommentPostView(FormView):
|
||||
form_class = CommentForm #zzr 使用评论表单
|
||||
template_name = 'blog/article_detail.html' #zzr 文章详情页面模板
|
||||
|
||||
@method_decorator(csrf_protect) #zzr 防止CSRF攻击
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(CommentPostView, self).dispatch(*args, **kwargs)
|
||||
|
||||
#zzr 处理GET请求
|
||||
def get(self, request, *args, **kwargs):
|
||||
article_id = self.kwargs['article_id'] #zzr 获取文章ID
|
||||
article = get_object_or_404(Article, pk=article_id) #zzr 获取文章对象
|
||||
url = article.get_absolute_url() #zzr 获取文章详情页URL
|
||||
return HttpResponseRedirect(url + "#comments") #zzr 重定向到评论区
|
||||
|
||||
#zzr 表单验证失败后的处理
|
||||
def form_invalid(self, form):
|
||||
article_id = self.kwargs['article_id'] #zzr 获取文章ID
|
||||
article = get_object_or_404(Article, pk=article_id) #zzr 获取文章对象
|
||||
|
||||
return self.render_to_response({
|
||||
'form': form, #zzr 带错误的表单
|
||||
'article': article #zzr 文章对象
|
||||
})
|
||||
|
||||
#zzr 表单验证成功后的处理
|
||||
def form_valid(self, form):
|
||||
"""提交的数据验证合法后的逻辑"""
|
||||
user = self.request.user #zzr 获取当前用户
|
||||
author = BlogUser.objects.get(pk=user.pk) #zzr 获取用户对象
|
||||
article_id = self.kwargs['article_id'] #zzr 获取文章ID
|
||||
article = get_object_or_404(Article, pk=article_id) #zzr 获取文章对象
|
||||
|
||||
#zzr 检查文章是否允许评论
|
||||
if article.comment_status == 'c' or article.status == 'c':
|
||||
raise ValidationError("该文章评论已关闭.")
|
||||
comment = form.save(False) #zzr 不保存到数据库
|
||||
comment.article = article #zzr 设置评论所属文章
|
||||
from djangoblog.utils import get_blog_setting
|
||||
settings = get_blog_setting() #zzr 获取博客设置
|
||||
if not settings.comment_need_review: #zzr 如果不需要审核
|
||||
comment.is_enable = True #zzr 直接启用评论
|
||||
comment.author = author #zzr 设置评论作者
|
||||
|
||||
#zzr 处理回复评论
|
||||
if form.cleaned_data['parent_comment_id']: #zzr 如果有父评论ID
|
||||
parent_comment = Comment.objects.get(
|
||||
pk=form.cleaned_data['parent_comment_id']) #zzr 获取父评论
|
||||
comment.parent_comment = parent_comment #zzr 设置父评论
|
||||
|
||||
comment.save(True) #zzr 保存评论到数据库
|
||||
return HttpResponseRedirect(
|
||||
"%s#div-comment-%d" %
|
||||
(article.get_absolute_url(), comment.pk)) #zzr 重定向到评论位置
|
||||
@ -0,0 +1,48 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
es:
|
||||
image: liangliangyy/elasticsearch-analysis-ik:8.6.1
|
||||
container_name: es
|
||||
restart: always
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||||
ports:
|
||||
- 9200:9200
|
||||
volumes:
|
||||
- ./bin/datas/es/:/usr/share/elasticsearch/data/
|
||||
|
||||
kibana:
|
||||
image: kibana:8.6.1
|
||||
restart: always
|
||||
container_name: kibana
|
||||
ports:
|
||||
- 5601:5601
|
||||
environment:
|
||||
- ELASTICSEARCH_HOSTS=http://es:9200
|
||||
|
||||
djangoblog:
|
||||
build: .
|
||||
restart: always
|
||||
command: bash -c 'sh /code/djangoblog/bin/docker_start.sh'
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./collectedstatic:/code/djangoblog/collectedstatic
|
||||
- ./uploads:/code/djangoblog/uploads
|
||||
environment:
|
||||
- DJANGO_MYSQL_DATABASE=djangoblog
|
||||
- DJANGO_MYSQL_USER=root
|
||||
- DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E
|
||||
- DJANGO_MYSQL_HOST=db
|
||||
- DJANGO_MYSQL_PORT=3306
|
||||
- DJANGO_MEMCACHED_LOCATION=memcached:11211
|
||||
- DJANGO_ELASTICSEARCH_HOST=es:9200
|
||||
links:
|
||||
- db
|
||||
- memcached
|
||||
depends_on:
|
||||
- db
|
||||
container_name: djangoblog
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mysql:latest
|
||||
restart: always
|
||||
environment:
|
||||
- MYSQL_DATABASE=djangoblog
|
||||
- MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E
|
||||
ports:
|
||||
- 3306:3306
|
||||
volumes:
|
||||
- ./bin/datas/mysql/:/var/lib/mysql
|
||||
depends_on:
|
||||
- redis
|
||||
container_name: db
|
||||
|
||||
djangoblog:
|
||||
build:
|
||||
context: ../../
|
||||
restart: always
|
||||
command: bash -c 'sh /code/djangoblog/bin/docker_start.sh'
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./collectedstatic:/code/djangoblog/collectedstatic
|
||||
- ./logs:/code/djangoblog/logs
|
||||
- ./uploads:/code/djangoblog/uploads
|
||||
environment:
|
||||
- DJANGO_MYSQL_DATABASE=djangoblog
|
||||
- DJANGO_MYSQL_USER=root
|
||||
- DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E
|
||||
- DJANGO_MYSQL_HOST=db
|
||||
- DJANGO_MYSQL_PORT=3306
|
||||
- DJANGO_REDIS_URL=redis:6379
|
||||
links:
|
||||
- db
|
||||
- redis
|
||||
depends_on:
|
||||
- db
|
||||
container_name: djangoblog
|
||||
nginx:
|
||||
restart: always
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./bin/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./collectedstatic:/code/djangoblog/collectedstatic
|
||||
links:
|
||||
- djangoblog:djangoblog
|
||||
container_name: nginx
|
||||
|
||||
redis:
|
||||
restart: always
|
||||
image: redis:latest
|
||||
container_name: redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
NAME="djangoblog"
|
||||
DJANGODIR=/code/djangoblog
|
||||
USER=root
|
||||
GROUP=root
|
||||
NUM_WORKERS=1
|
||||
DJANGO_WSGI_MODULE=djangoblog.wsgi
|
||||
|
||||
|
||||
echo "Starting $NAME as `whoami`"
|
||||
|
||||
cd $DJANGODIR
|
||||
|
||||
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
|
||||
|
||||
python manage.py makemigrations && \
|
||||
python manage.py migrate && \
|
||||
python manage.py collectstatic --noinput && \
|
||||
python manage.py compress --force && \
|
||||
python manage.py build_index && \
|
||||
python manage.py compilemessages || exit 1
|
||||
|
||||
exec gunicorn ${DJANGO_WSGI_MODULE}:application \
|
||||
--name $NAME \
|
||||
--workers $NUM_WORKERS \
|
||||
--user=$USER --group=$GROUP \
|
||||
--bind 0.0.0.0:8000 \
|
||||
--log-level=debug \
|
||||
--log-file=- \
|
||||
--worker-class gevent \
|
||||
--threads 4
|
||||
@ -0,0 +1,119 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: web-nginx-config
|
||||
namespace: djangoblog
|
||||
data:
|
||||
nginx.conf: |
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log notice;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
multi_accept on;
|
||||
use epoll;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 8;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
|
||||
|
||||
# Include server configurations
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
djangoblog.conf: |
|
||||
server {
|
||||
server_name lylinux.net;
|
||||
root /code/djangoblog/collectedstatic/;
|
||||
listen 80;
|
||||
keepalive_timeout 70;
|
||||
location /static/ {
|
||||
expires max;
|
||||
alias /code/djangoblog/collectedstatic/;
|
||||
}
|
||||
|
||||
location ~* (robots\.txt|ads\.txt|favicon\.ico|favion\.ico|crossdomain\.xml|google93fd32dbd906620a\.html|BingSiteAuth\.xml|baidu_verify_Ijeny6KrmS\.html)$ {
|
||||
root /resource/djangopub;
|
||||
expires 1d;
|
||||
access_log off;
|
||||
error_log off;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_redirect off;
|
||||
if (!-f $request_filename) {
|
||||
proxy_pass http://djangoblog:8000;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
server {
|
||||
server_name www.lylinux.net;
|
||||
listen 80;
|
||||
return 301 https://lylinux.net$request_uri;
|
||||
}
|
||||
resource.lylinux.net.conf: |
|
||||
server {
|
||||
index index.html index.htm;
|
||||
server_name resource.lylinux.net;
|
||||
root /resource/;
|
||||
|
||||
location /djangoblog/ {
|
||||
alias /code/djangoblog/collectedstatic/;
|
||||
}
|
||||
|
||||
access_log off;
|
||||
error_log off;
|
||||
include lylinux/resource.conf;
|
||||
}
|
||||
lylinux.resource.conf: |
|
||||
expires max;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
add_header Pragma public;
|
||||
add_header Cache-Control "public";
|
||||
add_header "Access-Control-Allow-Origin" "*";
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: djangoblog-env
|
||||
namespace: djangoblog
|
||||
data:
|
||||
DJANGO_MYSQL_DATABASE: djangoblog
|
||||
DJANGO_MYSQL_USER: db_user
|
||||
DJANGO_MYSQL_PASSWORD: db_password
|
||||
DJANGO_MYSQL_HOST: db_host
|
||||
DJANGO_MYSQL_PORT: db_port
|
||||
DJANGO_REDIS_URL: "redis:6379"
|
||||
DJANGO_DEBUG: "False"
|
||||
MYSQL_ROOT_PASSWORD: db_password
|
||||
MYSQL_DATABASE: djangoblog
|
||||
MYSQL_PASSWORD: db_password
|
||||
DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
@ -0,0 +1,274 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: djangoblog
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: djangoblog
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: djangoblog
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: djangoblog
|
||||
spec:
|
||||
containers:
|
||||
- name: djangoblog
|
||||
image: liangliangyy/djangoblog:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: djangoblog-env
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 100Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: 2Gi
|
||||
volumeMounts:
|
||||
- name: djangoblog
|
||||
mountPath: /code/djangoblog/collectedstatic
|
||||
- name: resource
|
||||
mountPath: /resource
|
||||
volumes:
|
||||
- name: djangoblog
|
||||
persistentVolumeClaim:
|
||||
claimName: djangoblog-pvc
|
||||
- name: resource
|
||||
persistentVolumeClaim:
|
||||
claimName: resource-pvc
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 100Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 2Gi
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: db
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: db
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: db
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: db
|
||||
spec:
|
||||
containers:
|
||||
- name: db
|
||||
image: mysql:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 3306
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: djangoblog-env
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- mysqladmin
|
||||
- ping
|
||||
- "-h"
|
||||
- "127.0.0.1"
|
||||
- "-u"
|
||||
- "root"
|
||||
- "-p$MYSQL_ROOT_PASSWORD"
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- mysqladmin
|
||||
- ping
|
||||
- "-h"
|
||||
- "127.0.0.1"
|
||||
- "-u"
|
||||
- "root"
|
||||
- "-p$MYSQL_ROOT_PASSWORD"
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 100Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: 2Gi
|
||||
volumeMounts:
|
||||
- name: db-data
|
||||
mountPath: /var/lib/mysql
|
||||
volumes:
|
||||
- name: db-data
|
||||
persistentVolumeClaim:
|
||||
claimName: db-pvc
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 80
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 100Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: 2Gi
|
||||
volumeMounts:
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/nginx.conf
|
||||
subPath: nginx.conf
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/conf.d/default.conf
|
||||
subPath: djangoblog.conf
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/conf.d/resource.lylinux.net.conf
|
||||
subPath: resource.lylinux.net.conf
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/lylinux/resource.conf
|
||||
subPath: lylinux.resource.conf
|
||||
- name: djangoblog-pvc
|
||||
mountPath: /code/djangoblog/collectedstatic
|
||||
- name: resource-pvc
|
||||
mountPath: /resource
|
||||
volumes:
|
||||
- name: nginx-config
|
||||
configMap:
|
||||
name: web-nginx-config
|
||||
- name: djangoblog-pvc
|
||||
persistentVolumeClaim:
|
||||
claimName: djangoblog-pvc
|
||||
- name: resource-pvc
|
||||
persistentVolumeClaim:
|
||||
claimName: resource-pvc
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: elasticsearch
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: elasticsearch
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: elasticsearch
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: elasticsearch
|
||||
spec:
|
||||
containers:
|
||||
- name: elasticsearch
|
||||
image: liangliangyy/elasticsearch-analysis-ik:8.6.1
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: discovery.type
|
||||
value: single-node
|
||||
- name: ES_JAVA_OPTS
|
||||
value: "-Xms256m -Xmx256m"
|
||||
- name: xpack.security.enabled
|
||||
value: "false"
|
||||
- name: xpack.monitoring.templates.enabled
|
||||
value: "false"
|
||||
ports:
|
||||
- containerPort: 9200
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 100Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: 2Gi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 9200
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 30
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 9200
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 30
|
||||
volumeMounts:
|
||||
- name: elasticsearch-data
|
||||
mountPath: /usr/share/elasticsearch/data/
|
||||
volumes:
|
||||
- name: elasticsearch-data
|
||||
persistentVolumeClaim:
|
||||
claimName: elasticsearch-pvc
|
||||
@ -0,0 +1,17 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: nginx
|
||||
namespace: djangoblog
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: nginx
|
||||
port:
|
||||
number: 80
|
||||
@ -0,0 +1,94 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: local-pv-db
|
||||
spec:
|
||||
capacity:
|
||||
storage: 10Gi
|
||||
volumeMode: Filesystem
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: local-storage
|
||||
local:
|
||||
path: /mnt/local-storage-db
|
||||
nodeAffinity:
|
||||
required:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- master
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: local-pv-djangoblog
|
||||
spec:
|
||||
capacity:
|
||||
storage: 5Gi
|
||||
volumeMode: Filesystem
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: local-storage
|
||||
local:
|
||||
path: /mnt/local-storage-djangoblog
|
||||
nodeAffinity:
|
||||
required:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- master
|
||||
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: local-pv-resource
|
||||
spec:
|
||||
capacity:
|
||||
storage: 5Gi
|
||||
volumeMode: Filesystem
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: local-storage
|
||||
local:
|
||||
path: /mnt/resource/
|
||||
nodeAffinity:
|
||||
required:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- master
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: local-pv-elasticsearch
|
||||
spec:
|
||||
capacity:
|
||||
storage: 5Gi
|
||||
volumeMode: Filesystem
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: local-storage
|
||||
local:
|
||||
path: /mnt/local-storage-elasticsearch
|
||||
nodeAffinity:
|
||||
required:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- master
|
||||
@ -0,0 +1,60 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: db-pvc
|
||||
namespace: djangoblog
|
||||
spec:
|
||||
storageClassName: local-storage
|
||||
volumeName: local-pv-db
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: djangoblog-pvc
|
||||
namespace: djangoblog
|
||||
spec:
|
||||
volumeName: local-pv-djangoblog
|
||||
storageClassName: local-storage
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: resource-pvc
|
||||
namespace: djangoblog
|
||||
spec:
|
||||
volumeName: local-pv-resource
|
||||
storageClassName: local-storage
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: elasticsearch-pvc
|
||||
namespace: djangoblog
|
||||
spec:
|
||||
volumeName: local-pv-elasticsearch
|
||||
storageClassName: local-storage
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
|
||||
@ -0,0 +1,80 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: djangoblog
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: djangoblog
|
||||
spec:
|
||||
selector:
|
||||
app: djangoblog
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8000
|
||||
targetPort: 8000
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
selector:
|
||||
app: nginx
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
selector:
|
||||
app: redis
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6379
|
||||
targetPort: 6379
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: db
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: db
|
||||
spec:
|
||||
selector:
|
||||
app: db
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 3306
|
||||
targetPort: 3306
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: elasticsearch
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: elasticsearch
|
||||
spec:
|
||||
selector:
|
||||
app: elasticsearch
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 9200
|
||||
targetPort: 9200
|
||||
type: ClusterIP
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: local-storage
|
||||
annotations:
|
||||
storageclass.kubernetes.io/is-default-class: "true"
|
||||
provisioner: kubernetes.io/no-provisioner
|
||||
volumeBindingMode: Immediate
|
||||
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /var/log/nginx/error.log notice;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
#gzip on;
|
||||
|
||||
server {
|
||||
root /code/djangoblog/collectedstatic/;
|
||||
listen 80;
|
||||
keepalive_timeout 70;
|
||||
location /static/ {
|
||||
expires max;
|
||||
alias /code/djangoblog/collectedstatic/;
|
||||
}
|
||||
location / {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_redirect off;
|
||||
if (!-f $request_filename) {
|
||||
proxy_pass http://djangoblog:8000;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
|
||||
@ -0,0 +1,64 @@
|
||||
from django.contrib.admin import AdminSite
|
||||
from django.contrib.admin.models import LogEntry
|
||||
from django.contrib.sites.admin import SiteAdmin
|
||||
from django.contrib.sites.models import Site
|
||||
|
||||
from accounts.admin import *
|
||||
from blog.admin import *
|
||||
from blog.models import *
|
||||
from comments.admin import *
|
||||
from comments.models import *
|
||||
from djangoblog.logentryadmin import LogEntryAdmin
|
||||
from oauth.admin import *
|
||||
from oauth.models import *
|
||||
from owntracks.admin import *
|
||||
from owntracks.models import *
|
||||
from servermanager.admin import *
|
||||
from servermanager.models import *
|
||||
|
||||
|
||||
class DjangoBlogAdminSite(AdminSite):
|
||||
site_header = 'djangoblog administration'
|
||||
site_title = 'djangoblog site admin'
|
||||
|
||||
def __init__(self, name='admin'):
|
||||
super().__init__(name)
|
||||
|
||||
def has_permission(self, request):
|
||||
return request.user.is_superuser
|
||||
|
||||
# def get_urls(self):
|
||||
# urls = super().get_urls()
|
||||
# from django.urls import path
|
||||
# from blog.views import refresh_memcache
|
||||
#
|
||||
# my_urls = [
|
||||
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
|
||||
# ]
|
||||
# return urls + my_urls
|
||||
|
||||
|
||||
admin_site = DjangoBlogAdminSite(name='admin')
|
||||
|
||||
admin_site.register(Article, ArticlelAdmin)
|
||||
admin_site.register(Category, CategoryAdmin)
|
||||
admin_site.register(Tag, TagAdmin)
|
||||
admin_site.register(Links, LinksAdmin)
|
||||
admin_site.register(SideBar, SideBarAdmin)
|
||||
admin_site.register(BlogSettings, BlogSettingsAdmin)
|
||||
|
||||
admin_site.register(commands, CommandsAdmin)
|
||||
admin_site.register(EmailSendLog, EmailSendLogAdmin)
|
||||
|
||||
admin_site.register(BlogUser, BlogUserAdmin)
|
||||
|
||||
admin_site.register(Comment, CommentAdmin)
|
||||
|
||||
admin_site.register(OAuthUser, OAuthUserAdmin)
|
||||
admin_site.register(OAuthConfig, OAuthConfigAdmin)
|
||||
|
||||
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
|
||||
|
||||
admin_site.register(Site, SiteAdmin)
|
||||
|
||||
admin_site.register(LogEntry, LogEntryAdmin)
|
||||
@ -0,0 +1,11 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class DjangoblogAppConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'djangoblog'
|
||||
|
||||
def ready(self):
|
||||
super().ready()
|
||||
# Import and load plugins here
|
||||
from .plugin_manage.loader import load_plugins
|
||||
load_plugins()
|
||||
@ -0,0 +1,122 @@
|
||||
import _thread
|
||||
import logging
|
||||
|
||||
import django.dispatch
|
||||
from django.conf import settings
|
||||
from django.contrib.admin.models import LogEntry
|
||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from comments.models import Comment
|
||||
from comments.utils import send_comment_email
|
||||
from djangoblog.spider_notify import SpiderNotify
|
||||
from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache
|
||||
from djangoblog.utils import get_current_site
|
||||
from oauth.models import OAuthUser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
oauth_user_login_signal = django.dispatch.Signal(['id'])
|
||||
send_email_signal = django.dispatch.Signal(
|
||||
['emailto', 'title', 'content'])
|
||||
|
||||
|
||||
@receiver(send_email_signal)
|
||||
def send_email_signal_handler(sender, **kwargs):
|
||||
emailto = kwargs['emailto']
|
||||
title = kwargs['title']
|
||||
content = kwargs['content']
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
title,
|
||||
content,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=emailto)
|
||||
msg.content_subtype = "html"
|
||||
|
||||
from servermanager.models import EmailSendLog
|
||||
log = EmailSendLog()
|
||||
log.title = title
|
||||
log.content = content
|
||||
log.emailto = ','.join(emailto)
|
||||
|
||||
try:
|
||||
result = msg.send()
|
||||
log.send_result = result > 0
|
||||
except Exception as e:
|
||||
logger.error(f"失败邮箱号: {emailto}, {e}")
|
||||
log.send_result = False
|
||||
log.save()
|
||||
|
||||
|
||||
@receiver(oauth_user_login_signal)
|
||||
def oauth_user_login_signal_handler(sender, **kwargs):
|
||||
id = kwargs['id']
|
||||
oauthuser = OAuthUser.objects.get(id=id)
|
||||
site = get_current_site().domain
|
||||
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
|
||||
from djangoblog.utils import save_user_avatar
|
||||
oauthuser.picture = save_user_avatar(oauthuser.picture)
|
||||
oauthuser.save()
|
||||
|
||||
delete_sidebar_cache()
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
def model_post_save_callback(
|
||||
sender,
|
||||
instance,
|
||||
created,
|
||||
raw,
|
||||
using,
|
||||
update_fields,
|
||||
**kwargs):
|
||||
clearcache = False
|
||||
if isinstance(instance, LogEntry):
|
||||
return
|
||||
if 'get_full_url' in dir(instance):
|
||||
is_update_views = update_fields == {'views'}
|
||||
if not settings.TESTING and not is_update_views:
|
||||
try:
|
||||
notify_url = instance.get_full_url()
|
||||
SpiderNotify.baidu_notify([notify_url])
|
||||
except Exception as ex:
|
||||
logger.error("notify sipder", ex)
|
||||
if not is_update_views:
|
||||
clearcache = True
|
||||
|
||||
if isinstance(instance, Comment):
|
||||
if instance.is_enable:
|
||||
path = instance.article.get_absolute_url()
|
||||
site = get_current_site().domain
|
||||
if site.find(':') > 0:
|
||||
site = site[0:site.find(':')]
|
||||
|
||||
expire_view_cache(
|
||||
path,
|
||||
servername=site,
|
||||
serverport=80,
|
||||
key_prefix='blogdetail')
|
||||
if cache.get('seo_processor'):
|
||||
cache.delete('seo_processor')
|
||||
comment_cache_key = 'article_comments_{id}'.format(
|
||||
id=instance.article.id)
|
||||
cache.delete(comment_cache_key)
|
||||
delete_sidebar_cache()
|
||||
delete_view_cache('article_comments', [str(instance.article.pk)])
|
||||
|
||||
_thread.start_new_thread(send_comment_email, (instance,))
|
||||
|
||||
if clearcache:
|
||||
cache.clear()
|
||||
|
||||
|
||||
@receiver(user_logged_in)
|
||||
@receiver(user_logged_out)
|
||||
def user_auth_callback(sender, request, user, **kwargs):
|
||||
if user and user.username:
|
||||
logger.info(user)
|
||||
delete_sidebar_cache()
|
||||
# cache.clear()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue