commit
a6a9f8cb0f
@ -0,0 +1,10 @@
|
||||
[run]
|
||||
source = .
|
||||
include = *.py
|
||||
omit =
|
||||
*migrations*
|
||||
*tests*
|
||||
*.html
|
||||
*whoosh_cn_backend*
|
||||
*settings.py*
|
||||
*venv*
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit e02cb6723a0ecb5c2e5eb8da42b2589389ed92b5
|
||||
@ -1,87 +0,0 @@
|
||||
codecov:
|
||||
require_ci_to_pass: yes
|
||||
|
||||
coverage:
|
||||
precision: 2
|
||||
round: down
|
||||
range: "70...100"
|
||||
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 1%
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 1%
|
||||
informational: true
|
||||
|
||||
parsers:
|
||||
gcov:
|
||||
branch_detection:
|
||||
conditional: yes
|
||||
loop: yes
|
||||
method: no
|
||||
macro: no
|
||||
|
||||
comment:
|
||||
layout: "reach,diff,flags,tree"
|
||||
behavior: default
|
||||
require_changes: no
|
||||
|
||||
ignore:
|
||||
# Django 相关
|
||||
- "*/migrations/*"
|
||||
- "manage.py"
|
||||
- "*/settings.py"
|
||||
- "*/wsgi.py"
|
||||
- "*/asgi.py"
|
||||
|
||||
# 测试相关
|
||||
- "*/tests/*"
|
||||
- "*/test_*.py"
|
||||
- "*/*test*.py"
|
||||
|
||||
# 静态文件和模板
|
||||
- "*/static/*"
|
||||
- "*/templates/*"
|
||||
- "*/collectedstatic/*"
|
||||
|
||||
# 国际化文件
|
||||
- "*/locale/*"
|
||||
- "**/*.po"
|
||||
- "**/*.mo"
|
||||
|
||||
# 文档和部署
|
||||
- "*/docs/*"
|
||||
- "*/deploy/*"
|
||||
- "README*.md"
|
||||
- "LICENSE"
|
||||
- "Dockerfile"
|
||||
- "docker-compose*.yml"
|
||||
- "*.yaml"
|
||||
- "*.yml"
|
||||
|
||||
# 开发环境
|
||||
- "*/venv/*"
|
||||
- "*/__pycache__/*"
|
||||
- "*.pyc"
|
||||
- ".coverage"
|
||||
- "coverage.xml"
|
||||
|
||||
# 日志文件
|
||||
- "*/logs/*"
|
||||
- "*.log"
|
||||
|
||||
# 特定文件
|
||||
- "*/whoosh_cn_backend.py" # 搜索后端
|
||||
- "*/elasticsearch_backend.py" # 搜索后端
|
||||
- "*/MemcacheStorage.py" # 缓存存储
|
||||
- "*/robot.py" # 机器人相关
|
||||
|
||||
# 配置文件
|
||||
- "codecov.yml"
|
||||
- ".coveragerc"
|
||||
- "requirements*.txt"
|
||||
@ -0,0 +1 @@
|
||||
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
|
||||
@ -0,0 +1,64 @@
|
||||
from django.contrib.admin import AdminSite
|
||||
from django.contrib.admin.models import LogEntry
|
||||
from django.contrib.sites.admin import SiteAdmin
|
||||
from django.contrib.sites.models import Site
|
||||
|
||||
from accounts.admin import *
|
||||
from blog.admin import *
|
||||
from blog.models import *
|
||||
from comments.admin import *
|
||||
from comments.models import *
|
||||
from djangoblog.logentryadmin import LogEntryAdmin
|
||||
from oauth.admin import *
|
||||
from oauth.models import *
|
||||
from owntracks.admin import *
|
||||
from owntracks.models import *
|
||||
from servermanager.admin import *
|
||||
from servermanager.models import *
|
||||
|
||||
|
||||
class DjangoBlogAdminSite(AdminSite):
|
||||
site_header = 'djangoblog administration'
|
||||
site_title = 'djangoblog site admin'
|
||||
|
||||
def __init__(self, name='admin'):
|
||||
super().__init__(name)
|
||||
|
||||
def has_permission(self, request):
|
||||
return request.user.is_superuser
|
||||
|
||||
# def get_urls(self):
|
||||
# urls = super().get_urls()
|
||||
# from django.urls import path
|
||||
# from blog.views import refresh_memcache
|
||||
#
|
||||
# my_urls = [
|
||||
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
|
||||
# ]
|
||||
# return urls + my_urls
|
||||
|
||||
|
||||
admin_site = DjangoBlogAdminSite(name='admin')
|
||||
|
||||
admin_site.register(Article, ArticlelAdmin)
|
||||
admin_site.register(Category, CategoryAdmin)
|
||||
admin_site.register(Tag, TagAdmin)
|
||||
admin_site.register(Links, LinksAdmin)
|
||||
admin_site.register(SideBar, SideBarAdmin)
|
||||
admin_site.register(BlogSettings, BlogSettingsAdmin)
|
||||
|
||||
admin_site.register(commands, CommandsAdmin)
|
||||
admin_site.register(EmailSendLog, EmailSendLogAdmin)
|
||||
|
||||
admin_site.register(BlogUser, BlogUserAdmin)
|
||||
|
||||
admin_site.register(Comment, CommentAdmin)
|
||||
|
||||
admin_site.register(OAuthUser, OAuthUserAdmin)
|
||||
admin_site.register(OAuthConfig, OAuthConfigAdmin)
|
||||
|
||||
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
|
||||
|
||||
admin_site.register(Site, SiteAdmin)
|
||||
|
||||
admin_site.register(LogEntry, LogEntryAdmin)
|
||||
@ -0,0 +1,11 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class DjangoblogAppConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'djangoblog'
|
||||
|
||||
def ready(self):
|
||||
super().ready()
|
||||
# Import and load plugins here
|
||||
from .plugin_manage.loader import load_plugins
|
||||
load_plugins()
|
||||
@ -0,0 +1,122 @@
|
||||
import _thread
|
||||
import logging
|
||||
|
||||
import django.dispatch
|
||||
from django.conf import settings
|
||||
from django.contrib.admin.models import LogEntry
|
||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from comments.models import Comment
|
||||
from comments.utils import send_comment_email
|
||||
from djangoblog.spider_notify import SpiderNotify
|
||||
from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache
|
||||
from djangoblog.utils import get_current_site
|
||||
from oauth.models import OAuthUser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
oauth_user_login_signal = django.dispatch.Signal(['id'])
|
||||
send_email_signal = django.dispatch.Signal(
|
||||
['emailto', 'title', 'content'])
|
||||
|
||||
|
||||
@receiver(send_email_signal)
|
||||
def send_email_signal_handler(sender, **kwargs):
|
||||
emailto = kwargs['emailto']
|
||||
title = kwargs['title']
|
||||
content = kwargs['content']
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
title,
|
||||
content,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=emailto)
|
||||
msg.content_subtype = "html"
|
||||
|
||||
from servermanager.models import EmailSendLog
|
||||
log = EmailSendLog()
|
||||
log.title = title
|
||||
log.content = content
|
||||
log.emailto = ','.join(emailto)
|
||||
|
||||
try:
|
||||
result = msg.send()
|
||||
log.send_result = result > 0
|
||||
except Exception as e:
|
||||
logger.error(f"失败邮箱号: {emailto}, {e}")
|
||||
log.send_result = False
|
||||
log.save()
|
||||
|
||||
|
||||
@receiver(oauth_user_login_signal)
|
||||
def oauth_user_login_signal_handler(sender, **kwargs):
|
||||
id = kwargs['id']
|
||||
oauthuser = OAuthUser.objects.get(id=id)
|
||||
site = get_current_site().domain
|
||||
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
|
||||
from djangoblog.utils import save_user_avatar
|
||||
oauthuser.picture = save_user_avatar(oauthuser.picture)
|
||||
oauthuser.save()
|
||||
|
||||
delete_sidebar_cache()
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
def model_post_save_callback(
|
||||
sender,
|
||||
instance,
|
||||
created,
|
||||
raw,
|
||||
using,
|
||||
update_fields,
|
||||
**kwargs):
|
||||
clearcache = False
|
||||
if isinstance(instance, LogEntry):
|
||||
return
|
||||
if 'get_full_url' in dir(instance):
|
||||
is_update_views = update_fields == {'views'}
|
||||
if not settings.TESTING and not is_update_views:
|
||||
try:
|
||||
notify_url = instance.get_full_url()
|
||||
SpiderNotify.baidu_notify([notify_url])
|
||||
except Exception as ex:
|
||||
logger.error("notify sipder", ex)
|
||||
if not is_update_views:
|
||||
clearcache = True
|
||||
|
||||
if isinstance(instance, Comment):
|
||||
if instance.is_enable:
|
||||
path = instance.article.get_absolute_url()
|
||||
site = get_current_site().domain
|
||||
if site.find(':') > 0:
|
||||
site = site[0:site.find(':')]
|
||||
|
||||
expire_view_cache(
|
||||
path,
|
||||
servername=site,
|
||||
serverport=80,
|
||||
key_prefix='blogdetail')
|
||||
if cache.get('seo_processor'):
|
||||
cache.delete('seo_processor')
|
||||
comment_cache_key = 'article_comments_{id}'.format(
|
||||
id=instance.article.id)
|
||||
cache.delete(comment_cache_key)
|
||||
delete_sidebar_cache()
|
||||
delete_view_cache('article_comments', [str(instance.article.pk)])
|
||||
|
||||
_thread.start_new_thread(send_comment_email, (instance,))
|
||||
|
||||
if clearcache:
|
||||
cache.clear()
|
||||
|
||||
|
||||
@receiver(user_logged_in)
|
||||
@receiver(user_logged_out)
|
||||
def user_auth_callback(sender, request, user, **kwargs):
|
||||
if user and user.username:
|
||||
logger.info(user)
|
||||
delete_sidebar_cache()
|
||||
# cache.clear()
|
||||
@ -0,0 +1,183 @@
|
||||
from django.utils.encoding import force_str
|
||||
from elasticsearch_dsl import Q
|
||||
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
|
||||
from haystack.forms import ModelSearchForm
|
||||
from haystack.models import SearchResult
|
||||
from haystack.utils import log as logging
|
||||
|
||||
from blog.documents import ArticleDocument, ArticleDocumentManager
|
||||
from blog.models import Article
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ElasticSearchBackend(BaseSearchBackend):
|
||||
def __init__(self, connection_alias, **connection_options):
|
||||
super(
|
||||
ElasticSearchBackend,
|
||||
self).__init__(
|
||||
connection_alias,
|
||||
**connection_options)
|
||||
self.manager = ArticleDocumentManager()
|
||||
self.include_spelling = True
|
||||
|
||||
def _get_models(self, iterable):
|
||||
models = iterable if iterable and iterable[0] else Article.objects.all()
|
||||
docs = self.manager.convert_to_doc(models)
|
||||
return docs
|
||||
|
||||
def _create(self, models):
|
||||
self.manager.create_index()
|
||||
docs = self._get_models(models)
|
||||
self.manager.rebuild(docs)
|
||||
|
||||
def _delete(self, models):
|
||||
for m in models:
|
||||
m.delete()
|
||||
return True
|
||||
|
||||
def _rebuild(self, models):
|
||||
models = models if models else Article.objects.all()
|
||||
docs = self.manager.convert_to_doc(models)
|
||||
self.manager.update_docs(docs)
|
||||
|
||||
def update(self, index, iterable, commit=True):
|
||||
|
||||
models = self._get_models(iterable)
|
||||
self.manager.update_docs(models)
|
||||
|
||||
def remove(self, obj_or_string):
|
||||
models = self._get_models([obj_or_string])
|
||||
self._delete(models)
|
||||
|
||||
def clear(self, models=None, commit=True):
|
||||
self.remove(None)
|
||||
|
||||
@staticmethod
|
||||
def get_suggestion(query: str) -> str:
|
||||
"""获取推荐词, 如果没有找到添加原搜索词"""
|
||||
|
||||
search = ArticleDocument.search() \
|
||||
.query("match", body=query) \
|
||||
.suggest('suggest_search', query, term={'field': 'body'}) \
|
||||
.execute()
|
||||
|
||||
keywords = []
|
||||
for suggest in search.suggest.suggest_search:
|
||||
if suggest["options"]:
|
||||
keywords.append(suggest["options"][0]["text"])
|
||||
else:
|
||||
keywords.append(suggest["text"])
|
||||
|
||||
return ' '.join(keywords)
|
||||
|
||||
@log_query
|
||||
def search(self, query_string, **kwargs):
|
||||
logger.info('search query_string:' + query_string)
|
||||
|
||||
start_offset = kwargs.get('start_offset')
|
||||
end_offset = kwargs.get('end_offset')
|
||||
|
||||
# 推荐词搜索
|
||||
if getattr(self, "is_suggest", None):
|
||||
suggestion = self.get_suggestion(query_string)
|
||||
else:
|
||||
suggestion = query_string
|
||||
|
||||
q = Q('bool',
|
||||
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
|
||||
minimum_should_match="70%")
|
||||
|
||||
search = ArticleDocument.search() \
|
||||
.query('bool', filter=[q]) \
|
||||
.filter('term', status='p') \
|
||||
.filter('term', type='a') \
|
||||
.source(False)[start_offset: end_offset]
|
||||
|
||||
results = search.execute()
|
||||
hits = results['hits'].total
|
||||
raw_results = []
|
||||
for raw_result in results['hits']['hits']:
|
||||
app_label = 'blog'
|
||||
model_name = 'Article'
|
||||
additional_fields = {}
|
||||
|
||||
result_class = SearchResult
|
||||
|
||||
result = result_class(
|
||||
app_label,
|
||||
model_name,
|
||||
raw_result['_id'],
|
||||
raw_result['_score'],
|
||||
**additional_fields)
|
||||
raw_results.append(result)
|
||||
facets = {}
|
||||
spelling_suggestion = None if query_string == suggestion else suggestion
|
||||
|
||||
return {
|
||||
'results': raw_results,
|
||||
'hits': hits,
|
||||
'facets': facets,
|
||||
'spelling_suggestion': spelling_suggestion,
|
||||
}
|
||||
|
||||
|
||||
class ElasticSearchQuery(BaseSearchQuery):
|
||||
def _convert_datetime(self, date):
|
||||
if hasattr(date, 'hour'):
|
||||
return force_str(date.strftime('%Y%m%d%H%M%S'))
|
||||
else:
|
||||
return force_str(date.strftime('%Y%m%d000000'))
|
||||
|
||||
def clean(self, query_fragment):
|
||||
"""
|
||||
Provides a mechanism for sanitizing user input before presenting the
|
||||
value to the backend.
|
||||
|
||||
Whoosh 1.X differs here in that you can no longer use a backslash
|
||||
to escape reserved characters. Instead, the whole word should be
|
||||
quoted.
|
||||
"""
|
||||
words = query_fragment.split()
|
||||
cleaned_words = []
|
||||
|
||||
for word in words:
|
||||
if word in self.backend.RESERVED_WORDS:
|
||||
word = word.replace(word, word.lower())
|
||||
|
||||
for char in self.backend.RESERVED_CHARACTERS:
|
||||
if char in word:
|
||||
word = "'%s'" % word
|
||||
break
|
||||
|
||||
cleaned_words.append(word)
|
||||
|
||||
return ' '.join(cleaned_words)
|
||||
|
||||
def build_query_fragment(self, field, filter_type, value):
|
||||
return value.query_string
|
||||
|
||||
def get_count(self):
|
||||
results = self.get_results()
|
||||
return len(results) if results else 0
|
||||
|
||||
def get_spelling_suggestion(self, preferred_query=None):
|
||||
return self._spelling_suggestion
|
||||
|
||||
def build_params(self, spelling_query=None):
|
||||
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
|
||||
return kwargs
|
||||
|
||||
|
||||
class ElasticSearchModelSearchForm(ModelSearchForm):
|
||||
|
||||
def search(self):
|
||||
# 是否建议搜索
|
||||
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
|
||||
sqs = super().search()
|
||||
return sqs
|
||||
|
||||
|
||||
class ElasticSearchEngine(BaseEngine):
|
||||
backend = ElasticSearchBackend
|
||||
query = ElasticSearchQuery
|
||||
@ -0,0 +1,40 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.syndication.views import Feed
|
||||
from django.utils import timezone
|
||||
from django.utils.feedgenerator import Rss201rev2Feed
|
||||
|
||||
from blog.models import Article
|
||||
from djangoblog.utils import CommonMarkdown
|
||||
|
||||
|
||||
class DjangoBlogFeed(Feed):
|
||||
feed_type = Rss201rev2Feed
|
||||
|
||||
description = '大巧无工,重剑无锋.'
|
||||
title = "且听风吟 大巧无工,重剑无锋. "
|
||||
link = "/feed/"
|
||||
|
||||
def author_name(self):
|
||||
return get_user_model().objects.first().nickname
|
||||
|
||||
def author_link(self):
|
||||
return get_user_model().objects.first().get_absolute_url()
|
||||
|
||||
def items(self):
|
||||
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
|
||||
|
||||
def item_title(self, item):
|
||||
return item.title
|
||||
|
||||
def item_description(self, item):
|
||||
return CommonMarkdown.get_markdown(item.body)
|
||||
|
||||
def feed_copyright(self):
|
||||
now = timezone.now()
|
||||
return "Copyright© {year} 且听风吟".format(year=now.year)
|
||||
|
||||
def item_link(self, item):
|
||||
return item.get_absolute_url()
|
||||
|
||||
def item_guid(self, item):
|
||||
return
|
||||
@ -0,0 +1,91 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.models import DELETION
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse, NoReverseMatch
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class LogEntryAdmin(admin.ModelAdmin):
|
||||
list_filter = [
|
||||
'content_type'
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'object_repr',
|
||||
'change_message'
|
||||
]
|
||||
|
||||
list_display_links = [
|
||||
'action_time',
|
||||
'get_change_message',
|
||||
]
|
||||
list_display = [
|
||||
'action_time',
|
||||
'user_link',
|
||||
'content_type',
|
||||
'object_link',
|
||||
'get_change_message',
|
||||
]
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return (
|
||||
request.user.is_superuser or
|
||||
request.user.has_perm('admin.change_logentry')
|
||||
) and request.method != 'POST'
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def object_link(self, obj):
|
||||
object_link = escape(obj.object_repr)
|
||||
content_type = obj.content_type
|
||||
|
||||
if obj.action_flag != DELETION and content_type is not None:
|
||||
# try returning an actual link instead of object repr string
|
||||
try:
|
||||
url = reverse(
|
||||
'admin:{}_{}_change'.format(content_type.app_label,
|
||||
content_type.model),
|
||||
args=[obj.object_id]
|
||||
)
|
||||
object_link = '<a href="{}">{}</a>'.format(url, object_link)
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
return mark_safe(object_link)
|
||||
|
||||
object_link.admin_order_field = 'object_repr'
|
||||
object_link.short_description = _('object')
|
||||
|
||||
def user_link(self, obj):
|
||||
content_type = ContentType.objects.get_for_model(type(obj.user))
|
||||
user_link = escape(force_str(obj.user))
|
||||
try:
|
||||
# try returning an actual link instead of object repr string
|
||||
url = reverse(
|
||||
'admin:{}_{}_change'.format(content_type.app_label,
|
||||
content_type.model),
|
||||
args=[obj.user.pk]
|
||||
)
|
||||
user_link = '<a href="{}">{}</a>'.format(url, user_link)
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
return mark_safe(user_link)
|
||||
|
||||
user_link.admin_order_field = 'user'
|
||||
user_link.short_description = _('user')
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = super(LogEntryAdmin, self).get_queryset(request)
|
||||
return queryset.prefetch_related('content_type')
|
||||
|
||||
def get_actions(self, request):
|
||||
actions = super(LogEntryAdmin, self).get_actions(request)
|
||||
if 'delete_selected' in actions:
|
||||
del actions['delete_selected']
|
||||
return actions
|
||||
@ -0,0 +1,41 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BasePlugin:
|
||||
# 插件元数据
|
||||
PLUGIN_NAME = None
|
||||
PLUGIN_DESCRIPTION = None
|
||||
PLUGIN_VERSION = None
|
||||
|
||||
def __init__(self):
|
||||
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
|
||||
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
|
||||
self.init_plugin()
|
||||
self.register_hooks()
|
||||
|
||||
def init_plugin(self):
|
||||
"""
|
||||
插件初始化逻辑
|
||||
子类可以重写此方法来实现特定的初始化操作
|
||||
"""
|
||||
logger.info(f'{self.PLUGIN_NAME} initialized.')
|
||||
|
||||
def register_hooks(self):
|
||||
"""
|
||||
注册插件钩子
|
||||
子类可以重写此方法来注册特定的钩子
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_plugin_info(self):
|
||||
"""
|
||||
获取插件信息
|
||||
:return: 包含插件元数据的字典
|
||||
"""
|
||||
return {
|
||||
'name': self.PLUGIN_NAME,
|
||||
'description': self.PLUGIN_DESCRIPTION,
|
||||
'version': self.PLUGIN_VERSION
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
ARTICLE_DETAIL_LOAD = 'article_detail_load'
|
||||
ARTICLE_CREATE = 'article_create'
|
||||
ARTICLE_UPDATE = 'article_update'
|
||||
ARTICLE_DELETE = 'article_delete'
|
||||
|
||||
ARTICLE_CONTENT_HOOK_NAME = "the_content"
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_hooks = {}
|
||||
|
||||
|
||||
def register(hook_name: str, callback: callable):
|
||||
"""
|
||||
注册一个钩子回调。
|
||||
"""
|
||||
if hook_name not in _hooks:
|
||||
_hooks[hook_name] = []
|
||||
_hooks[hook_name].append(callback)
|
||||
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
|
||||
|
||||
|
||||
def run_action(hook_name: str, *args, **kwargs):
|
||||
"""
|
||||
执行一个 Action Hook。
|
||||
它会按顺序执行所有注册到该钩子上的回调函数。
|
||||
"""
|
||||
if hook_name in _hooks:
|
||||
logger.debug(f"Running action hook '{hook_name}'")
|
||||
for callback in _hooks[hook_name]:
|
||||
try:
|
||||
callback(*args, **kwargs)
|
||||
except Exception as e:
|
||||
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
|
||||
|
||||
|
||||
def apply_filters(hook_name: str, value, *args, **kwargs):
|
||||
"""
|
||||
执行一个 Filter Hook。
|
||||
它会把 value 依次传递给所有注册的回调函数进行处理。
|
||||
"""
|
||||
if hook_name in _hooks:
|
||||
logger.debug(f"Applying filter hook '{hook_name}'")
|
||||
for callback in _hooks[hook_name]:
|
||||
try:
|
||||
value = callback(value, *args, **kwargs)
|
||||
except Exception as e:
|
||||
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
|
||||
return value
|
||||
@ -0,0 +1,19 @@
|
||||
import os
|
||||
import logging
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def load_plugins():
|
||||
"""
|
||||
Dynamically loads and initializes plugins from the 'plugins' directory.
|
||||
This function is intended to be called when the Django app registry is ready.
|
||||
"""
|
||||
for plugin_name in settings.ACTIVE_PLUGINS:
|
||||
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
|
||||
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
|
||||
try:
|
||||
__import__(f'plugins.{plugin_name}.plugin')
|
||||
logger.info(f"Successfully loaded plugin: {plugin_name}")
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
|
||||
@ -0,0 +1,343 @@
|
||||
"""
|
||||
Django settings for djangoblog project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 1.10.2.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.10/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.10/ref/settings/
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
def env_to_bool(env, default):
|
||||
str_val = os.environ.get(env)
|
||||
return default if str_val is None else str_val == 'True'
|
||||
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = os.environ.get(
|
||||
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = env_to_bool('DJANGO_DEBUG', True)
|
||||
# DEBUG = False
|
||||
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
|
||||
|
||||
# ALLOWED_HOSTS = []
|
||||
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
|
||||
# django 4.0新增配置
|
||||
CSRF_TRUSTED_ORIGINS = ['http://example.com']
|
||||
# Application definition
|
||||
|
||||
|
||||
INSTALLED_APPS = [
|
||||
# 'django.contrib.admin',
|
||||
'django.contrib.admin.apps.SimpleAdminConfig',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.sitemaps',
|
||||
'mdeditor',
|
||||
'haystack',
|
||||
'blog',
|
||||
'accounts',
|
||||
'comments',
|
||||
'oauth',
|
||||
'servermanager',
|
||||
'owntracks',
|
||||
'compressor',
|
||||
'djangoblog'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.gzip.GZipMiddleware',
|
||||
# 'django.middleware.cache.UpdateCacheMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
# 'django.middleware.cache.FetchFromCacheMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django.middleware.http.ConditionalGetMiddleware',
|
||||
'blog.middleware.OnlineMiddleware'
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'djangoblog.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [os.path.join(BASE_DIR, 'templates')],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'blog.context_processors.seo_processor'
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'djangoblog.wsgi.application'
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
|
||||
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog',
|
||||
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
|
||||
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root',
|
||||
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
|
||||
'PORT': int(
|
||||
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
|
||||
'OPTIONS': {
|
||||
'charset': 'utf8mb4'},
|
||||
}}
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
LANGUAGES = (
|
||||
('en', _('English')),
|
||||
('zh-hans', _('Simplified Chinese')),
|
||||
('zh-hant', _('Traditional Chinese')),
|
||||
)
|
||||
LOCALE_PATHS = (
|
||||
os.path.join(BASE_DIR, 'locale'),
|
||||
)
|
||||
|
||||
LANGUAGE_CODE = 'zh-hans'
|
||||
|
||||
TIME_ZONE = 'Asia/Shanghai'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = False
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
||||
|
||||
|
||||
HAYSTACK_CONNECTIONS = {
|
||||
'default': {
|
||||
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
|
||||
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
|
||||
},
|
||||
}
|
||||
# Automatically update searching index
|
||||
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
|
||||
# Allow user login with username and password
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'accounts.user_login_backend.EmailOrUsernameModelBackend']
|
||||
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATICFILES = os.path.join(BASE_DIR, 'static')
|
||||
|
||||
AUTH_USER_MODEL = 'accounts.BlogUser'
|
||||
LOGIN_URL = '/login/'
|
||||
|
||||
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||
DATE_TIME_FORMAT = '%Y-%m-%d'
|
||||
|
||||
# bootstrap color styles
|
||||
BOOTSTRAP_COLOR_TYPES = [
|
||||
'default', 'primary', 'success', 'info', 'warning', 'danger'
|
||||
]
|
||||
|
||||
# paginate
|
||||
PAGINATE_BY = 10
|
||||
# http cache timeout
|
||||
CACHE_CONTROL_MAX_AGE = 2592000
|
||||
# cache setting
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'TIMEOUT': 10800,
|
||||
'LOCATION': 'unique-snowflake',
|
||||
}
|
||||
}
|
||||
# 使用redis作为缓存
|
||||
if os.environ.get("DJANGO_REDIS_URL"):
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
||||
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
|
||||
}
|
||||
}
|
||||
|
||||
SITE_ID = 1
|
||||
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
|
||||
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
|
||||
|
||||
# Email:
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
|
||||
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True)
|
||||
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com'
|
||||
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465)
|
||||
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
|
||||
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
|
||||
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
|
||||
SERVER_EMAIL = EMAIL_HOST_USER
|
||||
# Setting debug=false did NOT handle except email notifications
|
||||
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
|
||||
# WX ADMIN password(Two times md5)
|
||||
WXADMIN = os.environ.get(
|
||||
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
|
||||
|
||||
LOG_PATH = os.path.join(BASE_DIR, 'logs')
|
||||
if not os.path.exists(LOG_PATH):
|
||||
os.makedirs(LOG_PATH, exist_ok=True)
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'root': {
|
||||
'level': 'INFO',
|
||||
'handlers': ['console', 'log_file'],
|
||||
},
|
||||
'formatters': {
|
||||
'verbose': {
|
||||
'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
|
||||
}
|
||||
},
|
||||
'filters': {
|
||||
'require_debug_false': {
|
||||
'()': 'django.utils.log.RequireDebugFalse',
|
||||
},
|
||||
'require_debug_true': {
|
||||
'()': 'django.utils.log.RequireDebugTrue',
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'log_file': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||
'filename': os.path.join(LOG_PATH, 'djangoblog.log'),
|
||||
'when': 'D',
|
||||
'formatter': 'verbose',
|
||||
'interval': 1,
|
||||
'delay': True,
|
||||
'backupCount': 5,
|
||||
'encoding': 'utf-8'
|
||||
},
|
||||
'console': {
|
||||
'level': 'DEBUG',
|
||||
'filters': ['require_debug_true'],
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'verbose'
|
||||
},
|
||||
'null': {
|
||||
'class': 'logging.NullHandler',
|
||||
},
|
||||
'mail_admins': {
|
||||
'level': 'ERROR',
|
||||
'filters': ['require_debug_false'],
|
||||
'class': 'django.utils.log.AdminEmailHandler'
|
||||
}
|
||||
},
|
||||
'loggers': {
|
||||
'djangoblog': {
|
||||
'handlers': ['log_file', 'console'],
|
||||
'level': 'INFO',
|
||||
'propagate': True,
|
||||
},
|
||||
'django.request': {
|
||||
'handlers': ['mail_admins'],
|
||||
'level': 'ERROR',
|
||||
'propagate': False,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
# other
|
||||
'compressor.finders.CompressorFinder',
|
||||
)
|
||||
COMPRESS_ENABLED = True
|
||||
# COMPRESS_OFFLINE = True
|
||||
|
||||
|
||||
COMPRESS_CSS_FILTERS = [
|
||||
# creates absolute urls from relative ones
|
||||
'compressor.filters.css_default.CssAbsoluteFilter',
|
||||
# css minimizer
|
||||
'compressor.filters.cssmin.CSSMinFilter'
|
||||
]
|
||||
COMPRESS_JS_FILTERS = [
|
||||
'compressor.filters.jsmin.JSMinFilter'
|
||||
]
|
||||
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
|
||||
MEDIA_URL = '/media/'
|
||||
X_FRAME_OPTIONS = 'SAMEORIGIN'
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
|
||||
ELASTICSEARCH_DSL = {
|
||||
'default': {
|
||||
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST')
|
||||
},
|
||||
}
|
||||
HAYSTACK_CONNECTIONS = {
|
||||
'default': {
|
||||
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
|
||||
},
|
||||
}
|
||||
|
||||
# Plugin System
|
||||
PLUGINS_DIR = BASE_DIR / 'plugins'
|
||||
ACTIVE_PLUGINS = [
|
||||
'article_copyright',
|
||||
'reading_time',
|
||||
'external_links',
|
||||
'view_count',
|
||||
'seo_optimizer'
|
||||
]
|
||||
@ -0,0 +1,59 @@
|
||||
from django.contrib.sitemaps import Sitemap
|
||||
from django.urls import reverse
|
||||
|
||||
from blog.models import Article, Category, Tag
|
||||
|
||||
|
||||
class StaticViewSitemap(Sitemap):
|
||||
priority = 0.5
|
||||
changefreq = 'daily'
|
||||
|
||||
def items(self):
|
||||
return ['blog:index', ]
|
||||
|
||||
def location(self, item):
|
||||
return reverse(item)
|
||||
|
||||
|
||||
class ArticleSiteMap(Sitemap):
|
||||
changefreq = "monthly"
|
||||
priority = "0.6"
|
||||
|
||||
def items(self):
|
||||
return Article.objects.filter(status='p')
|
||||
|
||||
def lastmod(self, obj):
|
||||
return obj.last_modify_time
|
||||
|
||||
|
||||
class CategorySiteMap(Sitemap):
|
||||
changefreq = "Weekly"
|
||||
priority = "0.6"
|
||||
|
||||
def items(self):
|
||||
return Category.objects.all()
|
||||
|
||||
def lastmod(self, obj):
|
||||
return obj.last_modify_time
|
||||
|
||||
|
||||
class TagSiteMap(Sitemap):
|
||||
changefreq = "Weekly"
|
||||
priority = "0.3"
|
||||
|
||||
def items(self):
|
||||
return Tag.objects.all()
|
||||
|
||||
def lastmod(self, obj):
|
||||
return obj.last_modify_time
|
||||
|
||||
|
||||
class UserSiteMap(Sitemap):
|
||||
changefreq = "Weekly"
|
||||
priority = "0.3"
|
||||
|
||||
def items(self):
|
||||
return list(set(map(lambda x: x.author, Article.objects.all())))
|
||||
|
||||
def lastmod(self, obj):
|
||||
return obj.date_joined
|
||||
@ -0,0 +1,21 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SpiderNotify():
|
||||
@staticmethod
|
||||
def baidu_notify(urls):
|
||||
try:
|
||||
data = '\n'.join(urls)
|
||||
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
|
||||
logger.info(result.text)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
@staticmethod
|
||||
def notify(url):
|
||||
SpiderNotify.baidu_notify(url)
|
||||
@ -0,0 +1,32 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from djangoblog.utils import *
|
||||
|
||||
|
||||
class DjangoBlogTest(TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def test_utils(self):
|
||||
md5 = get_sha256('test')
|
||||
self.assertIsNotNone(md5)
|
||||
c = CommonMarkdown.get_markdown('''
|
||||
# Title1
|
||||
|
||||
```python
|
||||
import os
|
||||
```
|
||||
|
||||
[url](https://www.lylinux.net/)
|
||||
|
||||
[ddd](http://www.baidu.com)
|
||||
|
||||
|
||||
''')
|
||||
self.assertIsNotNone(c)
|
||||
d = {
|
||||
'd': 'key1',
|
||||
'd2': 'key2'
|
||||
}
|
||||
data = parse_dict_to_url(d)
|
||||
self.assertIsNotNone(data)
|
||||
@ -0,0 +1,64 @@
|
||||
"""djangoblog URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/1.10/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.conf.urls import url, include
|
||||
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.conf.urls.i18n import i18n_patterns
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib.sitemaps.views import sitemap
|
||||
from django.urls import path, include
|
||||
from django.urls import re_path
|
||||
from haystack.views import search_view_factory
|
||||
|
||||
from blog.views import EsSearchView
|
||||
from djangoblog.admin_site import admin_site
|
||||
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
|
||||
from djangoblog.feeds import DjangoBlogFeed
|
||||
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
|
||||
|
||||
sitemaps = {
|
||||
|
||||
'blog': ArticleSiteMap,
|
||||
'Category': CategorySiteMap,
|
||||
'Tag': TagSiteMap,
|
||||
'User': UserSiteMap,
|
||||
'static': StaticViewSitemap
|
||||
}
|
||||
|
||||
handler404 = 'blog.views.page_not_found_view'
|
||||
handler500 = 'blog.views.server_error_view'
|
||||
handle403 = 'blog.views.permission_denied_view'
|
||||
|
||||
urlpatterns = [
|
||||
path('i18n/', include('django.conf.urls.i18n')),
|
||||
]
|
||||
urlpatterns += i18n_patterns(
|
||||
re_path(r'^admin/', admin_site.urls),
|
||||
re_path(r'', include('blog.urls', namespace='blog')),
|
||||
re_path(r'mdeditor/', include('mdeditor.urls')),
|
||||
re_path(r'', include('comments.urls', namespace='comment')),
|
||||
re_path(r'', include('accounts.urls', namespace='account')),
|
||||
re_path(r'', include('oauth.urls', namespace='oauth')),
|
||||
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
|
||||
name='django.contrib.sitemaps.views.sitemap'),
|
||||
re_path(r'^feed/$', DjangoBlogFeed()),
|
||||
re_path(r'^rss/$', DjangoBlogFeed()),
|
||||
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
|
||||
name='search'),
|
||||
re_path(r'', include('servermanager.urls', namespace='servermanager')),
|
||||
re_path(r'', include('owntracks.urls', namespace='owntracks'))
|
||||
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL,
|
||||
document_root=settings.MEDIA_ROOT)
|
||||
@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
from hashlib import sha256
|
||||
|
||||
import bleach
|
||||
import markdown
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.cache import cache
|
||||
from django.templatetags.static import static
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_max_articleid_commentid():
|
||||
from blog.models import Article
|
||||
from comments.models import Comment
|
||||
return (Article.objects.latest().pk, Comment.objects.latest().pk)
|
||||
|
||||
|
||||
def get_sha256(str):
|
||||
m = sha256(str.encode('utf-8'))
|
||||
return m.hexdigest()
|
||||
|
||||
|
||||
def cache_decorator(expiration=3 * 60):
|
||||
def wrapper(func):
|
||||
def news(*args, **kwargs):
|
||||
try:
|
||||
view = args[0]
|
||||
key = view.get_cache_key()
|
||||
except:
|
||||
key = None
|
||||
if not key:
|
||||
unique_str = repr((func, args, kwargs))
|
||||
|
||||
m = sha256(unique_str.encode('utf-8'))
|
||||
key = m.hexdigest()
|
||||
value = cache.get(key)
|
||||
if value is not None:
|
||||
# logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
|
||||
if str(value) == '__default_cache_value__':
|
||||
return None
|
||||
else:
|
||||
return value
|
||||
else:
|
||||
logger.debug(
|
||||
'cache_decorator set cache:%s key:%s' %
|
||||
(func.__name__, key))
|
||||
value = func(*args, **kwargs)
|
||||
if value is None:
|
||||
cache.set(key, '__default_cache_value__', expiration)
|
||||
else:
|
||||
cache.set(key, value, expiration)
|
||||
return value
|
||||
|
||||
return news
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def expire_view_cache(path, servername, serverport, key_prefix=None):
|
||||
'''
|
||||
刷新视图缓存
|
||||
:param path:url路径
|
||||
:param servername:host
|
||||
:param serverport:端口
|
||||
:param key_prefix:前缀
|
||||
:return:是否成功
|
||||
'''
|
||||
from django.http import HttpRequest
|
||||
from django.utils.cache import get_cache_key
|
||||
|
||||
request = HttpRequest()
|
||||
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
|
||||
request.path = path
|
||||
|
||||
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
|
||||
if key:
|
||||
logger.info('expire_view_cache:get key:{path}'.format(path=path))
|
||||
if cache.get(key):
|
||||
cache.delete(key)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@cache_decorator()
|
||||
def get_current_site():
|
||||
site = Site.objects.get_current()
|
||||
return site
|
||||
|
||||
|
||||
class CommonMarkdown:
|
||||
@staticmethod
|
||||
def _convert_markdown(value):
|
||||
md = markdown.Markdown(
|
||||
extensions=[
|
||||
'extra',
|
||||
'codehilite',
|
||||
'toc',
|
||||
'tables',
|
||||
]
|
||||
)
|
||||
body = md.convert(value)
|
||||
toc = md.toc
|
||||
return body, toc
|
||||
|
||||
@staticmethod
|
||||
def get_markdown_with_toc(value):
|
||||
body, toc = CommonMarkdown._convert_markdown(value)
|
||||
return body, toc
|
||||
|
||||
@staticmethod
|
||||
def get_markdown(value):
|
||||
body, toc = CommonMarkdown._convert_markdown(value)
|
||||
return body
|
||||
|
||||
|
||||
def send_email(emailto, title, content):
|
||||
from djangoblog.blog_signals import send_email_signal
|
||||
send_email_signal.send(
|
||||
send_email.__class__,
|
||||
emailto=emailto,
|
||||
title=title,
|
||||
content=content)
|
||||
|
||||
|
||||
def generate_code() -> str:
|
||||
"""生成随机数验证码"""
|
||||
return ''.join(random.sample(string.digits, 6))
|
||||
|
||||
|
||||
def parse_dict_to_url(dict):
|
||||
from urllib.parse import quote
|
||||
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
|
||||
for k, v in dict.items()])
|
||||
return url
|
||||
|
||||
|
||||
def get_blog_setting():
|
||||
value = cache.get('get_blog_setting')
|
||||
if value:
|
||||
return value
|
||||
else:
|
||||
from blog.models import BlogSettings
|
||||
if not BlogSettings.objects.count():
|
||||
setting = BlogSettings()
|
||||
setting.site_name = 'djangoblog'
|
||||
setting.site_description = '基于Django的博客系统'
|
||||
setting.site_seo_description = '基于Django的博客系统'
|
||||
setting.site_keywords = 'Django,Python'
|
||||
setting.article_sub_length = 300
|
||||
setting.sidebar_article_count = 10
|
||||
setting.sidebar_comment_count = 5
|
||||
setting.show_google_adsense = False
|
||||
setting.open_site_comment = True
|
||||
setting.analytics_code = ''
|
||||
setting.beian_code = ''
|
||||
setting.show_gongan_code = False
|
||||
setting.comment_need_review = False
|
||||
setting.save()
|
||||
value = BlogSettings.objects.first()
|
||||
logger.info('set cache get_blog_setting')
|
||||
cache.set('get_blog_setting', value)
|
||||
return value
|
||||
|
||||
|
||||
def save_user_avatar(url):
|
||||
'''
|
||||
保存用户头像
|
||||
:param url:头像url
|
||||
:return: 本地路径
|
||||
'''
|
||||
logger.info(url)
|
||||
|
||||
try:
|
||||
basedir = os.path.join(settings.STATICFILES, 'avatar')
|
||||
rsp = requests.get(url, timeout=2)
|
||||
if rsp.status_code == 200:
|
||||
if not os.path.exists(basedir):
|
||||
os.makedirs(basedir)
|
||||
|
||||
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
|
||||
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
|
||||
ext = os.path.splitext(url)[1] if isimage else '.jpg'
|
||||
save_filename = str(uuid.uuid4().hex) + ext
|
||||
logger.info('保存用户头像:' + basedir + save_filename)
|
||||
with open(os.path.join(basedir, save_filename), 'wb+') as file:
|
||||
file.write(rsp.content)
|
||||
return static('avatar/' + save_filename)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return static('blog/img/avatar.png')
|
||||
|
||||
|
||||
def delete_sidebar_cache():
|
||||
from blog.models import LinkShowType
|
||||
keys = ["sidebar" + x for x in LinkShowType.values]
|
||||
for k in keys:
|
||||
logger.info('delete sidebar key:' + k)
|
||||
cache.delete(k)
|
||||
|
||||
|
||||
def delete_view_cache(prefix, keys):
|
||||
from django.core.cache.utils import make_template_fragment_key
|
||||
key = make_template_fragment_key(prefix, keys)
|
||||
cache.delete(key)
|
||||
|
||||
|
||||
def get_resource_url():
|
||||
if settings.STATIC_URL:
|
||||
return settings.STATIC_URL
|
||||
else:
|
||||
site = get_current_site()
|
||||
return 'http://' + site.domain + '/static/'
|
||||
|
||||
|
||||
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
|
||||
'h2', 'p']
|
||||
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
|
||||
|
||||
|
||||
def sanitize_html(html):
|
||||
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for djangoblog project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
||||
# Image Lazy Loading Plugin
|
||||
Binary file not shown.
Loading…
Reference in new issue