Compare commits
11 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
0adac14502 | 3 months ago |
|
|
c003c5dc86 | 3 months ago |
|
|
4583e5cc70 | 3 months ago |
|
|
23ea3c67d1 | 3 months ago |
|
|
b8d07e1474 | 4 months ago |
|
|
3bfc16a831 | 4 months ago |
|
|
354c767e17 | 4 months ago |
|
|
d657b825e5 | 4 months ago |
|
|
dc54eb9e20 | 4 months ago |
|
|
03cacf1c93 | 4 months ago |
|
|
01a3b8b4ff | 4 months ago |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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()
|
||||||
@ -1,5 +1,6 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
#zr 评论应用配置类
|
||||||
class CommentsConfig(AppConfig):
|
class CommentsConfig(AppConfig):
|
||||||
|
#zr 定义应用名称
|
||||||
name = 'comments'
|
name = 'comments'
|
||||||
@ -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
|
||||||
@ -1,7 +1,26 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from .models import OwnTrackLog
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
|
|
||||||
|
@admin.register(OwnTrackLog)
|
||||||
|
class OwnTrackLogAdmin(admin.ModelAdmin):
|
||||||
|
"""
|
||||||
|
OwnTrackLog模型管理配置
|
||||||
|
"""
|
||||||
|
list_display = ('tid', 'lat', 'lon', 'creation_time', 'accuracy')
|
||||||
|
list_filter = ('tid', 'creation_time')
|
||||||
|
search_fields = ('tid',)
|
||||||
|
date_hierarchy = 'creation_time'
|
||||||
|
readonly_fields = ('creation_time',)
|
||||||
|
fieldsets = (
|
||||||
|
('基本信息', {
|
||||||
|
'fields': ('tid', 'creation_time')
|
||||||
|
}),
|
||||||
|
('位置信息', {
|
||||||
|
'fields': ('lat', 'lon', 'accuracy', 'battery')
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
class OwnTrackLogsAdmin(admin.ModelAdmin):
|
def get_queryset(self, request):
|
||||||
pass
|
"""优化查询,减少数据库访问"""
|
||||||
|
return super().get_queryset(request).select_related()
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
# 高德地图API配置
|
||||||
|
AMAP_API_KEY = os.getenv('AMAP_API_KEY', 'your-default-key-here')
|
||||||
|
|
||||||
|
# 缓存配置
|
||||||
|
CACHES = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
||||||
|
'LOCATION': 'redis://127.0.0.1:6379/1',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#management / commands / cleanup_old_locations.py
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.utils import timezone
|
||||||
|
from owntracks.models import OwnTrackLog
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = '清理过期的位置记录'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--days',
|
||||||
|
type=int,
|
||||||
|
default=365,
|
||||||
|
help='保留多少天内的数据'
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
cutoff_date = timezone.now() - timezone.timedelta(days=options['days'])
|
||||||
|
deleted_count, _ = OwnTrackLog.objects.filter(
|
||||||
|
creation_time__lt=cutoff_date
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(f'成功删除 {deleted_count} 条过期记录')
|
||||||
|
)
|
||||||
@ -1,12 +1,29 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
app_name = "owntracks"
|
app_name = "owntracks"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('owntracks/logtracks', views.manage_owntrack_log, name='logtracks'),
|
path('logtracks',
|
||||||
path('owntracks/show_maps', views.show_maps, name='show_maps'),
|
views.manage_owntrack_log,
|
||||||
path('owntracks/get_datas', views.get_datas, name='get_datas'),
|
name='logtracks'),
|
||||||
path('owntracks/show_dates', views.show_log_dates, name='show_dates')
|
path('show_maps',
|
||||||
|
views.show_maps,
|
||||||
|
name='show_maps'),
|
||||||
|
path('get_datas',
|
||||||
|
views.get_datas,
|
||||||
|
name='get_datas'),
|
||||||
|
path('show_dates',
|
||||||
|
views.show_log_dates,
|
||||||
|
name='show_dates')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
#zqx: 应该添加API版本控制
|
||||||
|
# urlpatterns = [
|
||||||
|
# path('api/v1/tracks', views.manage_owntrack_log, name='log-tracks'),
|
||||||
|
# path('api/v1/tracks/dates', views.show_log_dates, name='track-dates'),
|
||||||
|
# path('api/v1/tracks/<str:date>', views.get_datas, name='track-data'),
|
||||||
|
# ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,194 @@
|
|||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.template import TemplateDoesNotExist
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BasePlugin:
|
||||||
|
# 插件元数据
|
||||||
|
PLUGIN_NAME = None
|
||||||
|
PLUGIN_DESCRIPTION = None
|
||||||
|
PLUGIN_VERSION = None
|
||||||
|
PLUGIN_AUTHOR = None
|
||||||
|
|
||||||
|
# 插件配置
|
||||||
|
SUPPORTED_POSITIONS = [] # 支持的显示位置
|
||||||
|
DEFAULT_PRIORITY = 100 # 默认优先级(数字越小优先级越高)
|
||||||
|
POSITION_PRIORITIES = {} # 各位置的优先级 {'sidebar': 50, 'article_bottom': 80}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
|
||||||
|
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
|
||||||
|
|
||||||
|
# 设置插件路径
|
||||||
|
self.plugin_dir = self._get_plugin_directory()
|
||||||
|
self.plugin_slug = self._get_plugin_slug()
|
||||||
|
|
||||||
|
self.init_plugin()
|
||||||
|
self.register_hooks()
|
||||||
|
|
||||||
|
def _get_plugin_directory(self):
|
||||||
|
"""获取插件目录路径"""
|
||||||
|
import inspect
|
||||||
|
plugin_file = inspect.getfile(self.__class__)
|
||||||
|
return Path(plugin_file).parent
|
||||||
|
|
||||||
|
def _get_plugin_slug(self):
|
||||||
|
"""获取插件标识符(目录名)"""
|
||||||
|
return self.plugin_dir.name
|
||||||
|
|
||||||
|
def init_plugin(self):
|
||||||
|
"""
|
||||||
|
插件初始化逻辑
|
||||||
|
子类可以重写此方法来实现特定的初始化操作
|
||||||
|
"""
|
||||||
|
logger.info(f'{self.PLUGIN_NAME} initialized.')
|
||||||
|
|
||||||
|
def register_hooks(self):
|
||||||
|
"""
|
||||||
|
注册插件钩子
|
||||||
|
子类可以重写此方法来注册特定的钩子
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# === 位置渲染系统 ===
|
||||||
|
def render_position_widget(self, position, context, **kwargs):
|
||||||
|
"""
|
||||||
|
根据位置渲染插件组件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
position: 位置标识
|
||||||
|
context: 模板上下文
|
||||||
|
**kwargs: 额外参数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {'html': 'HTML内容', 'priority': 优先级} 或 None
|
||||||
|
"""
|
||||||
|
if position not in self.SUPPORTED_POSITIONS:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 检查条件显示
|
||||||
|
if not self.should_display(position, context, **kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 调用具体的位置渲染方法
|
||||||
|
method_name = f'render_{position}_widget'
|
||||||
|
if hasattr(self, method_name):
|
||||||
|
html = getattr(self, method_name)(context, **kwargs)
|
||||||
|
if html:
|
||||||
|
priority = self.POSITION_PRIORITIES.get(position, self.DEFAULT_PRIORITY)
|
||||||
|
return {
|
||||||
|
'html': html,
|
||||||
|
'priority': priority,
|
||||||
|
'plugin_name': self.PLUGIN_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def should_display(self, position, context, **kwargs):
|
||||||
|
"""
|
||||||
|
判断插件是否应该在指定位置显示
|
||||||
|
子类可重写此方法实现条件显示逻辑
|
||||||
|
|
||||||
|
Args:
|
||||||
|
position: 位置标识
|
||||||
|
context: 模板上下文
|
||||||
|
**kwargs: 额外参数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否显示
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
# === 各位置渲染方法 - 子类重写 ===
|
||||||
|
def render_sidebar_widget(self, context, **kwargs):
|
||||||
|
"""渲染侧边栏组件"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def render_article_bottom_widget(self, context, **kwargs):
|
||||||
|
"""渲染文章底部组件"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def render_article_top_widget(self, context, **kwargs):
|
||||||
|
"""渲染文章顶部组件"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def render_header_widget(self, context, **kwargs):
|
||||||
|
"""渲染页头组件"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def render_footer_widget(self, context, **kwargs):
|
||||||
|
"""渲染页脚组件"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def render_comment_before_widget(self, context, **kwargs):
|
||||||
|
"""渲染评论前组件"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def render_comment_after_widget(self, context, **kwargs):
|
||||||
|
"""渲染评论后组件"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
# === 模板系统 ===
|
||||||
|
def render_template(self, template_name, context=None):
|
||||||
|
"""
|
||||||
|
渲染插件模板
|
||||||
|
|
||||||
|
Args:
|
||||||
|
template_name: 模板文件名
|
||||||
|
context: 模板上下文
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HTML字符串
|
||||||
|
"""
|
||||||
|
if context is None:
|
||||||
|
context = {}
|
||||||
|
|
||||||
|
template_path = f"plugins/{self.plugin_slug}/{template_name}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
return render_to_string(template_path, context)
|
||||||
|
except TemplateDoesNotExist:
|
||||||
|
logger.warning(f"Plugin template not found: {template_path}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# === 静态资源系统 ===
|
||||||
|
def get_static_url(self, static_file):
|
||||||
|
"""获取插件静态文件URL"""
|
||||||
|
from django.templatetags.static import static
|
||||||
|
return static(f"{self.plugin_slug}/static/{self.plugin_slug}/{static_file}")
|
||||||
|
|
||||||
|
def get_css_files(self):
|
||||||
|
"""获取插件CSS文件列表"""
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_js_files(self):
|
||||||
|
"""获取插件JavaScript文件列表"""
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_head_html(self, context=None):
|
||||||
|
"""获取需要插入到<head>中的HTML内容"""
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_body_html(self, context=None):
|
||||||
|
"""获取需要插入到<body>底部的HTML内容"""
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_plugin_info(self):
|
||||||
|
"""
|
||||||
|
获取插件信息
|
||||||
|
:return: 包含插件元数据的字典
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'name': self.PLUGIN_NAME,
|
||||||
|
'description': self.PLUGIN_DESCRIPTION,
|
||||||
|
'version': self.PLUGIN_VERSION,
|
||||||
|
'author': self.PLUGIN_AUTHOR,
|
||||||
|
'slug': self.plugin_slug,
|
||||||
|
'directory': str(self.plugin_dir),
|
||||||
|
'supported_positions': self.SUPPORTED_POSITIONS,
|
||||||
|
'priorities': self.POSITION_PRIORITIES
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
ARTICLE_DETAIL_LOAD = 'article_detail_load'
|
||||||
|
ARTICLE_CREATE = 'article_create'
|
||||||
|
ARTICLE_UPDATE = 'article_update'
|
||||||
|
ARTICLE_DELETE = 'article_delete'
|
||||||
|
|
||||||
|
ARTICLE_CONTENT_HOOK_NAME = "the_content"
|
||||||
|
|
||||||
|
# 位置钩子常量
|
||||||
|
POSITION_HOOKS = {
|
||||||
|
'article_top': 'article_top_widgets',
|
||||||
|
'article_bottom': 'article_bottom_widgets',
|
||||||
|
'sidebar': 'sidebar_widgets',
|
||||||
|
'header': 'header_widgets',
|
||||||
|
'footer': 'footer_widgets',
|
||||||
|
'comment_before': 'comment_before_widgets',
|
||||||
|
'comment_after': 'comment_after_widgets',
|
||||||
|
}
|
||||||
|
|
||||||
|
# 资源注入钩子
|
||||||
|
HEAD_RESOURCES_HOOK = 'head_resources'
|
||||||
|
BODY_RESOURCES_HOOK = 'body_resources'
|
||||||
|
|
||||||
@ -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,64 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 全局插件注册表
|
||||||
|
_loaded_plugins = []
|
||||||
|
|
||||||
|
def load_plugins():
|
||||||
|
"""
|
||||||
|
Dynamically loads and initializes plugins from the 'plugins' directory.
|
||||||
|
This function is intended to be called when the Django app registry is ready.
|
||||||
|
"""
|
||||||
|
global _loaded_plugins
|
||||||
|
_loaded_plugins = []
|
||||||
|
|
||||||
|
for plugin_name in settings.ACTIVE_PLUGINS:
|
||||||
|
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
|
||||||
|
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
|
||||||
|
try:
|
||||||
|
# 导入插件模块
|
||||||
|
plugin_module = __import__(f'plugins.{plugin_name}.plugin', fromlist=['plugin'])
|
||||||
|
|
||||||
|
# 获取插件实例
|
||||||
|
if hasattr(plugin_module, 'plugin'):
|
||||||
|
plugin_instance = plugin_module.plugin
|
||||||
|
_loaded_plugins.append(plugin_instance)
|
||||||
|
logger.info(f"Successfully loaded plugin: {plugin_name} - {plugin_instance.PLUGIN_NAME}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Plugin {plugin_name} does not have 'plugin' instance")
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
|
||||||
|
except AttributeError as e:
|
||||||
|
logger.error(f"Failed to get plugin instance: {plugin_name}", exc_info=e)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error loading plugin: {plugin_name}", exc_info=e)
|
||||||
|
|
||||||
|
def get_loaded_plugins():
|
||||||
|
"""获取所有已加载的插件"""
|
||||||
|
return _loaded_plugins
|
||||||
|
|
||||||
|
def get_plugin_by_name(plugin_name):
|
||||||
|
"""根据名称获取插件"""
|
||||||
|
for plugin in _loaded_plugins:
|
||||||
|
if plugin.plugin_slug == plugin_name:
|
||||||
|
return plugin
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_plugin_by_slug(plugin_slug):
|
||||||
|
"""根据slug获取插件"""
|
||||||
|
for plugin in _loaded_plugins:
|
||||||
|
if plugin.plugin_slug == plugin_slug:
|
||||||
|
return plugin
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_plugins_info():
|
||||||
|
"""获取所有插件的信息"""
|
||||||
|
return [plugin.get_plugin_info() for plugin in _loaded_plugins]
|
||||||
|
|
||||||
|
def get_plugins_by_position(position):
|
||||||
|
"""获取支持指定位置的插件"""
|
||||||
|
return [plugin for plugin in _loaded_plugins if position in plugin.SUPPORTED_POSITIONS]
|
||||||
@ -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)
|
||||||
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()
|
||||||
Loading…
Reference in new issue