Compare commits
15 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
6a32fdc6da | 3 months ago |
|
|
1f54ef64cd | 4 months ago |
|
|
85bd807b48 | 4 months ago |
|
|
b584eb9968 | 4 months ago |
|
|
435309f2e4 | 4 months ago |
|
|
90ffcf644a | 4 months ago |
|
|
7d5c5ccd04 | 4 months ago |
|
|
71cc28fc32 | 4 months ago |
|
|
fac7ac60a8 | 4 months ago |
|
|
70c7b14954 | 4 months ago |
|
|
34630ce0c0 | 4 months ago |
|
|
082d344c33 | 4 months ago |
|
|
dd5470e51f | 4 months ago |
|
|
4cf444c7de | 4 months ago |
|
|
1cf0e47393 | 4 months ago |
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.
@ -1,10 +0,0 @@
|
|||||||
[run]
|
|
||||||
source = .
|
|
||||||
include = *.py
|
|
||||||
omit =
|
|
||||||
*migrations*
|
|
||||||
*tests*
|
|
||||||
*.html
|
|
||||||
*whoosh_cn_backend*
|
|
||||||
*settings.py*
|
|
||||||
*venv*
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
bin/data/
|
|
||||||
# virtualenv
|
|
||||||
venv/
|
|
||||||
collectedstatic/
|
|
||||||
djangoblog/whoosh_index/
|
|
||||||
uploads/
|
|
||||||
settings_production.py
|
|
||||||
*.md
|
|
||||||
docs/
|
|
||||||
logs/
|
|
||||||
static/
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
blog/static/* linguist-vendored
|
|
||||||
*.js linguist-vendored
|
|
||||||
*.css linguist-vendored
|
|
||||||
* text=auto
|
|
||||||
*.sh text eol=lf
|
|
||||||
*.conf text eol=lf
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
<!--
|
|
||||||
如果你不认真勾选下面的内容,我可能会直接关闭你的 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 反馈
|
|
||||||
- [ ] 添加新的特性或者功能
|
|
||||||
- [ ] 请求技术支持
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
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}}
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
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 }}
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
env/
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
|
|
||||||
# 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/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*,cover
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
logs/
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
target/
|
|
||||||
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# http://www.jetbrains.com/pycharm/webhelp/project.html
|
|
||||||
.idea
|
|
||||||
.iml
|
|
||||||
static/
|
|
||||||
# virtualenv
|
|
||||||
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/
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
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/*
|
|
||||||
ADD 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
|
|
||||||
|
|
||||||
ADD . .
|
|
||||||
RUN chmod +x /code/djangoblog/deploy/entrypoint.sh
|
|
||||||
ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"]
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
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.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
from django.contrib.auth.models import AbstractUser
|
|
||||||
from django.db import models
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.timezone import now
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from djangoblog.utils import get_current_site
|
|
||||||
|
|
||||||
|
|
||||||
#lht: Create your models here.
|
|
||||||
|
|
||||||
class BlogUser(AbstractUser):
|
|
||||||
#lht: 用户昵称字段
|
|
||||||
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
|
|
||||||
#lht: 用户创建时间
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
#lht: 用户最后修改时间
|
|
||||||
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
|
|
||||||
#lht: 用户来源标识(如通过注册、后台创建等)
|
|
||||||
source = models.CharField(_('create source'), max_length=100, blank=True)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
#lht: 返回用户个人页面的URL
|
|
||||||
return reverse(
|
|
||||||
'blog:author_detail', kwargs={
|
|
||||||
'author_name': self.username})
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
#lht: 字符串表示,返回用户邮箱
|
|
||||||
return self.email
|
|
||||||
|
|
||||||
def get_full_url(self):
|
|
||||||
#lht: 获取用户页面的完整URL
|
|
||||||
site = get_current_site().domain
|
|
||||||
url = "https://{site}{path}".format(site=site,
|
|
||||||
path=self.get_absolute_url())
|
|
||||||
return url
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
#lht: 模型元数据配置
|
|
||||||
ordering = ['-id'] #lht: 默认按ID倒序排列
|
|
||||||
verbose_name = _('user') #lht: 单数名称
|
|
||||||
verbose_name_plural = verbose_name #lht: 复数名称
|
|
||||||
get_latest_by = 'id' #lht: 获取最新记录的字段
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
from django.urls import re_path
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
from .forms import LoginForm
|
|
||||||
|
|
||||||
app_name = "accounts" #lht: 应用命名空间
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
#lht: 登录URL
|
|
||||||
re_path(r'^login/$',
|
|
||||||
views.LoginView.as_view(success_url='/'),
|
|
||||||
name='login',
|
|
||||||
kwargs={'authentication_form': LoginForm}),
|
|
||||||
#lht: 注册URL
|
|
||||||
re_path(r'^register/$',
|
|
||||||
views.RegisterView.as_view(success_url="/"),
|
|
||||||
name='register'),
|
|
||||||
#lht: 登出URL
|
|
||||||
re_path(r'^logout/$',
|
|
||||||
views.LogoutView.as_view(),
|
|
||||||
name='logout'),
|
|
||||||
#lht: 账户操作结果页面
|
|
||||||
path(r'account/result.html',
|
|
||||||
views.account_result,
|
|
||||||
name='result'),
|
|
||||||
#lht: 忘记密码页面
|
|
||||||
re_path(r'^forget_password/$',
|
|
||||||
views.ForgetPasswordView.as_view(),
|
|
||||||
name='forget_password'),
|
|
||||||
#lht: 获取忘记密码验证码
|
|
||||||
re_path(r'^forget_password_code/$',
|
|
||||||
views.ForgetPasswordEmailCode.as_view(),
|
|
||||||
name='forget_password_code'),
|
|
||||||
]
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.auth.backends import ModelBackend
|
|
||||||
|
|
||||||
|
|
||||||
class EmailOrUsernameModelBackend(ModelBackend):
|
|
||||||
#lht: """
|
|
||||||
#lht: 允许使用用户名或邮箱登录
|
|
||||||
#lht: """
|
|
||||||
|
|
||||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
|
||||||
#lht: 根据输入内容判断是邮箱还是用户名
|
|
||||||
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):
|
|
||||||
#lht: 根据用户名获取用户对象
|
|
||||||
try:
|
|
||||||
return get_user_model().objects.get(pk=username)
|
|
||||||
except get_user_model().DoesNotExist:
|
|
||||||
return None
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
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)
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
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()
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
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 _
|
|
||||||
|
|
||||||
#zr 禁用评论状态的管理动作
|
|
||||||
def disable_commentstatus(modeladmin, request, queryset):
|
|
||||||
queryset.update(is_enable=False)
|
|
||||||
|
|
||||||
#zr 启用评论状态的管理动作
|
|
||||||
def enable_commentstatus(modeladmin, request, queryset):
|
|
||||||
queryset.update(is_enable=True)
|
|
||||||
|
|
||||||
#zr 设置动作的描述信息
|
|
||||||
disable_commentstatus.short_description = _('Disable comments')
|
|
||||||
enable_commentstatus.short_description = _('Enable comments')
|
|
||||||
|
|
||||||
#zr 评论管理后台配置类
|
|
||||||
class CommentAdmin(admin.ModelAdmin):
|
|
||||||
#zr 设置每页显示数量
|
|
||||||
list_per_page = 20
|
|
||||||
#zr 设置列表页显示的字段
|
|
||||||
list_display = (
|
|
||||||
'id',
|
|
||||||
'body',
|
|
||||||
'link_to_userinfo',
|
|
||||||
'link_to_article',
|
|
||||||
'is_enable',
|
|
||||||
'creation_time')
|
|
||||||
#zr 设置可点击链接的字段
|
|
||||||
list_display_links = ('id', 'body', 'is_enable')
|
|
||||||
#zr 设置过滤器字段
|
|
||||||
list_filter = ('is_enable',)
|
|
||||||
#zr 设置排除的表单字段
|
|
||||||
exclude = ('creation_time', 'last_modify_time')
|
|
||||||
#zr 设置可用的批量动作
|
|
||||||
actions = [disable_commentstatus, enable_commentstatus]
|
|
||||||
|
|
||||||
#zr 生成用户信息链接的方法
|
|
||||||
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))
|
|
||||||
|
|
||||||
#zr 生成文章链接的方法
|
|
||||||
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))
|
|
||||||
|
|
||||||
#zr 设置自定义字段的显示名称
|
|
||||||
link_to_userinfo.short_description = _('User')
|
|
||||||
link_to_article.short_description = _('Article')
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
#zr 评论应用配置类
|
|
||||||
class CommentsConfig(AppConfig):
|
|
||||||
#zr 定义应用名称
|
|
||||||
name = 'comments'
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
#zr 初始数据库迁移文件:创建评论表结构
|
|
||||||
# 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
|
|
||||||
|
|
||||||
#zr 数据库迁移类
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
#zr 初始迁移
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
#zr 依赖关系
|
|
||||||
dependencies = [
|
|
||||||
('blog', '0001_initial'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
#zr 迁移操作
|
|
||||||
operations = [
|
|
||||||
#zr 创建评论表
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Comment',
|
|
||||||
fields=[
|
|
||||||
#zr 主键ID字段
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
#zr 评论正文字段
|
|
||||||
('body', models.TextField(max_length=300, verbose_name='正文')),
|
|
||||||
#zr 创建时间字段
|
|
||||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
|
||||||
#zr 最后修改时间字段
|
|
||||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
|
||||||
#zr 是否显示字段
|
|
||||||
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
|
|
||||||
#zr 文章外键关联
|
|
||||||
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
|
|
||||||
#zr 作者外键关联
|
|
||||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
|
|
||||||
#zr 父评论自关联
|
|
||||||
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
|
|
||||||
],
|
|
||||||
#zr 模型元选项
|
|
||||||
options={
|
|
||||||
'verbose_name': '评论',
|
|
||||||
'verbose_name_plural': '评论',
|
|
||||||
'ordering': ['-id'],
|
|
||||||
'get_latest_by': 'id',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
#zr 数据库迁移文件:更新评论模型字段和选项
|
|
||||||
# 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
|
|
||||||
|
|
||||||
#zr 数据库迁移类
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
#zr 依赖的迁移文件
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('blog', '0005_alter_article_options_alter_category_options_and_more'),
|
|
||||||
('comments', '0002_alter_comment_is_enable'),
|
|
||||||
]
|
|
||||||
|
|
||||||
#zr 迁移操作列表
|
|
||||||
operations = [
|
|
||||||
#zr 更新评论模型的元选项
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='comment',
|
|
||||||
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
|
|
||||||
),
|
|
||||||
#zr 移除旧的创建时间字段
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='comment',
|
|
||||||
name='created_time',
|
|
||||||
),
|
|
||||||
#zr 移除旧的最后修改时间字段
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='comment',
|
|
||||||
name='last_mod_time',
|
|
||||||
),
|
|
||||||
#zr 添加新的创建时间字段
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='comment',
|
|
||||||
name='creation_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
|
||||||
),
|
|
||||||
#zr 添加新的最后修改时间字段
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='comment',
|
|
||||||
name='last_modify_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
|
|
||||||
),
|
|
||||||
#zr 更新文章外键字段配置
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='comment',
|
|
||||||
name='article',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
|
|
||||||
),
|
|
||||||
#zr 更新作者外键字段配置
|
|
||||||
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'),
|
|
||||||
),
|
|
||||||
#zr 更新是否启用字段配置
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='comment',
|
|
||||||
name='is_enable',
|
|
||||||
field=models.BooleanField(default=False, verbose_name='enable'),
|
|
||||||
),
|
|
||||||
#zr 更新父评论外键字段配置
|
|
||||||
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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
from django import template
|
|
||||||
|
|
||||||
#zr 注册模板标签库
|
|
||||||
register = template.Library()
|
|
||||||
|
|
||||||
#zr 解析评论树的模板标签
|
|
||||||
@register.simple_tag
|
|
||||||
def parse_commenttree(commentlist, comment):
|
|
||||||
"""获得当前评论子评论的列表
|
|
||||||
用法: {% parse_commenttree article_comments comment as childcomments %}
|
|
||||||
"""
|
|
||||||
datas = []
|
|
||||||
|
|
||||||
#zr 递归解析子评论的内部函数
|
|
||||||
def parse(c):
|
|
||||||
#zr 获取当前评论的直接子评论
|
|
||||||
childs = commentlist.filter(parent_comment=c, is_enable=True)
|
|
||||||
for child in childs:
|
|
||||||
#zr 将子评论添加到结果列表
|
|
||||||
datas.append(child)
|
|
||||||
#zr 递归解析子评论的子评论
|
|
||||||
parse(child)
|
|
||||||
|
|
||||||
#zr 从传入的评论开始解析
|
|
||||||
parse(comment)
|
|
||||||
return datas
|
|
||||||
|
|
||||||
#zr 显示评论项的包含标签
|
|
||||||
@register.inclusion_tag('comments/tags/comment_item.html')
|
|
||||||
def show_comment_item(comment, ischild):
|
|
||||||
"""评论"""
|
|
||||||
#zr 根据是否为子评论设置不同的深度
|
|
||||||
depth = 1 if ischild else 2
|
|
||||||
return {
|
|
||||||
'comment_item': comment,
|
|
||||||
'depth': depth
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
#zr 定义评论应用的命名空间
|
|
||||||
app_name = "comments"
|
|
||||||
#zr 评论模块URL路由配置
|
|
||||||
urlpatterns = [
|
|
||||||
#zr 文章评论提交路由
|
|
||||||
path(
|
|
||||||
'article/<int:article_id>/postcomment',
|
|
||||||
views.CommentPostView.as_view(),
|
|
||||||
name='postcomment'),
|
|
||||||
]
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from djangoblog.utils import get_current_site
|
|
||||||
from djangoblog.utils import send_email
|
|
||||||
|
|
||||||
# zr 获取当前模块的日志记录器
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
# zr 发送评论邮件功能
|
|
||||||
def send_comment_email(comment):
|
|
||||||
# zr 获取当前站点域名
|
|
||||||
site = get_current_site().domain
|
|
||||||
# zr 设置邮件主题
|
|
||||||
subject = _('Thanks for your comment')
|
|
||||||
# zr 构建文章完整URL
|
|
||||||
article_url = f"https://{site}{comment.article.get_absolute_url()}"
|
|
||||||
# zr 构建给评论作者的邮件内容
|
|
||||||
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}
|
|
||||||
# zr 获取评论作者邮箱并发送邮件
|
|
||||||
tomail = comment.author.email
|
|
||||||
send_email([tomail], subject, html_content)
|
|
||||||
|
|
||||||
# zr 如果是回复评论,同时发送邮件给被回复的评论作者
|
|
||||||
try:
|
|
||||||
if comment.parent_comment:
|
|
||||||
# zr 构建回复通知邮件内容
|
|
||||||
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}
|
|
||||||
# zr 获取被回复评论作者的邮箱并发送通知
|
|
||||||
tomail = comment.parent_comment.author.email
|
|
||||||
send_email([tomail], subject, html_content)
|
|
||||||
except Exception as e:
|
|
||||||
# zr 记录邮件发送异常
|
|
||||||
logger.error(e)
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
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"
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@ -1,119 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
@ -1,274 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
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)
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
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()
|
|
||||||
@ -1,183 +0,0 @@
|
|||||||
from django.utils.encoding import force_str
|
|
||||||
from elasticsearch_dsl import Q
|
|
||||||
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
|
|
||||||
from haystack.forms import ModelSearchForm
|
|
||||||
from haystack.models import SearchResult
|
|
||||||
from haystack.utils import log as logging
|
|
||||||
|
|
||||||
from blog.documents import ArticleDocument, ArticleDocumentManager
|
|
||||||
from blog.models import Article
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ElasticSearchBackend(BaseSearchBackend):
|
|
||||||
def __init__(self, connection_alias, **connection_options):
|
|
||||||
super(
|
|
||||||
ElasticSearchBackend,
|
|
||||||
self).__init__(
|
|
||||||
connection_alias,
|
|
||||||
**connection_options)
|
|
||||||
self.manager = ArticleDocumentManager()
|
|
||||||
self.include_spelling = True
|
|
||||||
|
|
||||||
def _get_models(self, iterable):
|
|
||||||
models = iterable if iterable and iterable[0] else Article.objects.all()
|
|
||||||
docs = self.manager.convert_to_doc(models)
|
|
||||||
return docs
|
|
||||||
|
|
||||||
def _create(self, models):
|
|
||||||
self.manager.create_index()
|
|
||||||
docs = self._get_models(models)
|
|
||||||
self.manager.rebuild(docs)
|
|
||||||
|
|
||||||
def _delete(self, models):
|
|
||||||
for m in models:
|
|
||||||
m.delete()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _rebuild(self, models):
|
|
||||||
models = models if models else Article.objects.all()
|
|
||||||
docs = self.manager.convert_to_doc(models)
|
|
||||||
self.manager.update_docs(docs)
|
|
||||||
|
|
||||||
def update(self, index, iterable, commit=True):
|
|
||||||
|
|
||||||
models = self._get_models(iterable)
|
|
||||||
self.manager.update_docs(models)
|
|
||||||
|
|
||||||
def remove(self, obj_or_string):
|
|
||||||
models = self._get_models([obj_or_string])
|
|
||||||
self._delete(models)
|
|
||||||
|
|
||||||
def clear(self, models=None, commit=True):
|
|
||||||
self.remove(None)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_suggestion(query: str) -> str:
|
|
||||||
"""获取推荐词, 如果没有找到添加原搜索词"""
|
|
||||||
|
|
||||||
search = ArticleDocument.search() \
|
|
||||||
.query("match", body=query) \
|
|
||||||
.suggest('suggest_search', query, term={'field': 'body'}) \
|
|
||||||
.execute()
|
|
||||||
|
|
||||||
keywords = []
|
|
||||||
for suggest in search.suggest.suggest_search:
|
|
||||||
if suggest["options"]:
|
|
||||||
keywords.append(suggest["options"][0]["text"])
|
|
||||||
else:
|
|
||||||
keywords.append(suggest["text"])
|
|
||||||
|
|
||||||
return ' '.join(keywords)
|
|
||||||
|
|
||||||
@log_query
|
|
||||||
def search(self, query_string, **kwargs):
|
|
||||||
logger.info('search query_string:' + query_string)
|
|
||||||
|
|
||||||
start_offset = kwargs.get('start_offset')
|
|
||||||
end_offset = kwargs.get('end_offset')
|
|
||||||
|
|
||||||
# 推荐词搜索
|
|
||||||
if getattr(self, "is_suggest", None):
|
|
||||||
suggestion = self.get_suggestion(query_string)
|
|
||||||
else:
|
|
||||||
suggestion = query_string
|
|
||||||
|
|
||||||
q = Q('bool',
|
|
||||||
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
|
|
||||||
minimum_should_match="70%")
|
|
||||||
|
|
||||||
search = ArticleDocument.search() \
|
|
||||||
.query('bool', filter=[q]) \
|
|
||||||
.filter('term', status='p') \
|
|
||||||
.filter('term', type='a') \
|
|
||||||
.source(False)[start_offset: end_offset]
|
|
||||||
|
|
||||||
results = search.execute()
|
|
||||||
hits = results['hits'].total
|
|
||||||
raw_results = []
|
|
||||||
for raw_result in results['hits']['hits']:
|
|
||||||
app_label = 'blog'
|
|
||||||
model_name = 'Article'
|
|
||||||
additional_fields = {}
|
|
||||||
|
|
||||||
result_class = SearchResult
|
|
||||||
|
|
||||||
result = result_class(
|
|
||||||
app_label,
|
|
||||||
model_name,
|
|
||||||
raw_result['_id'],
|
|
||||||
raw_result['_score'],
|
|
||||||
**additional_fields)
|
|
||||||
raw_results.append(result)
|
|
||||||
facets = {}
|
|
||||||
spelling_suggestion = None if query_string == suggestion else suggestion
|
|
||||||
|
|
||||||
return {
|
|
||||||
'results': raw_results,
|
|
||||||
'hits': hits,
|
|
||||||
'facets': facets,
|
|
||||||
'spelling_suggestion': spelling_suggestion,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ElasticSearchQuery(BaseSearchQuery):
|
|
||||||
def _convert_datetime(self, date):
|
|
||||||
if hasattr(date, 'hour'):
|
|
||||||
return force_str(date.strftime('%Y%m%d%H%M%S'))
|
|
||||||
else:
|
|
||||||
return force_str(date.strftime('%Y%m%d000000'))
|
|
||||||
|
|
||||||
def clean(self, query_fragment):
|
|
||||||
"""
|
|
||||||
Provides a mechanism for sanitizing user input before presenting the
|
|
||||||
value to the backend.
|
|
||||||
|
|
||||||
Whoosh 1.X differs here in that you can no longer use a backslash
|
|
||||||
to escape reserved characters. Instead, the whole word should be
|
|
||||||
quoted.
|
|
||||||
"""
|
|
||||||
words = query_fragment.split()
|
|
||||||
cleaned_words = []
|
|
||||||
|
|
||||||
for word in words:
|
|
||||||
if word in self.backend.RESERVED_WORDS:
|
|
||||||
word = word.replace(word, word.lower())
|
|
||||||
|
|
||||||
for char in self.backend.RESERVED_CHARACTERS:
|
|
||||||
if char in word:
|
|
||||||
word = "'%s'" % word
|
|
||||||
break
|
|
||||||
|
|
||||||
cleaned_words.append(word)
|
|
||||||
|
|
||||||
return ' '.join(cleaned_words)
|
|
||||||
|
|
||||||
def build_query_fragment(self, field, filter_type, value):
|
|
||||||
return value.query_string
|
|
||||||
|
|
||||||
def get_count(self):
|
|
||||||
results = self.get_results()
|
|
||||||
return len(results) if results else 0
|
|
||||||
|
|
||||||
def get_spelling_suggestion(self, preferred_query=None):
|
|
||||||
return self._spelling_suggestion
|
|
||||||
|
|
||||||
def build_params(self, spelling_query=None):
|
|
||||||
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
|
|
||||||
class ElasticSearchModelSearchForm(ModelSearchForm):
|
|
||||||
|
|
||||||
def search(self):
|
|
||||||
# 是否建议搜索
|
|
||||||
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
|
|
||||||
sqs = super().search()
|
|
||||||
return sqs
|
|
||||||
|
|
||||||
|
|
||||||
class ElasticSearchEngine(BaseEngine):
|
|
||||||
backend = ElasticSearchBackend
|
|
||||||
query = ElasticSearchQuery
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.contrib.syndication.views import Feed
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.feedgenerator import Rss201rev2Feed
|
|
||||||
|
|
||||||
from blog.models import Article
|
|
||||||
from djangoblog.utils import CommonMarkdown
|
|
||||||
|
|
||||||
|
|
||||||
class DjangoBlogFeed(Feed):
|
|
||||||
feed_type = Rss201rev2Feed
|
|
||||||
|
|
||||||
description = '大巧无工,重剑无锋.'
|
|
||||||
title = "且听风吟 大巧无工,重剑无锋. "
|
|
||||||
link = "/feed/"
|
|
||||||
|
|
||||||
def author_name(self):
|
|
||||||
return get_user_model().objects.first().nickname
|
|
||||||
|
|
||||||
def author_link(self):
|
|
||||||
return get_user_model().objects.first().get_absolute_url()
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
|
|
||||||
|
|
||||||
def item_title(self, item):
|
|
||||||
return item.title
|
|
||||||
|
|
||||||
def item_description(self, item):
|
|
||||||
return CommonMarkdown.get_markdown(item.body)
|
|
||||||
|
|
||||||
def feed_copyright(self):
|
|
||||||
now = timezone.now()
|
|
||||||
return "Copyright© {year} 且听风吟".format(year=now.year)
|
|
||||||
|
|
||||||
def item_link(self, item):
|
|
||||||
return item.get_absolute_url()
|
|
||||||
|
|
||||||
def item_guid(self, item):
|
|
||||||
return
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
from django.contrib.admin.models import DELETION
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.urls import reverse, NoReverseMatch
|
|
||||||
from django.utils.encoding import force_str
|
|
||||||
from django.utils.html import escape
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class LogEntryAdmin(admin.ModelAdmin):
|
|
||||||
list_filter = [
|
|
||||||
'content_type'
|
|
||||||
]
|
|
||||||
|
|
||||||
search_fields = [
|
|
||||||
'object_repr',
|
|
||||||
'change_message'
|
|
||||||
]
|
|
||||||
|
|
||||||
list_display_links = [
|
|
||||||
'action_time',
|
|
||||||
'get_change_message',
|
|
||||||
]
|
|
||||||
list_display = [
|
|
||||||
'action_time',
|
|
||||||
'user_link',
|
|
||||||
'content_type',
|
|
||||||
'object_link',
|
|
||||||
'get_change_message',
|
|
||||||
]
|
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
|
||||||
return (
|
|
||||||
request.user.is_superuser or
|
|
||||||
request.user.has_perm('admin.change_logentry')
|
|
||||||
) and request.method != 'POST'
|
|
||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def object_link(self, obj):
|
|
||||||
object_link = escape(obj.object_repr)
|
|
||||||
content_type = obj.content_type
|
|
||||||
|
|
||||||
if obj.action_flag != DELETION and content_type is not None:
|
|
||||||
# try returning an actual link instead of object repr string
|
|
||||||
try:
|
|
||||||
url = reverse(
|
|
||||||
'admin:{}_{}_change'.format(content_type.app_label,
|
|
||||||
content_type.model),
|
|
||||||
args=[obj.object_id]
|
|
||||||
)
|
|
||||||
object_link = '<a href="{}">{}</a>'.format(url, object_link)
|
|
||||||
except NoReverseMatch:
|
|
||||||
pass
|
|
||||||
return mark_safe(object_link)
|
|
||||||
|
|
||||||
object_link.admin_order_field = 'object_repr'
|
|
||||||
object_link.short_description = _('object')
|
|
||||||
|
|
||||||
def user_link(self, obj):
|
|
||||||
content_type = ContentType.objects.get_for_model(type(obj.user))
|
|
||||||
user_link = escape(force_str(obj.user))
|
|
||||||
try:
|
|
||||||
# try returning an actual link instead of object repr string
|
|
||||||
url = reverse(
|
|
||||||
'admin:{}_{}_change'.format(content_type.app_label,
|
|
||||||
content_type.model),
|
|
||||||
args=[obj.user.pk]
|
|
||||||
)
|
|
||||||
user_link = '<a href="{}">{}</a>'.format(url, user_link)
|
|
||||||
except NoReverseMatch:
|
|
||||||
pass
|
|
||||||
return mark_safe(user_link)
|
|
||||||
|
|
||||||
user_link.admin_order_field = 'user'
|
|
||||||
user_link.short_description = _('user')
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
|
||||||
queryset = super(LogEntryAdmin, self).get_queryset(request)
|
|
||||||
return queryset.prefetch_related('content_type')
|
|
||||||
|
|
||||||
def get_actions(self, request):
|
|
||||||
actions = super(LogEntryAdmin, self).get_actions(request)
|
|
||||||
if 'delete_selected' in actions:
|
|
||||||
del actions['delete_selected']
|
|
||||||
return actions
|
|
||||||
@ -1,194 +0,0 @@
|
|||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from django.template import TemplateDoesNotExist
|
|
||||||
from django.template.loader import render_to_string
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class BasePlugin:
|
|
||||||
# 插件元数据
|
|
||||||
PLUGIN_NAME = None
|
|
||||||
PLUGIN_DESCRIPTION = None
|
|
||||||
PLUGIN_VERSION = None
|
|
||||||
PLUGIN_AUTHOR = None
|
|
||||||
|
|
||||||
# 插件配置
|
|
||||||
SUPPORTED_POSITIONS = [] # 支持的显示位置
|
|
||||||
DEFAULT_PRIORITY = 100 # 默认优先级(数字越小优先级越高)
|
|
||||||
POSITION_PRIORITIES = {} # 各位置的优先级 {'sidebar': 50, 'article_bottom': 80}
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
|
|
||||||
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
|
|
||||||
|
|
||||||
# 设置插件路径
|
|
||||||
self.plugin_dir = self._get_plugin_directory()
|
|
||||||
self.plugin_slug = self._get_plugin_slug()
|
|
||||||
|
|
||||||
self.init_plugin()
|
|
||||||
self.register_hooks()
|
|
||||||
|
|
||||||
def _get_plugin_directory(self):
|
|
||||||
"""获取插件目录路径"""
|
|
||||||
import inspect
|
|
||||||
plugin_file = inspect.getfile(self.__class__)
|
|
||||||
return Path(plugin_file).parent
|
|
||||||
|
|
||||||
def _get_plugin_slug(self):
|
|
||||||
"""获取插件标识符(目录名)"""
|
|
||||||
return self.plugin_dir.name
|
|
||||||
|
|
||||||
def init_plugin(self):
|
|
||||||
"""
|
|
||||||
插件初始化逻辑
|
|
||||||
子类可以重写此方法来实现特定的初始化操作
|
|
||||||
"""
|
|
||||||
logger.info(f'{self.PLUGIN_NAME} initialized.')
|
|
||||||
|
|
||||||
def register_hooks(self):
|
|
||||||
"""
|
|
||||||
注册插件钩子
|
|
||||||
子类可以重写此方法来注册特定的钩子
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
# === 位置渲染系统 ===
|
|
||||||
def render_position_widget(self, position, context, **kwargs):
|
|
||||||
"""
|
|
||||||
根据位置渲染插件组件
|
|
||||||
|
|
||||||
Args:
|
|
||||||
position: 位置标识
|
|
||||||
context: 模板上下文
|
|
||||||
**kwargs: 额外参数
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: {'html': 'HTML内容', 'priority': 优先级} 或 None
|
|
||||||
"""
|
|
||||||
if position not in self.SUPPORTED_POSITIONS:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 检查条件显示
|
|
||||||
if not self.should_display(position, context, **kwargs):
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 调用具体的位置渲染方法
|
|
||||||
method_name = f'render_{position}_widget'
|
|
||||||
if hasattr(self, method_name):
|
|
||||||
html = getattr(self, method_name)(context, **kwargs)
|
|
||||||
if html:
|
|
||||||
priority = self.POSITION_PRIORITIES.get(position, self.DEFAULT_PRIORITY)
|
|
||||||
return {
|
|
||||||
'html': html,
|
|
||||||
'priority': priority,
|
|
||||||
'plugin_name': self.PLUGIN_NAME
|
|
||||||
}
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def should_display(self, position, context, **kwargs):
|
|
||||||
"""
|
|
||||||
判断插件是否应该在指定位置显示
|
|
||||||
子类可重写此方法实现条件显示逻辑
|
|
||||||
|
|
||||||
Args:
|
|
||||||
position: 位置标识
|
|
||||||
context: 模板上下文
|
|
||||||
**kwargs: 额外参数
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 是否显示
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
# === 各位置渲染方法 - 子类重写 ===
|
|
||||||
def render_sidebar_widget(self, context, **kwargs):
|
|
||||||
"""渲染侧边栏组件"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
def render_article_bottom_widget(self, context, **kwargs):
|
|
||||||
"""渲染文章底部组件"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
def render_article_top_widget(self, context, **kwargs):
|
|
||||||
"""渲染文章顶部组件"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
def render_header_widget(self, context, **kwargs):
|
|
||||||
"""渲染页头组件"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
def render_footer_widget(self, context, **kwargs):
|
|
||||||
"""渲染页脚组件"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
def render_comment_before_widget(self, context, **kwargs):
|
|
||||||
"""渲染评论前组件"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
def render_comment_after_widget(self, context, **kwargs):
|
|
||||||
"""渲染评论后组件"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
# === 模板系统 ===
|
|
||||||
def render_template(self, template_name, context=None):
|
|
||||||
"""
|
|
||||||
渲染插件模板
|
|
||||||
|
|
||||||
Args:
|
|
||||||
template_name: 模板文件名
|
|
||||||
context: 模板上下文
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
HTML字符串
|
|
||||||
"""
|
|
||||||
if context is None:
|
|
||||||
context = {}
|
|
||||||
|
|
||||||
template_path = f"plugins/{self.plugin_slug}/{template_name}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
return render_to_string(template_path, context)
|
|
||||||
except TemplateDoesNotExist:
|
|
||||||
logger.warning(f"Plugin template not found: {template_path}")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# === 静态资源系统 ===
|
|
||||||
def get_static_url(self, static_file):
|
|
||||||
"""获取插件静态文件URL"""
|
|
||||||
from django.templatetags.static import static
|
|
||||||
return static(f"{self.plugin_slug}/static/{self.plugin_slug}/{static_file}")
|
|
||||||
|
|
||||||
def get_css_files(self):
|
|
||||||
"""获取插件CSS文件列表"""
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_js_files(self):
|
|
||||||
"""获取插件JavaScript文件列表"""
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_head_html(self, context=None):
|
|
||||||
"""获取需要插入到<head>中的HTML内容"""
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_body_html(self, context=None):
|
|
||||||
"""获取需要插入到<body>底部的HTML内容"""
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_plugin_info(self):
|
|
||||||
"""
|
|
||||||
获取插件信息
|
|
||||||
:return: 包含插件元数据的字典
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'name': self.PLUGIN_NAME,
|
|
||||||
'description': self.PLUGIN_DESCRIPTION,
|
|
||||||
'version': self.PLUGIN_VERSION,
|
|
||||||
'author': self.PLUGIN_AUTHOR,
|
|
||||||
'slug': self.plugin_slug,
|
|
||||||
'directory': str(self.plugin_dir),
|
|
||||||
'supported_positions': self.SUPPORTED_POSITIONS,
|
|
||||||
'priorities': self.POSITION_PRIORITIES
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
ARTICLE_DETAIL_LOAD = 'article_detail_load'
|
|
||||||
ARTICLE_CREATE = 'article_create'
|
|
||||||
ARTICLE_UPDATE = 'article_update'
|
|
||||||
ARTICLE_DELETE = 'article_delete'
|
|
||||||
|
|
||||||
ARTICLE_CONTENT_HOOK_NAME = "the_content"
|
|
||||||
|
|
||||||
# 位置钩子常量
|
|
||||||
POSITION_HOOKS = {
|
|
||||||
'article_top': 'article_top_widgets',
|
|
||||||
'article_bottom': 'article_bottom_widgets',
|
|
||||||
'sidebar': 'sidebar_widgets',
|
|
||||||
'header': 'header_widgets',
|
|
||||||
'footer': 'footer_widgets',
|
|
||||||
'comment_before': 'comment_before_widgets',
|
|
||||||
'comment_after': 'comment_after_widgets',
|
|
||||||
}
|
|
||||||
|
|
||||||
# 资源注入钩子
|
|
||||||
HEAD_RESOURCES_HOOK = 'head_resources'
|
|
||||||
BODY_RESOURCES_HOOK = 'body_resources'
|
|
||||||
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_hooks = {}
|
|
||||||
|
|
||||||
|
|
||||||
def register(hook_name: str, callback: callable):
|
|
||||||
"""
|
|
||||||
注册一个钩子回调。
|
|
||||||
"""
|
|
||||||
if hook_name not in _hooks:
|
|
||||||
_hooks[hook_name] = []
|
|
||||||
_hooks[hook_name].append(callback)
|
|
||||||
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
|
|
||||||
|
|
||||||
|
|
||||||
def run_action(hook_name: str, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
执行一个 Action Hook。
|
|
||||||
它会按顺序执行所有注册到该钩子上的回调函数。
|
|
||||||
"""
|
|
||||||
if hook_name in _hooks:
|
|
||||||
logger.debug(f"Running action hook '{hook_name}'")
|
|
||||||
for callback in _hooks[hook_name]:
|
|
||||||
try:
|
|
||||||
callback(*args, **kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
|
|
||||||
|
|
||||||
|
|
||||||
def apply_filters(hook_name: str, value, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
执行一个 Filter Hook。
|
|
||||||
它会把 value 依次传递给所有注册的回调函数进行处理。
|
|
||||||
"""
|
|
||||||
if hook_name in _hooks:
|
|
||||||
logger.debug(f"Applying filter hook '{hook_name}'")
|
|
||||||
for callback in _hooks[hook_name]:
|
|
||||||
try:
|
|
||||||
value = callback(value, *args, **kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
|
|
||||||
return value
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
import os
|
|
||||||
import logging
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# 全局插件注册表
|
|
||||||
_loaded_plugins = []
|
|
||||||
|
|
||||||
def load_plugins():
|
|
||||||
"""
|
|
||||||
Dynamically loads and initializes plugins from the 'plugins' directory.
|
|
||||||
This function is intended to be called when the Django app registry is ready.
|
|
||||||
"""
|
|
||||||
global _loaded_plugins
|
|
||||||
_loaded_plugins = []
|
|
||||||
|
|
||||||
for plugin_name in settings.ACTIVE_PLUGINS:
|
|
||||||
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
|
|
||||||
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
|
|
||||||
try:
|
|
||||||
# 导入插件模块
|
|
||||||
plugin_module = __import__(f'plugins.{plugin_name}.plugin', fromlist=['plugin'])
|
|
||||||
|
|
||||||
# 获取插件实例
|
|
||||||
if hasattr(plugin_module, 'plugin'):
|
|
||||||
plugin_instance = plugin_module.plugin
|
|
||||||
_loaded_plugins.append(plugin_instance)
|
|
||||||
logger.info(f"Successfully loaded plugin: {plugin_name} - {plugin_instance.PLUGIN_NAME}")
|
|
||||||
else:
|
|
||||||
logger.warning(f"Plugin {plugin_name} does not have 'plugin' instance")
|
|
||||||
|
|
||||||
except ImportError as e:
|
|
||||||
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
|
|
||||||
except AttributeError as e:
|
|
||||||
logger.error(f"Failed to get plugin instance: {plugin_name}", exc_info=e)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Unexpected error loading plugin: {plugin_name}", exc_info=e)
|
|
||||||
|
|
||||||
def get_loaded_plugins():
|
|
||||||
"""获取所有已加载的插件"""
|
|
||||||
return _loaded_plugins
|
|
||||||
|
|
||||||
def get_plugin_by_name(plugin_name):
|
|
||||||
"""根据名称获取插件"""
|
|
||||||
for plugin in _loaded_plugins:
|
|
||||||
if plugin.plugin_slug == plugin_name:
|
|
||||||
return plugin
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_plugin_by_slug(plugin_slug):
|
|
||||||
"""根据slug获取插件"""
|
|
||||||
for plugin in _loaded_plugins:
|
|
||||||
if plugin.plugin_slug == plugin_slug:
|
|
||||||
return plugin
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_plugins_info():
|
|
||||||
"""获取所有插件的信息"""
|
|
||||||
return [plugin.get_plugin_info() for plugin in _loaded_plugins]
|
|
||||||
|
|
||||||
def get_plugins_by_position(position):
|
|
||||||
"""获取支持指定位置的插件"""
|
|
||||||
return [plugin for plugin in _loaded_plugins if position in plugin.SUPPORTED_POSITIONS]
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue