From 7a23a1891defd5e0ced892d26b50a76f366e326d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=94=B0=E6=B5=A9=E6=B1=A0?= <2314016651@qq.com>
Date: Sun, 12 Oct 2025 00:15:02 +0800
Subject: [PATCH] update files
---
src/.coveragerc | 10 +
src/.dockerignore | 3 +-
src/.gitignore | 1 +
src/accounts/__init__.py | 0
src/accounts/admin.py | 59 +++
src/accounts/apps.py | 5 +
src/accounts/forms.py | 117 ++++++
src/accounts/migrations/0001_initial.py | 49 +++
...s_remove_bloguser_created_time_and_more.py | 46 +++
src/accounts/migrations/__init__.py | 0
src/accounts/models.py | 35 ++
src/accounts/templatetags/__init__.py | 0
src/accounts/tests.py | 207 ++++++++++
src/accounts/urls.py | 28 ++
src/accounts/user_login_backend.py | 26 ++
src/accounts/utils.py | 49 +++
src/accounts/views.py | 204 ++++++++++
src/blog/admin.py | 4 +-
src/blog/management/__init__.py | 0
src/blog/management/commands/__init__.py | 0
src/blog/management/commands/build_index.py | 18 +
.../management/commands/build_search_words.py | 13 +
src/blog/management/commands/clear_cache.py | 11 +
.../management/commands/create_testdata.py | 40 ++
src/blog/management/commands/ping_baidu.py | 50 +++
.../management/commands/sync_user_avatar.py | 47 +++
src/blog/static/blog/css/style.css | 370 ++----------------
src/blog/templatetags/blog_tags.py | 123 ++----
src/blog/views.py | 4 +
src/comments/admin.py | 2 -
src/deploy/k8s/deployment.yaml | 4 +-
src/djangoblog/settings.py | 37 +-
src/djangoblog/urls.py | 14 -
src/djangoblog/utils.py | 48 +--
src/plugins/article_copyright/plugin.py | 5 -
src/plugins/reading_time/plugin.py | 8 -
src/plugins/seo_optimizer/plugin.py | 7 +-
src/requirements.txt | Bin 1690 -> 2554 bytes
src/templates/blog/tags/article_info.html | 4 +-
src/templates/blog/tags/sidebar.html | 2 +-
src/templates/comments/tags/comment_item.html | 7 +-
.../comments/tags/comment_item_tree.html | 7 +-
src/templates/share_layout/base.html | 68 ++--
43 files changed, 1132 insertions(+), 600 deletions(-)
create mode 100644 src/.coveragerc
create mode 100644 src/accounts/__init__.py
create mode 100644 src/accounts/admin.py
create mode 100644 src/accounts/apps.py
create mode 100644 src/accounts/forms.py
create mode 100644 src/accounts/migrations/0001_initial.py
create mode 100644 src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
create mode 100644 src/accounts/migrations/__init__.py
create mode 100644 src/accounts/models.py
create mode 100644 src/accounts/templatetags/__init__.py
create mode 100644 src/accounts/tests.py
create mode 100644 src/accounts/urls.py
create mode 100644 src/accounts/user_login_backend.py
create mode 100644 src/accounts/utils.py
create mode 100644 src/accounts/views.py
create mode 100644 src/blog/management/__init__.py
create mode 100644 src/blog/management/commands/__init__.py
create mode 100644 src/blog/management/commands/build_index.py
create mode 100644 src/blog/management/commands/build_search_words.py
create mode 100644 src/blog/management/commands/clear_cache.py
create mode 100644 src/blog/management/commands/create_testdata.py
create mode 100644 src/blog/management/commands/ping_baidu.py
create mode 100644 src/blog/management/commands/sync_user_avatar.py
diff --git a/src/.coveragerc b/src/.coveragerc
new file mode 100644
index 0000000..9757484
--- /dev/null
+++ b/src/.coveragerc
@@ -0,0 +1,10 @@
+[run]
+source = .
+include = *.py
+omit =
+ *migrations*
+ *tests*
+ *.html
+ *whoosh_cn_backend*
+ *settings.py*
+ *venv*
diff --git a/src/.dockerignore b/src/.dockerignore
index bd68a58..2818c38 100644
--- a/src/.dockerignore
+++ b/src/.dockerignore
@@ -8,5 +8,4 @@ settings_production.py
*.md
docs/
logs/
-static/
-.github/
+static/
\ No newline at end of file
diff --git a/src/.gitignore b/src/.gitignore
index 76302b1..3015816 100644
--- a/src/.gitignore
+++ b/src/.gitignore
@@ -62,6 +62,7 @@ target/
# http://www.jetbrains.com/pycharm/webhelp/project.html
.idea
.iml
+static/
# virtualenv
venv/
diff --git a/src/accounts/__init__.py b/src/accounts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/accounts/admin.py b/src/accounts/admin.py
new file mode 100644
index 0000000..32e483c
--- /dev/null
+++ b/src/accounts/admin.py
@@ -0,0 +1,59 @@
+from django import forms
+from django.contrib.auth.admin import UserAdmin
+from django.contrib.auth.forms import UserChangeForm
+from django.contrib.auth.forms import UsernameField
+from django.utils.translation import gettext_lazy as _
+
+# Register your models here.
+from .models import BlogUser
+
+
+class BlogUserCreationForm(forms.ModelForm):
+ password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
+ password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
+
+ class Meta:
+ model = BlogUser
+ fields = ('email',)
+
+ def clean_password2(self):
+ # Check that the two password entries match
+ password1 = self.cleaned_data.get("password1")
+ password2 = self.cleaned_data.get("password2")
+ if password1 and password2 and password1 != password2:
+ raise forms.ValidationError(_("passwords do not match"))
+ return password2
+
+ def save(self, commit=True):
+ # Save the provided password in hashed format
+ user = super().save(commit=False)
+ user.set_password(self.cleaned_data["password1"])
+ if commit:
+ user.source = 'adminsite'
+ user.save()
+ return user
+
+
+class BlogUserChangeForm(UserChangeForm):
+ class Meta:
+ model = BlogUser
+ fields = '__all__'
+ field_classes = {'username': UsernameField}
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+
+class BlogUserAdmin(UserAdmin):
+ form = BlogUserChangeForm
+ add_form = BlogUserCreationForm
+ list_display = (
+ 'id',
+ 'nickname',
+ 'username',
+ 'email',
+ 'last_login',
+ 'date_joined',
+ 'source')
+ list_display_links = ('id', 'username')
+ ordering = ('-id',)
diff --git a/src/accounts/apps.py b/src/accounts/apps.py
new file mode 100644
index 0000000..9b3fc5a
--- /dev/null
+++ b/src/accounts/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class AccountsConfig(AppConfig):
+ name = 'accounts'
diff --git a/src/accounts/forms.py b/src/accounts/forms.py
new file mode 100644
index 0000000..fce4137
--- /dev/null
+++ b/src/accounts/forms.py
@@ -0,0 +1,117 @@
+from django import forms
+from django.contrib.auth import get_user_model, password_validation
+from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
+from django.core.exceptions import ValidationError
+from django.forms import widgets
+from django.utils.translation import gettext_lazy as _
+from . import utils
+from .models import BlogUser
+
+
+class LoginForm(AuthenticationForm):
+ def __init__(self, *args, **kwargs):
+ super(LoginForm, self).__init__(*args, **kwargs)
+ self.fields['username'].widget = widgets.TextInput(
+ attrs={'placeholder': "username", "class": "form-control"})
+ self.fields['password'].widget = widgets.PasswordInput(
+ attrs={'placeholder': "password", "class": "form-control"})
+
+
+class RegisterForm(UserCreationForm):
+ def __init__(self, *args, **kwargs):
+ super(RegisterForm, self).__init__(*args, **kwargs)
+
+ self.fields['username'].widget = widgets.TextInput(
+ attrs={'placeholder': "username", "class": "form-control"})
+ self.fields['email'].widget = widgets.EmailInput(
+ attrs={'placeholder': "email", "class": "form-control"})
+ self.fields['password1'].widget = widgets.PasswordInput(
+ attrs={'placeholder': "password", "class": "form-control"})
+ self.fields['password2'].widget = widgets.PasswordInput(
+ attrs={'placeholder': "repeat password", "class": "form-control"})
+
+ def clean_email(self):
+ email = self.cleaned_data['email']
+ if get_user_model().objects.filter(email=email).exists():
+ raise ValidationError(_("email already exists"))
+ return email
+
+ class Meta:
+ model = get_user_model()
+ fields = ("username", "email")
+
+
+class ForgetPasswordForm(forms.Form):
+ new_password1 = forms.CharField(
+ label=_("New password"),
+ widget=forms.PasswordInput(
+ attrs={
+ "class": "form-control",
+ 'placeholder': _("New password")
+ }
+ ),
+ )
+
+ new_password2 = forms.CharField(
+ label="确认密码",
+ widget=forms.PasswordInput(
+ attrs={
+ "class": "form-control",
+ 'placeholder': _("Confirm password")
+ }
+ ),
+ )
+
+ email = forms.EmailField(
+ label='邮箱',
+ widget=forms.TextInput(
+ attrs={
+ 'class': 'form-control',
+ 'placeholder': _("Email")
+ }
+ ),
+ )
+
+ code = forms.CharField(
+ label=_('Code'),
+ widget=forms.TextInput(
+ attrs={
+ 'class': 'form-control',
+ 'placeholder': _("Code")
+ }
+ ),
+ )
+
+ def clean_new_password2(self):
+ password1 = self.data.get("new_password1")
+ password2 = self.data.get("new_password2")
+ if password1 and password2 and password1 != password2:
+ raise ValidationError(_("passwords do not match"))
+ password_validation.validate_password(password2)
+
+ return password2
+
+ def clean_email(self):
+ user_email = self.cleaned_data.get("email")
+ if not BlogUser.objects.filter(
+ email=user_email
+ ).exists():
+ # todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
+ raise ValidationError(_("email does not exist"))
+ return user_email
+
+ def clean_code(self):
+ code = self.cleaned_data.get("code")
+ error = utils.verify(
+ email=self.cleaned_data.get("email"),
+ code=code,
+ )
+ if error:
+ raise ValidationError(error)
+ return code
+
+
+class ForgetPasswordCodeForm(forms.Form):
+ email = forms.EmailField(
+ label=_('Email'),
+ )
diff --git a/src/accounts/migrations/0001_initial.py b/src/accounts/migrations/0001_initial.py
new file mode 100644
index 0000000..d2fbcab
--- /dev/null
+++ b/src/accounts/migrations/0001_initial.py
@@ -0,0 +1,49 @@
+# 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()),
+ ],
+ ),
+ ]
diff --git a/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
new file mode 100644
index 0000000..1a9f509
--- /dev/null
+++ b/src/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py
@@ -0,0 +1,46 @@
+# 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'),
+ ),
+ ]
diff --git a/src/accounts/migrations/__init__.py b/src/accounts/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/accounts/models.py b/src/accounts/models.py
new file mode 100644
index 0000000..3baddbb
--- /dev/null
+++ b/src/accounts/models.py
@@ -0,0 +1,35 @@
+from django.contrib.auth.models import AbstractUser
+from django.db import models
+from django.urls import reverse
+from django.utils.timezone import now
+from django.utils.translation import gettext_lazy as _
+from djangoblog.utils import get_current_site
+
+
+# Create your models here.
+
+class BlogUser(AbstractUser):
+ nickname = models.CharField(_('nick name'), max_length=100, blank=True)
+ creation_time = models.DateTimeField(_('creation time'), default=now)
+ last_modify_time = models.DateTimeField(_('last modify time'), default=now)
+ source = models.CharField(_('create source'), max_length=100, blank=True)
+
+ def get_absolute_url(self):
+ return reverse(
+ 'blog:author_detail', kwargs={
+ 'author_name': self.username})
+
+ def __str__(self):
+ return self.email
+
+ def get_full_url(self):
+ site = get_current_site().domain
+ url = "https://{site}{path}".format(site=site,
+ path=self.get_absolute_url())
+ return url
+
+ class Meta:
+ ordering = ['-id']
+ verbose_name = _('user')
+ verbose_name_plural = verbose_name
+ get_latest_by = 'id'
diff --git a/src/accounts/templatetags/__init__.py b/src/accounts/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/accounts/tests.py b/src/accounts/tests.py
new file mode 100644
index 0000000..6893411
--- /dev/null
+++ b/src/accounts/tests.py
@@ -0,0 +1,207 @@
+from django.test import Client, RequestFactory, TestCase
+from django.urls import reverse
+from django.utils import timezone
+from django.utils.translation import gettext_lazy as _
+
+from accounts.models import BlogUser
+from blog.models import Article, Category
+from djangoblog.utils import *
+from . import utils
+
+
+# Create your tests here.
+
+class AccountTest(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.factory = RequestFactory()
+ self.blog_user = BlogUser.objects.create_user(
+ username="test",
+ email="admin@admin.com",
+ password="12345678"
+ )
+ self.new_test = "xxx123--="
+
+ def test_validate_account(self):
+ site = get_current_site().domain
+ user = BlogUser.objects.create_superuser(
+ email="liangliangyy1@gmail.com",
+ username="liangliangyy1",
+ password="qwer!@#$ggg")
+ testuser = BlogUser.objects.get(username='liangliangyy1')
+
+ loginresult = self.client.login(
+ username='liangliangyy1',
+ password='qwer!@#$ggg')
+ self.assertEqual(loginresult, True)
+ response = self.client.get('/admin/')
+ self.assertEqual(response.status_code, 200)
+
+ category = Category()
+ category.name = "categoryaaa"
+ category.creation_time = timezone.now()
+ category.last_modify_time = timezone.now()
+ category.save()
+
+ article = Article()
+ article.title = "nicetitleaaa"
+ article.body = "nicecontentaaa"
+ article.author = user
+ article.category = category
+ article.type = 'a'
+ article.status = 'p'
+ article.save()
+
+ response = self.client.get(article.get_admin_url())
+ self.assertEqual(response.status_code, 200)
+
+ def test_validate_register(self):
+ self.assertEquals(
+ 0, len(
+ BlogUser.objects.filter(
+ email='user123@user.com')))
+ response = self.client.post(reverse('account:register'), {
+ 'username': 'user1233',
+ 'email': 'user123@user.com',
+ 'password1': 'password123!q@wE#R$T',
+ 'password2': 'password123!q@wE#R$T',
+ })
+ self.assertEquals(
+ 1, len(
+ BlogUser.objects.filter(
+ email='user123@user.com')))
+ user = BlogUser.objects.filter(email='user123@user.com')[0]
+ sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
+ path = reverse('accounts:result')
+ url = '{path}?type=validation&id={id}&sign={sign}'.format(
+ path=path, id=user.id, sign=sign)
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+
+ self.client.login(username='user1233', password='password123!q@wE#R$T')
+ user = BlogUser.objects.filter(email='user123@user.com')[0]
+ user.is_superuser = True
+ user.is_staff = True
+ user.save()
+ delete_sidebar_cache()
+ category = Category()
+ category.name = "categoryaaa"
+ category.creation_time = timezone.now()
+ category.last_modify_time = timezone.now()
+ category.save()
+
+ article = Article()
+ article.category = category
+ article.title = "nicetitle333"
+ article.body = "nicecontentttt"
+ article.author = user
+
+ article.type = 'a'
+ article.status = 'p'
+ article.save()
+
+ response = self.client.get(article.get_admin_url())
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.get(reverse('account:logout'))
+ self.assertIn(response.status_code, [301, 302, 200])
+
+ response = self.client.get(article.get_admin_url())
+ self.assertIn(response.status_code, [301, 302, 200])
+
+ response = self.client.post(reverse('account:login'), {
+ 'username': 'user1233',
+ 'password': 'password123'
+ })
+ self.assertIn(response.status_code, [301, 302, 200])
+
+ response = self.client.get(article.get_admin_url())
+ self.assertIn(response.status_code, [301, 302, 200])
+
+ def test_verify_email_code(self):
+ to_email = "admin@admin.com"
+ code = generate_code()
+ utils.set_code(to_email, code)
+ utils.send_verify_email(to_email, code)
+
+ err = utils.verify("admin@admin.com", code)
+ self.assertEqual(err, None)
+
+ err = utils.verify("admin@123.com", code)
+ self.assertEqual(type(err), str)
+
+ def test_forget_password_email_code_success(self):
+ resp = self.client.post(
+ path=reverse("account:forget_password_code"),
+ data=dict(email="admin@admin.com")
+ )
+
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(resp.content.decode("utf-8"), "ok")
+
+ def test_forget_password_email_code_fail(self):
+ resp = self.client.post(
+ path=reverse("account:forget_password_code"),
+ data=dict()
+ )
+ self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
+
+ resp = self.client.post(
+ path=reverse("account:forget_password_code"),
+ data=dict(email="admin@com")
+ )
+ self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
+
+ def test_forget_password_email_success(self):
+ code = generate_code()
+ utils.set_code(self.blog_user.email, code)
+ data = dict(
+ new_password1=self.new_test,
+ new_password2=self.new_test,
+ email=self.blog_user.email,
+ code=code,
+ )
+ resp = self.client.post(
+ path=reverse("account:forget_password"),
+ data=data
+ )
+ self.assertEqual(resp.status_code, 302)
+
+ # 验证用户密码是否修改成功
+ blog_user = BlogUser.objects.filter(
+ email=self.blog_user.email,
+ ).first() # type: BlogUser
+ self.assertNotEqual(blog_user, None)
+ self.assertEqual(blog_user.check_password(data["new_password1"]), True)
+
+ def test_forget_password_email_not_user(self):
+ data = dict(
+ new_password1=self.new_test,
+ new_password2=self.new_test,
+ email="123@123.com",
+ code="123456",
+ )
+ resp = self.client.post(
+ path=reverse("account:forget_password"),
+ data=data
+ )
+
+ self.assertEqual(resp.status_code, 200)
+
+
+ def test_forget_password_email_code_error(self):
+ code = generate_code()
+ utils.set_code(self.blog_user.email, code)
+ data = dict(
+ new_password1=self.new_test,
+ new_password2=self.new_test,
+ email=self.blog_user.email,
+ code="111111",
+ )
+ resp = self.client.post(
+ path=reverse("account:forget_password"),
+ data=data
+ )
+
+ self.assertEqual(resp.status_code, 200)
+
diff --git a/src/accounts/urls.py b/src/accounts/urls.py
new file mode 100644
index 0000000..107a801
--- /dev/null
+++ b/src/accounts/urls.py
@@ -0,0 +1,28 @@
+from django.urls import path
+from django.urls import re_path
+
+from . import views
+from .forms import LoginForm
+
+app_name = "accounts"
+
+urlpatterns = [re_path(r'^login/$',
+ views.LoginView.as_view(success_url='/'),
+ name='login',
+ kwargs={'authentication_form': LoginForm}),
+ re_path(r'^register/$',
+ views.RegisterView.as_view(success_url="/"),
+ name='register'),
+ re_path(r'^logout/$',
+ views.LogoutView.as_view(),
+ name='logout'),
+ path(r'account/result.html',
+ views.account_result,
+ name='result'),
+ re_path(r'^forget_password/$',
+ views.ForgetPasswordView.as_view(),
+ name='forget_password'),
+ re_path(r'^forget_password_code/$',
+ views.ForgetPasswordEmailCode.as_view(),
+ name='forget_password_code'),
+ ]
diff --git a/src/accounts/user_login_backend.py b/src/accounts/user_login_backend.py
new file mode 100644
index 0000000..73cdca1
--- /dev/null
+++ b/src/accounts/user_login_backend.py
@@ -0,0 +1,26 @@
+from django.contrib.auth import get_user_model
+from django.contrib.auth.backends import ModelBackend
+
+
+class EmailOrUsernameModelBackend(ModelBackend):
+ """
+ 允许使用用户名或邮箱登录
+ """
+
+ def authenticate(self, request, username=None, password=None, **kwargs):
+ if '@' in username:
+ kwargs = {'email': username}
+ else:
+ kwargs = {'username': username}
+ try:
+ user = get_user_model().objects.get(**kwargs)
+ if user.check_password(password):
+ return user
+ except get_user_model().DoesNotExist:
+ return None
+
+ def get_user(self, username):
+ try:
+ return get_user_model().objects.get(pk=username)
+ except get_user_model().DoesNotExist:
+ return None
diff --git a/src/accounts/utils.py b/src/accounts/utils.py
new file mode 100644
index 0000000..4b94bdf
--- /dev/null
+++ b/src/accounts/utils.py
@@ -0,0 +1,49 @@
+import typing
+from datetime import timedelta
+
+from django.core.cache import cache
+from django.utils.translation import gettext
+from django.utils.translation import gettext_lazy as _
+
+from djangoblog.utils import send_email
+
+_code_ttl = timedelta(minutes=5)
+
+
+def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
+ """发送重设密码验证码
+ Args:
+ to_mail: 接受邮箱
+ subject: 邮件主题
+ code: 验证码
+ """
+ html_content = _(
+ "You are resetting the password, the verification code is:%(code)s, valid within 5 minutes, please keep it "
+ "properly") % {'code': code}
+ send_email([to_mail], subject, html_content)
+
+
+def verify(email: str, code: str) -> typing.Optional[str]:
+ """验证code是否有效
+ Args:
+ email: 请求邮箱
+ code: 验证码
+ Return:
+ 如果有错误就返回错误str
+ Node:
+ 这里的错误处理不太合理,应该采用raise抛出
+ 否测调用方也需要对error进行处理
+ """
+ cache_code = get_code(email)
+ if cache_code != code:
+ return gettext("Verification code error")
+
+
+def set_code(email: str, code: str):
+ """设置code"""
+ cache.set(email, code, _code_ttl.seconds)
+
+
+def get_code(email: str) -> typing.Optional[str]:
+ """获取code"""
+ return cache.get(email)
diff --git a/src/accounts/views.py b/src/accounts/views.py
new file mode 100644
index 0000000..ae67aec
--- /dev/null
+++ b/src/accounts/views.py
@@ -0,0 +1,204 @@
+import logging
+from django.utils.translation import gettext_lazy as _
+from django.conf import settings
+from django.contrib import auth
+from django.contrib.auth import REDIRECT_FIELD_NAME
+from django.contrib.auth import get_user_model
+from django.contrib.auth import logout
+from django.contrib.auth.forms import AuthenticationForm
+from django.contrib.auth.hashers import make_password
+from django.http import HttpResponseRedirect, HttpResponseForbidden
+from django.http.request import HttpRequest
+from django.http.response import HttpResponse
+from django.shortcuts import get_object_or_404
+from django.shortcuts import render
+from django.urls import reverse
+from django.utils.decorators import method_decorator
+from django.utils.http import url_has_allowed_host_and_scheme
+from django.views import View
+from django.views.decorators.cache import never_cache
+from django.views.decorators.csrf import csrf_protect
+from django.views.decorators.debug import sensitive_post_parameters
+from django.views.generic import FormView, RedirectView
+
+from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
+from . import utils
+from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
+from .models import BlogUser
+
+logger = logging.getLogger(__name__)
+
+
+# Create your views here.
+
+class RegisterView(FormView):
+ form_class = RegisterForm
+ template_name = 'account/registration_form.html'
+
+ @method_decorator(csrf_protect)
+ def dispatch(self, *args, **kwargs):
+ return super(RegisterView, self).dispatch(*args, **kwargs)
+
+ def form_valid(self, form):
+ if form.is_valid():
+ user = form.save(False)
+ user.is_active = False
+ user.source = 'Register'
+ user.save(True)
+ site = get_current_site().domain
+ sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
+
+ if settings.DEBUG:
+ site = '127.0.0.1:8000'
+ path = reverse('account:result')
+ url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
+ site=site, path=path, id=user.id, sign=sign)
+
+ content = """
+
请点击下面链接验证您的邮箱
+
+ {url}
+
+ 再次感谢您!
+
+ 如果上面链接无法打开,请将此链接复制至浏览器。
+ {url}
+ """.format(url=url)
+ send_email(
+ emailto=[
+ user.email,
+ ],
+ title='验证您的电子邮箱',
+ content=content)
+
+ url = reverse('accounts:result') + \
+ '?type=register&id=' + str(user.id)
+ return HttpResponseRedirect(url)
+ else:
+ return self.render_to_response({
+ 'form': form
+ })
+
+
+class LogoutView(RedirectView):
+ url = '/login/'
+
+ @method_decorator(never_cache)
+ def dispatch(self, request, *args, **kwargs):
+ return super(LogoutView, self).dispatch(request, *args, **kwargs)
+
+ def get(self, request, *args, **kwargs):
+ logout(request)
+ delete_sidebar_cache()
+ return super(LogoutView, self).get(request, *args, **kwargs)
+
+
+class LoginView(FormView):
+ form_class = LoginForm
+ template_name = 'account/login.html'
+ success_url = '/'
+ redirect_field_name = REDIRECT_FIELD_NAME
+ login_ttl = 2626560 # 一个月的时间
+
+ @method_decorator(sensitive_post_parameters('password'))
+ @method_decorator(csrf_protect)
+ @method_decorator(never_cache)
+ def dispatch(self, request, *args, **kwargs):
+
+ return super(LoginView, self).dispatch(request, *args, **kwargs)
+
+ def get_context_data(self, **kwargs):
+ redirect_to = self.request.GET.get(self.redirect_field_name)
+ if redirect_to is None:
+ redirect_to = '/'
+ kwargs['redirect_to'] = redirect_to
+
+ return super(LoginView, self).get_context_data(**kwargs)
+
+ def form_valid(self, form):
+ form = AuthenticationForm(data=self.request.POST, request=self.request)
+
+ if form.is_valid():
+ delete_sidebar_cache()
+ logger.info(self.redirect_field_name)
+
+ auth.login(self.request, form.get_user())
+ if self.request.POST.get("remember"):
+ self.request.session.set_expiry(self.login_ttl)
+ return super(LoginView, self).form_valid(form)
+ # return HttpResponseRedirect('/')
+ else:
+ return self.render_to_response({
+ 'form': form
+ })
+
+ def get_success_url(self):
+
+ redirect_to = self.request.POST.get(self.redirect_field_name)
+ if not url_has_allowed_host_and_scheme(
+ url=redirect_to, allowed_hosts=[
+ self.request.get_host()]):
+ redirect_to = self.success_url
+ return redirect_to
+
+
+def account_result(request):
+ type = request.GET.get('type')
+ id = request.GET.get('id')
+
+ user = get_object_or_404(get_user_model(), id=id)
+ logger.info(type)
+ if user.is_active:
+ return HttpResponseRedirect('/')
+ if type and type in ['register', 'validation']:
+ if type == 'register':
+ content = '''
+ 恭喜您注册成功,一封验证邮件已经发送到您的邮箱,请验证您的邮箱后登录本站。
+ '''
+ title = '注册成功'
+ else:
+ c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
+ sign = request.GET.get('sign')
+ if sign != c_sign:
+ return HttpResponseForbidden()
+ user.is_active = True
+ user.save()
+ content = '''
+ 恭喜您已经成功的完成邮箱验证,您现在可以使用您的账号来登录本站。
+ '''
+ title = '验证成功'
+ return render(request, 'account/result.html', {
+ 'title': title,
+ 'content': content
+ })
+ else:
+ return HttpResponseRedirect('/')
+
+
+class ForgetPasswordView(FormView):
+ form_class = ForgetPasswordForm
+ template_name = 'account/forget_password.html'
+
+ def form_valid(self, form):
+ if form.is_valid():
+ blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
+ blog_user.password = make_password(form.cleaned_data["new_password2"])
+ blog_user.save()
+ return HttpResponseRedirect('/login/')
+ else:
+ return self.render_to_response({'form': form})
+
+
+class ForgetPasswordEmailCode(View):
+
+ def post(self, request: HttpRequest):
+ form = ForgetPasswordCodeForm(request.POST)
+ if not form.is_valid():
+ return HttpResponse("错误的邮箱")
+ to_email = form.cleaned_data["email"]
+
+ code = generate_code()
+ utils.send_verify_email(to_email, code)
+ utils.set_code(to_email, code)
+
+ return HttpResponse("ok")
diff --git a/src/blog/admin.py b/src/blog/admin.py
index 69d7f8e..46c3420 100644
--- a/src/blog/admin.py
+++ b/src/blog/admin.py
@@ -6,7 +6,7 @@ from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# Register your models here.
-from .models import Article, Category, Tag, Links, SideBar, BlogSettings
+from .models import Article
class ArticleForm(forms.ModelForm):
@@ -55,7 +55,6 @@ class ArticlelAdmin(admin.ModelAdmin):
'article_order')
list_display_links = ('id', 'title')
list_filter = ('status', 'type', 'category')
- date_hierarchy = 'creation_time'
filter_horizontal = ('tags',)
exclude = ('creation_time', 'last_modify_time')
view_on_site = True
@@ -64,7 +63,6 @@ class ArticlelAdmin(admin.ModelAdmin):
draft_article,
close_article_commentstatus,
open_article_commentstatus]
- raw_id_fields = ('author', 'category',)
def link_to_category(self, obj):
info = (obj.category._meta.app_label, obj.category._meta.model_name)
diff --git a/src/blog/management/__init__.py b/src/blog/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/blog/management/commands/__init__.py b/src/blog/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/blog/management/commands/build_index.py b/src/blog/management/commands/build_index.py
new file mode 100644
index 0000000..3c4acd7
--- /dev/null
+++ b/src/blog/management/commands/build_index.py
@@ -0,0 +1,18 @@
+from django.core.management.base import BaseCommand
+
+from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
+ ELASTICSEARCH_ENABLED
+
+
+# TODO 参数化
+class Command(BaseCommand):
+ help = 'build search index'
+
+ def handle(self, *args, **options):
+ if ELASTICSEARCH_ENABLED:
+ ElaspedTimeDocumentManager.build_index()
+ manager = ElapsedTimeDocument()
+ manager.init()
+ manager = ArticleDocumentManager()
+ manager.delete_index()
+ manager.rebuild()
diff --git a/src/blog/management/commands/build_search_words.py b/src/blog/management/commands/build_search_words.py
new file mode 100644
index 0000000..cfe7e0d
--- /dev/null
+++ b/src/blog/management/commands/build_search_words.py
@@ -0,0 +1,13 @@
+from django.core.management.base import BaseCommand
+
+from blog.models import Tag, Category
+
+
+# TODO 参数化
+class Command(BaseCommand):
+ help = 'build search words'
+
+ def handle(self, *args, **options):
+ datas = set([t.name for t in Tag.objects.all()] +
+ [t.name for t in Category.objects.all()])
+ print('\n'.join(datas))
diff --git a/src/blog/management/commands/clear_cache.py b/src/blog/management/commands/clear_cache.py
new file mode 100644
index 0000000..0d66172
--- /dev/null
+++ b/src/blog/management/commands/clear_cache.py
@@ -0,0 +1,11 @@
+from django.core.management.base import BaseCommand
+
+from djangoblog.utils import cache
+
+
+class Command(BaseCommand):
+ help = 'clear the whole cache'
+
+ def handle(self, *args, **options):
+ cache.clear()
+ self.stdout.write(self.style.SUCCESS('Cleared cache\n'))
diff --git a/src/blog/management/commands/create_testdata.py b/src/blog/management/commands/create_testdata.py
new file mode 100644
index 0000000..675d2ba
--- /dev/null
+++ b/src/blog/management/commands/create_testdata.py
@@ -0,0 +1,40 @@
+from django.contrib.auth import get_user_model
+from django.contrib.auth.hashers import make_password
+from django.core.management.base import BaseCommand
+
+from blog.models import Article, Tag, Category
+
+
+class Command(BaseCommand):
+ help = 'create test datas'
+
+ def handle(self, *args, **options):
+ user = get_user_model().objects.get_or_create(
+ email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
+
+ pcategory = Category.objects.get_or_create(
+ name='我是父类目', parent_category=None)[0]
+
+ category = Category.objects.get_or_create(
+ name='子类目', parent_category=pcategory)[0]
+
+ category.save()
+ basetag = Tag()
+ basetag.name = "标签"
+ basetag.save()
+ for i in range(1, 20):
+ article = Article.objects.get_or_create(
+ category=category,
+ title='nice title ' + str(i),
+ body='nice content ' + str(i),
+ author=user)[0]
+ tag = Tag()
+ tag.name = "标签" + str(i)
+ tag.save()
+ article.tags.add(tag)
+ article.tags.add(basetag)
+ article.save()
+
+ from djangoblog.utils import cache
+ cache.clear()
+ self.stdout.write(self.style.SUCCESS('created test datas \n'))
diff --git a/src/blog/management/commands/ping_baidu.py b/src/blog/management/commands/ping_baidu.py
new file mode 100644
index 0000000..2c7fbdd
--- /dev/null
+++ b/src/blog/management/commands/ping_baidu.py
@@ -0,0 +1,50 @@
+from django.core.management.base import BaseCommand
+
+from djangoblog.spider_notify import SpiderNotify
+from djangoblog.utils import get_current_site
+from blog.models import Article, Tag, Category
+
+site = get_current_site().domain
+
+
+class Command(BaseCommand):
+ help = 'notify baidu url'
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ 'data_type',
+ type=str,
+ choices=[
+ 'all',
+ 'article',
+ 'tag',
+ 'category'],
+ help='article : all article,tag : all tag,category: all category,all: All of these')
+
+ def get_full_url(self, path):
+ url = "https://{site}{path}".format(site=site, path=path)
+ return url
+
+ def handle(self, *args, **options):
+ type = options['data_type']
+ self.stdout.write('start get %s' % type)
+
+ urls = []
+ if type == 'article' or type == 'all':
+ for article in Article.objects.filter(status='p'):
+ urls.append(article.get_full_url())
+ if type == 'tag' or type == 'all':
+ for tag in Tag.objects.all():
+ url = tag.get_absolute_url()
+ urls.append(self.get_full_url(url))
+ if type == 'category' or type == 'all':
+ for category in Category.objects.all():
+ url = category.get_absolute_url()
+ urls.append(self.get_full_url(url))
+
+ self.stdout.write(
+ self.style.SUCCESS(
+ 'start notify %d urls' %
+ len(urls)))
+ SpiderNotify.baidu_notify(urls)
+ self.stdout.write(self.style.SUCCESS('finish notify'))
diff --git a/src/blog/management/commands/sync_user_avatar.py b/src/blog/management/commands/sync_user_avatar.py
new file mode 100644
index 0000000..d0f4612
--- /dev/null
+++ b/src/blog/management/commands/sync_user_avatar.py
@@ -0,0 +1,47 @@
+import requests
+from django.core.management.base import BaseCommand
+from django.templatetags.static import static
+
+from djangoblog.utils import save_user_avatar
+from oauth.models import OAuthUser
+from oauth.oauthmanager import get_manager_by_type
+
+
+class Command(BaseCommand):
+ help = 'sync user avatar'
+
+ def test_picture(self, url):
+ try:
+ if requests.get(url, timeout=2).status_code == 200:
+ return True
+ except:
+ pass
+
+ def handle(self, *args, **options):
+ static_url = static("../")
+ users = OAuthUser.objects.all()
+ self.stdout.write(f'开始同步{len(users)}个用户头像')
+ for u in users:
+ self.stdout.write(f'开始同步:{u.nickname}')
+ url = u.picture
+ if url:
+ if url.startswith(static_url):
+ if self.test_picture(url):
+ continue
+ else:
+ if u.metadata:
+ manage = get_manager_by_type(u.type)
+ url = manage.get_picture(u.metadata)
+ url = save_user_avatar(url)
+ else:
+ url = static('blog/img/avatar.png')
+ else:
+ url = save_user_avatar(url)
+ else:
+ url = static('blog/img/avatar.png')
+ if url:
+ self.stdout.write(
+ f'结束同步:{u.nickname}.url:{url}')
+ u.picture = url
+ u.save()
+ self.stdout.write('结束同步')
diff --git a/src/blog/static/blog/css/style.css b/src/blog/static/blog/css/style.css
index 390bba3..d43f7f3 100644
--- a/src/blog/static/blog/css/style.css
+++ b/src/blog/static/blog/css/style.css
@@ -2017,7 +2017,12 @@ img#wpstats {
width: auto;
}
-
+ .commentlist .avatar {
+ height: 39px;
+ left: 2.2em;
+ top: 2.2em;
+ width: 39px;
+ }
.comments-area article header cite,
.comments-area article header time {
@@ -2145,70 +2150,17 @@ div {
word-break: break-all;
}
-/* 评论整体布局 - 使用相对定位实现头像左侧布局 */
-.commentlist .comment-body {
- position: relative;
- padding-left: 60px; /* 为48px头像 + 12px间距留出空间 */
- min-height: 48px; /* 确保有足够高度容纳头像 */
-}
-
-/* 评论作者信息 - 用户名和时间在同一行 */
-.commentlist .comment-author {
- display: inline-block;
- margin: 0 10px 5px 0;
- font-size: 13px;
- position: relative;
-}
-
-.commentlist .comment-meta {
- display: inline-block;
- margin: 0 0 8px 0;
- font-size: 12px;
- color: #666;
-}
-
+.commentlist .comment-author,
+.commentlist .comment-meta,
.commentlist .comment-awaiting-moderation {
+ float: left;
display: block;
font-size: 13px;
line-height: 22px;
}
-/* 头像样式 - 绝对定位到左侧 */
-.commentlist .comment-author .avatar {
- position: absolute !important;
- left: -60px; /* 定位到容器左侧 */
- top: 0;
- width: 48px !important;
- height: 48px !important;
- border-radius: 50%;
- display: block;
- object-fit: cover;
- background-color: #f5f5f5;
- border: 1px solid #ddd;
-}
-
-/* 评论作者名称样式 */
-.commentlist .comment-author .fn {
- display: inline;
- margin: 0;
- font-weight: 600;
- color: #2e7bb8;
- font-size: 13px;
-}
-
-.commentlist .comment-author .fn a {
- color: #2e7bb8;
- text-decoration: none;
-}
-
-.commentlist .comment-author .fn a:hover {
- text-decoration: underline;
-}
-
-/* 评论内容样式 */
-.commentlist .comment-body p {
- margin: 5px 0 10px 0;
- line-height: 1.5;
+.commentlist .comment-author {
+ margin-right: 6px;
}
.commentlist .fn, .pinglist .ping-link {
@@ -2222,15 +2174,13 @@ div {
display: none;
}
-/* 通用头像样式 */
.commentlist .avatar {
- width: 48px !important;
- height: 48px !important;
- border-radius: 50%;
- display: block;
- object-fit: cover;
- background-color: #f5f5f5;
- border: 1px solid #ddd;
+ position: absolute;
+ left: -60px;
+ top: 0;
+ width: 48px;
+ height: 48px;
+ border-radius: 100%;
}
.commentlist .comment-meta:before, .pinglist .ping-meta:before {
@@ -2340,87 +2290,15 @@ div {
padding-left: 48px;
}
-/* 嵌套评论整体布局 */
-.commentlist li li .comment-body {
- padding-left: 60px; /* 为48px头像 + 12px间距留出空间 */
- min-height: 48px; /* 确保有足够高度容纳头像 */
-}
-
-/* 嵌套评论作者信息 */
-.commentlist li li .comment-author {
- display: inline-block;
- margin: 0 8px 5px 0;
- font-size: 12px; /* 稍小一点 */
+.commentlist li li .avatar {
+ top: 0;
+ left: -48px;
+ width: 36px;
+ height: 36px;
}
.commentlist li li .comment-meta {
- display: inline-block;
- margin: 0 0 8px 0;
- font-size: 11px; /* 稍小一点 */
- color: #666;
-}
-
-/* 评论容器整体左移 - 使用更高优先级 */
-#comments #commentlist-container.comment-tab {
- margin-left: -15px !important; /* 在小屏幕上向左移动15px */
- padding-left: 0 !important; /* 移除左内边距 */
- position: relative !important; /* 确保定位正确 */
-}
-
-/* 在较大屏幕上进一步左移 */
-@media screen and (min-width: 600px) {
- #comments #commentlist-container.comment-tab {
- margin-left: -30px !important; /* 在大屏幕上向左移动30px */
- }
-
- /* 响应式设计下的评论布局 - 保持48px头像 */
- .commentlist .comment-body {
- padding-left: 60px !important; /* 为48px头像 + 12px间距留出空间 */
- min-height: 48px !important;
- }
-
- .commentlist .comment-author {
- display: inline-block !important;
- margin: 0 8px 5px 0 !important;
- }
-
- .commentlist .comment-meta {
- display: inline-block !important;
- margin: 0 0 8px 0 !important;
- }
-
- /* 响应式设计下头像保持48px */
- .commentlist .comment-author .avatar {
- left: -60px !important;
- width: 48px !important;
- height: 48px !important;
- }
-
- /* 嵌套评论在响应式设计下也保持48px头像 */
- .commentlist li li .comment-body {
- padding-left: 60px !important;
- min-height: 48px !important;
- }
-
- .commentlist li li .comment-author .avatar {
- left: -60px !important;
- width: 48px !important;
- height: 48px !important;
- }
-}
-
-/* 嵌套评论头像 */
-.commentlist li li .comment-author .avatar {
- position: absolute !important;
- left: -60px; /* 定位到容器左侧 */
- top: 0;
- width: 48px !important;
- height: 48px !important;
- border-radius: 50%;
- display: block;
- object-fit: cover;
- background-color: #f5f5f5;
- border: 1px solid #ddd;
+ left: 70px;
}
/* comments : nav
@@ -2623,206 +2501,4 @@ li #reply-title {
height: 1px;
border: none;
/*border-top: 1px dashed #f5d6d6;*/
-}
-
-/* =============================================================================
- 评论内容溢出修复样式
- 解决代码块和长文本撑开页面布局的问题
- ============================================================================= */
-
-/* 评论容器基础样式 */
-.comment-body {
- overflow-wrap: break-word;
- word-wrap: break-word;
- word-break: break-word;
- max-width: 100%;
- box-sizing: border-box;
-}
-
-/* 修复评论中的代码块溢出 */
-.comment-content pre,
-.comment-body pre {
- white-space: pre-wrap !important;
- word-wrap: break-word !important;
- overflow-wrap: break-word !important;
- max-width: 100% !important;
- overflow-x: auto;
- padding: 10px;
- background-color: #f8f8f8;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 12px;
- line-height: 1.4;
- margin: 10px 0;
-}
-
-/* 修复评论中的行内代码 */
-.comment-content code,
-.comment-body code {
- word-wrap: break-word !important;
- overflow-wrap: break-word !important;
- white-space: pre-wrap;
- max-width: 100%;
- display: inline-block;
- vertical-align: top;
-}
-
-/* 修复评论中的长链接 */
-.comment-content a,
-.comment-body a {
- word-wrap: break-word !important;
- overflow-wrap: break-word !important;
- word-break: break-all;
- max-width: 100%;
-}
-
-/* 修复评论段落 */
-.comment-content p,
-.comment-body p {
- word-wrap: break-word !important;
- overflow-wrap: break-word !important;
- max-width: 100%;
- margin: 10px 0;
-}
-
-/* 特殊处理代码高亮块 - 关键修复! */
-.comment-content .codehilite,
-.comment-body .codehilite {
- max-width: 100% !important;
- overflow-x: auto;
- margin: 10px 0;
- background: #f8f8f8 !important;
- border: 1px solid #ddd;
- border-radius: 4px;
- padding: 10px;
- font-size: 12px;
- line-height: 1.4;
- /* 关键:防止内容撑开容器 */
- width: 100%;
- box-sizing: border-box;
- display: block;
-}
-
-.comment-content .codehilite pre,
-.comment-body .codehilite pre {
- white-space: pre-wrap !important;
- word-wrap: break-word !important;
- overflow-wrap: break-word !important;
- margin: 0 !important;
- padding: 0 !important;
- background: transparent !important;
- border: none !important;
- font-size: inherit;
- line-height: inherit;
- /* 确保pre标签不会超出父容器 */
- max-width: 100%;
- width: 100%;
- box-sizing: border-box;
-}
-
-/* 修复代码高亮中的span标签 */
-.comment-content .codehilite span,
-.comment-body .codehilite span {
- word-wrap: break-word !important;
- overflow-wrap: break-word !important;
- /* 防止行内元素导致的溢出 */
- display: inline;
- max-width: 100%;
-}
-
-/* 针对特定的代码高亮类 */
-.comment-content .codehilite .kt,
-.comment-content .codehilite .nf,
-.comment-content .codehilite .n,
-.comment-content .codehilite .p,
-.comment-content .codehilite .w,
-.comment-content .codehilite .o,
-.comment-body .codehilite .kt,
-.comment-body .codehilite .nf,
-.comment-body .codehilite .n,
-.comment-body .codehilite .p,
-.comment-body .codehilite .w,
-.comment-body .codehilite .o {
- word-break: break-all;
- overflow-wrap: break-word;
-}
-
-/* 修复评论列表项 */
-.commentlist li {
- max-width: 100%;
- overflow: hidden;
- box-sizing: border-box;
-}
-
-/* 确保评论内容不超出容器 */
-.commentlist .comment-body {
- max-width: calc(100% - 20px); /* 留出一些边距 */
- margin-left: 10px;
- margin-right: 10px;
- overflow: hidden; /* 防止内容溢出 */
- word-wrap: break-word;
-}
-
-/* 重要:限制评论列表项的最大宽度 */
-.commentlist li[style*="margin-left"] {
- max-width: calc(100% - 2rem) !important;
- overflow: hidden;
- box-sizing: border-box;
-}
-
-/* 特别处理深层嵌套的评论 */
-.commentlist li[style*="margin-left: 3rem"],
-.commentlist li[style*="margin-left: 6rem"],
-.commentlist li[style*="margin-left: 9rem"] {
- max-width: calc(100% - 1rem) !important;
-}
-
-/* 移动端优化 */
-@media (max-width: 768px) {
- .comment-content pre,
- .comment-body pre {
- font-size: 11px;
- padding: 8px;
- margin: 8px 0;
- }
-
- .commentlist .comment-body {
- max-width: calc(100% - 10px);
- margin-left: 5px;
- margin-right: 5px;
- }
-
- /* 移动端评论缩进调整 */
- .commentlist li[style*="margin-left"] {
- margin-left: 1rem !important;
- max-margin-left: 2rem !important;
- }
-}
-
-/* 防止表格溢出 */
-.comment-content table,
-.comment-body table {
- max-width: 100%;
- overflow-x: auto;
- display: block;
- white-space: nowrap;
-}
-
-/* 修复图片溢出 */
-.comment-content img,
-.comment-body img {
- max-width: 100% !important;
- height: auto !important;
-}
-
-/* 修复引用块 */
-.comment-content blockquote,
-.comment-body blockquote {
- max-width: 100%;
- overflow-wrap: break-word;
- word-wrap: break-word;
- padding: 10px 15px;
- margin: 10px 0;
- border-left: 4px solid #ddd;
- background-color: #f9f9f9;
}
\ No newline at end of file
diff --git a/src/blog/templatetags/blog_tags.py b/src/blog/templatetags/blog_tags.py
index 1f994bc..d6cd5d5 100644
--- a/src/blog/templatetags/blog_tags.py
+++ b/src/blog/templatetags/blog_tags.py
@@ -51,75 +51,7 @@ def datetimeformat(data):
@register.filter()
@stringfilter
def custom_markdown(content):
- """
- 通用markdown过滤器,应用文章内容插件
- 主要用于文章内容处理
- """
- html_content = CommonMarkdown.get_markdown(content)
-
- # 然后应用插件过滤器优化HTML
- from djangoblog.plugin_manage import hooks
- from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
- optimized_html = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, html_content)
-
- return mark_safe(optimized_html)
-
-
-@register.filter()
-@stringfilter
-def sidebar_markdown(content):
- html_content = CommonMarkdown.get_markdown(content)
- return mark_safe(html_content)
-
-
-@register.simple_tag(takes_context=True)
-def render_article_content(context, article, is_summary=False):
- """
- 渲染文章内容,包含完整的上下文信息供插件使用
-
- Args:
- context: 模板上下文
- article: 文章对象
- is_summary: 是否为摘要模式(首页使用)
- """
- if not article or not hasattr(article, 'body'):
- return ''
-
- # 先转换Markdown为HTML
- html_content = CommonMarkdown.get_markdown(article.body)
-
- # 如果是摘要模式,先截断内容再应用插件
- if is_summary:
- # 截断HTML内容到合适的长度(约300字符)
- from django.utils.html import strip_tags
- from django.template.defaultfilters import truncatechars
-
- # 先去除HTML标签,截断纯文本,然后重新转换为HTML
- plain_text = strip_tags(html_content)
- truncated_text = truncatechars(plain_text, 300)
-
- # 重新转换截断后的文本为HTML(简化版,避免复杂的插件处理)
- html_content = CommonMarkdown.get_markdown(truncated_text)
-
- # 然后应用插件过滤器,传递完整的上下文
- from djangoblog.plugin_manage import hooks
- from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
-
- # 获取request对象
- request = context.get('request')
-
- # 应用所有文章内容相关的插件
- # 注意:摘要模式下某些插件(如版权声明)可能不适用
- optimized_html = hooks.apply_filters(
- ARTICLE_CONTENT_HOOK_NAME,
- html_content,
- article=article,
- request=request,
- context=context,
- is_summary=is_summary # 传递摘要标志,插件可以据此调整行为
- )
-
- return mark_safe(optimized_html)
+ return mark_safe(CommonMarkdown.get_markdown(content))
@register.simple_tag
@@ -360,49 +292,38 @@ def load_article_detail(article, isindex, user):
}
-# 返回用户头像URL
-# 模板使用方法: {{ email|gravatar_url:150 }}
+# return only the URL of the gravatar
+# TEMPLATE USE: {{ email|gravatar_url:150 }}
@register.filter
def gravatar_url(email, size=40):
- """获得用户头像 - 优先使用OAuth头像,否则使用默认头像"""
- cachekey = 'avatar/' + email
+ """获得gravatar头像"""
+ cachekey = 'gravatat/' + email
url = cache.get(cachekey)
if url:
return url
-
- # 检查OAuth用户是否有自定义头像
- usermodels = OAuthUser.objects.filter(email=email)
- if usermodels:
- # 过滤出有头像的用户
- users_with_picture = list(filter(lambda x: x.picture is not None, usermodels))
- if users_with_picture:
- # 获取默认头像路径用于比较
- default_avatar_path = static('blog/img/avatar.png')
-
- # 优先选择非默认头像的用户,否则选择第一个
- non_default_users = [u for u in users_with_picture if u.picture != default_avatar_path and not u.picture.endswith('/avatar.png')]
- selected_user = non_default_users[0] if non_default_users else users_with_picture[0]
-
- url = selected_user.picture
- cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时
-
- avatar_type = 'non-default' if non_default_users else 'default'
- logger.info('Using {} OAuth avatar for {} from {}'.format(avatar_type, email, selected_user.type))
- return url
-
- # 使用默认头像
- url = static('blog/img/avatar.png')
- cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时
- logger.info('Using default avatar for {}'.format(email))
- return url
+ else:
+ usermodels = OAuthUser.objects.filter(email=email)
+ if usermodels:
+ o = list(filter(lambda x: x.picture is not None, usermodels))
+ if o:
+ return o[0].picture
+ email = email.encode('utf-8')
+
+ default = static('blog/img/avatar.png')
+
+ url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5(
+ email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)}))
+ cache.set(cachekey, url, 60 * 60 * 10)
+ logger.info('set gravatar cache.key:{key}'.format(key=cachekey))
+ return url
@register.filter
def gravatar(email, size=40):
- """获得用户头像HTML标签"""
+ """获得gravatar头像"""
url = gravatar_url(email, size)
return mark_safe(
- '
' %
+ '
' %
(url, size, size))
diff --git a/src/blog/views.py b/src/blog/views.py
index ace9e63..d5dc7ec 100644
--- a/src/blog/views.py
+++ b/src/blog/views.py
@@ -154,6 +154,10 @@ class ArticleDetailView(DetailView):
article = self.object
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
+ # # Filter Hook, 允许插件修改文章正文
+ article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
+ request=self.request)
+
return context
diff --git a/src/comments/admin.py b/src/comments/admin.py
index dbde14f..a814f3f 100644
--- a/src/comments/admin.py
+++ b/src/comments/admin.py
@@ -29,8 +29,6 @@ class CommentAdmin(admin.ModelAdmin):
list_filter = ('is_enable',)
exclude = ('creation_time', 'last_modify_time')
actions = [disable_commentstatus, enable_commentstatus]
- raw_id_fields = ('author', 'article')
- search_fields = ('body',)
def link_to_userinfo(self, obj):
info = (obj.author._meta.app_label, obj.author._meta.model_name)
diff --git a/src/deploy/k8s/deployment.yaml b/src/deploy/k8s/deployment.yaml
index b50c411..414fdcc 100644
--- a/src/deploy/k8s/deployment.yaml
+++ b/src/deploy/k8s/deployment.yaml
@@ -26,13 +26,13 @@ spec:
name: djangoblog-env
readinessProbe:
httpGet:
- path: /health/
+ path: /
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
livenessProbe:
httpGet:
- path: /health/
+ path: /
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
diff --git a/src/djangoblog/settings.py b/src/djangoblog/settings.py
index 692409c..d076bb6 100644
--- a/src/djangoblog/settings.py
+++ b/src/djangoblog/settings.py
@@ -109,13 +109,15 @@ WSGI_APPLICATION = 'djangoblog.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
- 'NAME': 'djangoblog',
- 'USER': 'root',
- 'PASSWORD': 'root',
- 'HOST': '127.0.0.1',
- 'PORT': 3306,
- }
-}
+ '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
@@ -316,21 +318,6 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
X_FRAME_OPTIONS = 'SAMEORIGIN'
-# 安全头部配置 - 防XSS和其他攻击
-SECURE_BROWSER_XSS_FILTER = True
-SECURE_CONTENT_TYPE_NOSNIFF = True
-SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
-
-# 内容安全策略 (CSP) - 防XSS攻击
-CSP_DEFAULT_SRC = ["'self'"]
-CSP_SCRIPT_SRC = ["'self'", "'unsafe-inline'", "cdn.mathjax.org", "*.googleapis.com"]
-CSP_STYLE_SRC = ["'self'", "'unsafe-inline'", "*.googleapis.com", "*.gstatic.com"]
-CSP_IMG_SRC = ["'self'", "data:", "*.lylinux.net", "*.gravatar.com", "*.githubusercontent.com"]
-CSP_FONT_SRC = ["'self'", "*.googleapis.com", "*.gstatic.com"]
-CSP_CONNECT_SRC = ["'self'"]
-CSP_FRAME_SRC = ["'none'"]
-CSP_OBJECT_SRC = ["'none'"]
-
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
@@ -352,7 +339,5 @@ ACTIVE_PLUGINS = [
'reading_time',
'external_links',
'view_count',
- 'seo_optimizer',
- 'image_lazy_loading',
-]
-DATABASES
+ 'seo_optimizer'
+]
\ No newline at end of file
diff --git a/src/djangoblog/urls.py b/src/djangoblog/urls.py
index 6a9e1de..4aae58a 100644
--- a/src/djangoblog/urls.py
+++ b/src/djangoblog/urls.py
@@ -20,8 +20,6 @@ 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 django.http import JsonResponse
-import time
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
@@ -42,20 +40,8 @@ handler404 = 'blog.views.page_not_found_view'
handler500 = 'blog.views.server_error_view'
handle403 = 'blog.views.permission_denied_view'
-
-def health_check(request):
- """
- 健康检查接口
- 简单返回服务健康状态
- """
- return JsonResponse({
- 'status': 'healthy',
- 'timestamp': time.time()
- })
-
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
- path('health/', health_check, name='health_check'),
]
urlpatterns += i18n_patterns(
re_path(r'^admin/', admin_site.urls),
diff --git a/src/djangoblog/utils.py b/src/djangoblog/utils.py
index 91d2b91..57f63dc 100644
--- a/src/djangoblog/utils.py
+++ b/src/djangoblog/utils.py
@@ -224,49 +224,9 @@ def get_resource_url():
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
- 'h2', 'p', 'span', 'div']
-
-# 安全的class值白名单 - 只允许代码高亮相关的class
-ALLOWED_CLASSES = [
- 'codehilite', 'highlight', 'hll', 'c', 'err', 'k', 'l', 'n', 'o', 'p', 'cm', 'cp', 'c1', 'cs',
- 'gd', 'ge', 'gr', 'gh', 'gi', 'go', 'gp', 'gs', 'gu', 'gt', 'kc', 'kd', 'kn', 'kp', 'kr', 'kt',
- 'ld', 'm', 'mf', 'mh', 'mi', 'mo', 'na', 'nb', 'nc', 'no', 'nd', 'ni', 'ne', 'nf', 'nl', 'nn',
- 'nt', 'nv', 'ow', 'w', 'mb', 'mh', 'mi', 'mo', 'sb', 'sc', 'sd', 'se', 'sh', 'si', 'sx', 's2',
- 's1', 'ss', 'bp', 'vc', 'vg', 'vi', 'il'
-]
-
-def class_filter(tag, name, value):
- """自定义class属性过滤器"""
- if name == 'class':
- # 只允许预定义的安全class值
- allowed_classes = [cls for cls in value.split() if cls in ALLOWED_CLASSES]
- return ' '.join(allowed_classes) if allowed_classes else False
- return value
-
-# 安全的属性白名单
-ALLOWED_ATTRIBUTES = {
- 'a': ['href', 'title'],
- 'abbr': ['title'],
- 'acronym': ['title'],
- 'span': class_filter,
- 'div': class_filter,
- 'pre': class_filter,
- 'code': class_filter
-}
-
-# 安全的协议白名单 - 防止javascript:等危险协议
-ALLOWED_PROTOCOLS = ['http', 'https', 'mailto']
+ 'h2', 'p']
+ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
+
def sanitize_html(html):
- """
- 安全的HTML清理函数
- 使用bleach库进行白名单过滤,防止XSS攻击
- """
- return bleach.clean(
- html,
- tags=ALLOWED_TAGS,
- attributes=ALLOWED_ATTRIBUTES,
- protocols=ALLOWED_PROTOCOLS, # 限制允许的协议
- strip=True, # 移除不允许的标签而不是转义
- strip_comments=True # 移除HTML注释
- )
+ return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
diff --git a/src/plugins/article_copyright/plugin.py b/src/plugins/article_copyright/plugin.py
index 5dba3b3..317fed2 100644
--- a/src/plugins/article_copyright/plugin.py
+++ b/src/plugins/article_copyright/plugin.py
@@ -22,11 +22,6 @@ class ArticleCopyrightPlugin(BasePlugin):
article = kwargs.get('article')
if not article:
return content
-
- # 如果是摘要模式(首页),不添加版权声明
- is_summary = kwargs.get('is_summary', False)
- if is_summary:
- return content
copyright_info = f"\n
本文由 {article.author.username} 原创,转载请注明出处。
"
return content + copyright_info
diff --git a/src/plugins/reading_time/plugin.py b/src/plugins/reading_time/plugin.py
index 4b929d8..35f9db1 100644
--- a/src/plugins/reading_time/plugin.py
+++ b/src/plugins/reading_time/plugin.py
@@ -17,15 +17,7 @@ class ReadingTimePlugin(BasePlugin):
def add_reading_time(self, content, *args, **kwargs):
"""
计算阅读时间并添加到内容开头。
- 只在文章详情页显示,首页(文章列表页)不显示。
"""
- # 检查是否为摘要模式(首页/文章列表页)
- # 通过kwargs中的is_summary参数判断
- is_summary = kwargs.get('is_summary', False)
- if is_summary:
- # 如果是摘要模式(首页),直接返回原内容,不添加阅读时间
- return content
-
# 移除HTML标签和空白字符,以获得纯文本
clean_content = re.sub(r'<[^>]*>', '', content)
clean_content = clean_content.strip()
diff --git a/src/plugins/seo_optimizer/plugin.py b/src/plugins/seo_optimizer/plugin.py
index de12c15..b5b19a3 100644
--- a/src/plugins/seo_optimizer/plugin.py
+++ b/src/plugins/seo_optimizer/plugin.py
@@ -97,8 +97,6 @@ class SeoOptimizerPlugin(BasePlugin):
structured_data = {
"@context": "https://schema.org",
"@type": "WebSite",
- "name": blog_setting.site_name,
- "description": blog_setting.site_description,
"url": request.build_absolute_uri('/'),
"potentialAction": {
"@type": "SearchAction",
@@ -133,15 +131,12 @@ class SeoOptimizerPlugin(BasePlugin):
json_ld_script = f''
- seo_html = f"""
+ return f"""
{seo_data.get("title", "")}
{seo_data.get("meta_tags", "")}
{json_ld_script}
"""
-
- # 将SEO内容追加到现有的metas内容上
- return metas + seo_html
plugin = SeoOptimizerPlugin()
diff --git a/src/requirements.txt b/src/requirements.txt
index 4b0963860353db73cc790ee3d2e46c02921cd440..9dc5c935191f166db408850beb747475a262f65f 100644
GIT binary patch
delta 904
zcmY*Yy-vbl6up$-U_u-i35gpMq~%W+LmZ5QgE4V)Fr|Q^{I*4Y7M{RF)6KUaJcN(p
zBbYdO&i&dFLcWspp6{G<&%OQ1{}nz?HyxRXFRm=*0>6e-#lfjdUXF#e5Wk`Hv}$X$
zj%Q8l9^U(?52TO7`!%s~nkkDD!x}TPKzX+9RU%S!`z-+lOtY)CciINybVSK!Q-ZXhvCURTxMYl7@g1j@Z${
zx-7rI&;*q!OnCB|X29MkWP{lgC)!yzr8%lhp{rP<&;q7LT=Z+*7G33VuK^*0?)`Ru$(>HO
{% if isindex %}
- {% render_article_content article True %}
+ {{ article.body|custom_markdown|escape|truncatechars_content }}
Read more
{% else %}
@@ -62,7 +62,7 @@
{% endif %}
- {% render_article_content article False %}
+ {{ article.body|custom_markdown|escape }}
{% endif %}
diff --git a/src/templates/blog/tags/sidebar.html b/src/templates/blog/tags/sidebar.html
index ecb6d20..f70544c 100644
--- a/src/templates/blog/tags/sidebar.html
+++ b/src/templates/blog/tags/sidebar.html
@@ -16,7 +16,7 @@
{% endfor %}
diff --git a/src/templates/comments/tags/comment_item.html b/src/templates/comments/tags/comment_item.html
index 0693649..ebb0388 100644
--- a/src/templates/comments/tags/comment_item.html
+++ b/src/templates/comments/tags/comment_item.html
@@ -2,13 +2,10 @@