Compare commits
15 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
ba510782bb | 4 months ago |
|
|
b2467cec3f | 4 months ago |
|
|
324680501f | 4 months ago |
|
|
7fe8b5db6b | 4 months ago |
|
|
69199c2565 | 4 months ago |
|
|
aed8284a2d | 4 months ago |
|
|
9ebaf12bee | 4 months ago |
|
|
b96174e67e | 4 months ago |
|
|
54de0ea2d8 | 4 months ago |
|
|
487275f3ed | 4 months ago |
|
|
3cbbca1208 | 4 months ago |
|
|
5211731dd4 | 4 months ago |
|
|
fbc229a5f2 | 4 months ago |
|
|
1c3715a9df | 4 months ago |
|
|
83095fa072 | 5 months ago |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
# 项目报告
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
#初始化
|
|
||||||
@ -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,82 +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/
|
|
||||||
|
|
||||||
.venv/
|
|
||||||
.ruff_cache/
|
|
||||||
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,376 +0,0 @@
|
|||||||
import logging
|
|
||||||
import re
|
|
||||||
from abc import abstractmethod
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
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 mdeditor.fields import MDTextField
|
|
||||||
from uuslug import slugify
|
|
||||||
|
|
||||||
from djangoblog.utils import cache_decorator, cache
|
|
||||||
from djangoblog.utils import get_current_site
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class LinkShowType(models.TextChoices):
|
|
||||||
I = ('i', _('index'))
|
|
||||||
L = ('l', _('list'))
|
|
||||||
P = ('p', _('post'))
|
|
||||||
A = ('a', _('all'))
|
|
||||||
S = ('s', _('slide'))
|
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_modify_time = models.DateTimeField(_('modify time'), default=now)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
is_update_views = isinstance(
|
|
||||||
self,
|
|
||||||
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
|
|
||||||
if is_update_views:
|
|
||||||
Article.objects.filter(pk=self.pk).update(views=self.views)
|
|
||||||
else:
|
|
||||||
if 'slug' in self.__dict__:
|
|
||||||
slug = getattr(
|
|
||||||
self, 'title') if 'title' in self.__dict__ else getattr(
|
|
||||||
self, 'name')
|
|
||||||
setattr(self, 'slug', slugify(slug))
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_full_url(self):
|
|
||||||
site = get_current_site().domain
|
|
||||||
url = "https://{site}{path}".format(site=site,
|
|
||||||
path=self.get_absolute_url())
|
|
||||||
return url
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_absolute_url(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Article(BaseModel):
|
|
||||||
"""文章"""
|
|
||||||
STATUS_CHOICES = (
|
|
||||||
('d', _('Draft')),
|
|
||||||
('p', _('Published')),
|
|
||||||
)
|
|
||||||
COMMENT_STATUS = (
|
|
||||||
('o', _('Open')),
|
|
||||||
('c', _('Close')),
|
|
||||||
)
|
|
||||||
TYPE = (
|
|
||||||
('a', _('Article')),
|
|
||||||
('p', _('Page')),
|
|
||||||
)
|
|
||||||
title = models.CharField(_('title'), max_length=200, unique=True)
|
|
||||||
body = MDTextField(_('body'))
|
|
||||||
pub_time = models.DateTimeField(
|
|
||||||
_('publish time'), blank=False, null=False, default=now)
|
|
||||||
status = models.CharField(
|
|
||||||
_('status'),
|
|
||||||
max_length=1,
|
|
||||||
choices=STATUS_CHOICES,
|
|
||||||
default='p')
|
|
||||||
comment_status = models.CharField(
|
|
||||||
_('comment status'),
|
|
||||||
max_length=1,
|
|
||||||
choices=COMMENT_STATUS,
|
|
||||||
default='o')
|
|
||||||
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
|
|
||||||
views = models.PositiveIntegerField(_('views'), default=0)
|
|
||||||
author = models.ForeignKey(
|
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
verbose_name=_('author'),
|
|
||||||
blank=False,
|
|
||||||
null=False,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
article_order = models.IntegerField(
|
|
||||||
_('order'), blank=False, null=False, default=0)
|
|
||||||
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
|
|
||||||
category = models.ForeignKey(
|
|
||||||
'Category',
|
|
||||||
verbose_name=_('category'),
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
blank=False,
|
|
||||||
null=False)
|
|
||||||
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
|
|
||||||
|
|
||||||
def body_to_string(self):
|
|
||||||
return self.body
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-article_order', '-pub_time']
|
|
||||||
verbose_name = _('article')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
get_latest_by = 'id'
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('blog:detailbyid', kwargs={
|
|
||||||
'article_id': self.id,
|
|
||||||
'year': self.creation_time.year,
|
|
||||||
'month': self.creation_time.month,
|
|
||||||
'day': self.creation_time.day
|
|
||||||
})
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_category_tree(self):
|
|
||||||
tree = self.category.get_category_tree()
|
|
||||||
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
|
|
||||||
|
|
||||||
return names
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def viewed(self):
|
|
||||||
self.views += 1
|
|
||||||
self.save(update_fields=['views'])
|
|
||||||
|
|
||||||
def comment_list(self):
|
|
||||||
cache_key = 'article_comments_{id}'.format(id=self.id)
|
|
||||||
value = cache.get(cache_key)
|
|
||||||
if value:
|
|
||||||
logger.info('get article comments:{id}'.format(id=self.id))
|
|
||||||
return value
|
|
||||||
else:
|
|
||||||
comments = self.comment_set.filter(is_enable=True).order_by('-id')
|
|
||||||
cache.set(cache_key, comments, 60 * 100)
|
|
||||||
logger.info('set article comments:{id}'.format(id=self.id))
|
|
||||||
return comments
|
|
||||||
|
|
||||||
def get_admin_url(self):
|
|
||||||
info = (self._meta.app_label, self._meta.model_name)
|
|
||||||
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
|
|
||||||
|
|
||||||
@cache_decorator(expiration=60 * 100)
|
|
||||||
def next_article(self):
|
|
||||||
# 下一篇
|
|
||||||
return Article.objects.filter(
|
|
||||||
id__gt=self.id, status='p').order_by('id').first()
|
|
||||||
|
|
||||||
@cache_decorator(expiration=60 * 100)
|
|
||||||
def prev_article(self):
|
|
||||||
# 前一篇
|
|
||||||
return Article.objects.filter(id__lt=self.id, status='p').first()
|
|
||||||
|
|
||||||
def get_first_image_url(self):
|
|
||||||
"""
|
|
||||||
Get the first image url from article.body.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
|
|
||||||
if match:
|
|
||||||
return match.group(1)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
class Category(BaseModel):
|
|
||||||
"""文章分类"""
|
|
||||||
name = models.CharField(_('category name'), max_length=30, unique=True)
|
|
||||||
parent_category = models.ForeignKey(
|
|
||||||
'self',
|
|
||||||
verbose_name=_('parent category'),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
|
|
||||||
index = models.IntegerField(default=0, verbose_name=_('index'))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-index']
|
|
||||||
verbose_name = _('category')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse(
|
|
||||||
'blog:category_detail', kwargs={
|
|
||||||
'category_name': self.slug})
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_category_tree(self):
|
|
||||||
"""
|
|
||||||
递归获得分类目录的父级
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
categorys = []
|
|
||||||
|
|
||||||
def parse(category):
|
|
||||||
categorys.append(category)
|
|
||||||
if category.parent_category:
|
|
||||||
parse(category.parent_category)
|
|
||||||
|
|
||||||
parse(self)
|
|
||||||
return categorys
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_sub_categorys(self):
|
|
||||||
"""
|
|
||||||
获得当前分类目录所有子集
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
categorys = []
|
|
||||||
all_categorys = Category.objects.all()
|
|
||||||
|
|
||||||
def parse(category):
|
|
||||||
if category not in categorys:
|
|
||||||
categorys.append(category)
|
|
||||||
childs = all_categorys.filter(parent_category=category)
|
|
||||||
for child in childs:
|
|
||||||
if category not in categorys:
|
|
||||||
categorys.append(child)
|
|
||||||
parse(child)
|
|
||||||
|
|
||||||
parse(self)
|
|
||||||
return categorys
|
|
||||||
|
|
||||||
|
|
||||||
class Tag(BaseModel):
|
|
||||||
"""文章标签"""
|
|
||||||
name = models.CharField(_('tag name'), max_length=30, unique=True)
|
|
||||||
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_article_count(self):
|
|
||||||
return Article.objects.filter(tags__name=self.name).distinct().count()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['name']
|
|
||||||
verbose_name = _('tag')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
|
|
||||||
class Links(models.Model):
|
|
||||||
"""友情链接"""
|
|
||||||
|
|
||||||
name = models.CharField(_('link name'), max_length=30, unique=True)
|
|
||||||
link = models.URLField(_('link'))
|
|
||||||
sequence = models.IntegerField(_('order'), unique=True)
|
|
||||||
is_enable = models.BooleanField(
|
|
||||||
_('is show'), default=True, blank=False, null=False)
|
|
||||||
show_type = models.CharField(
|
|
||||||
_('show type'),
|
|
||||||
max_length=1,
|
|
||||||
choices=LinkShowType.choices,
|
|
||||||
default=LinkShowType.I)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_mod_time = models.DateTimeField(_('modify time'), default=now)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['sequence']
|
|
||||||
verbose_name = _('link')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class SideBar(models.Model):
|
|
||||||
"""侧边栏,可以展示一些html内容"""
|
|
||||||
name = models.CharField(_('title'), max_length=100)
|
|
||||||
content = models.TextField(_('content'))
|
|
||||||
sequence = models.IntegerField(_('order'), unique=True)
|
|
||||||
is_enable = models.BooleanField(_('is enable'), default=True)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_mod_time = models.DateTimeField(_('modify time'), default=now)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['sequence']
|
|
||||||
verbose_name = _('sidebar')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class BlogSettings(models.Model):
|
|
||||||
"""blog的配置"""
|
|
||||||
site_name = models.CharField(
|
|
||||||
_('site name'),
|
|
||||||
max_length=200,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
site_description = models.TextField(
|
|
||||||
_('site description'),
|
|
||||||
max_length=1000,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
site_seo_description = models.TextField(
|
|
||||||
_('site seo description'), max_length=1000, null=False, blank=False, default='')
|
|
||||||
site_keywords = models.TextField(
|
|
||||||
_('site keywords'),
|
|
||||||
max_length=1000,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
article_sub_length = models.IntegerField(_('article sub length'), default=300)
|
|
||||||
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
|
|
||||||
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
|
|
||||||
article_comment_count = models.IntegerField(_('article comment count'), default=5)
|
|
||||||
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
|
|
||||||
google_adsense_codes = models.TextField(
|
|
||||||
_('adsense code'), max_length=2000, null=True, blank=True, default='')
|
|
||||||
open_site_comment = models.BooleanField(_('open site comment'), default=True)
|
|
||||||
global_header = models.TextField("公共头部", null=True, blank=True, default='')
|
|
||||||
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
|
|
||||||
beian_code = models.CharField(
|
|
||||||
'备案号',
|
|
||||||
max_length=2000,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
default='')
|
|
||||||
analytics_code = models.TextField(
|
|
||||||
"网站统计代码",
|
|
||||||
max_length=1000,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
show_gongan_code = models.BooleanField(
|
|
||||||
'是否显示公安备案号', default=False, null=False)
|
|
||||||
gongan_beiancode = models.TextField(
|
|
||||||
'公安备案号',
|
|
||||||
max_length=2000,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
default='')
|
|
||||||
comment_need_review = models.BooleanField(
|
|
||||||
'评论是否需要审核', default=False, null=False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _('Website configuration')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.site_name
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if BlogSettings.objects.exclude(id=self.id).count():
|
|
||||||
raise ValidationError(_('There can only be one configuration'))
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
from djangoblog.utils import cache
|
|
||||||
cache.clear()
|
|
||||||
@ -1,372 +0,0 @@
|
|||||||
import logging
|
|
||||||
import re
|
|
||||||
from abc import abstractmethod
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
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 mdeditor.fields import MDTextField
|
|
||||||
from uuslug import slugify
|
|
||||||
|
|
||||||
from djangoblog.utils import cache_decorator, cache
|
|
||||||
from djangoblog.utils import get_current_site
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class LinkShowType(models.TextChoices):
|
|
||||||
I = ('i', _('index'))
|
|
||||||
L = ('l', _('list'))
|
|
||||||
P = ('p', _('post'))
|
|
||||||
A = ('a', _('all'))
|
|
||||||
S = ('s', _('slide'))
|
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_modify_time = models.DateTimeField(_('modify time'), default=now)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
is_update_views = isinstance(
|
|
||||||
self,
|
|
||||||
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
|
|
||||||
if is_update_views:
|
|
||||||
Article.objects.filter(pk=self.pk).update(views=self.views)
|
|
||||||
else:
|
|
||||||
if 'slug' in self.__dict__:
|
|
||||||
slug = getattr(
|
|
||||||
self, 'title') if 'title' in self.__dict__ else getattr(
|
|
||||||
self, 'name')
|
|
||||||
setattr(self, 'slug', slugify(slug))
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_full_url(self):
|
|
||||||
site = get_current_site().domain
|
|
||||||
url = "https://{site}{path}".format(site=site,
|
|
||||||
path=self.get_absolute_url())
|
|
||||||
return url
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_absolute_url(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Article(BaseModel):
|
|
||||||
"""文章"""
|
|
||||||
STATUS_CHOICES = (
|
|
||||||
('d', _('Draft')),
|
|
||||||
('p', _('Published')),
|
|
||||||
)
|
|
||||||
COMMENT_STATUS = (
|
|
||||||
('o', _('Open')),
|
|
||||||
('c', _('Close')),
|
|
||||||
)
|
|
||||||
TYPE = (
|
|
||||||
('a', _('Article')),
|
|
||||||
('p', _('Page')),
|
|
||||||
)
|
|
||||||
title = models.CharField(_('title'), max_length=200, unique=True)
|
|
||||||
body = MDTextField(_('body'))
|
|
||||||
pub_time = models.DateTimeField(
|
|
||||||
_('publish time'), blank=False, null=False, default=now)
|
|
||||||
status = models.CharField(
|
|
||||||
_('status'),
|
|
||||||
max_length=1,
|
|
||||||
choices=STATUS_CHOICES,
|
|
||||||
default='p')
|
|
||||||
comment_status = models.CharField(
|
|
||||||
_('comment status'),
|
|
||||||
max_length=1,
|
|
||||||
choices=COMMENT_STATUS,
|
|
||||||
default='o')
|
|
||||||
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
|
|
||||||
views = models.PositiveIntegerField(_('views'), default=0)
|
|
||||||
author = models.ForeignKey(
|
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
verbose_name=_('author'),
|
|
||||||
blank=False,
|
|
||||||
null=False,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
article_order = models.IntegerField(
|
|
||||||
_('order'), blank=False, null=False, default=0)
|
|
||||||
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
|
|
||||||
category = models.ForeignKey(
|
|
||||||
'Category',
|
|
||||||
verbose_name=_('category'),
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
blank=False,
|
|
||||||
null=False)
|
|
||||||
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
|
|
||||||
|
|
||||||
def body_to_string(self):
|
|
||||||
return self.body
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-article_order', '-pub_time']
|
|
||||||
verbose_name = _('article')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
get_latest_by = 'id'
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('blog:detailbyid', kwargs={
|
|
||||||
'article_id': self.id,
|
|
||||||
'year': self.creation_time.year,
|
|
||||||
'month': self.creation_time.month,
|
|
||||||
'day': self.creation_time.day
|
|
||||||
})
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_category_tree(self):
|
|
||||||
tree = self.category.get_category_tree()
|
|
||||||
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
|
|
||||||
|
|
||||||
return names
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
cache.clear()
|
|
||||||
|
|
||||||
def viewed(self):
|
|
||||||
self.views += 1
|
|
||||||
self.save(update_fields=['views'])
|
|
||||||
|
|
||||||
def comment_list(self):
|
|
||||||
cache_key = 'article_comments_{id}'.format(id=self.id)
|
|
||||||
value = cache.get(cache_key)
|
|
||||||
if value:
|
|
||||||
logger.info('get article comments:{id}'.format(id=self.id))
|
|
||||||
return value
|
|
||||||
else:
|
|
||||||
comments = self.comment_set.filter(is_enable=True).order_by('-id')
|
|
||||||
cache.set(cache_key, comments, 60 * 100)
|
|
||||||
logger.info('set article comments:{id}'.format(id=self.id))
|
|
||||||
return comments
|
|
||||||
|
|
||||||
def get_admin_url(self):
|
|
||||||
info = (self._meta.app_label, self._meta.model_name)
|
|
||||||
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
|
|
||||||
|
|
||||||
@cache_decorator(expiration=60 * 100)
|
|
||||||
def next_article(self):
|
|
||||||
# 下一篇
|
|
||||||
return Article.objects.filter(
|
|
||||||
id__gt=self.id, status='p').order_by('id').first()
|
|
||||||
|
|
||||||
@cache_decorator(expiration=60 * 100)
|
|
||||||
def prev_article(self):
|
|
||||||
# 前一篇
|
|
||||||
return Article.objects.filter(id__lt=self.id, status='p').first()
|
|
||||||
|
|
||||||
def get_first_image_url(self):
|
|
||||||
"""
|
|
||||||
Get the first image url from article.body.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
|
|
||||||
if match:
|
|
||||||
return match.group(1)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
class Category(BaseModel):
|
|
||||||
"""文章分类"""
|
|
||||||
name = models.CharField(_('category name'), max_length=30, unique=True)
|
|
||||||
parent_category = models.ForeignKey(
|
|
||||||
'self',
|
|
||||||
verbose_name=_('parent category'),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
|
|
||||||
index = models.IntegerField(default=0, verbose_name=_('index'))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-index']
|
|
||||||
verbose_name = _('category')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse(
|
|
||||||
'blog:category_detail', kwargs={
|
|
||||||
'category_name': self.slug})
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_category_tree(self):
|
|
||||||
"""
|
|
||||||
递归获得分类目录的父级
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
categorys = []
|
|
||||||
|
|
||||||
def parse(category):
|
|
||||||
categorys.append(category)
|
|
||||||
if category.parent_category:
|
|
||||||
parse(category.parent_category)
|
|
||||||
|
|
||||||
parse(self)
|
|
||||||
return categorys
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_sub_categorys(self):
|
|
||||||
"""
|
|
||||||
获得当前分类目录所有子集
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
categorys = []
|
|
||||||
all_categorys = Category.objects.all()
|
|
||||||
|
|
||||||
def parse(category):
|
|
||||||
if category not in categorys:
|
|
||||||
categorys.append(category)
|
|
||||||
childs = all_categorys.filter(parent_category=category)
|
|
||||||
for child in childs:
|
|
||||||
if category not in categorys:
|
|
||||||
categorys.append(child)
|
|
||||||
parse(child)
|
|
||||||
|
|
||||||
parse(self)
|
|
||||||
return categorys
|
|
||||||
|
|
||||||
|
|
||||||
class Tag(BaseModel):
|
|
||||||
"""文章标签"""
|
|
||||||
name = models.CharField(_('tag name'), max_length=30, unique=True)
|
|
||||||
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_article_count(self):
|
|
||||||
return Article.objects.filter(tags__name=self.name).distinct().count()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['name']
|
|
||||||
verbose_name = _('tag')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
|
|
||||||
class Links(models.Model):
|
|
||||||
"""友情链接"""
|
|
||||||
|
|
||||||
name = models.CharField(_('link name'), max_length=30, unique=True)
|
|
||||||
link = models.URLField(_('link'))
|
|
||||||
sequence = models.IntegerField(_('order'), unique=True)
|
|
||||||
is_enable = models.BooleanField(
|
|
||||||
_('is show'), default=True, blank=False, null=False)
|
|
||||||
show_type = models.CharField(
|
|
||||||
_('show type'),
|
|
||||||
max_length=1,
|
|
||||||
choices=LinkShowType.choices,
|
|
||||||
default=LinkShowType.I)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_mod_time = models.DateTimeField(_('modify time'), default=now)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['sequence']
|
|
||||||
verbose_name = _('link')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class SideBar(models.Model):
|
|
||||||
"""侧边栏,可以展示一些html内容"""
|
|
||||||
name = models.CharField(_('title'), max_length=100)
|
|
||||||
content = models.TextField(_('content'))
|
|
||||||
sequence = models.IntegerField(_('order'), unique=True)
|
|
||||||
is_enable = models.BooleanField(_('is enable'), default=True)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_mod_time = models.DateTimeField(_('modify time'), default=now)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['sequence']
|
|
||||||
verbose_name = _('sidebar')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class BlogSettings(models.Model):
|
|
||||||
"""blog的配置"""
|
|
||||||
site_name = models.CharField(
|
|
||||||
_('site name'),
|
|
||||||
max_length=200,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
site_description = models.TextField(
|
|
||||||
_('site description'),
|
|
||||||
max_length=1000,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
site_seo_description = models.TextField(
|
|
||||||
_('site seo description'), max_length=1000, null=False, blank=False, default='')
|
|
||||||
site_keywords = models.TextField(
|
|
||||||
_('site keywords'),
|
|
||||||
max_length=1000,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
article_sub_length = models.IntegerField(_('article sub length'), default=300)
|
|
||||||
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
|
|
||||||
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
|
|
||||||
article_comment_count = models.IntegerField(_('article comment count'), default=5)
|
|
||||||
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
|
|
||||||
google_adsense_codes = models.TextField(
|
|
||||||
_('adsense code'), max_length=2000, null=True, blank=True, default='')
|
|
||||||
open_site_comment = models.BooleanField(_('open site comment'), default=True)
|
|
||||||
global_header = models.TextField("公共头部", null=True, blank=True, default='')
|
|
||||||
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
|
|
||||||
beian_code = models.CharField(
|
|
||||||
'备案号',
|
|
||||||
max_length=2000,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
default='')
|
|
||||||
analytics_code = models.TextField(
|
|
||||||
"网站统计代码",
|
|
||||||
max_length=1000,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
show_gongan_code = models.BooleanField(
|
|
||||||
'是否显示公安备案号', default=False, null=False)
|
|
||||||
gongan_beiancode = models.TextField(
|
|
||||||
'公安备案号',
|
|
||||||
max_length=2000,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
default='')
|
|
||||||
comment_need_review = models.BooleanField(
|
|
||||||
'评论是否需要审核', default=False, null=False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _('Website configuration')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.site_name
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if BlogSettings.objects.exclude(id=self.id).count():
|
|
||||||
raise ValidationError(_('There can only be one configuration'))
|
|
||||||
@ -1,47 +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 _
|
|
||||||
|
|
||||||
|
|
||||||
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')
|
|
||||||
@ -1,50 +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 _
|
|
||||||
|
|
||||||
|
|
||||||
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(
|
|
||||||
'<a href="{}">{}</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(
|
|
||||||
'<a href="{}">{}</a>',
|
|
||||||
link,
|
|
||||||
obj.article.title)
|
|
||||||
|
|
||||||
link_to_userinfo.short_description = _('User')
|
|
||||||
link_to_article.short_description = _('Article')
|
|
||||||
@ -1,376 +0,0 @@
|
|||||||
import logging
|
|
||||||
import re
|
|
||||||
from abc import abstractmethod
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
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 mdeditor.fields import MDTextField
|
|
||||||
from uuslug import slugify
|
|
||||||
|
|
||||||
from djangoblog.utils import cache_decorator, cache
|
|
||||||
from djangoblog.utils import get_current_site
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class LinkShowType(models.TextChoices):
|
|
||||||
I = ('i', _('index'))
|
|
||||||
L = ('l', _('list'))
|
|
||||||
P = ('p', _('post'))
|
|
||||||
A = ('a', _('all'))
|
|
||||||
S = ('s', _('slide'))
|
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_modify_time = models.DateTimeField(_('modify time'), default=now)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
is_update_views = isinstance(
|
|
||||||
self,
|
|
||||||
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
|
|
||||||
if is_update_views:
|
|
||||||
Article.objects.filter(pk=self.pk).update(views=self.views)
|
|
||||||
else:
|
|
||||||
if 'slug' in self.__dict__:
|
|
||||||
slug = getattr(
|
|
||||||
self, 'title') if 'title' in self.__dict__ else getattr(
|
|
||||||
self, 'name')
|
|
||||||
setattr(self, 'slug', slugify(slug))
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_full_url(self):
|
|
||||||
site = get_current_site().domain
|
|
||||||
url = "https://{site}{path}".format(site=site,
|
|
||||||
path=self.get_absolute_url())
|
|
||||||
return url
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_absolute_url(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Article(BaseModel):
|
|
||||||
"""文章"""
|
|
||||||
STATUS_CHOICES = (
|
|
||||||
('d', _('Draft')),
|
|
||||||
('p', _('Published')),
|
|
||||||
)
|
|
||||||
COMMENT_STATUS = (
|
|
||||||
('o', _('Open')),
|
|
||||||
('c', _('Close')),
|
|
||||||
)
|
|
||||||
TYPE = (
|
|
||||||
('a', _('Article')),
|
|
||||||
('p', _('Page')),
|
|
||||||
)
|
|
||||||
title = models.CharField(_('title'), max_length=200, unique=True)
|
|
||||||
body = MDTextField(_('body'))
|
|
||||||
pub_time = models.DateTimeField(
|
|
||||||
_('publish time'), blank=False, null=False, default=now)
|
|
||||||
status = models.CharField(
|
|
||||||
_('status'),
|
|
||||||
max_length=1,
|
|
||||||
choices=STATUS_CHOICES,
|
|
||||||
default='p')
|
|
||||||
comment_status = models.CharField(
|
|
||||||
_('comment status'),
|
|
||||||
max_length=1,
|
|
||||||
choices=COMMENT_STATUS,
|
|
||||||
default='o')
|
|
||||||
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
|
|
||||||
views = models.PositiveIntegerField(_('views'), default=0)
|
|
||||||
author = models.ForeignKey(
|
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
verbose_name=_('author'),
|
|
||||||
blank=False,
|
|
||||||
null=False,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
article_order = models.IntegerField(
|
|
||||||
_('order'), blank=False, null=False, default=0)
|
|
||||||
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
|
|
||||||
category = models.ForeignKey(
|
|
||||||
'Category',
|
|
||||||
verbose_name=_('category'),
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
blank=False,
|
|
||||||
null=False)
|
|
||||||
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
|
|
||||||
|
|
||||||
def body_to_string(self):
|
|
||||||
return self.body
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-article_order', '-pub_time']
|
|
||||||
verbose_name = _('article')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
get_latest_by = 'id'
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('blog:detailbyid', kwargs={
|
|
||||||
'article_id': self.id,
|
|
||||||
'year': self.creation_time.year,
|
|
||||||
'month': self.creation_time.month,
|
|
||||||
'day': self.creation_time.day
|
|
||||||
})
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_category_tree(self):
|
|
||||||
tree = self.category.get_category_tree()
|
|
||||||
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
|
|
||||||
|
|
||||||
return names
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def viewed(self):
|
|
||||||
self.views += 1
|
|
||||||
self.save(update_fields=['views'])
|
|
||||||
|
|
||||||
def comment_list(self):
|
|
||||||
cache_key = 'article_comments_{id}'.format(id=self.id)
|
|
||||||
value = cache.get(cache_key)
|
|
||||||
if value:
|
|
||||||
logger.info('get article comments:{id}'.format(id=self.id))
|
|
||||||
return value
|
|
||||||
else:
|
|
||||||
comments = self.comment_set.filter(is_enable=True).order_by('-id')
|
|
||||||
cache.set(cache_key, comments, 60 * 100)
|
|
||||||
logger.info('set article comments:{id}'.format(id=self.id))
|
|
||||||
return comments
|
|
||||||
|
|
||||||
def get_admin_url(self):
|
|
||||||
info = (self._meta.app_label, self._meta.model_name)
|
|
||||||
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
|
|
||||||
|
|
||||||
@cache_decorator(expiration=60 * 100)
|
|
||||||
def next_article(self):
|
|
||||||
# 下一篇
|
|
||||||
return Article.objects.filter(
|
|
||||||
id__gt=self.id, status='p').order_by('id').first()
|
|
||||||
|
|
||||||
@cache_decorator(expiration=60 * 100)
|
|
||||||
def prev_article(self):
|
|
||||||
# 前一篇
|
|
||||||
return Article.objects.filter(id__lt=self.id, status='p').first()
|
|
||||||
|
|
||||||
def get_first_image_url(self):
|
|
||||||
"""
|
|
||||||
Get the first image url from article.body.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
|
|
||||||
if match:
|
|
||||||
return match.group(1)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
class Category(BaseModel):
|
|
||||||
"""文章分类"""
|
|
||||||
name = models.CharField(_('category name'), max_length=30, unique=True)
|
|
||||||
parent_category = models.ForeignKey(
|
|
||||||
'self',
|
|
||||||
verbose_name=_('parent category'),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
|
|
||||||
index = models.IntegerField(default=0, verbose_name=_('index'))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-index']
|
|
||||||
verbose_name = _('category')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse(
|
|
||||||
'blog:category_detail', kwargs={
|
|
||||||
'category_name': self.slug})
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_category_tree(self):
|
|
||||||
"""
|
|
||||||
递归获得分类目录的父级
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
categorys = []
|
|
||||||
|
|
||||||
def parse(category):
|
|
||||||
categorys.append(category)
|
|
||||||
if category.parent_category:
|
|
||||||
parse(category.parent_category)
|
|
||||||
|
|
||||||
parse(self)
|
|
||||||
return categorys
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_sub_categorys(self):
|
|
||||||
"""
|
|
||||||
获得当前分类目录所有子集
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
categorys = []
|
|
||||||
all_categorys = Category.objects.all()
|
|
||||||
|
|
||||||
def parse(category):
|
|
||||||
if category not in categorys:
|
|
||||||
categorys.append(category)
|
|
||||||
childs = all_categorys.filter(parent_category=category)
|
|
||||||
for child in childs:
|
|
||||||
if category not in categorys:
|
|
||||||
categorys.append(child)
|
|
||||||
parse(child)
|
|
||||||
|
|
||||||
parse(self)
|
|
||||||
return categorys
|
|
||||||
|
|
||||||
|
|
||||||
class Tag(BaseModel):
|
|
||||||
"""文章标签"""
|
|
||||||
name = models.CharField(_('tag name'), max_length=30, unique=True)
|
|
||||||
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_article_count(self):
|
|
||||||
return Article.objects.filter(tags__name=self.name).distinct().count()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['name']
|
|
||||||
verbose_name = _('tag')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
|
|
||||||
class Links(models.Model):
|
|
||||||
"""友情链接"""
|
|
||||||
|
|
||||||
name = models.CharField(_('link name'), max_length=30, unique=True)
|
|
||||||
link = models.URLField(_('link'))
|
|
||||||
sequence = models.IntegerField(_('order'), unique=True)
|
|
||||||
is_enable = models.BooleanField(
|
|
||||||
_('is show'), default=True, blank=False, null=False)
|
|
||||||
show_type = models.CharField(
|
|
||||||
_('show type'),
|
|
||||||
max_length=1,
|
|
||||||
choices=LinkShowType.choices,
|
|
||||||
default=LinkShowType.I)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_mod_time = models.DateTimeField(_('modify time'), default=now)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['sequence']
|
|
||||||
verbose_name = _('link')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class SideBar(models.Model):
|
|
||||||
"""侧边栏,可以展示一些html内容"""
|
|
||||||
name = models.CharField(_('title'), max_length=100)
|
|
||||||
content = models.TextField(_('content'))
|
|
||||||
sequence = models.IntegerField(_('order'), unique=True)
|
|
||||||
is_enable = models.BooleanField(_('is enable'), default=True)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_mod_time = models.DateTimeField(_('modify time'), default=now)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['sequence']
|
|
||||||
verbose_name = _('sidebar')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class BlogSettings(models.Model):
|
|
||||||
"""blog的配置"""
|
|
||||||
site_name = models.CharField(
|
|
||||||
_('site name'),
|
|
||||||
max_length=200,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
site_description = models.TextField(
|
|
||||||
_('site description'),
|
|
||||||
max_length=1000,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
site_seo_description = models.TextField(
|
|
||||||
_('site seo description'), max_length=1000, null=False, blank=False, default='')
|
|
||||||
site_keywords = models.TextField(
|
|
||||||
_('site keywords'),
|
|
||||||
max_length=1000,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
article_sub_length = models.IntegerField(_('article sub length'), default=300)
|
|
||||||
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
|
|
||||||
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
|
|
||||||
article_comment_count = models.IntegerField(_('article comment count'), default=5)
|
|
||||||
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
|
|
||||||
google_adsense_codes = models.TextField(
|
|
||||||
_('adsense code'), max_length=2000, null=True, blank=True, default='')
|
|
||||||
open_site_comment = models.BooleanField(_('open site comment'), default=True)
|
|
||||||
global_header = models.TextField("公共头部", null=True, blank=True, default='')
|
|
||||||
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
|
|
||||||
beian_code = models.CharField(
|
|
||||||
'备案号',
|
|
||||||
max_length=2000,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
default='')
|
|
||||||
analytics_code = models.TextField(
|
|
||||||
"网站统计代码",
|
|
||||||
max_length=1000,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
show_gongan_code = models.BooleanField(
|
|
||||||
'是否显示公安备案号', default=False, null=False)
|
|
||||||
gongan_beiancode = models.TextField(
|
|
||||||
'公安备案号',
|
|
||||||
max_length=2000,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
default='')
|
|
||||||
comment_need_review = models.BooleanField(
|
|
||||||
'评论是否需要审核', default=False, null=False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _('Website configuration')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.site_name
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if BlogSettings.objects.exclude(id=self.id).count():
|
|
||||||
raise ValidationError(_('There can only be one configuration'))
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
from djangoblog.utils import cache
|
|
||||||
cache.clear()
|
|
||||||
@ -1,372 +0,0 @@
|
|||||||
import logging
|
|
||||||
import re
|
|
||||||
from abc import abstractmethod
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
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 mdeditor.fields import MDTextField
|
|
||||||
from uuslug import slugify
|
|
||||||
|
|
||||||
from djangoblog.utils import cache_decorator, cache
|
|
||||||
from djangoblog.utils import get_current_site
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class LinkShowType(models.TextChoices):
|
|
||||||
I = ('i', _('index'))
|
|
||||||
L = ('l', _('list'))
|
|
||||||
P = ('p', _('post'))
|
|
||||||
A = ('a', _('all'))
|
|
||||||
S = ('s', _('slide'))
|
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_modify_time = models.DateTimeField(_('modify time'), default=now)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
is_update_views = isinstance(
|
|
||||||
self,
|
|
||||||
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
|
|
||||||
if is_update_views:
|
|
||||||
Article.objects.filter(pk=self.pk).update(views=self.views)
|
|
||||||
else:
|
|
||||||
if 'slug' in self.__dict__:
|
|
||||||
slug = getattr(
|
|
||||||
self, 'title') if 'title' in self.__dict__ else getattr(
|
|
||||||
self, 'name')
|
|
||||||
setattr(self, 'slug', slugify(slug))
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_full_url(self):
|
|
||||||
site = get_current_site().domain
|
|
||||||
url = "https://{site}{path}".format(site=site,
|
|
||||||
path=self.get_absolute_url())
|
|
||||||
return url
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_absolute_url(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Article(BaseModel):
|
|
||||||
"""文章"""
|
|
||||||
STATUS_CHOICES = (
|
|
||||||
('d', _('Draft')),
|
|
||||||
('p', _('Published')),
|
|
||||||
)
|
|
||||||
COMMENT_STATUS = (
|
|
||||||
('o', _('Open')),
|
|
||||||
('c', _('Close')),
|
|
||||||
)
|
|
||||||
TYPE = (
|
|
||||||
('a', _('Article')),
|
|
||||||
('p', _('Page')),
|
|
||||||
)
|
|
||||||
title = models.CharField(_('title'), max_length=200, unique=True)
|
|
||||||
body = MDTextField(_('body'))
|
|
||||||
pub_time = models.DateTimeField(
|
|
||||||
_('publish time'), blank=False, null=False, default=now)
|
|
||||||
status = models.CharField(
|
|
||||||
_('status'),
|
|
||||||
max_length=1,
|
|
||||||
choices=STATUS_CHOICES,
|
|
||||||
default='p')
|
|
||||||
comment_status = models.CharField(
|
|
||||||
_('comment status'),
|
|
||||||
max_length=1,
|
|
||||||
choices=COMMENT_STATUS,
|
|
||||||
default='o')
|
|
||||||
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
|
|
||||||
views = models.PositiveIntegerField(_('views'), default=0)
|
|
||||||
author = models.ForeignKey(
|
|
||||||
settings.AUTH_USER_MODEL,
|
|
||||||
verbose_name=_('author'),
|
|
||||||
blank=False,
|
|
||||||
null=False,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
article_order = models.IntegerField(
|
|
||||||
_('order'), blank=False, null=False, default=0)
|
|
||||||
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
|
|
||||||
category = models.ForeignKey(
|
|
||||||
'Category',
|
|
||||||
verbose_name=_('category'),
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
blank=False,
|
|
||||||
null=False)
|
|
||||||
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
|
|
||||||
|
|
||||||
def body_to_string(self):
|
|
||||||
return self.body
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-article_order', '-pub_time']
|
|
||||||
verbose_name = _('article')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
get_latest_by = 'id'
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('blog:detailbyid', kwargs={
|
|
||||||
'article_id': self.id,
|
|
||||||
'year': self.creation_time.year,
|
|
||||||
'month': self.creation_time.month,
|
|
||||||
'day': self.creation_time.day
|
|
||||||
})
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_category_tree(self):
|
|
||||||
tree = self.category.get_category_tree()
|
|
||||||
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
|
|
||||||
|
|
||||||
return names
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
cache.clear()
|
|
||||||
|
|
||||||
def viewed(self):
|
|
||||||
self.views += 1
|
|
||||||
self.save(update_fields=['views'])
|
|
||||||
|
|
||||||
def comment_list(self):
|
|
||||||
cache_key = 'article_comments_{id}'.format(id=self.id)
|
|
||||||
value = cache.get(cache_key)
|
|
||||||
if value:
|
|
||||||
logger.info('get article comments:{id}'.format(id=self.id))
|
|
||||||
return value
|
|
||||||
else:
|
|
||||||
comments = self.comment_set.filter(is_enable=True).order_by('-id')
|
|
||||||
cache.set(cache_key, comments, 60 * 100)
|
|
||||||
logger.info('set article comments:{id}'.format(id=self.id))
|
|
||||||
return comments
|
|
||||||
|
|
||||||
def get_admin_url(self):
|
|
||||||
info = (self._meta.app_label, self._meta.model_name)
|
|
||||||
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
|
|
||||||
|
|
||||||
@cache_decorator(expiration=60 * 100)
|
|
||||||
def next_article(self):
|
|
||||||
# 下一篇
|
|
||||||
return Article.objects.filter(
|
|
||||||
id__gt=self.id, status='p').order_by('id').first()
|
|
||||||
|
|
||||||
@cache_decorator(expiration=60 * 100)
|
|
||||||
def prev_article(self):
|
|
||||||
# 前一篇
|
|
||||||
return Article.objects.filter(id__lt=self.id, status='p').first()
|
|
||||||
|
|
||||||
def get_first_image_url(self):
|
|
||||||
"""
|
|
||||||
Get the first image url from article.body.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
|
|
||||||
if match:
|
|
||||||
return match.group(1)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
class Category(BaseModel):
|
|
||||||
"""文章分类"""
|
|
||||||
name = models.CharField(_('category name'), max_length=30, unique=True)
|
|
||||||
parent_category = models.ForeignKey(
|
|
||||||
'self',
|
|
||||||
verbose_name=_('parent category'),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=models.CASCADE)
|
|
||||||
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
|
|
||||||
index = models.IntegerField(default=0, verbose_name=_('index'))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-index']
|
|
||||||
verbose_name = _('category')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse(
|
|
||||||
'blog:category_detail', kwargs={
|
|
||||||
'category_name': self.slug})
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_category_tree(self):
|
|
||||||
"""
|
|
||||||
递归获得分类目录的父级
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
categorys = []
|
|
||||||
|
|
||||||
def parse(category):
|
|
||||||
categorys.append(category)
|
|
||||||
if category.parent_category:
|
|
||||||
parse(category.parent_category)
|
|
||||||
|
|
||||||
parse(self)
|
|
||||||
return categorys
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_sub_categorys(self):
|
|
||||||
"""
|
|
||||||
获得当前分类目录所有子集
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
categorys = []
|
|
||||||
all_categorys = Category.objects.all()
|
|
||||||
|
|
||||||
def parse(category):
|
|
||||||
if category not in categorys:
|
|
||||||
categorys.append(category)
|
|
||||||
childs = all_categorys.filter(parent_category=category)
|
|
||||||
for child in childs:
|
|
||||||
if category not in categorys:
|
|
||||||
categorys.append(child)
|
|
||||||
parse(child)
|
|
||||||
|
|
||||||
parse(self)
|
|
||||||
return categorys
|
|
||||||
|
|
||||||
|
|
||||||
class Tag(BaseModel):
|
|
||||||
"""文章标签"""
|
|
||||||
name = models.CharField(_('tag name'), max_length=30, unique=True)
|
|
||||||
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
|
|
||||||
|
|
||||||
@cache_decorator(60 * 60 * 10)
|
|
||||||
def get_article_count(self):
|
|
||||||
return Article.objects.filter(tags__name=self.name).distinct().count()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['name']
|
|
||||||
verbose_name = _('tag')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
|
|
||||||
class Links(models.Model):
|
|
||||||
"""友情链接"""
|
|
||||||
|
|
||||||
name = models.CharField(_('link name'), max_length=30, unique=True)
|
|
||||||
link = models.URLField(_('link'))
|
|
||||||
sequence = models.IntegerField(_('order'), unique=True)
|
|
||||||
is_enable = models.BooleanField(
|
|
||||||
_('is show'), default=True, blank=False, null=False)
|
|
||||||
show_type = models.CharField(
|
|
||||||
_('show type'),
|
|
||||||
max_length=1,
|
|
||||||
choices=LinkShowType.choices,
|
|
||||||
default=LinkShowType.I)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_mod_time = models.DateTimeField(_('modify time'), default=now)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['sequence']
|
|
||||||
verbose_name = _('link')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class SideBar(models.Model):
|
|
||||||
"""侧边栏,可以展示一些html内容"""
|
|
||||||
name = models.CharField(_('title'), max_length=100)
|
|
||||||
content = models.TextField(_('content'))
|
|
||||||
sequence = models.IntegerField(_('order'), unique=True)
|
|
||||||
is_enable = models.BooleanField(_('is enable'), default=True)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_mod_time = models.DateTimeField(_('modify time'), default=now)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['sequence']
|
|
||||||
verbose_name = _('sidebar')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class BlogSettings(models.Model):
|
|
||||||
"""blog的配置"""
|
|
||||||
site_name = models.CharField(
|
|
||||||
_('site name'),
|
|
||||||
max_length=200,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
site_description = models.TextField(
|
|
||||||
_('site description'),
|
|
||||||
max_length=1000,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
site_seo_description = models.TextField(
|
|
||||||
_('site seo description'), max_length=1000, null=False, blank=False, default='')
|
|
||||||
site_keywords = models.TextField(
|
|
||||||
_('site keywords'),
|
|
||||||
max_length=1000,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
article_sub_length = models.IntegerField(_('article sub length'), default=300)
|
|
||||||
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
|
|
||||||
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
|
|
||||||
article_comment_count = models.IntegerField(_('article comment count'), default=5)
|
|
||||||
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
|
|
||||||
google_adsense_codes = models.TextField(
|
|
||||||
_('adsense code'), max_length=2000, null=True, blank=True, default='')
|
|
||||||
open_site_comment = models.BooleanField(_('open site comment'), default=True)
|
|
||||||
global_header = models.TextField("公共头部", null=True, blank=True, default='')
|
|
||||||
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
|
|
||||||
beian_code = models.CharField(
|
|
||||||
'备案号',
|
|
||||||
max_length=2000,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
default='')
|
|
||||||
analytics_code = models.TextField(
|
|
||||||
"网站统计代码",
|
|
||||||
max_length=1000,
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
default='')
|
|
||||||
show_gongan_code = models.BooleanField(
|
|
||||||
'是否显示公安备案号', default=False, null=False)
|
|
||||||
gongan_beiancode = models.TextField(
|
|
||||||
'公安备案号',
|
|
||||||
max_length=2000,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
default='')
|
|
||||||
comment_need_review = models.BooleanField(
|
|
||||||
'评论是否需要审核', default=False, null=False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _('Website configuration')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.site_name
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if BlogSettings.objects.exclude(id=self.id).count():
|
|
||||||
raise ValidationError(_('There can only be one configuration'))
|
|
||||||
@ -1,47 +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 _
|
|
||||||
|
|
||||||
|
|
||||||
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')
|
|
||||||
@ -1,50 +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 _
|
|
||||||
|
|
||||||
|
|
||||||
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(
|
|
||||||
'<a href="{}">{}</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(
|
|
||||||
'<a href="{}">{}</a>',
|
|
||||||
link,
|
|
||||||
obj.article.title)
|
|
||||||
|
|
||||||
link_to_userinfo.short_description = _('User')
|
|
||||||
link_to_article.short_description = _('Article')
|
|
||||||
@ -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,5 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class AccountsConfig(AppConfig):
|
|
||||||
name = 'accounts'
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
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)
|
|
||||||
|
|
||||||
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"))
|
|
||||||
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():
|
|
||||||
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
|
|
||||||
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'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarUploadForm(forms.ModelForm):
|
|
||||||
max_upload_size = 2 * 1024 * 1024 # 2MB
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = BlogUser
|
|
||||||
fields = ('avatar',)
|
|
||||||
widgets = {
|
|
||||||
'avatar': widgets.ClearableFileInput(
|
|
||||||
attrs={
|
|
||||||
'class': 'form-control-file',
|
|
||||||
'accept': 'image/*',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
def clean_avatar(self):
|
|
||||||
avatar = self.cleaned_data.get('avatar')
|
|
||||||
if avatar and avatar.size > self.max_upload_size:
|
|
||||||
raise ValidationError(_("Avatar size must be smaller than 2MB."))
|
|
||||||
return avatar
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
# 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()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('accounts', '0002_alter_bloguser_options_remove_bloguser_created_time_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='bloguser',
|
|
||||||
name='avatar',
|
|
||||||
field=models.ImageField(blank=True, null=True, upload_to='avatars/%Y/%m/%d', verbose_name='avatar'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
from django.contrib.auth.models import AbstractUser
|
|
||||||
from django.db import models
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.templatetags.static import static
|
|
||||||
from django.utils.timezone import now
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from djangoblog.utils import get_current_site
|
|
||||||
|
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
|
|
||||||
class BlogUser(AbstractUser):
|
|
||||||
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
|
|
||||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
|
||||||
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
|
|
||||||
source = models.CharField(_('create source'), max_length=100, blank=True)
|
|
||||||
avatar = models.ImageField(
|
|
||||||
_('avatar'),
|
|
||||||
upload_to='avatars/%Y/%m/%d',
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse(
|
|
||||||
'blog:author_detail', kwargs={
|
|
||||||
'author_name': self.username})
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.email
|
|
||||||
|
|
||||||
def get_full_url(self):
|
|
||||||
site = get_current_site().domain
|
|
||||||
url = "https://{site}{path}".format(site=site,
|
|
||||||
path=self.get_absolute_url())
|
|
||||||
return url
|
|
||||||
|
|
||||||
def get_avatar_url(self):
|
|
||||||
if self.avatar:
|
|
||||||
return self.avatar.url
|
|
||||||
return static('blog/img/avatar.png')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ['-id']
|
|
||||||
verbose_name = _('user')
|
|
||||||
verbose_name_plural = verbose_name
|
|
||||||
get_latest_by = 'id'
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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'),
|
|
||||||
path('profile/avatar/', views.AvatarUpdateView.as_view(), name='avatar'),
|
|
||||||
]
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
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',
|
|
||||||
'type',
|
|
||||||
'article_order')
|
|
||||||
list_display_links = ('id', 'title')
|
|
||||||
list_filter = ('status', '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
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class BlogConfig(AppConfig):
|
|
||||||
name = 'blog'
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
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(
|
|
||||||
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
|
|
||||||
@ -1,213 +0,0 @@
|
|||||||
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.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()
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from haystack.forms import SearchForm
|
|
||||||
|
|
||||||
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
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
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()
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
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))
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
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'))
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
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'))
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
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'))
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
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('结束同步')
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
# 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',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
# 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='公共头部'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
# 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='评论是否需要审核'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
# 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',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,364 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
# 定义常量以避免重复字符串字面量
|
|
||||||
CREATION_TIME_VERBOSE = 'creation time'
|
|
||||||
MODIFY_TIME_VERBOSE = 'modify time'
|
|
||||||
|
|
||||||
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_VERBOSE),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='article',
|
|
||||||
name='last_modify_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name=MODIFY_TIME_VERBOSE),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='category',
|
|
||||||
name='creation_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name=CREATION_TIME_VERBOSE),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='category',
|
|
||||||
name='last_modify_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name=MODIFY_TIME_VERBOSE),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='links',
|
|
||||||
name='creation_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name=CREATION_TIME_VERBOSE),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='sidebar',
|
|
||||||
name='creation_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name=CREATION_TIME_VERBOSE),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='tag',
|
|
||||||
name='creation_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name=CREATION_TIME_VERBOSE),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='tag',
|
|
||||||
name='last_modify_time',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name=MODIFY_TIME_VERBOSE),
|
|
||||||
),
|
|
||||||
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_VERBOSE),
|
|
||||||
),
|
|
||||||
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_VERBOSE),
|
|
||||||
),
|
|
||||||
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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
# 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'},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue