parent
29c9867b21
commit
d9244acbe7
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,47 +0,0 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
def disable_commentstatus(modeladmin, request, queryset):
|
||||
queryset.update(is_enable=False)
|
||||
|
||||
|
||||
def enable_commentstatus(modeladmin, request, queryset):
|
||||
queryset.update(is_enable=True)
|
||||
|
||||
|
||||
disable_commentstatus.short_description = _('Disable comments')
|
||||
enable_commentstatus.short_description = _('Enable comments')
|
||||
|
||||
|
||||
class CommentAdmin(admin.ModelAdmin):
|
||||
list_per_page = 20
|
||||
list_display = (
|
||||
'id',
|
||||
'body',
|
||||
'link_to_userinfo',
|
||||
'link_to_article',
|
||||
'is_enable',
|
||||
'creation_time')
|
||||
list_display_links = ('id', 'body', 'is_enable')
|
||||
list_filter = ('is_enable',)
|
||||
exclude = ('creation_time', 'last_modify_time')
|
||||
actions = [disable_commentstatus, enable_commentstatus]
|
||||
|
||||
def link_to_userinfo(self, obj):
|
||||
info = (obj.author._meta.app_label, obj.author._meta.model_name)
|
||||
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
|
||||
return format_html(
|
||||
u'<a href="%s">%s</a>' %
|
||||
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
|
||||
|
||||
def link_to_article(self, obj):
|
||||
info = (obj.article._meta.app_label, obj.article._meta.model_name)
|
||||
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
|
||||
return format_html(
|
||||
u'<a href="%s">%s</a>' % (link, obj.article.title))
|
||||
|
||||
link_to_userinfo.short_description = _('User')
|
||||
link_to_article.short_description = _('Article')
|
||||
@ -1,5 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CommentsConfig(AppConfig):
|
||||
name = 'comments'
|
||||
@ -1,13 +0,0 @@
|
||||
from django import forms
|
||||
from django.forms import ModelForm
|
||||
|
||||
from .models import Comment
|
||||
|
||||
|
||||
class CommentForm(ModelForm):
|
||||
parent_comment_id = forms.IntegerField(
|
||||
widget=forms.HiddenInput, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = ['body']
|
||||
@ -1,38 +0,0 @@
|
||||
# Generated by Django 4.1.7 on 2023-03-02 07:14
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('blog', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Comment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('body', models.TextField(max_length=300, verbose_name='正文')),
|
||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
|
||||
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
|
||||
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '评论',
|
||||
'verbose_name_plural': '评论',
|
||||
'ordering': ['-id'],
|
||||
'get_latest_by': 'id',
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.7 on 2023-04-24 13:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('comments', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='is_enable',
|
||||
field=models.BooleanField(default=False, verbose_name='是否显示'),
|
||||
),
|
||||
]
|
||||
@ -1,60 +0,0 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-06 13:13
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('blog', '0005_alter_article_options_alter_category_options_and_more'),
|
||||
('comments', '0002_alter_comment_is_enable'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='comment',
|
||||
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='comment',
|
||||
name='created_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='comment',
|
||||
name='last_mod_time',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='comment',
|
||||
name='creation_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='comment',
|
||||
name='last_modify_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='article',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='author',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='is_enable',
|
||||
field=models.BooleanField(default=False, verbose_name='enable'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='parent_comment',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'),
|
||||
),
|
||||
]
|
||||
@ -1,39 +0,0 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from blog.models import Article
|
||||
|
||||
|
||||
# Create your models here.
|
||||
|
||||
class Comment(models.Model):
|
||||
body = models.TextField('正文', max_length=300)
|
||||
creation_time = models.DateTimeField(_('creation time'), default=now)
|
||||
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
|
||||
author = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name=_('author'),
|
||||
on_delete=models.CASCADE)
|
||||
article = models.ForeignKey(
|
||||
Article,
|
||||
verbose_name=_('article'),
|
||||
on_delete=models.CASCADE)
|
||||
parent_comment = models.ForeignKey(
|
||||
'self',
|
||||
verbose_name=_('parent comment'),
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.CASCADE)
|
||||
is_enable = models.BooleanField(_('enable'),
|
||||
default=False, blank=False, null=False)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-id']
|
||||
verbose_name = _('comment')
|
||||
verbose_name_plural = verbose_name
|
||||
get_latest_by = 'id'
|
||||
|
||||
def __str__(self):
|
||||
return self.body
|
||||
@ -1,30 +0,0 @@
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def parse_commenttree(commentlist, comment):
|
||||
"""获得当前评论子评论的列表
|
||||
用法: {% parse_commenttree article_comments comment as childcomments %}
|
||||
"""
|
||||
datas = []
|
||||
|
||||
def parse(c):
|
||||
childs = commentlist.filter(parent_comment=c, is_enable=True)
|
||||
for child in childs:
|
||||
datas.append(child)
|
||||
parse(child)
|
||||
|
||||
parse(comment)
|
||||
return datas
|
||||
|
||||
|
||||
@register.inclusion_tag('comments/tags/comment_item.html')
|
||||
def show_comment_item(comment, ischild):
|
||||
"""评论"""
|
||||
depth = 1 if ischild else 2
|
||||
return {
|
||||
'comment_item': comment,
|
||||
'depth': depth
|
||||
}
|
||||
@ -1,109 +0,0 @@
|
||||
from django.test import Client, RequestFactory, TransactionTestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from accounts.models import BlogUser
|
||||
from blog.models import Category, Article
|
||||
from comments.models import Comment
|
||||
from comments.templatetags.comments_tags import *
|
||||
from djangoblog.utils import get_max_articleid_commentid
|
||||
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
class CommentsTest(TransactionTestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.factory = RequestFactory()
|
||||
from blog.models import BlogSettings
|
||||
value = BlogSettings()
|
||||
value.comment_need_review = True
|
||||
value.save()
|
||||
|
||||
self.user = BlogUser.objects.create_superuser(
|
||||
email="liangliangyy1@gmail.com",
|
||||
username="liangliangyy1",
|
||||
password="liangliangyy1")
|
||||
|
||||
def update_article_comment_status(self, article):
|
||||
comments = article.comment_set.all()
|
||||
for comment in comments:
|
||||
comment.is_enable = True
|
||||
comment.save()
|
||||
|
||||
def test_validate_comment(self):
|
||||
self.client.login(username='liangliangyy1', password='liangliangyy1')
|
||||
|
||||
category = Category()
|
||||
category.name = "categoryccc"
|
||||
category.save()
|
||||
|
||||
article = Article()
|
||||
article.title = "nicetitleccc"
|
||||
article.body = "nicecontentccc"
|
||||
article.author = self.user
|
||||
article.category = category
|
||||
article.type = 'a'
|
||||
article.status = 'p'
|
||||
article.save()
|
||||
|
||||
comment_url = reverse(
|
||||
'comments:postcomment', kwargs={
|
||||
'article_id': article.id})
|
||||
|
||||
response = self.client.post(comment_url,
|
||||
{
|
||||
'body': '123ffffffffff'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
article = Article.objects.get(pk=article.pk)
|
||||
self.assertEqual(len(article.comment_list()), 0)
|
||||
self.update_article_comment_status(article)
|
||||
|
||||
self.assertEqual(len(article.comment_list()), 1)
|
||||
|
||||
response = self.client.post(comment_url,
|
||||
{
|
||||
'body': '123ffffffffff',
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
article = Article.objects.get(pk=article.pk)
|
||||
self.update_article_comment_status(article)
|
||||
self.assertEqual(len(article.comment_list()), 2)
|
||||
parent_comment_id = article.comment_list()[0].id
|
||||
|
||||
response = self.client.post(comment_url,
|
||||
{
|
||||
'body': '''
|
||||
# Title1
|
||||
|
||||
```python
|
||||
import os
|
||||
```
|
||||
|
||||
[url](https://www.lylinux.net/)
|
||||
|
||||
[ddd](http://www.baidu.com)
|
||||
|
||||
|
||||
''',
|
||||
'parent_comment_id': parent_comment_id
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.update_article_comment_status(article)
|
||||
article = Article.objects.get(pk=article.pk)
|
||||
self.assertEqual(len(article.comment_list()), 3)
|
||||
comment = Comment.objects.get(id=parent_comment_id)
|
||||
tree = parse_commenttree(article.comment_list(), comment)
|
||||
self.assertEqual(len(tree), 1)
|
||||
data = show_comment_item(comment, True)
|
||||
self.assertIsNotNone(data)
|
||||
s = get_max_articleid_commentid()
|
||||
self.assertIsNotNone(s)
|
||||
|
||||
from comments.utils import send_comment_email
|
||||
send_comment_email(comment)
|
||||
@ -1,11 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "comments"
|
||||
urlpatterns = [
|
||||
path(
|
||||
'article/<int:article_id>/postcomment',
|
||||
views.CommentPostView.as_view(),
|
||||
name='postcomment'),
|
||||
]
|
||||
@ -1,38 +0,0 @@
|
||||
import logging
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from djangoblog.utils import get_current_site
|
||||
from djangoblog.utils import send_email
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_comment_email(comment):
|
||||
site = get_current_site().domain
|
||||
subject = _('Thanks for your comment')
|
||||
article_url = f"https://{site}{comment.article.get_absolute_url()}"
|
||||
html_content = _("""<p>Thank you very much for your comments on this site</p>
|
||||
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
|
||||
to review your comments,
|
||||
Thank you again!
|
||||
<br />
|
||||
If the link above cannot be opened, please copy this link to your browser.
|
||||
%(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
|
||||
tomail = comment.author.email
|
||||
send_email([tomail], subject, html_content)
|
||||
try:
|
||||
if comment.parent_comment:
|
||||
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
|
||||
received a reply. <br/> %(comment_body)s
|
||||
<br/>
|
||||
go check it out!
|
||||
<br/>
|
||||
If the link above cannot be opened, please copy this link to your browser.
|
||||
%(article_url)s
|
||||
""") % {'article_url': article_url, 'article_title': comment.article.title,
|
||||
'comment_body': comment.parent_comment.body}
|
||||
tomail = comment.parent_comment.author.email
|
||||
send_email([tomail], subject, html_content)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
@ -1,63 +0,0 @@
|
||||
# Create your views here.
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_protect
|
||||
from django.views.generic.edit import FormView
|
||||
|
||||
from accounts.models import BlogUser
|
||||
from blog.models import Article
|
||||
from .forms import CommentForm
|
||||
from .models import Comment
|
||||
|
||||
|
||||
class CommentPostView(FormView):
|
||||
form_class = CommentForm
|
||||
template_name = 'blog/article_detail.html'
|
||||
|
||||
@method_decorator(csrf_protect)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(CommentPostView, self).dispatch(*args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
article_id = self.kwargs['article_id']
|
||||
article = get_object_or_404(Article, pk=article_id)
|
||||
url = article.get_absolute_url()
|
||||
return HttpResponseRedirect(url + "#comments")
|
||||
|
||||
def form_invalid(self, form):
|
||||
article_id = self.kwargs['article_id']
|
||||
article = get_object_or_404(Article, pk=article_id)
|
||||
|
||||
return self.render_to_response({
|
||||
'form': form,
|
||||
'article': article
|
||||
})
|
||||
|
||||
def form_valid(self, form):
|
||||
"""提交的数据验证合法后的逻辑"""
|
||||
user = self.request.user
|
||||
author = BlogUser.objects.get(pk=user.pk)
|
||||
article_id = self.kwargs['article_id']
|
||||
article = get_object_or_404(Article, pk=article_id)
|
||||
|
||||
if article.comment_status == 'c' or article.status == 'c':
|
||||
raise ValidationError("该文章评论已关闭.")
|
||||
comment = form.save(False)
|
||||
comment.article = article
|
||||
from djangoblog.utils import get_blog_setting
|
||||
settings = get_blog_setting()
|
||||
if not settings.comment_need_review:
|
||||
comment.is_enable = True
|
||||
comment.author = author
|
||||
|
||||
if form.cleaned_data['parent_comment_id']:
|
||||
parent_comment = Comment.objects.get(
|
||||
pk=form.cleaned_data['parent_comment_id'])
|
||||
comment.parent_comment = parent_comment
|
||||
|
||||
comment.save(True)
|
||||
return HttpResponseRedirect(
|
||||
"%s#div-comment-%d" %
|
||||
(article.get_absolute_url(), comment.pk))
|
||||
@ -1 +0,0 @@
|
||||
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
|
||||
@ -1,64 +0,0 @@
|
||||
from django.contrib.admin import AdminSite
|
||||
from django.contrib.admin.models import LogEntry
|
||||
from django.contrib.sites.admin import SiteAdmin
|
||||
from django.contrib.sites.models import Site
|
||||
|
||||
from accounts.admin import *
|
||||
from blog.admin import *
|
||||
from blog.models import *
|
||||
from comments.admin import *
|
||||
from comments.models import *
|
||||
from djangoblog.logentryadmin import LogEntryAdmin
|
||||
from oauth.admin import *
|
||||
from oauth.models import *
|
||||
from owntracks.admin import *
|
||||
from owntracks.models import *
|
||||
from servermanager.admin import *
|
||||
from servermanager.models import *
|
||||
|
||||
|
||||
class DjangoBlogAdminSite(AdminSite):
|
||||
site_header = 'djangoblog administration'
|
||||
site_title = 'djangoblog site admin'
|
||||
|
||||
def __init__(self, name='admin'):
|
||||
super().__init__(name)
|
||||
|
||||
def has_permission(self, request):
|
||||
return request.user.is_superuser
|
||||
|
||||
# def get_urls(self):
|
||||
# urls = super().get_urls()
|
||||
# from django.urls import path
|
||||
# from blog.views import refresh_memcache
|
||||
#
|
||||
# my_urls = [
|
||||
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
|
||||
# ]
|
||||
# return urls + my_urls
|
||||
|
||||
|
||||
admin_site = DjangoBlogAdminSite(name='admin')
|
||||
|
||||
admin_site.register(Article, ArticlelAdmin)
|
||||
admin_site.register(Category, CategoryAdmin)
|
||||
admin_site.register(Tag, TagAdmin)
|
||||
admin_site.register(Links, LinksAdmin)
|
||||
admin_site.register(SideBar, SideBarAdmin)
|
||||
admin_site.register(BlogSettings, BlogSettingsAdmin)
|
||||
|
||||
admin_site.register(commands, CommandsAdmin)
|
||||
admin_site.register(EmailSendLog, EmailSendLogAdmin)
|
||||
|
||||
admin_site.register(BlogUser, BlogUserAdmin)
|
||||
|
||||
admin_site.register(Comment, CommentAdmin)
|
||||
|
||||
admin_site.register(OAuthUser, OAuthUserAdmin)
|
||||
admin_site.register(OAuthConfig, OAuthConfigAdmin)
|
||||
|
||||
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
|
||||
|
||||
admin_site.register(Site, SiteAdmin)
|
||||
|
||||
admin_site.register(LogEntry, LogEntryAdmin)
|
||||
@ -1,11 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class DjangoblogAppConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'djangoblog'
|
||||
|
||||
def ready(self):
|
||||
super().ready()
|
||||
# Import and load plugins here
|
||||
from .plugin_manage.loader import load_plugins
|
||||
load_plugins()
|
||||
@ -1,122 +0,0 @@
|
||||
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()
|
||||
@ -1,183 +0,0 @@
|
||||
from django.utils.encoding import force_str
|
||||
from elasticsearch_dsl import Q
|
||||
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
|
||||
from haystack.forms import ModelSearchForm
|
||||
from haystack.models import SearchResult
|
||||
from haystack.utils import log as logging
|
||||
|
||||
from blog.documents import ArticleDocument, ArticleDocumentManager
|
||||
from blog.models import Article
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ElasticSearchBackend(BaseSearchBackend):
|
||||
def __init__(self, connection_alias, **connection_options):
|
||||
super(
|
||||
ElasticSearchBackend,
|
||||
self).__init__(
|
||||
connection_alias,
|
||||
**connection_options)
|
||||
self.manager = ArticleDocumentManager()
|
||||
self.include_spelling = True
|
||||
|
||||
def _get_models(self, iterable):
|
||||
models = iterable if iterable and iterable[0] else Article.objects.all()
|
||||
docs = self.manager.convert_to_doc(models)
|
||||
return docs
|
||||
|
||||
def _create(self, models):
|
||||
self.manager.create_index()
|
||||
docs = self._get_models(models)
|
||||
self.manager.rebuild(docs)
|
||||
|
||||
def _delete(self, models):
|
||||
for m in models:
|
||||
m.delete()
|
||||
return True
|
||||
|
||||
def _rebuild(self, models):
|
||||
models = models if models else Article.objects.all()
|
||||
docs = self.manager.convert_to_doc(models)
|
||||
self.manager.update_docs(docs)
|
||||
|
||||
def update(self, index, iterable, commit=True):
|
||||
|
||||
models = self._get_models(iterable)
|
||||
self.manager.update_docs(models)
|
||||
|
||||
def remove(self, obj_or_string):
|
||||
models = self._get_models([obj_or_string])
|
||||
self._delete(models)
|
||||
|
||||
def clear(self, models=None, commit=True):
|
||||
self.remove(None)
|
||||
|
||||
@staticmethod
|
||||
def get_suggestion(query: str) -> str:
|
||||
"""获取推荐词, 如果没有找到添加原搜索词"""
|
||||
|
||||
search = ArticleDocument.search() \
|
||||
.query("match", body=query) \
|
||||
.suggest('suggest_search', query, term={'field': 'body'}) \
|
||||
.execute()
|
||||
|
||||
keywords = []
|
||||
for suggest in search.suggest.suggest_search:
|
||||
if suggest["options"]:
|
||||
keywords.append(suggest["options"][0]["text"])
|
||||
else:
|
||||
keywords.append(suggest["text"])
|
||||
|
||||
return ' '.join(keywords)
|
||||
|
||||
@log_query
|
||||
def search(self, query_string, **kwargs):
|
||||
logger.info('search query_string:' + query_string)
|
||||
|
||||
start_offset = kwargs.get('start_offset')
|
||||
end_offset = kwargs.get('end_offset')
|
||||
|
||||
# 推荐词搜索
|
||||
if getattr(self, "is_suggest", None):
|
||||
suggestion = self.get_suggestion(query_string)
|
||||
else:
|
||||
suggestion = query_string
|
||||
|
||||
q = Q('bool',
|
||||
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
|
||||
minimum_should_match="70%")
|
||||
|
||||
search = ArticleDocument.search() \
|
||||
.query('bool', filter=[q]) \
|
||||
.filter('term', status='p') \
|
||||
.filter('term', type='a') \
|
||||
.source(False)[start_offset: end_offset]
|
||||
|
||||
results = search.execute()
|
||||
hits = results['hits'].total
|
||||
raw_results = []
|
||||
for raw_result in results['hits']['hits']:
|
||||
app_label = 'blog'
|
||||
model_name = 'Article'
|
||||
additional_fields = {}
|
||||
|
||||
result_class = SearchResult
|
||||
|
||||
result = result_class(
|
||||
app_label,
|
||||
model_name,
|
||||
raw_result['_id'],
|
||||
raw_result['_score'],
|
||||
**additional_fields)
|
||||
raw_results.append(result)
|
||||
facets = {}
|
||||
spelling_suggestion = None if query_string == suggestion else suggestion
|
||||
|
||||
return {
|
||||
'results': raw_results,
|
||||
'hits': hits,
|
||||
'facets': facets,
|
||||
'spelling_suggestion': spelling_suggestion,
|
||||
}
|
||||
|
||||
|
||||
class ElasticSearchQuery(BaseSearchQuery):
|
||||
def _convert_datetime(self, date):
|
||||
if hasattr(date, 'hour'):
|
||||
return force_str(date.strftime('%Y%m%d%H%M%S'))
|
||||
else:
|
||||
return force_str(date.strftime('%Y%m%d000000'))
|
||||
|
||||
def clean(self, query_fragment):
|
||||
"""
|
||||
Provides a mechanism for sanitizing user input before presenting the
|
||||
value to the backend.
|
||||
|
||||
Whoosh 1.X differs here in that you can no longer use a backslash
|
||||
to escape reserved characters. Instead, the whole word should be
|
||||
quoted.
|
||||
"""
|
||||
words = query_fragment.split()
|
||||
cleaned_words = []
|
||||
|
||||
for word in words:
|
||||
if word in self.backend.RESERVED_WORDS:
|
||||
word = word.replace(word, word.lower())
|
||||
|
||||
for char in self.backend.RESERVED_CHARACTERS:
|
||||
if char in word:
|
||||
word = "'%s'" % word
|
||||
break
|
||||
|
||||
cleaned_words.append(word)
|
||||
|
||||
return ' '.join(cleaned_words)
|
||||
|
||||
def build_query_fragment(self, field, filter_type, value):
|
||||
return value.query_string
|
||||
|
||||
def get_count(self):
|
||||
results = self.get_results()
|
||||
return len(results) if results else 0
|
||||
|
||||
def get_spelling_suggestion(self, preferred_query=None):
|
||||
return self._spelling_suggestion
|
||||
|
||||
def build_params(self, spelling_query=None):
|
||||
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
|
||||
return kwargs
|
||||
|
||||
|
||||
class ElasticSearchModelSearchForm(ModelSearchForm):
|
||||
|
||||
def search(self):
|
||||
# 是否建议搜索
|
||||
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
|
||||
sqs = super().search()
|
||||
return sqs
|
||||
|
||||
|
||||
class ElasticSearchEngine(BaseEngine):
|
||||
backend = ElasticSearchBackend
|
||||
query = ElasticSearchQuery
|
||||
@ -1,40 +0,0 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.syndication.views import Feed
|
||||
from django.utils import timezone
|
||||
from django.utils.feedgenerator import Rss201rev2Feed
|
||||
|
||||
from blog.models import Article
|
||||
from djangoblog.utils import CommonMarkdown
|
||||
|
||||
|
||||
class DjangoBlogFeed(Feed):
|
||||
feed_type = Rss201rev2Feed
|
||||
|
||||
description = '大巧无工,重剑无锋.'
|
||||
title = "且听风吟 大巧无工,重剑无锋. "
|
||||
link = "/feed/"
|
||||
|
||||
def author_name(self):
|
||||
return get_user_model().objects.first().nickname
|
||||
|
||||
def author_link(self):
|
||||
return get_user_model().objects.first().get_absolute_url()
|
||||
|
||||
def items(self):
|
||||
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
|
||||
|
||||
def item_title(self, item):
|
||||
return item.title
|
||||
|
||||
def item_description(self, item):
|
||||
return CommonMarkdown.get_markdown(item.body)
|
||||
|
||||
def feed_copyright(self):
|
||||
now = timezone.now()
|
||||
return "Copyright© {year} 且听风吟".format(year=now.year)
|
||||
|
||||
def item_link(self, item):
|
||||
return item.get_absolute_url()
|
||||
|
||||
def item_guid(self, item):
|
||||
return
|
||||
@ -1,91 +0,0 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.models import DELETION
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse, NoReverseMatch
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class LogEntryAdmin(admin.ModelAdmin):
|
||||
list_filter = [
|
||||
'content_type'
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'object_repr',
|
||||
'change_message'
|
||||
]
|
||||
|
||||
list_display_links = [
|
||||
'action_time',
|
||||
'get_change_message',
|
||||
]
|
||||
list_display = [
|
||||
'action_time',
|
||||
'user_link',
|
||||
'content_type',
|
||||
'object_link',
|
||||
'get_change_message',
|
||||
]
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return (
|
||||
request.user.is_superuser or
|
||||
request.user.has_perm('admin.change_logentry')
|
||||
) and request.method != 'POST'
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def object_link(self, obj):
|
||||
object_link = escape(obj.object_repr)
|
||||
content_type = obj.content_type
|
||||
|
||||
if obj.action_flag != DELETION and content_type is not None:
|
||||
# try returning an actual link instead of object repr string
|
||||
try:
|
||||
url = reverse(
|
||||
'admin:{}_{}_change'.format(content_type.app_label,
|
||||
content_type.model),
|
||||
args=[obj.object_id]
|
||||
)
|
||||
object_link = '<a href="{}">{}</a>'.format(url, object_link)
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
return mark_safe(object_link)
|
||||
|
||||
object_link.admin_order_field = 'object_repr'
|
||||
object_link.short_description = _('object')
|
||||
|
||||
def user_link(self, obj):
|
||||
content_type = ContentType.objects.get_for_model(type(obj.user))
|
||||
user_link = escape(force_str(obj.user))
|
||||
try:
|
||||
# try returning an actual link instead of object repr string
|
||||
url = reverse(
|
||||
'admin:{}_{}_change'.format(content_type.app_label,
|
||||
content_type.model),
|
||||
args=[obj.user.pk]
|
||||
)
|
||||
user_link = '<a href="{}">{}</a>'.format(url, user_link)
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
return mark_safe(user_link)
|
||||
|
||||
user_link.admin_order_field = 'user'
|
||||
user_link.short_description = _('user')
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = super(LogEntryAdmin, self).get_queryset(request)
|
||||
return queryset.prefetch_related('content_type')
|
||||
|
||||
def get_actions(self, request):
|
||||
actions = super(LogEntryAdmin, self).get_actions(request)
|
||||
if 'delete_selected' in actions:
|
||||
del actions['delete_selected']
|
||||
return actions
|
||||
@ -1,41 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
ARTICLE_DETAIL_LOAD = 'article_detail_load'
|
||||
ARTICLE_CREATE = 'article_create'
|
||||
ARTICLE_UPDATE = 'article_update'
|
||||
ARTICLE_DELETE = 'article_delete'
|
||||
|
||||
ARTICLE_CONTENT_HOOK_NAME = "the_content"
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_hooks = {}
|
||||
|
||||
|
||||
def register(hook_name: str, callback: callable):
|
||||
"""
|
||||
注册一个钩子回调。
|
||||
"""
|
||||
if hook_name not in _hooks:
|
||||
_hooks[hook_name] = []
|
||||
_hooks[hook_name].append(callback)
|
||||
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
|
||||
|
||||
|
||||
def run_action(hook_name: str, *args, **kwargs):
|
||||
"""
|
||||
执行一个 Action Hook。
|
||||
它会按顺序执行所有注册到该钩子上的回调函数。
|
||||
"""
|
||||
if hook_name in _hooks:
|
||||
logger.debug(f"Running action hook '{hook_name}'")
|
||||
for callback in _hooks[hook_name]:
|
||||
try:
|
||||
callback(*args, **kwargs)
|
||||
except Exception as e:
|
||||
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
|
||||
|
||||
|
||||
def apply_filters(hook_name: str, value, *args, **kwargs):
|
||||
"""
|
||||
执行一个 Filter Hook。
|
||||
它会把 value 依次传递给所有注册的回调函数进行处理。
|
||||
"""
|
||||
if hook_name in _hooks:
|
||||
logger.debug(f"Applying filter hook '{hook_name}'")
|
||||
for callback in _hooks[hook_name]:
|
||||
try:
|
||||
value = callback(value, *args, **kwargs)
|
||||
except Exception as e:
|
||||
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
|
||||
return value
|
||||
@ -1,19 +0,0 @@
|
||||
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)
|
||||
@ -1,343 +0,0 @@
|
||||
"""
|
||||
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'
|
||||
]
|
||||
@ -1,59 +0,0 @@
|
||||
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
|
||||
@ -1,21 +0,0 @@
|
||||
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)
|
||||
@ -1,32 +0,0 @@
|
||||
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)
|
||||
@ -1,64 +0,0 @@
|
||||
"""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)
|
||||
@ -1,232 +0,0 @@
|
||||
#!/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
@ -1,16 +0,0 @@
|
||||
"""
|
||||
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()
|
||||
@ -1,158 +0,0 @@
|
||||
# DjangoBlog
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml/badge.svg" alt="Django CI"></a>
|
||||
<a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml/badge.svg" alt="CodeQL"></a>
|
||||
<a href="https://codecov.io/gh/liangliangyy/DjangoBlog"><img src="https://codecov.io/gh/liangliangyy/DjangoBlog/branch/master/graph/badge.svg" alt="codecov"></a>
|
||||
<a href="https://github.com/liangliangyy/DjangoBlog/blob/master/LICENSE"><img src="https://img.shields.io/github/license/liangliangyy/djangoblog.svg" alt="license"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<b>A powerful, elegant, and modern blog system.</b>
|
||||
<br>
|
||||
<b>English</b> • <a href="/README.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
DjangoBlog is a high-performance blog platform built with Python 3.10 and Django 4.0. It not only provides all the core functionalities of a traditional blog but also features a flexible plugin system, allowing you to easily extend and customize your website. Whether you are a personal blogger, a tech enthusiast, or a content creator, DjangoBlog aims to provide a stable, efficient, and easy-to-maintain environment for writing and publishing.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **Powerful Content Management**: Full support for managing articles, standalone pages, categories, and tags. Comes with a powerful built-in Markdown editor with syntax highlighting.
|
||||
- **Full-Text Search**: Integrated search engine for fast and accurate content searching.
|
||||
- **Interactive Comment System**: Supports replies, email notifications, and Markdown formatting in comments.
|
||||
- **Flexible Sidebar**: Customizable modules for displaying recent articles, most viewed posts, tag cloud, and more.
|
||||
- **Social Login**: Built-in OAuth support, with integrations for Google, GitHub, Facebook, Weibo, QQ, and other major platforms.
|
||||
- **High-Performance Caching**: Native support for Redis caching with an automatic refresh mechanism to ensure high-speed website responses.
|
||||
- **SEO Friendly**: Basic SEO features are included, with automatic notifications to Google and Baidu upon new content publication.
|
||||
- **Extensible Plugin System**: Extend blog functionalities by creating standalone plugins, ensuring decoupled and maintainable code. We have already implemented features like view counting and SEO optimization through plugins!
|
||||
- **Integrated Image Hosting**: A simple, built-in image hosting feature for easy uploads and management.
|
||||
- **Automated Frontend**: Integrated with `django-compressor` to automatically compress and optimize CSS and JavaScript files.
|
||||
- **Robust Operations**: Built-in email notifications for website exceptions and management capabilities through a WeChat Official Account.
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- **Backend**: Python 3.10, Django 4.0
|
||||
- **Database**: MySQL, SQLite (configurable)
|
||||
- **Cache**: Redis
|
||||
- **Frontend**: HTML5, CSS3, JavaScript
|
||||
- **Search**: Whoosh, Elasticsearch (configurable)
|
||||
- **Editor**: Markdown (mdeditor)
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### 1. Prerequisites
|
||||
|
||||
Ensure you have Python 3.10+ and MySQL/MariaDB installed on your system.
|
||||
|
||||
### 2. Clone & Installation
|
||||
|
||||
```bash
|
||||
# Clone the project to your local machine
|
||||
git clone https://github.com/liangliangyy/DjangoBlog.git
|
||||
cd DjangoBlog
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. Project Configuration
|
||||
|
||||
- **Database**:
|
||||
Open `djangoblog/settings.py`, locate the `DATABASES` section, and update it with your MySQL connection details.
|
||||
|
||||
```python
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': 'djangoblog',
|
||||
'USER': 'root',
|
||||
'PASSWORD': 'your_password',
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': 3306,
|
||||
}
|
||||
}
|
||||
```
|
||||
Create the database in MySQL:
|
||||
```sql
|
||||
CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
- **More Configurations**:
|
||||
For advanced settings such as email, OAuth, caching, and more, please refer to our [Detailed Configuration Guide](/docs/config-en.md).
|
||||
|
||||
### 4. Database Initialization
|
||||
|
||||
```bash
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
|
||||
# Create a superuser account
|
||||
python manage.py createsuperuser
|
||||
```
|
||||
|
||||
### 5. Running the Project
|
||||
|
||||
```bash
|
||||
# (Optional) Generate some test data
|
||||
python manage.py create_testdata
|
||||
|
||||
# (Optional) Collect and compress static files
|
||||
python manage.py collectstatic --noinput
|
||||
python manage.py compress --force
|
||||
|
||||
# Start the development server
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
Now, open your browser and navigate to `http://127.0.0.1:8000/`. You should see the DjangoBlog homepage!
|
||||
|
||||
## Deployment
|
||||
|
||||
- **Traditional Deployment**: A detailed guide for server deployment is available here: [Deployment Tutorial](https://www.lylinux.net/article/2019/8/5/58.html) (in Chinese).
|
||||
- **Docker Deployment**: This project fully supports Docker. If you are familiar with containerization, please refer to the [Docker Deployment Guide](/docs/docker-en.md) for a quick start.
|
||||
- **Kubernetes Deployment**: We also provide a complete [Kubernetes Deployment Guide](/docs/k8s-en.md) to help you go cloud-native easily.
|
||||
|
||||
## 🧩 Plugin System
|
||||
|
||||
The plugin system is a core feature of DjangoBlog. It allows you to add new functionalities to your blog without modifying the core codebase by writing standalone plugins.
|
||||
|
||||
- **How it Works**: Plugins operate by registering callback functions to predefined "hooks". For instance, when an article is rendered, the `after_article_body_get` hook is triggered, and all functions registered to this hook are executed.
|
||||
- **Existing Plugins**: Features like `view_count` and `seo_optimizer` are implemented through this plugin system.
|
||||
- **Develop Your Own Plugin**: Simply create a new folder under the `plugins` directory and write your `plugin.py`. We welcome you to explore and contribute your creative ideas to the DjangoBlog community!
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We warmly welcome contributions of any kind! If you have great ideas or have found a bug, please feel free to open an issue or submit a pull request.
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is open-sourced under the [MIT License](LICENSE).
|
||||
|
||||
---
|
||||
|
||||
## ❤️ Support & Sponsorship
|
||||
|
||||
If you find this project helpful and wish to support its continued maintenance and development, please consider buying me a coffee! Your support is my greatest motivation.
|
||||
|
||||
<p align="center">
|
||||
<img src="/docs/imgs/alipay.jpg" width="150" alt="Alipay Sponsorship">
|
||||
<img src="/docs/imgs/wechat.jpg" width="150" alt="WeChat Sponsorship">
|
||||
</p>
|
||||
<p align="center">
|
||||
<i>(Left) Alipay / (Right) WeChat</i>
|
||||
</p>
|
||||
|
||||
## 🙏 Acknowledgements
|
||||
|
||||
A special thanks to **JetBrains** for providing a free open-source license for this project.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.jetbrains.com/?from=DjangoBlog">
|
||||
<img src="/docs/imgs/pycharm_logo.png" width="150" alt="JetBrains Logo">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
> If this project has helped you, please leave your website URL [here](https://github.com/liangliangyy/DjangoBlog/issues/214) to let more people see it. Your feedback is the driving force for my continued updates and maintenance.
|
||||
@ -1,64 +0,0 @@
|
||||
# Introduction to main features settings
|
||||
|
||||
## Cache:
|
||||
Cache using `memcache` for default. If you don't have `memcache` environment, you can remove the `default` setting in `CACHES` and change `locmemcache` to `default`.
|
||||
```python
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
|
||||
'LOCATION': '127.0.0.1:11211',
|
||||
'KEY_PREFIX': 'django_test' if TESTING else 'djangoblog',
|
||||
'TIMEOUT': 60 * 60 * 10
|
||||
},
|
||||
'locmemcache': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'TIMEOUT': 10800,
|
||||
'LOCATION': 'unique-snowflake',
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## OAuth Login:
|
||||
QQ, Weibo, Google, GitHub and Facebook are now supported for OAuth login. Fetch OAuth login permissions from the corresponding open platform, and save them with `appkey`, `appsecret` and callback address in **Backend->OAuth** configuration.
|
||||
|
||||
### Callback address examples:
|
||||
QQ: http://your-domain-name/oauth/authorize?type=qq
|
||||
Weibo: http://your-domain-name/oauth/authorize?type=weibo
|
||||
type is in the type field of `oauthmanager`.
|
||||
|
||||
## owntracks:
|
||||
owntracks is a location tracking application. It will send your locaiton to the server by timing.Simple support owntracks features. Just install owntracks app and set api address as `your-domain-name/owntracks/logtracks`. Visit `your-domain-name/owntracks/show_dates` and you will see the date with latitude and langitude, click it and see the motion track. The map is drawn by AMap.
|
||||
|
||||
## Email feature:
|
||||
Same as before, Configure your own error msg recvie email information with`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]` in `settings.py`. And modify:
|
||||
```python
|
||||
EMAIL_HOST = 'smtp.zoho.com'
|
||||
EMAIL_PORT = 587
|
||||
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 = os.environ.get('DJANGO_EMAIL_USER')
|
||||
```
|
||||
with your email account information.
|
||||
|
||||
## WeChat Official Account
|
||||
Simple wechat official account features integrated. Set token as `your-domain-name/robot` in wechat backend. Default token is `lylinux`, you can change it to your own in `servermanager/robot.py`. Add a new command in `Backend->Servermanager->command`, in this way, you can manage the system through wechat official account.
|
||||
|
||||
## Introduction to website configuration
|
||||
You can add website configuration in **Backend->BLOG->WebSiteConfiguration**. Such as: keywords, description, Google Ad, website stats code, case number, etc.
|
||||
OAuth user avatar path is saved in *StaticFileSavedAddress*. Please input absolute path, code directory for default.
|
||||
|
||||
## Source code highlighting
|
||||
If the code block in your article didn't show hightlight, please write the code blocks as following:
|
||||
|
||||

|
||||
|
||||
That is, you should add the corresponding language name before the code block.
|
||||
|
||||
## Update
|
||||
If you get errors as following while executing database migrations:
|
||||
```python
|
||||
django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the django_migrations table ((1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(6) NOT NULL)' at line 1"))
|
||||
```
|
||||
This problem may cause by the mysql version under 5.6, a new version( >= 5.6 ) mysql is needed.
|
||||
|
||||
@ -1,114 +0,0 @@
|
||||
# Deploying DjangoBlog with Docker
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
This project fully supports containerized deployment using Docker, providing you with a fast, consistent, and isolated runtime environment. We recommend using `docker-compose` to launch the entire blog service stack with a single command.
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
Before you begin, please ensure you have the following software installed on your system:
|
||||
- [Docker Engine](https://docs.docker.com/engine/install/)
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) (Included with Docker Desktop for Mac and Windows)
|
||||
|
||||
## 2. Recommended Method: Using `docker-compose` (One-Click Deployment)
|
||||
|
||||
This is the simplest and most recommended way to deploy. It automatically creates and manages the Django application, a MySQL database, and an optional Elasticsearch service for you.
|
||||
|
||||
### Step 1: Start the Basic Services
|
||||
|
||||
From the project's root directory, run the following command:
|
||||
|
||||
```bash
|
||||
# Build and start the containers in detached mode (includes Django app and MySQL)
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
`docker-compose` will read the `docker-compose.yml` file, pull the necessary images, build the project image, and start all services.
|
||||
|
||||
- **Access Your Blog**: Once the services are up, you can access the blog by navigating to `http://127.0.0.1` in your browser.
|
||||
- **Data Persistence**: MySQL data files will be stored in the `data/mysql` directory within the project root, ensuring that your data persists across container restarts.
|
||||
|
||||
### Step 2: (Optional) Enable Elasticsearch for Full-Text Search
|
||||
|
||||
If you want to use Elasticsearch for more powerful full-text search capabilities, you can include the `docker-compose.es.yml` configuration file:
|
||||
|
||||
```bash
|
||||
# Build and start all services in detached mode (Django, MySQL, Elasticsearch)
|
||||
docker-compose -f docker-compose.yml -f deploy/docker-compose/docker-compose.es.yml up -d --build
|
||||
```
|
||||
- **Data Persistence**: Elasticsearch data will be stored in the `data/elasticsearch` directory.
|
||||
|
||||
### Step 3: First-Time Initialization
|
||||
|
||||
After the containers start for the first time, you'll need to execute some initialization commands inside the application container.
|
||||
|
||||
```bash
|
||||
# Get a shell inside the djangoblog application container (named 'web')
|
||||
docker-compose exec web bash
|
||||
|
||||
# Inside the container, run the following commands:
|
||||
# Create a superuser account (follow the prompts to set username, email, and password)
|
||||
python manage.py createsuperuser
|
||||
|
||||
# (Optional) Create some test data
|
||||
python manage.py create_testdata
|
||||
|
||||
# (Optional, if ES is enabled) Create the search index
|
||||
python manage.py rebuild_index
|
||||
|
||||
# Exit the container
|
||||
exit
|
||||
```
|
||||
|
||||
## 3. Alternative Method: Using the Standalone Docker Image
|
||||
|
||||
If you already have an external MySQL database running, you can run the DjangoBlog application image by itself.
|
||||
|
||||
```bash
|
||||
# Pull the latest image from Docker Hub
|
||||
docker pull liangliangyy/djangoblog:latest
|
||||
|
||||
# Run the container and connect it to your external database
|
||||
docker run -d \
|
||||
-p 8000:8000 \
|
||||
-e DJANGO_SECRET_KEY='your-strong-secret-key' \
|
||||
-e DJANGO_MYSQL_HOST='your-mysql-host' \
|
||||
-e DJANGO_MYSQL_USER='your-mysql-user' \
|
||||
-e DJANGO_MYSQL_PASSWORD='your-mysql-password' \
|
||||
-e DJANGO_MYSQL_DATABASE='djangoblog' \
|
||||
--name djangoblog \
|
||||
liangliangyy/djangoblog:latest
|
||||
```
|
||||
|
||||
- **Access Your Blog**: After startup, visit `http://127.0.0.1:8000`.
|
||||
- **Create Superuser**: `docker exec -it djangoblog python manage.py createsuperuser`
|
||||
|
||||
## 4. Configuration (Environment Variables)
|
||||
|
||||
Most of the project's configuration is managed through environment variables. You can modify them in the `docker-compose.yml` file or pass them using the `-e` flag with the `docker run` command.
|
||||
|
||||
| Environment Variable | Default/Example Value | Notes |
|
||||
|---------------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------|
|
||||
| `DJANGO_SECRET_KEY` | `your-strong-secret-key` | **Must be changed to a random, complex string!** |
|
||||
| `DJANGO_DEBUG` | `False` | Toggles Django's debug mode. |
|
||||
| `DJANGO_MYSQL_HOST` | `mysql` | Database hostname. |
|
||||
| `DJANGO_MYSQL_PORT` | `3306` | Database port. |
|
||||
| `DJANGO_MYSQL_DATABASE` | `djangoblog` | Database name. |
|
||||
| `DJANGO_MYSQL_USER` | `root` | Database username. |
|
||||
| `DJANGO_MYSQL_PASSWORD` | `djangoblog_123` | Database password. |
|
||||
| `DJANGO_REDIS_URL` | `redis:6379/0` | Redis connection URL (for caching). |
|
||||
| `DJANGO_ELASTICSEARCH_HOST`| `elasticsearch:9200` | Elasticsearch host address. |
|
||||
| `DJANGO_EMAIL_HOST` | `smtp.example.org` | Email server address. |
|
||||
| `DJANGO_EMAIL_PORT` | `465` | Email server port. |
|
||||
| `DJANGO_EMAIL_USER` | `user@example.org` | Email account username. |
|
||||
| `DJANGO_EMAIL_PASSWORD` | `your-email-password` | Email account password. |
|
||||
| `DJANGO_EMAIL_USE_SSL` | `True` | Whether to use SSL. |
|
||||
| `DJANGO_EMAIL_USE_TLS` | `False` | Whether to use TLS. |
|
||||
| `DJANGO_ADMIN_EMAIL` | `admin@example.org` | Admin email for receiving error reports. |
|
||||
| `DJANGO_BAIDU_NOTIFY_URL` | `http://data.zz.baidu.com/...` | Push API from [Baidu Webmaster Tools](https://ziyuan.baidu.com/linksubmit/index). |
|
||||
|
||||
---
|
||||
|
||||
After deployment, please review and adjust these environment variables according to your needs, especially `DJANGO_SECRET_KEY` and the database and email settings.
|
||||
@ -1,114 +0,0 @@
|
||||
# 使用 Docker 部署 DjangoBlog
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
本项目全面支持使用 Docker 进行容器化部署,为您提供了快速、一致且隔离的运行环境。我们推荐使用 `docker-compose` 来一键启动整个博客服务栈。
|
||||
|
||||
## 1. 环境准备
|
||||
|
||||
在开始之前,请确保您的系统中已经安装了以下软件:
|
||||
- [Docker Engine](https://docs.docker.com/engine/install/)
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) (对于 Docker Desktop 用户,它已内置)
|
||||
|
||||
## 2. 推荐方式:使用 `docker-compose` (一键部署)
|
||||
|
||||
这是最简单、最推荐的部署方式。它会自动为您创建并管理 Django 应用、MySQL 数据库,以及可选的 Elasticsearch 服务。
|
||||
|
||||
### 步骤 1: 启动基础服务
|
||||
|
||||
在项目根目录下,执行以下命令:
|
||||
|
||||
```bash
|
||||
# 构建并以后台模式启动容器 (包含 Django 应用和 MySQL)
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
`docker-compose` 会读取 `docker-compose.yml` 文件,自动拉取所需镜像、构建项目镜像,并启动所有服务。
|
||||
|
||||
- **访问您的博客**: 服务启动后,在浏览器中访问 `http://127.0.0.1` 即可看到博客首页。
|
||||
- **数据持久化**: MySQL 的数据文件将存储在项目根目录下的 `data/mysql` 文件夹中,确保数据在容器重启后不丢失。
|
||||
|
||||
### 步骤 2: (可选) 启用 Elasticsearch 全文搜索
|
||||
|
||||
如果您希望使用 Elasticsearch 提供更强大的全文搜索功能,可以额外加载 `docker-compose.es.yml` 配置文件:
|
||||
|
||||
```bash
|
||||
# 构建并以后台模式启动所有服务 (Django, MySQL, Elasticsearch)
|
||||
docker-compose -f docker-compose.yml -f deploy/docker-compose/docker-compose.es.yml up -d --build
|
||||
```
|
||||
- **数据持久化**: Elasticsearch 的数据将存储在 `data/elasticsearch` 文件夹中。
|
||||
|
||||
### 步骤 3: 首次运行的初始化操作
|
||||
|
||||
当容器首次启动后,您需要进入容器来执行一些初始化命令。
|
||||
|
||||
```bash
|
||||
# 进入 djangoblog 应用容器
|
||||
docker-compose exec web bash
|
||||
|
||||
# 在容器内执行以下命令:
|
||||
# 创建超级管理员账户 (请按照提示设置用户名、邮箱和密码)
|
||||
python manage.py createsuperuser
|
||||
|
||||
# (可选) 创建一些测试数据
|
||||
python manage.py create_testdata
|
||||
|
||||
# (可选,如果启用了 ES) 创建索引
|
||||
python manage.py rebuild_index
|
||||
|
||||
# 退出容器
|
||||
exit
|
||||
```
|
||||
|
||||
## 3. 备选方式:使用独立的 Docker 镜像
|
||||
|
||||
如果您已经拥有一个正在运行的外部 MySQL 数据库,您也可以只运行 DjangoBlog 的应用镜像。
|
||||
|
||||
```bash
|
||||
# 从 Docker Hub 拉取最新镜像
|
||||
docker pull liangliangyy/djangoblog:latest
|
||||
|
||||
# 运行容器,并链接到您的外部数据库
|
||||
docker run -d \
|
||||
-p 8000:8000 \
|
||||
-e DJANGO_SECRET_KEY='your-strong-secret-key' \
|
||||
-e DJANGO_MYSQL_HOST='your-mysql-host' \
|
||||
-e DJANGO_MYSQL_USER='your-mysql-user' \
|
||||
-e DJANGO_MYSQL_PASSWORD='your-mysql-password' \
|
||||
-e DJANGO_MYSQL_DATABASE='djangoblog' \
|
||||
--name djangoblog \
|
||||
liangliangyy/djangoblog:latest
|
||||
```
|
||||
|
||||
- **访问您的博客**: 启动完成后,访问 `http://127.0.0.1:8000`。
|
||||
- **创建管理员**: `docker exec -it djangoblog python manage.py createsuperuser`
|
||||
|
||||
## 4. 配置说明 (环境变量)
|
||||
|
||||
本项目的大部分配置都通过环境变量来管理。您可以在 `docker-compose.yml` 文件中修改它们,或者在使用 `docker run` 命令时通过 `-e` 参数传入。
|
||||
|
||||
| 环境变量名称 | 默认值/示例 | 备注 |
|
||||
|-------------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------|
|
||||
| `DJANGO_SECRET_KEY` | `your-strong-secret-key` | **请务必修改为一个随机且复杂的字符串!** |
|
||||
| `DJANGO_DEBUG` | `False` | 是否开启 Django 的调试模式 |
|
||||
| `DJANGO_MYSQL_HOST` | `mysql` | 数据库主机名 |
|
||||
| `DJANGO_MYSQL_PORT` | `3306` | 数据库端口 |
|
||||
| `DJANGO_MYSQL_DATABASE` | `djangoblog` | 数据库名称 |
|
||||
| `DJANGO_MYSQL_USER` | `root` | 数据库用户名 |
|
||||
| `DJANGO_MYSQL_PASSWORD` | `djangoblog_123` | 数据库密码 |
|
||||
| `DJANGO_REDIS_URL` | `redis:6379/0` | Redis 连接地址 (用于缓存) |
|
||||
| `DJANGO_ELASTICSEARCH_HOST` | `elasticsearch:9200` | Elasticsearch 主机地址 |
|
||||
| `DJANGO_EMAIL_HOST` | `smtp.example.org` | 邮件服务器地址 |
|
||||
| `DJANGO_EMAIL_PORT` | `465` | 邮件服务器端口 |
|
||||
| `DJANGO_EMAIL_USER` | `user@example.org` | 邮件账户 |
|
||||
| `DJANGO_EMAIL_PASSWORD` | `your-email-password` | 邮件密码 |
|
||||
| `DJANGO_EMAIL_USE_SSL` | `True` | 是否使用 SSL |
|
||||
| `DJANGO_EMAIL_USE_TLS` | `False` | 是否使用 TLS |
|
||||
| `DJANGO_ADMIN_EMAIL` | `admin@example.org` | 接收异常报告的管理员邮箱 |
|
||||
| `DJANGO_BAIDU_NOTIFY_URL` | `http://data.zz.baidu.com/...` | [百度站长平台](https://ziyuan.baidu.com/linksubmit/index) 的推送接口 |
|
||||
|
||||
---
|
||||
|
||||
部署完成后,请务必检查并根据您的实际需求调整这些环境变量,特别是 `DJANGO_SECRET_KEY` 和数据库、邮件相关的配置。
|
||||
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@ -1,141 +0,0 @@
|
||||
# Deploying DjangoBlog with Kubernetes
|
||||
|
||||
This document guides you through deploying the DjangoBlog application on a Kubernetes (K8s) cluster. We provide a complete set of `.yaml` configuration files in the `deploy/k8s` directory to deploy a full service stack, including the DjangoBlog application, Nginx, MySQL, Redis, and Elasticsearch.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
This deployment utilizes a microservices-based, cloud-native architecture:
|
||||
|
||||
- **Core Components**: Each core service (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) runs as a separate `Deployment`.
|
||||
- **Configuration Management**: Nginx configurations and Django application environment variables are managed via `ConfigMap`. **Note: For sensitive information like passwords, using `Secret` is highly recommended.**
|
||||
- **Service Discovery**: All services are exposed internally within the cluster as `ClusterIP` type `Service`, enabling communication via service names.
|
||||
- **External Access**: An `Ingress` resource is used to route external HTTP traffic to the Nginx service, which acts as the single entry point for the entire blog application.
|
||||
- **Data Persistence**: A `local-storage` solution based on node-local paths is used. This requires you to manually create storage directories on a specific K8s node and statically bind them using `PersistentVolume` (PV) and `PersistentVolumeClaim` (PVC).
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
Before you begin, please ensure you have the following:
|
||||
|
||||
- A running Kubernetes cluster.
|
||||
- The `kubectl` command-line tool configured to connect to your cluster.
|
||||
- An [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/) installed and configured in your cluster.
|
||||
- Filesystem access to one of the nodes in your cluster (defaulted to `master` in the configs) to create local storage directories.
|
||||
|
||||
## 2. Deployment Steps
|
||||
|
||||
### Step 1: Create a Namespace
|
||||
|
||||
We recommend deploying all DjangoBlog-related resources in a dedicated namespace for better management.
|
||||
|
||||
```bash
|
||||
# Create a namespace named 'djangoblog'
|
||||
kubectl create namespace djangoblog
|
||||
```
|
||||
|
||||
### Step 2: Configure Persistent Storage
|
||||
|
||||
This setup uses Local Persistent Volumes. You need to create the data storage directories on a node within your cluster (the default is the `master` node in `pv.yaml`).
|
||||
|
||||
```bash
|
||||
# Log in to your master node
|
||||
ssh user@master-node
|
||||
|
||||
# Create the required storage directories
|
||||
sudo mkdir -p /mnt/local-storage-db
|
||||
sudo mkdir -p /mnt/local-storage-djangoblog
|
||||
sudo mkdir -p /mnt/resource/
|
||||
sudo mkdir -p /mnt/local-storage-elasticsearch
|
||||
|
||||
# Log out from the node
|
||||
exit
|
||||
```
|
||||
**Note**: If you wish to store data on a different node or use different paths, you must modify the `nodeAffinity` and `local.path` settings in the `deploy/k8s/pv.yaml` file.
|
||||
|
||||
After creating the directories, apply the storage-related configurations:
|
||||
|
||||
```bash
|
||||
# Apply the StorageClass
|
||||
kubectl apply -f deploy/k8s/storageclass.yaml
|
||||
|
||||
# Apply the PersistentVolumes (PVs)
|
||||
kubectl apply -f deploy/k8s/pv.yaml
|
||||
|
||||
# Apply the PersistentVolumeClaims (PVCs)
|
||||
kubectl apply -f deploy/k8s/pvc.yaml
|
||||
```
|
||||
|
||||
### Step 3: Configure the Application
|
||||
|
||||
Before deploying the application, you need to edit the `deploy/k8s/configmap.yaml` file to modify sensitive information and custom settings.
|
||||
|
||||
**It is strongly recommended to change the following fields:**
|
||||
- `DJANGO_SECRET_KEY`: Change to a random, complex string.
|
||||
- `DJANGO_MYSQL_PASSWORD` and `MYSQL_ROOT_PASSWORD`: Change to your own secure database password.
|
||||
|
||||
```bash
|
||||
# Edit the ConfigMap file
|
||||
vim deploy/k8s/configmap.yaml
|
||||
|
||||
# Apply the configuration
|
||||
kubectl apply -f deploy/k8s/configmap.yaml
|
||||
```
|
||||
|
||||
### Step 4: Deploy the Application Stack
|
||||
|
||||
Now, we can deploy all the core services.
|
||||
|
||||
```bash
|
||||
# Deploy the Deployments (DjangoBlog, MySQL, Redis, Nginx, ES)
|
||||
kubectl apply -f deploy/k8s/deployment.yaml
|
||||
|
||||
# Deploy the Services (to create internal endpoints for the Deployments)
|
||||
kubectl apply -f deploy/k8s/service.yaml
|
||||
```
|
||||
|
||||
The deployment may take some time. You can run the following command to check if all Pods are running successfully (STATUS should be `Running`):
|
||||
|
||||
```bash
|
||||
kubectl get pods -n djangoblog -w
|
||||
```
|
||||
|
||||
### Step 5: Expose the Application Externally
|
||||
|
||||
Finally, expose the Nginx service to external traffic by applying the `Ingress` rule.
|
||||
|
||||
```bash
|
||||
# Apply the Ingress rule
|
||||
kubectl apply -f deploy/k8s/gateway.yaml
|
||||
```
|
||||
|
||||
Once deployed, you can access your blog via the external IP address of your Ingress Controller. Use the following command to find the address:
|
||||
|
||||
```bash
|
||||
kubectl get ingress -n djangoblog
|
||||
```
|
||||
|
||||
### Step 6: First-Time Initialization
|
||||
|
||||
Similar to the Docker deployment, you need to get a shell into the DjangoBlog application Pod to perform database initialization and create a superuser on the first run.
|
||||
|
||||
```bash
|
||||
# First, get the name of a djangoblog pod
|
||||
kubectl get pods -n djangoblog | grep djangoblog
|
||||
|
||||
# Exec into one of the Pods (replace [pod-name] with the name from the previous step)
|
||||
kubectl exec -it [pod-name] -n djangoblog -- bash
|
||||
|
||||
# Inside the Pod, run the following commands:
|
||||
# Create a superuser account (follow the prompts)
|
||||
python manage.py createsuperuser
|
||||
|
||||
# (Optional) Create some test data
|
||||
python manage.py create_testdata
|
||||
|
||||
# (Optional, if ES is enabled) Create the search index
|
||||
python manage.py rebuild_index
|
||||
|
||||
# Exit the Pod
|
||||
exit
|
||||
```
|
||||
|
||||
Congratulations! You have successfully deployed DjangoBlog on your Kubernetes cluster.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,51 +0,0 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.sites.models import Site
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '修复Django Sites框架的配置问题'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--domain', type=str, help='站点域名', default='127.0.0.1:8000')
|
||||
parser.add_argument('--name', type=str, help='站点名称', default='DjangoBlog')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
domain = options['domain']
|
||||
name = options['name']
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('=== 修复Django Sites配置 ===\n'))
|
||||
|
||||
# 检查当前的SITE_ID设置
|
||||
site_id = getattr(settings, 'SITE_ID', 1)
|
||||
self.stdout.write(f'当前SITE_ID: {site_id}')
|
||||
|
||||
# 检查是否存在对应的Site对象
|
||||
try:
|
||||
site = Site.objects.get(pk=site_id)
|
||||
self.stdout.write(f'✅ 找到现有站点: {site.domain} - {site.name}')
|
||||
|
||||
# 更新站点信息
|
||||
site.domain = domain
|
||||
site.name = name
|
||||
site.save()
|
||||
self.stdout.write(self.style.SUCCESS(f'✅ 更新站点信息: {domain} - {name}'))
|
||||
|
||||
except Site.DoesNotExist:
|
||||
# 创建新的Site对象
|
||||
site = Site.objects.create(
|
||||
pk=site_id,
|
||||
domain=domain,
|
||||
name=name
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f'✅ 创建新站点: {domain} - {name}'))
|
||||
|
||||
# 验证修复结果
|
||||
try:
|
||||
current_site = Site.objects.get_current()
|
||||
self.stdout.write(self.style.SUCCESS(f'✅ 验证成功: {current_site.domain} - {current_site.name}'))
|
||||
except Site.DoesNotExist:
|
||||
self.stdout.write(self.style.ERROR('❌ 验证失败: 仍然无法获取当前站点'))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('\n=== 修复完成 ==='))
|
||||
self.stdout.write('现在可以正常使用注册功能了!')
|
||||
@ -1,64 +0,0 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth import authenticate, get_user_model
|
||||
from accounts.user_login_backend import EmailOrUsernameModelBackend
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '测试邮箱和用户名登录功能'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--username', type=str, help='测试用户名')
|
||||
parser.add_argument('--email', type=str, help='测试邮箱')
|
||||
parser.add_argument('--password', type=str, help='测试密码', default='testpassword123')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write(self.style.SUCCESS('=== 测试邮箱和用户名登录功能 ===\n'))
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
# 如果没有指定用户,使用第一个用户
|
||||
if not options['username'] and not options['email']:
|
||||
if not User.objects.exists():
|
||||
self.stdout.write(self.style.ERROR('❌ 数据库中没有用户,请先创建用户进行测试'))
|
||||
return
|
||||
|
||||
user = User.objects.first()
|
||||
test_username = user.username
|
||||
test_email = user.email
|
||||
else:
|
||||
test_username = options['username']
|
||||
test_email = options['email']
|
||||
|
||||
password = options['password']
|
||||
|
||||
# 测试用户名登录
|
||||
if test_username:
|
||||
self.stdout.write('1. 测试用户名登录:')
|
||||
test_user = authenticate(username=test_username, password=password)
|
||||
if test_user:
|
||||
self.stdout.write(self.style.SUCCESS(f'✅ 用户名登录成功: {test_user.username}'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f'❌ 用户名登录失败: {test_username}'))
|
||||
|
||||
# 测试邮箱登录
|
||||
if test_email:
|
||||
self.stdout.write('\n2. 测试邮箱登录:')
|
||||
test_user = authenticate(username=test_email, password=password)
|
||||
if test_user:
|
||||
self.stdout.write(self.style.SUCCESS(f'✅ 邮箱登录成功: {test_user.email}'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f'❌ 邮箱登录失败: {test_email}'))
|
||||
|
||||
# 测试大小写不敏感的邮箱登录
|
||||
self.stdout.write('\n3. 测试大小写不敏感的邮箱登录:')
|
||||
upper_email = test_email.upper()
|
||||
test_user = authenticate(username=upper_email, password=password)
|
||||
if test_user:
|
||||
self.stdout.write(self.style.SUCCESS(f'✅ 大写邮箱登录成功: {upper_email}'))
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR(f'❌ 大写邮箱登录失败: {upper_email}'))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('\n=== 测试完成 ==='))
|
||||
self.stdout.write('使用方法:')
|
||||
self.stdout.write('python manage.py test_login --username=your_username --password=your_password')
|
||||
self.stdout.write('python manage.py test_login --email=your_email@example.com --password=your_password')
|
||||
@ -1,49 +0,0 @@
|
||||
# Generated by Django 4.1.7 on 2023-03-02 07:14
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BlogUser',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
|
||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户',
|
||||
'verbose_name_plural': '用户',
|
||||
'ordering': ['-id'],
|
||||
'get_latest_by': 'id',
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -1,46 +0,0 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-06 13:13
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='bloguser',
|
||||
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='bloguser',
|
||||
name='created_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='bloguser',
|
||||
name='last_mod_time',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='bloguser',
|
||||
name='creation_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='bloguser',
|
||||
name='last_modify_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='bloguser',
|
||||
name='nickname',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='bloguser',
|
||||
name='source',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue