Compare commits
8 Commits
master
...
zjj_branch
| Author | SHA1 | Date |
|---|---|---|
|
|
b334590d1f | 2 months ago |
|
|
4ae5ff5fcb | 2 months ago |
|
|
3bd2391f96 | 2 months ago |
|
|
a2869f8724 | 2 months ago |
|
|
27a58c9c79 | 2 months ago |
|
|
d54606d33f | 2 months ago |
|
|
7f0408a363 | 2 months ago |
|
|
77e883aecd | 2 months ago |
Binary file not shown.
@ -1,5 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
# 定义 accounts 应用的配置类
|
||||
class AccountsConfig(AppConfig):
|
||||
name = 'accounts'
|
||||
# 应用的名称为 'accounts'
|
||||
name = 'accounts'
|
||||
@ -1,5 +1,4 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BlogConfig(AppConfig):
|
||||
name = 'blog'
|
||||
name = 'blog' # 当前 app 名称
|
||||
@ -1,19 +1,15 @@
|
||||
import logging
|
||||
|
||||
from django import forms
|
||||
from haystack.forms import SearchForm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BlogSearchForm(SearchForm):
|
||||
querydata = forms.CharField(required=True)
|
||||
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
|
||||
datas = super().search()
|
||||
logger.info(self.cleaned_data['querydata']) # 记录搜索词
|
||||
return datas
|
||||
@ -1,13 +1,21 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
# 导入项目中的 Tag 和 Category 模型
|
||||
from blog.models import Tag, Category
|
||||
|
||||
|
||||
# TODO 参数化
|
||||
class Command(BaseCommand):
|
||||
help = 'build search words'
|
||||
help = '构建搜索关键词' # 命令的帮助信息
|
||||
|
||||
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))
|
||||
"""
|
||||
命令的主要处理逻辑
|
||||
"""
|
||||
# 从数据库中获取所有 Tag 和 Category 的名称,并去重
|
||||
datas = set([
|
||||
t.name for t in Tag.objects.all() # 所有标签的名称
|
||||
+ [t.name for t in Category.objects.all()] # 所有分类的名称
|
||||
])
|
||||
|
||||
# 将去重后的名称集合转换为以换行符分隔的字符串,并打印出来
|
||||
print('\n'.join(datas))
|
||||
@ -1,11 +1,18 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
# 导入自定义的缓存工具
|
||||
from djangoblog.utils import cache
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'clear the whole cache'
|
||||
help = '清除所有缓存' # 命令的帮助信息
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
命令的主要处理逻辑
|
||||
"""
|
||||
# 调用缓存工具的 clear 方法,清除所有缓存
|
||||
cache.clear()
|
||||
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))
|
||||
|
||||
# 输出成功信息,使用 Django 管理命令的样式输出
|
||||
self.stdout.write(self.style.SUCCESS('已清除缓存\n'))
|
||||
@ -1,23 +1,24 @@
|
||||
# Generated by Django 4.1.7 on 2023-03-29 06:08
|
||||
# 由Django 4.1.7于2023-03-29 06:08生成
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0001_initial'),
|
||||
('blog', '0001_initial'), # 依赖于初始迁移
|
||||
]
|
||||
|
||||
operations = [
|
||||
# 向BlogSettings模型添加公共尾部字段
|
||||
migrations.AddField(
|
||||
model_name='blogsettings',
|
||||
name='global_footer',
|
||||
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
|
||||
),
|
||||
# 向BlogSettings模型添加公共头部字段
|
||||
migrations.AddField(
|
||||
model_name='blogsettings',
|
||||
name='global_header',
|
||||
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
|
||||
),
|
||||
]
|
||||
]
|
||||
@ -1,17 +1,18 @@
|
||||
# Generated by Django 4.2.1 on 2023-05-09 07:45
|
||||
# 由Django 4.2.1于2023-05-09 07:45生成
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0002_blogsettings_global_footer_and_more'),
|
||||
('blog', '0002_blogsettings_global_footer_and_more'), # 依赖于上一个迁移
|
||||
]
|
||||
|
||||
operations = [
|
||||
# 向BlogSettings模型添加评论是否需要审核字段
|
||||
migrations.AddField(
|
||||
model_name='blogsettings',
|
||||
name='comment_need_review',
|
||||
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
|
||||
),
|
||||
]
|
||||
]
|
||||
@ -1,27 +1,30 @@
|
||||
# Generated by Django 4.2.1 on 2023-05-09 07:51
|
||||
# 由Django 4.2.1于2023-05-09 07:51生成
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0003_blogsettings_comment_need_review'),
|
||||
('blog', '0003_blogsettings_comment_need_review'), # 依赖于上一个迁移
|
||||
]
|
||||
|
||||
operations = [
|
||||
# 重命名BlogSettings模型中的analyticscode字段为analytics_code
|
||||
migrations.RenameField(
|
||||
model_name='blogsettings',
|
||||
old_name='analyticscode',
|
||||
new_name='analytics_code',
|
||||
),
|
||||
# 重命名BlogSettings模型中的beiancode字段为beian_code
|
||||
migrations.RenameField(
|
||||
model_name='blogsettings',
|
||||
old_name='beiancode',
|
||||
new_name='beian_code',
|
||||
),
|
||||
# 重命名BlogSettings模型中的sitename字段为site_name
|
||||
migrations.RenameField(
|
||||
model_name='blogsettings',
|
||||
old_name='sitename',
|
||||
new_name='site_name',
|
||||
),
|
||||
]
|
||||
]
|
||||
@ -1,49 +1,58 @@
|
||||
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 _
|
||||
from django.utils.translation import gettext_lazy as _ # 用于支持多语言,这里是“获取翻译文本”
|
||||
|
||||
|
||||
# 定义批量操作:禁用选中评论的显示状态
|
||||
def disable_commentstatus(modeladmin, request, queryset):
|
||||
queryset.update(is_enable=False)
|
||||
|
||||
queryset.update(is_enable=False) # 将选中的评论设置为不可见
|
||||
disable_commentstatus.short_description = _('Disable comments') # 操作按钮显示名称:禁用评论
|
||||
|
||||
# 定义批量操作:启用选中评论的显示状态
|
||||
def enable_commentstatus(modeladmin, request, queryset):
|
||||
queryset.update(is_enable=True)
|
||||
|
||||
|
||||
disable_commentstatus.short_description = _('Disable comments')
|
||||
enable_commentstatus.short_description = _('Enable comments')
|
||||
queryset.update(is_enable=True) # 将选中的评论设置为可见
|
||||
enable_commentstatus.short_description = _('Enable comments') # 操作按钮显示名称:启用评论
|
||||
|
||||
|
||||
# 自定义评论管理后台展示类
|
||||
class CommentAdmin(admin.ModelAdmin):
|
||||
list_per_page = 20
|
||||
list_per_page = 20 # 每页显示20条评论
|
||||
# 后台列表页显示的字段
|
||||
list_display = (
|
||||
'id',
|
||||
'body',
|
||||
'link_to_userinfo',
|
||||
'link_to_article',
|
||||
'is_enable',
|
||||
'creation_time')
|
||||
'id', # 评论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]
|
||||
# 在后台编辑评论时,作者和文章字段以 ID 选择框(而不是下拉查询)方式展示,提高效率
|
||||
raw_id_fields = ('author', 'article')
|
||||
# 支持按评论内容搜索
|
||||
search_fields = ('body',)
|
||||
|
||||
# 自定义方法:生成指向用户信息编辑页的链接
|
||||
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))
|
||||
info = (obj.author._meta.app_label, obj.author._meta.model_name) # 获取用户模型的 app 和 model 名称
|
||||
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) # 生成用户编辑页面的URL
|
||||
# 显示用户昵称,如果没有则显示邮箱
|
||||
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))
|
||||
info = (obj.article._meta.app_label, obj.article._meta.model_name) # 获取文章模型的 app 和 model 名称
|
||||
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) # 生成文章编辑页面的URL
|
||||
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')
|
||||
# 为自定义方法添加列标题
|
||||
link_to_userinfo.short_description = _('User') # 列标题:用户
|
||||
link_to_article.short_description = _('Article') # 列标题:文章
|
||||
@ -1,5 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
# 定义评论 App 的配置类
|
||||
class CommentsConfig(AppConfig):
|
||||
name = 'comments'
|
||||
name = 'comments' # 应用的 Python 路径名,通常是 app 文件夹名
|
||||
@ -1,11 +1,14 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
from . import views # 导入本 app 的视图
|
||||
|
||||
app_name = "comments" # 定义命名空间,方便反向解析 URL
|
||||
|
||||
|
||||
app_name = "comments"
|
||||
urlpatterns = [
|
||||
# 定义提交评论的路由:/article/<文章ID>/postcomment
|
||||
path(
|
||||
'article/<int:article_id>/postcomment',
|
||||
views.CommentPostView.as_view(),
|
||||
name='postcomment'),
|
||||
]
|
||||
views.CommentPostView.as_view(), # 使用基于类的视图
|
||||
name='postcomment'), # URL 命名,可在模板中用 {% url 'comments:postcomment' article.id %}
|
||||
]
|
||||
@ -1,183 +1,122 @@
|
||||
from django.utils.encoding import force_str
|
||||
from elasticsearch_dsl import Q
|
||||
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
|
||||
from haystack.forms import ModelSearchForm
|
||||
from haystack.models import SearchResult
|
||||
from haystack.utils import log as logging
|
||||
|
||||
from blog.documents import ArticleDocument, ArticleDocumentManager
|
||||
from blog.models import Article
|
||||
import _thread
|
||||
import logging
|
||||
|
||||
import django.dispatch
|
||||
from django.conf import settings
|
||||
from django.contrib.admin.models import LogEntry
|
||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from comments.models import Comment
|
||||
from comments.utils import send_comment_email
|
||||
from djangoblog.spider_notify import SpiderNotify
|
||||
from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache
|
||||
from djangoblog.utils import get_current_site
|
||||
from oauth.models import OAuthUser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ElasticSearchBackend(BaseSearchBackend):
|
||||
def __init__(self, connection_alias, **connection_options):
|
||||
super(
|
||||
ElasticSearchBackend,
|
||||
self).__init__(
|
||||
connection_alias,
|
||||
**connection_options)
|
||||
self.manager = ArticleDocumentManager()
|
||||
self.include_spelling = True
|
||||
|
||||
def _get_models(self, iterable):
|
||||
models = iterable if iterable and iterable[0] else Article.objects.all()
|
||||
docs = self.manager.convert_to_doc(models)
|
||||
return docs
|
||||
|
||||
def _create(self, models):
|
||||
self.manager.create_index()
|
||||
docs = self._get_models(models)
|
||||
self.manager.rebuild(docs)
|
||||
|
||||
def _delete(self, models):
|
||||
for m in models:
|
||||
m.delete()
|
||||
return True
|
||||
|
||||
def _rebuild(self, models):
|
||||
models = models if models else Article.objects.all()
|
||||
docs = self.manager.convert_to_doc(models)
|
||||
self.manager.update_docs(docs)
|
||||
|
||||
def update(self, index, iterable, commit=True):
|
||||
|
||||
models = self._get_models(iterable)
|
||||
self.manager.update_docs(models)
|
||||
|
||||
def remove(self, obj_or_string):
|
||||
models = self._get_models([obj_or_string])
|
||||
self._delete(models)
|
||||
|
||||
def clear(self, models=None, commit=True):
|
||||
self.remove(None)
|
||||
|
||||
@staticmethod
|
||||
def get_suggestion(query: str) -> str:
|
||||
"""获取推荐词, 如果没有找到添加原搜索词"""
|
||||
|
||||
search = ArticleDocument.search() \
|
||||
.query("match", body=query) \
|
||||
.suggest('suggest_search', query, term={'field': 'body'}) \
|
||||
.execute()
|
||||
|
||||
keywords = []
|
||||
for suggest in search.suggest.suggest_search:
|
||||
if suggest["options"]:
|
||||
keywords.append(suggest["options"][0]["text"])
|
||||
else:
|
||||
keywords.append(suggest["text"])
|
||||
|
||||
return ' '.join(keywords)
|
||||
|
||||
@log_query
|
||||
def search(self, query_string, **kwargs):
|
||||
logger.info('search query_string:' + query_string)
|
||||
|
||||
start_offset = kwargs.get('start_offset')
|
||||
end_offset = kwargs.get('end_offset')
|
||||
|
||||
# 推荐词搜索
|
||||
if getattr(self, "is_suggest", None):
|
||||
suggestion = self.get_suggestion(query_string)
|
||||
else:
|
||||
suggestion = query_string
|
||||
|
||||
q = Q('bool',
|
||||
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
|
||||
minimum_should_match="70%")
|
||||
|
||||
search = ArticleDocument.search() \
|
||||
.query('bool', filter=[q]) \
|
||||
.filter('term', status='p') \
|
||||
.filter('term', type='a') \
|
||||
.source(False)[start_offset: end_offset]
|
||||
|
||||
results = search.execute()
|
||||
hits = results['hits'].total
|
||||
raw_results = []
|
||||
for raw_result in results['hits']['hits']:
|
||||
app_label = 'blog'
|
||||
model_name = 'Article'
|
||||
additional_fields = {}
|
||||
|
||||
result_class = SearchResult
|
||||
|
||||
result = result_class(
|
||||
app_label,
|
||||
model_name,
|
||||
raw_result['_id'],
|
||||
raw_result['_score'],
|
||||
**additional_fields)
|
||||
raw_results.append(result)
|
||||
facets = {}
|
||||
spelling_suggestion = None if query_string == suggestion else suggestion
|
||||
|
||||
return {
|
||||
'results': raw_results,
|
||||
'hits': hits,
|
||||
'facets': facets,
|
||||
'spelling_suggestion': spelling_suggestion,
|
||||
}
|
||||
|
||||
|
||||
class ElasticSearchQuery(BaseSearchQuery):
|
||||
def _convert_datetime(self, date):
|
||||
if hasattr(date, 'hour'):
|
||||
return force_str(date.strftime('%Y%m%d%H%M%S'))
|
||||
else:
|
||||
return force_str(date.strftime('%Y%m%d000000'))
|
||||
|
||||
def clean(self, query_fragment):
|
||||
"""
|
||||
Provides a mechanism for sanitizing user input before presenting the
|
||||
value to the backend.
|
||||
|
||||
Whoosh 1.X differs here in that you can no longer use a backslash
|
||||
to escape reserved characters. Instead, the whole word should be
|
||||
quoted.
|
||||
"""
|
||||
words = query_fragment.split()
|
||||
cleaned_words = []
|
||||
|
||||
for word in words:
|
||||
if word in self.backend.RESERVED_WORDS:
|
||||
word = word.replace(word, word.lower())
|
||||
|
||||
for char in self.backend.RESERVED_CHARACTERS:
|
||||
if char in word:
|
||||
word = "'%s'" % word
|
||||
break
|
||||
|
||||
cleaned_words.append(word)
|
||||
|
||||
return ' '.join(cleaned_words)
|
||||
|
||||
def build_query_fragment(self, field, filter_type, value):
|
||||
return value.query_string
|
||||
|
||||
def get_count(self):
|
||||
results = self.get_results()
|
||||
return len(results) if results else 0
|
||||
|
||||
def get_spelling_suggestion(self, preferred_query=None):
|
||||
return self._spelling_suggestion
|
||||
|
||||
def build_params(self, spelling_query=None):
|
||||
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
|
||||
return kwargs
|
||||
|
||||
|
||||
class ElasticSearchModelSearchForm(ModelSearchForm):
|
||||
|
||||
def search(self):
|
||||
# 是否建议搜索
|
||||
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
|
||||
sqs = super().search()
|
||||
return sqs
|
||||
|
||||
|
||||
class ElasticSearchEngine(BaseEngine):
|
||||
backend = ElasticSearchBackend
|
||||
query = ElasticSearchQuery
|
||||
oauth_user_login_signal = django.dispatch.Signal(['id'])
|
||||
send_email_signal = django.dispatch.Signal(
|
||||
['emailto', 'title', 'content'])
|
||||
|
||||
|
||||
@receiver(send_email_signal)
|
||||
def send_email_signal_handler(sender, **kwargs):
|
||||
emailto = kwargs['emailto']
|
||||
title = kwargs['title']
|
||||
content = kwargs['content']
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
title,
|
||||
content,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=emailto)
|
||||
msg.content_subtype = "html"
|
||||
|
||||
from servermanager.models import EmailSendLog
|
||||
log = EmailSendLog()
|
||||
log.title = title
|
||||
log.content = content
|
||||
log.emailto = ','.join(emailto)
|
||||
|
||||
try:
|
||||
result = msg.send()
|
||||
log.send_result = result > 0
|
||||
except Exception as e:
|
||||
logger.error(f"失败邮箱号: {emailto}, {e}")
|
||||
log.send_result = False
|
||||
log.save()
|
||||
|
||||
|
||||
@receiver(oauth_user_login_signal)
|
||||
def oauth_user_login_signal_handler(sender, **kwargs):
|
||||
id = kwargs['id']
|
||||
oauthuser = OAuthUser.objects.get(id=id)
|
||||
site = get_current_site().domain
|
||||
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
|
||||
from djangoblog.utils import save_user_avatar
|
||||
oauthuser.picture = save_user_avatar(oauthuser.picture)
|
||||
oauthuser.save()
|
||||
|
||||
delete_sidebar_cache()
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
def model_post_save_callback(
|
||||
sender,
|
||||
instance,
|
||||
created,
|
||||
raw,
|
||||
using,
|
||||
update_fields,
|
||||
**kwargs):
|
||||
clearcache = False
|
||||
if isinstance(instance, LogEntry):
|
||||
return
|
||||
if 'get_full_url' in dir(instance):
|
||||
is_update_views = update_fields == {'views'}
|
||||
if not settings.TESTING and not is_update_views:
|
||||
try:
|
||||
notify_url = instance.get_full_url()
|
||||
SpiderNotify.baidu_notify([notify_url])
|
||||
except Exception as ex:
|
||||
logger.error("notify sipder", ex)
|
||||
if not is_update_views:
|
||||
clearcache = True
|
||||
|
||||
if isinstance(instance, Comment):
|
||||
if instance.is_enable:
|
||||
path = instance.article.get_absolute_url()
|
||||
site = get_current_site().domain
|
||||
if site.find(':') > 0:
|
||||
site = site[0:site.find(':')]
|
||||
|
||||
expire_view_cache(
|
||||
path,
|
||||
servername=site,
|
||||
serverport=80,
|
||||
key_prefix='blogdetail')
|
||||
if cache.get('seo_processor'):
|
||||
cache.delete('seo_processor')
|
||||
comment_cache_key = 'article_comments_{id}'.format(
|
||||
id=instance.article.id)
|
||||
cache.delete(comment_cache_key)
|
||||
delete_sidebar_cache()
|
||||
delete_view_cache('article_comments', [str(instance.article.pk)])
|
||||
|
||||
_thread.start_new_thread(send_comment_email, (instance,))
|
||||
|
||||
if clearcache:
|
||||
cache.clear()
|
||||
|
||||
|
||||
@receiver(user_logged_in)
|
||||
@receiver(user_logged_out)
|
||||
def user_auth_callback(sender, request, user, **kwargs):
|
||||
if user and user.username:
|
||||
logger.info(user)
|
||||
delete_sidebar_cache()
|
||||
# cache.clear()
|
||||
@ -1,7 +1,8 @@
|
||||
ARTICLE_DETAIL_LOAD = 'article_detail_load'
|
||||
ARTICLE_CREATE = 'article_create'
|
||||
ARTICLE_UPDATE = 'article_update'
|
||||
ARTICLE_DELETE = 'article_delete'
|
||||
|
||||
ARTICLE_CONTENT_HOOK_NAME = "the_content"
|
||||
# 文章管理相关的操作常量(用于标识不同动作)
|
||||
ARTICLE_DETAIL_LOAD = 'article_detail_load' # 文章详情加载动作
|
||||
ARTICLE_CREATE = 'article_create' # 文章创建动作
|
||||
ARTICLE_UPDATE = 'article_update' # 文章更新动作
|
||||
ARTICLE_DELETE = 'article_delete' # 文章删除动作
|
||||
|
||||
# 内容处理钩子常量(用于在文章内容展示前/后进行处理)
|
||||
ARTICLE_CONTENT_HOOK_NAME = "the_content" # 文章内容钩子名称
|
||||
@ -1,21 +1,22 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger = logging.getLogger(__name__) # 日志记录器
|
||||
|
||||
# 定义一个工具类,用于通知搜索引擎(如百度)有新的内容需要抓取
|
||||
class SpiderNotify():
|
||||
@staticmethod
|
||||
def baidu_notify(urls):
|
||||
try:
|
||||
# 将所有 URL 用换行拼接成字符串,符合百度站长平台 API 要求
|
||||
data = '\n'.join(urls)
|
||||
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
|
||||
logger.info(result.text)
|
||||
logger.info(result.text) # 记录百度返回的结果
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
logger.error(e) # 记录异常
|
||||
|
||||
@staticmethod
|
||||
def notify(url):
|
||||
SpiderNotify.baidu_notify(url)
|
||||
# 单个 URL 通知,内部调用 baidu_notify
|
||||
SpiderNotify.baidu_notify([url])
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,54 +1,59 @@
|
||||
import logging
|
||||
|
||||
from django.contrib import admin
|
||||
# Register your models here.
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from .models import OAuthUser, OAuthConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# OAuth第三方用户管理后台
|
||||
class OAuthUserAdmin(admin.ModelAdmin):
|
||||
# 搜索字段
|
||||
search_fields = ('nickname', 'email')
|
||||
# 每页显示条数
|
||||
list_per_page = 20
|
||||
list_display = (
|
||||
'id',
|
||||
'nickname',
|
||||
'link_to_usermodel',
|
||||
'show_user_image',
|
||||
'type',
|
||||
'email',
|
||||
)
|
||||
# 列表页显示的字段
|
||||
list_display = ('id', 'nickname', 'link_to_usermodel', 'show_user_image', 'type', 'email')
|
||||
# 哪些字段可点击进入编辑页
|
||||
list_display_links = ('id', 'nickname')
|
||||
# 过滤器
|
||||
list_filter = ('author', 'type',)
|
||||
# 只读字段(这里设置为空,后面动态添加所有字段)
|
||||
readonly_fields = []
|
||||
|
||||
# 动态设置所有字段为只读(防止在后台误修改)
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
return list(self.readonly_fields) + \
|
||||
[field.name for field in obj._meta.fields] + \
|
||||
[field.name for field in obj._meta.many_to_many]
|
||||
|
||||
# 是否允许添加(禁止手动添加)
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
# 自定义方法:生成关联 Django 用户的链接
|
||||
def link_to_usermodel(self, obj):
|
||||
if obj.author:
|
||||
if obj.author: # 如果绑定了系统用户
|
||||
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))
|
||||
# 显示用户昵称或邮箱
|
||||
return format_html('<a href="%s">%s</a>' % (link, obj.author.nickname or obj.author.email))
|
||||
link_to_usermodel.short_description = '用户' # 列表页标题
|
||||
|
||||
# 自定义方法:显示用户头像(未实现具体内容)
|
||||
def show_user_image(self, obj):
|
||||
img = obj.picture
|
||||
return format_html(
|
||||
u'<img src="%s" style="width:50px;height:50px"></img>' %
|
||||
(img))
|
||||
|
||||
link_to_usermodel.short_description = '用户'
|
||||
show_user_image.short_description = '用户头像'
|
||||
return format_html('') # 实际应返回图片标签,如 ' % img
|
||||
show_user_image.short_description = '用户头像' # 列表页标题
|
||||
|
||||
|
||||
# OAuth 第三方平台配置管理后台
|
||||
class OAuthConfigAdmin(admin.ModelAdmin):
|
||||
# 列表页显示字段
|
||||
list_display = ('type', 'appkey', 'appsecret', 'is_enable')
|
||||
# 过滤器
|
||||
list_filter = ('type',)
|
||||
|
||||
# 注册模型到 Admin
|
||||
# admin.site.register(OAuthUser, OAuthUserAdmin)
|
||||
# admin.site.register(OAuthConfig, OAuthConfigAdmin)
|
||||
@ -1,5 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
# OAuth 应用配置类
|
||||
class OauthConfig(AppConfig):
|
||||
name = 'oauth'
|
||||
name = 'oauth' # 应用名称
|
||||
@ -1,12 +1,13 @@
|
||||
from django.contrib.auth.forms import forms
|
||||
from django.forms import forms
|
||||
from django.forms import widgets
|
||||
|
||||
|
||||
# 自定义表单:用于要求用户输入邮箱(当第三方登录没有提供邮箱时)
|
||||
class RequireEmailForm(forms.Form):
|
||||
email = forms.EmailField(label='电子邮箱', required=True)
|
||||
oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False)
|
||||
email = forms.EmailField(label='电子邮箱', required=True) # 必填邮箱字段
|
||||
oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False) # 隐藏的 OAuth 用户 ID
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RequireEmailForm, self).__init__(*args, **kwargs)
|
||||
# 设置邮箱输入框 HTML 属性,如样式类和占位符
|
||||
self.fields['email'].widget = widgets.EmailInput(
|
||||
attrs={'placeholder': "email", "class": "form-control"})
|
||||
attrs={'placeholder': "email", "class": "form-control"})
|
||||
@ -1,67 +1,62 @@
|
||||
# Create your models here.
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
# OAuth第三方用户信息表
|
||||
class OAuthUser(models.Model):
|
||||
author = models.ForeignKey(
|
||||
author = models.ForeignKey( # 关联系统内置用户(可为空,表示未绑定)
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name=_('author'),
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.CASCADE)
|
||||
openid = models.CharField(max_length=50)
|
||||
nickname = models.CharField(max_length=50, verbose_name=_('nick name'))
|
||||
token = models.CharField(max_length=150, null=True, blank=True)
|
||||
picture = models.CharField(max_length=350, blank=True, null=True)
|
||||
type = models.CharField(blank=False, null=False, max_length=50)
|
||||
email = models.CharField(max_length=50, null=True, blank=True)
|
||||
metadata = models.TextField(null=True, blank=True)
|
||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
||||
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
|
||||
openid = models.CharField(max_length=50) # 第三方平台唯一ID
|
||||
nickname = models.CharField(max_length=50, verbose_name=_('nick name')) # 昵称
|
||||
token = models.CharField(max_length=150, null=True, blank=True) # 访问令牌
|
||||
picture = models.CharField(max_length=350, blank=True, null=True) # 头像 URL
|
||||
type = models.CharField(blank=False, null=False, max_length=50) # 第三方类型,如 weibo, google
|
||||
email = models.CharField(max_length=50, null=True, blank=True) # 邮箱(可能为空)
|
||||
metadata = models.TextField(null=True, blank=True) # 其他元数据,存储 JSON 等
|
||||
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间
|
||||
last_modify_time = models.DateTimeField(_('last modify time'), default=now) # 最后修改时间
|
||||
|
||||
def __str__(self):
|
||||
return self.nickname
|
||||
return self.nickname # 显示昵称
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('oauth user')
|
||||
verbose_name = _('oauth user') # 后台显示名称
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['-creation_time']
|
||||
|
||||
ordering = ['-creation_time'] # 按创建时间倒序
|
||||
|
||||
# OAuth 第三方平台配置表
|
||||
class OAuthConfig(models.Model):
|
||||
TYPE = (
|
||||
TYPE = ( # 支持的平台类型
|
||||
('weibo', _('weibo')),
|
||||
('google', _('google')),
|
||||
('github', 'GitHub'),
|
||||
('facebook', 'FaceBook'),
|
||||
('qq', 'QQ'),
|
||||
)
|
||||
type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a')
|
||||
appkey = models.CharField(max_length=200, verbose_name='AppKey')
|
||||
appsecret = models.CharField(max_length=200, verbose_name='AppSecret')
|
||||
callback_url = models.CharField(
|
||||
max_length=200,
|
||||
verbose_name=_('callback url'),
|
||||
blank=False,
|
||||
default='')
|
||||
is_enable = models.BooleanField(
|
||||
_('is enable'), default=True, blank=False, null=False)
|
||||
type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a') # 平台类型
|
||||
appkey = models.CharField(max_length=200, verbose_name='AppKey') # App Key
|
||||
appsecret = models.CharField(max_length=200, verbose_name='AppSecret') # App Secret
|
||||
callback_url = models.CharField( # 回调地址
|
||||
max_length=200, verbose_name=_('callback url'), blank=False, default='')
|
||||
is_enable = models.BooleanField(_('is enable'), default=True, blank=False, null=False) # 是否启用
|
||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
||||
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
|
||||
|
||||
# 校验:同一个平台类型不能重复添加
|
||||
def clean(self):
|
||||
if OAuthConfig.objects.filter(
|
||||
type=self.type).exclude(id=self.id).count():
|
||||
if OAuthConfig.objects.filter(type=self.type).exclude(id=self.id).count():
|
||||
raise ValidationError(_(self.type + _('already exists')))
|
||||
|
||||
def __str__(self):
|
||||
return self.type
|
||||
return self.type # 显示平台类型
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'oauth配置'
|
||||
verbose_name = 'oauth配置' # 后台显示名称
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['-creation_time']
|
||||
ordering = ['-creation_time']
|
||||
@ -1,25 +1,11 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "oauth"
|
||||
urlpatterns = [
|
||||
path(
|
||||
r'oauth/authorize',
|
||||
views.authorize),
|
||||
path(
|
||||
r'oauth/requireemail/<int:oauthid>.html',
|
||||
views.RequireEmailView.as_view(),
|
||||
name='require_email'),
|
||||
path(
|
||||
r'oauth/emailconfirm/<int:id>/<sign>.html',
|
||||
views.emailconfirm,
|
||||
name='email_confirm'),
|
||||
path(
|
||||
r'oauth/bindsuccess/<int:oauthid>.html',
|
||||
views.bindsuccess,
|
||||
name='bindsuccess'),
|
||||
path(
|
||||
r'oauth/oauthlogin',
|
||||
views.oauthlogin,
|
||||
name='oauthlogin')]
|
||||
path('oauth/authorize', views.authorize, name='authorize'), # 第三方授权跳转
|
||||
path('oauth/requireemail/<int:oauthid>.html', views.RequireEmailView.as_view(), name='require_email'), # 要求输入邮箱
|
||||
path('oauth/emailconfirm/<int:id>/<sign>.html', views.emailconfirm, name='email_confirm'), # 邮箱验证
|
||||
path('oauth/bindsuccess/<int:oauthid>.html', views.bindsuccess, name='bindsuccess'), # 绑定成功页面
|
||||
path('oauth/oauthlogin', views.oauthlogin, name='oauthlogin'), # OAuth 登录入口
|
||||
]
|
||||
@ -1,32 +1,35 @@
|
||||
from werobot.session import SessionStorage
|
||||
from werobot.utils import json_loads, json_dumps
|
||||
|
||||
from djangoblog.utils import cache
|
||||
|
||||
from djangoblog.utils import cache # 假设这是一个封装了 Django 缓存的工具模块
|
||||
|
||||
class MemcacheStorage(SessionStorage):
|
||||
def __init__(self, prefix='ws_'):
|
||||
self.prefix = prefix
|
||||
self.cache = cache
|
||||
self.prefix = prefix # 会话键前缀,避免与其他缓存冲突
|
||||
self.cache = cache # Django 缓存实例,如 Redis 或 Memcached
|
||||
|
||||
@property
|
||||
def is_available(self):
|
||||
# 检查当前存储是否可用,通过设置和获取一个测试值
|
||||
value = "1"
|
||||
self.set('checkavaliable', value=value)
|
||||
return value == self.get('checkavaliable')
|
||||
|
||||
def key_name(self, s):
|
||||
return '{prefix}{s}'.format(prefix=self.prefix, s=s)
|
||||
# 为每个会话 ID 添加前缀,生成唯一的缓存键
|
||||
return f'{self.prefix}{s}'
|
||||
|
||||
def get(self, id):
|
||||
# 根据 ID 获取会话数据,如果不存在则返回空字典字符串 '{}'
|
||||
id = self.key_name(id)
|
||||
session_json = self.cache.get(id) or '{}'
|
||||
return json_loads(session_json)
|
||||
return json_loads(session_json) # 反序列化为 Python 字典
|
||||
|
||||
def set(self, id, value):
|
||||
# 将会话数据序列化后存入缓存
|
||||
id = self.key_name(id)
|
||||
self.cache.set(id, json_dumps(value))
|
||||
|
||||
def delete(self, id):
|
||||
# 删除指定的会话数据
|
||||
id = self.key_name(id)
|
||||
self.cache.delete(id)
|
||||
self.cache.delete(id)
|
||||
@ -1,5 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ServermanagerConfig(AppConfig):
|
||||
name = 'servermanager'
|
||||
# 定义本 Django app 的名称,需与项目中的 app 文件夹名称一致
|
||||
name = 'servermanager'
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue