diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8206f0c --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ + +# 系统和用户目录 +Application Data/ +Cookies/ +Local Settings/ +My Documents/ +NetHood/ +PrintHood/ +Recent/ +SendTo/ +Templates/ +「开始」菜单/ +AppData/ +Contacts/ +Desktop/ +Documents/ +Downloads/ +Favorites/ +Links/ +Music/ +NTUSER.* +OneDrive/ +Pictures/ +Saved Games/ +Searches/ +Videos/ +WPS Cloud Files/ +wechat_files/ + +# 临时文件和IDE配置 +.bash_history +.eclipse/ +.gitconfig +.idlerc/ +.matplotlib/ +.p2/ +.ssh/ +.vscode/ +eclipse-workspace/ +eclipse/ +ntuser.* + diff --git a/.idea/.gitignore b/.idea/.gitignore index 359bb53..10b731c 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -1,3 +1,5 @@ # 默认忽略的文件 /shelf/ /workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ diff --git a/.idea/misc.xml b/.idea/misc.xml index 8d53134..db8786c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,10 +1,7 @@ -<<<<<<< HEAD -======= ->>>>>>> ccy_branch \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index 340639d..11045d5 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/.idea/Django.iml b/.idea/software-engineering-methodology-djq-branch(1).iml similarity index 90% rename from .idea/Django.iml rename to .idea/software-engineering-methodology-djq-branch(1).iml index a5f510f..07abf20 100644 --- a/.idea/Django.iml +++ b/.idea/software-engineering-methodology-djq-branch(1).iml @@ -5,11 +5,8 @@ -<<<<<<< HEAD -======= ->>>>>>> ccy_branch \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..288b36b --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index b5c9c6c..0000000 --- a/README.md +++ /dev/null @@ -1,6 +0,0 @@ -<<<<<<< HEAD -这是我的个人分支说明 -======= -# software-engineering-methodology - ->>>>>>> f783378e06d6abd4513ad3220bf6f630b2fb7263 diff --git a/doc/26组开源软件泛读报告.docx b/doc/26组开源软件泛读报告.docx deleted file mode 100644 index 0796b69..0000000 Binary files a/doc/26组开源软件泛读报告.docx and /dev/null differ diff --git a/doc/26组开源软件的质量分析报告文档.docx b/doc/26组开源软件的质量分析报告文档.docx deleted file mode 100644 index 1f84e97..0000000 Binary files a/doc/26组开源软件的质量分析报告文档.docx and /dev/null differ diff --git a/doc/26组编码规范.docx b/doc/26组编码规范.docx deleted file mode 100644 index 1e2dd4a..0000000 Binary files a/doc/26组编码规范.docx and /dev/null differ diff --git a/doc/26组软件数据模型设计说明书.docx b/doc/26组软件数据模型设计说明书.docx deleted file mode 100644 index 66ccd68..0000000 Binary files a/doc/26组软件数据模型设计说明书.docx and /dev/null differ diff --git a/doc/djq.txt b/doc/djq.txt index e69de29..d00491f 100644 --- a/doc/djq.txt +++ b/doc/djq.txt @@ -0,0 +1 @@ +1 diff --git a/doc/test.txt b/doc/test.txt deleted file mode 100644 index e69de29..0000000 diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/forms.py b/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/forms.py index 04c3e00..fce4137 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/forms.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/forms.py @@ -8,73 +8,40 @@ from . import utils from .models import BlogUser -# 登录表单,继承自Django的AuthenticationForm class LoginForm(AuthenticationForm): def __init__(self, *args, **kwargs): - # 调用父类的初始化方法 super(LoginForm, self).__init__(*args, **kwargs) - # 为用户名字段添加占位符和CSS类 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) - # 为密码字段添加占位符和CSS类 self.fields['password'].widget = widgets.PasswordInput( attrs={'placeholder': "password", "class": "form-control"}) -# 注册表单,继承自Django的UserCreationForm class RegisterForm(UserCreationForm): def __init__(self, *args, **kwargs): - # 调用父类的初始化方法 super(RegisterForm, self).__init__(*args, **kwargs) - # 为各个表单字段添加占位符和CSS类 - # 为用户名字段自定义widget self.fields['username'].widget = widgets.TextInput( - attrs={ - 'placeholder': "username", # 输入框内的提示文本,用户点击时会消失 - "class": "form-control" # CSS类名,用于应用Bootstrap等UI框架的样式 - }) - - # 为邮箱字段自定义widget + attrs={'placeholder': "username", "class": "form-control"}) self.fields['email'].widget = widgets.EmailInput( - attrs={ - 'placeholder': "email", # 提示用户输入邮箱地址 - "class": "form-control" # 统一的表单控件样式类 - }) - - # 为密码字段自定义widget(密码输入框1) + attrs={'placeholder': "email", "class": "form-control"}) self.fields['password1'].widget = widgets.PasswordInput( - attrs={ - 'placeholder': "password", # 提示用户输入密码 - "class": "form-control" # 密码输入框会显示为圆点或星号,隐藏实际内容 - }) - - # 为确认密码字段自定义widget(密码输入框2) + attrs={'placeholder': "password", "class": "form-control"}) self.fields['password2'].widget = widgets.PasswordInput( - attrs={ - 'placeholder': "repeat password", # 提示用户再次输入密码进行确认 - "class": "form-control" # 同样使用密码输入框类型 - }) + 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( @@ -85,7 +52,6 @@ class ForgetPasswordForm(forms.Form): ), ) - # 确认密码字段 new_password2 = forms.CharField( label="确认密码", widget=forms.PasswordInput( @@ -96,7 +62,6 @@ class ForgetPasswordForm(forms.Form): ), ) - # 邮箱字段 email = forms.EmailField( label='邮箱', widget=forms.TextInput( @@ -107,7 +72,6 @@ class ForgetPasswordForm(forms.Form): ), ) - # 验证码字段 code = forms.CharField( label=_('Code'), widget=forms.TextInput( @@ -118,21 +82,17 @@ class ForgetPasswordForm(forms.Form): ), ) - # 确认密码验证方法 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(): @@ -140,10 +100,8 @@ class ForgetPasswordForm(forms.Form): 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, @@ -153,9 +111,7 @@ class ForgetPasswordForm(forms.Form): return code -# 忘记密码验证码请求表单 class ForgetPasswordCodeForm(forms.Form): - # 邮箱字段,用于发送验证码 email = forms.EmailField( label=_('Email'), ) diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py b/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/migrations/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.py new file mode 100644 index 0000000..1a9f509 --- /dev/null +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/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/DjangoBlog-master(1)/DjangoBlog-master/accounts/models.py b/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/models.py index 1eba368..3baddbb 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/models.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/models.py @@ -6,46 +6,30 @@ from django.utils.translation import gettext_lazy as _ from djangoblog.utils import get_current_site -#ccy: 用户管理模块 - 扩展Django默认用户模型,添加博客系统所需的用户字段和功能 # Create your models here. -#ccy: 博客用户模型 - 继承AbstractUser扩展自定义用户字段 class BlogUser(AbstractUser): - #ccy: 用户昵称字段,最大长度100字符,允许为空 nickname = models.CharField(_('nick name'), max_length=100, blank=True) - #ccy: 用户创建时间,自动设置为当前时间 creation_time = models.DateTimeField(_('creation time'), default=now) - #ccy: 用户最后修改时间,自动更新为当前时间 last_modify_time = models.DateTimeField(_('last modify time'), default=now) - #ccy: 用户创建来源,记录用户注册渠道 source = models.CharField(_('create source'), max_length=100, blank=True) - #ccy: 获取用户绝对URL - 用于生成用户详情页链接 def get_absolute_url(self): return reverse( 'blog:author_detail', kwargs={ 'author_name': self.username}) - #ccy: 字符串表示方法 - 返回用户邮箱作为标识 def __str__(self): return self.email - #ccy: 获取用户完整URL - 包含域名的完整用户链接 def get_full_url(self): - #ccy: 获取当前站点域名 site = get_current_site().domain - #ccy: 拼接完整URL,包含协议和域名 url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) return url - #ccy: Meta类 - 定义用户模型的元数据配置 class Meta: - #ccy: 按ID降序排列,新用户显示在前面 ordering = ['-id'] - #ccy: 单数模型名称 verbose_name = _('user') - #ccy: 复数模型名称 verbose_name_plural = verbose_name - #ccy: 指定获取最新记录的依据字段 - get_latest_by = 'id' \ No newline at end of file + get_latest_by = 'id' diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/tests.py b/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/tests.py index b820a15..6893411 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/tests.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/tests.py @@ -13,244 +13,195 @@ from . import utils class AccountTest(TestCase): def setUp(self): - """测试初始化方法,每个测试方法运行前都会执行""" - self.client = Client() # Django测试客户端,用于模拟HTTP请求 - self.factory = RequestFactory() # 用于创建请求对象 - # 创建测试用户 + 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--=" # 测试用的新密码 + self.new_test = "xxx123--=" def test_validate_account(self): - """测试账户验证功能:创建超级用户、登录、文章管理""" - site = get_current_site().domain # 获取当前站点域名 - - # 创建超级用户 + 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) # 断言登录成功 - - # 测试访问管理员页面 + self.assertEqual(loginresult, True) response = self.client.get('/admin/') - self.assertEqual(response.status_code, 200) # 断言能正常访问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.type = 'a' + article.status = 'p' article.save() - # 测试访问文章管理页面 response = self.client.get(article.get_admin_url()) - self.assertEqual(response.status_code, 200) # 断言能正常访问文章管理页面 + 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.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() # 清除边栏缓存 - - # 创建分类 + 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]) # 断言重定向或成功 + self.assertIn(response.status_code, [301, 302, 200]) - # 测试退出后访问文章管理页面(应该被重定向到登录页) response = self.client.get(article.get_admin_url()) - self.assertIn(response.status_code, [301, 302, 200]) # 断言重定向 + self.assertIn(response.status_code, [301, 302, 200]) - # 测试重新登录(使用错误密码) response = self.client.post(reverse('account:login'), { 'username': 'user1233', - 'password': 'password123' # 错误的密码 + '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() # 生成验证码 - - # 保存验证码 + 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) # 断言验证成功,错误为None + self.assertEqual(err, None) - # 测试错误邮箱 err = utils.verify("admin@123.com", code) - self.assertEqual(type(err), str) # 断言返回错误信息字符串 + 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") # 正确的邮箱格式 + data=dict(email="admin@admin.com") ) - self.assertEqual(resp.status_code, 200) # 断言请求成功 - self.assertEqual(resp.content.decode("utf-8"), "ok") # 断言返回成功消息 + 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() # 空数据 + data=dict() ) - self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # 断言返回错误消息 + self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") - # 测试错误邮箱格式 resp = self.client.post( path=reverse("account:forget_password_code"), - data=dict(email="admin@com") # 无效的邮箱格式 + data=dict(email="admin@com") ) - self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱") # 断言返回错误消息 + 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) # 保存验证码 - - # 准备重置密码数据 + 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, # 正确的验证码 + 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) # 断言重定向(通常表示成功) + 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) # 断言密码修改成功 + 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", # 任意验证码 + email="123@123.com", + code="123456", ) resp = self.client.post( path=reverse("account:forget_password"), data=data ) - self.assertEqual(resp.status_code, 200) # 断言请求完成(但应该失败) + 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) # 保存正确的验证码 - + 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", # 错误的验证码 + code="111111", ) resp = self.client.post( path=reverse("account:forget_password"), data=data ) - self.assertEqual(resp.status_code, 200) # 断言请求完成(但验证码错误) + self.assertEqual(resp.status_code, 200) + diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/urls.py b/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/urls.py index cf82b73..107a801 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/urls.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/urls.py @@ -1,45 +1,28 @@ -# zjp: 用户账户管理URL配置模块 -# zjp: 定义用户认证、注册、密码管理等账户相关功能的URL路由 - from django.urls import path from django.urls import re_path from . import views from .forms import LoginForm -# zjp: 定义应用的命名空间,用于反向解析URL app_name = "accounts" -# zjp: URL模式配置 - 用户账户相关功能 -urlpatterns = [ - # zjp: 用户登录功能 - 处理用户登录认证 - re_path(r'^login/$', # zjp: 使用正则表达式匹配登录路径,必须以/login/结尾 - views.LoginView.as_view(success_url='/'), # zjp: 登录类视图,登录成功后跳转到首页 - name='login', # zjp: URL名称,用于反向解析 - kwargs={'authentication_form': LoginForm}), # zjp: 传递自定义登录表单类,增强安全性 - - # zjp: 用户注册功能 - 处理新用户注册 - re_path(r'^register/$', # zjp: 注册路径,必须以/register/结尾 - views.RegisterView.as_view(success_url="/"), # zjp: 注册类视图,注册成功后跳转到首页 - name='register'), # zjp: URL名称,用于模板中的反向解析 - - # zjp: 用户退出登录功能 - 处理用户登出 - re_path(r'^logout/$', # zjp: 退出登录路径,必须以/logout/结尾 - views.LogoutView.as_view(), # zjp: 退出登录类视图,清理用户会话 - name='logout'), # zjp: URL名称,用于安全退出链接 - - # zjp: 账户操作结果页面 - 显示注册、登录等操作的结果信息 - path(r'account/result.html', # zjp: 结果页面路径,使用path函数定义静态URL - views.account_result, # zjp: 使用函数视图显示账户操作结果页面 - name='result'), # zjp: URL名称,用于操作完成后的跳转 - - # zjp: 忘记密码页面 - 处理密码重置请求 - re_path(r'^forget_password/$', # zjp: 忘记密码路径,必须以/forget_password/结尾 - views.ForgetPasswordView.as_view(), # zjp: 忘记密码类视图,处理密码重置逻辑 - name='forget_password'), # zjp: URL名称,用于密码重置入口 - - # zjp: 忘记密码验证码处理 - 接收和验证邮箱验证码 - re_path(r'^forget_password_code/$', # zjp: 忘记密码验证码路径,必须以/forget_password_code/结尾 - views.ForgetPasswordEmailCode.as_view(), # zjp: 忘记密码邮箱验证码类视图,处理验证逻辑 - name='forget_password_code'), # zjp: URL名称,用于验证码提交接口 -] \ No newline at end of file +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/DjangoBlog-master(1)/DjangoBlog-master/blog/admin.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/admin.py index e5232f2..46c3420 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/admin.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/admin.py @@ -4,192 +4,109 @@ from django.contrib.auth import get_user_model from django.urls import reverse from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ -from django.core.exceptions import ValidationError -# 导入 blog 应用下的所有模型 -from .models import ( - Article, Category, Tag, Links, SideBar, BlogSettings, - UserProfile, Favorite # 确保导入了 UserProfile 和 Favorite -) +# Register your models here. +from .models import Article + -# ------------------------------------------------------------------------------ -# Article Admin -# ------------------------------------------------------------------------------ class ArticleForm(forms.ModelForm): + # body = forms.CharField(widget=AdminPagedownWidget()) + class Meta: model = Article fields = '__all__' -def make_article_publish(modeladmin, request, queryset): + +def makr_article_publish(modeladmin, request, queryset): queryset.update(status='p') -make_article_publish.short_description = _('Publish selected articles') + def draft_article(modeladmin, request, queryset): queryset.update(status='d') -draft_article.short_description = _('Set selected articles to draft') + def close_article_commentstatus(modeladmin, request, queryset): queryset.update(comment_status='c') -close_article_commentstatus.short_description = _('Close comments for selected articles') + def open_article_commentstatus(modeladmin, request, queryset): queryset.update(comment_status='o') -open_article_commentstatus.short_description = _('Open comments for selected articles') -@admin.register(Article) -class ArticleAdmin(admin.ModelAdmin): + +makr_article_publish.short_description = _('Publish selected articles') +draft_article.short_description = _('Draft selected articles') +close_article_commentstatus.short_description = _('Close article comments') +open_article_commentstatus.short_description = _('Open article comments') + + +class ArticlelAdmin(admin.ModelAdmin): list_per_page = 20 search_fields = ('body', 'title') form = ArticleForm list_display = ( - 'id', 'title', 'author', 'link_to_category', - 'pub_time', 'views', 'status', 'type', 'article_order' - ) + 'id', + 'title', + 'author', + 'link_to_category', + 'creation_time', + 'views', + 'status', + 'type', + 'article_order') list_display_links = ('id', 'title') - list_filter = ('status', 'type', 'category', 'tags') + list_filter = ('status', 'type', 'category') filter_horizontal = ('tags',) - # 使用 fieldsets 来组织编辑页面的字段布局,更清晰 - fieldsets = ( - (_('Basic Information'), { - 'fields': ('title', 'author', 'category', 'tags', 'status', 'type') - }), - (_('Content'), { - 'fields': ('body',) - }), - (_('Settings'), { - 'fields': ('article_order', 'show_toc', 'comment_status'), - 'classes': ('collapse',) # 默认折叠 - }), - ) exclude = ('creation_time', 'last_modify_time') view_on_site = True actions = [ - make_article_publish, draft_article, - close_article_commentstatus, open_article_commentstatus - ] + makr_article_publish, + draft_article, + close_article_commentstatus, + open_article_commentstatus] def link_to_category(self, obj): - if obj.category: - info = (obj.category._meta.app_label, obj.category._meta.model_name) - link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) - return format_html(u'{}', link, obj.category.name) - return _('None') - link_to_category.short_description = _('Category') + info = (obj.category._meta.app_label, obj.category._meta.model_name) + link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,)) + return format_html(u'%s' % (link, obj.category.name)) + + link_to_category.short_description = _('category') def get_form(self, request, obj=None, **kwargs): - form = super().get_form(request, obj,** kwargs) - # 限制作者只能是超级管理员 - form.base_fields['author'].queryset = get_user_model().objects.filter(is_superuser=True) + form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) + form.base_fields['author'].queryset = get_user_model( + ).objects.filter(is_superuser=True) return form + def save_model(self, request, obj, form, change): + super(ArticlelAdmin, self).save_model(request, obj, form, change) + def get_view_on_site_url(self, obj=None): if obj: - return obj.get_full_url() - return super().get_view_on_site_url(obj) + url = obj.get_full_url() + return url + else: + from djangoblog.utils import get_current_site + site = get_current_site().domain + return site -# ------------------------------------------------------------------------------ -# Category Admin -# ------------------------------------------------------------------------------ -@admin.register(Category) -class CategoryAdmin(admin.ModelAdmin): - list_display = ('id', 'name', 'parent_category', 'index') - list_display_links = ('id', 'name') - list_filter = ('parent_category',) - search_fields = ('name',) - exclude = ('slug', 'last_mod_time', 'creation_time') -# ------------------------------------------------------------------------------ -# Tag Admin -# ------------------------------------------------------------------------------ -@admin.register(Tag) class TagAdmin(admin.ModelAdmin): - list_display = ('id', 'name') - list_display_links = ('id', 'name') - search_fields = ('name',) exclude = ('slug', 'last_mod_time', 'creation_time') -# ------------------------------------------------------------------------------ -# Links Admin -# ------------------------------------------------------------------------------ -@admin.register(Links) + +class CategoryAdmin(admin.ModelAdmin): + list_display = ('name', 'parent_category', 'index') + exclude = ('slug', 'last_mod_time', 'creation_time') + + class LinksAdmin(admin.ModelAdmin): - list_display = ('id', 'name', 'link', 'sequence', 'is_enable', 'show_type') - list_display_links = ('id', 'name') - list_filter = ('is_enable', 'show_type') - search_fields = ('name', 'link') exclude = ('last_mod_time', 'creation_time') -# ------------------------------------------------------------------------------ -# SideBar Admin -# ------------------------------------------------------------------------------ -@admin.register(SideBar) -class SideBarAdmin(admin.ModelAdmin): - list_display = ('id', 'name', 'is_enable', 'sequence') - list_display_links = ('id', 'name') - list_filter = ('is_enable',) - search_fields = ('name', 'content') - exclude = ('last_mod_time', 'creation_time') -# ------------------------------------------------------------------------------ -# BlogSettings Admin (Singleton Pattern) -# ------------------------------------------------------------------------------ -@admin.register(BlogSettings) -class BlogSettingsAdmin(admin.ModelAdmin): - list_display = ('id', 'site_name') +class SideBarAdmin(admin.ModelAdmin): + list_display = ('name', 'content', 'is_enable', 'sequence') exclude = ('last_mod_time', 'creation_time') - def has_add_permission(self, request): - """ - 限制只能有一个配置实例 - 如果已经存在一条记录,则禁用“添加”按钮 - """ - if BlogSettings.objects.exists(): - return False - return True - def save_model(self, request, obj, form, change): - """ - 确保始终只有一个配置实例 - """ - if not change and BlogSettings.objects.exists(): - raise ValidationError(_('There can be only one Blog Settings instance.')) - super().save_model(request, obj, form, change) - -# ------------------------------------------------------------------------------ -# UserProfile Admin -# ------------------------------------------------------------------------------ -@admin.register(UserProfile) -class UserProfileAdmin(admin.ModelAdmin): - list_display = ('id', 'user', 'created_at') - list_display_links = ('id', 'user') - list_filter = ('created_at',) - search_fields = ('user__username', 'user__email', 'bio') - fieldsets = ( - (_('User'), { - 'fields': ('user',) - }), - (_('Profile Information'), { - 'fields': ('bio', 'avatar') - }), - (_('Social Links'), { - 'fields': ('website', 'github', 'twitter', 'weibo'), - 'classes': ('collapse',) - }), - ) - readonly_fields = ('created_at', 'updated_at') # 时间戳设为只读 - -# ------------------------------------------------------------------------------ -# Favorite Admin (Optional) -# ------------------------------------------------------------------------------ -@admin.register(Favorite) -class FavoriteAdmin(admin.ModelAdmin): - list_display = ('id', 'user', 'article', 'created_at') - list_display_links = ('id',) - list_filter = ('created_at',) - search_fields = ('user__username', 'article__title') - readonly_fields = ('created_at',) - # 通常不希望管理员手动创建或修改收藏,所以可以禁用相关权限 - def has_add_permission(self, request): - return False - def has_change_permission(self, request, obj=None): - return False \ No newline at end of file +class BlogSettingsAdmin(admin.ModelAdmin): + pass diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/context_processors.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/context_processors.py index 051cc1c..4a62f34 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/context_processors.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/context_processors.py @@ -1,12 +1,3 @@ -<<<<<<< HEAD -import logging - -from django.utils import timezone - -from djangoblog.utils import cache, get_blog_setting -from .models import Category, Article - -======= # 导入日志模块,用于记录系统运行时的信息和错误 import logging @@ -19,46 +10,10 @@ from djangoblog.utils import cache, get_blog_setting from .models import Category, Article # 创建日志记录器,用于记录当前模块的日志信息 ->>>>>>> ccy_branch logger = logging.getLogger(__name__) def seo_processor(requests): -<<<<<<< HEAD - key = 'seo_processor' - value = cache.get(key) - if value: - return value - else: - logger.info('set processor cache.') - setting = get_blog_setting() - value = { - 'SITE_NAME': setting.site_name, - 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, - 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, - 'SITE_SEO_DESCRIPTION': setting.site_seo_description, - 'SITE_DESCRIPTION': setting.site_description, - 'SITE_KEYWORDS': setting.site_keywords, - 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', - 'ARTICLE_SUB_LENGTH': setting.article_sub_length, - 'nav_category_list': Category.objects.all(), - 'nav_pages': Article.objects.filter( - type='p', - status='p'), - 'OPEN_SITE_COMMENT': setting.open_site_comment, - 'BEIAN_CODE': setting.beian_code, - 'ANALYTICS_CODE': setting.analytics_code, - "BEIAN_CODE_GONGAN": setting.gongan_beiancode, - "SHOW_GONGAN_CODE": setting.show_gongan_code, - "CURRENT_YEAR": timezone.now().year, - "GLOBAL_HEADER": setting.global_header, - "GLOBAL_FOOTER": setting.global_footer, - "COMMENT_NEED_REVIEW": setting.comment_need_review, - } - cache.set(key, value, 60 * 60 * 10) - return value - -======= """ 自定义上下文处理器:用于在所有模板中全局共享SEO相关的配置和数据 上下文处理器会在每次请求时被调用,返回的字典会自动注入到所有模板中 @@ -130,5 +85,4 @@ def seo_processor(requests): # 将数据存入缓存,有效期为10小时(60秒*60分*10小时) # 减少重复计算和数据库查询,提升性能 cache.set(key, value, 60 * 60 * 10) - return value ->>>>>>> ccy_branch + return value \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/documents.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/documents.py index 0f1db7b..2c07654 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/documents.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/documents.py @@ -1,213 +1,253 @@ -import time - -import elasticsearch.client -from django.conf import settings +import time # 用于生成时间戳作为文档ID +import elasticsearch.client # Elasticsearch客户端工具 +from django.conf import settings # 导入Django项目配置 +# 导入Elasticsearch DSL相关模块,用于定义文档结构和字段类型 from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean -from elasticsearch_dsl.connections import connections +from elasticsearch_dsl.connections import connections # 用于创建Elasticsearch连接 -from blog.models import Article +from blog.models import Article # 导入Django博客文章模型 +# 检查是否启用了Elasticsearch(通过判断配置中是否有ELASTICSEARCH_DSL) ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') if ELASTICSEARCH_ENABLED: + # 创建Elasticsearch连接,连接地址从Django配置中获取 connections.create_connection( hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) - from elasticsearch import Elasticsearch + from elasticsearch import Elasticsearch # 导入Elasticsearch客户端 + # 初始化Elasticsearch客户端 es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) - from elasticsearch.client import IngestClient + from elasticsearch.client import IngestClient # 导入Ingest API客户端(用于处理数据管道) c = IngestClient(es) try: + # 检查是否存在名为'geoip'的数据管道(用于解析IP地址的地理位置信息) c.get_pipeline('geoip') except elasticsearch.exceptions.NotFoundError: + # 若不存在,则创建'geoip'管道:通过IP地址添加地理位置信息 c.put_pipeline('geoip', body='''{ - "description" : "Add geoip info", + "description" : "Add geoip info", # 管道描述:添加IP的地理信息 "processors" : [ { "geoip" : { - "field" : "ip" + "field" : "ip" # 基于文档中的'ip'字段解析地理信息 } } ] }''') +# 内部文档类:存储IP地址解析后的地理位置信息(嵌套在ElapsedTimeDocument中) class GeoIp(InnerDoc): - continent_name = Keyword() - country_iso_code = Keyword() - country_name = Keyword() - location = GeoPoint() + continent_name = Keyword() # 大陆名称(Keyword类型:精确匹配,不分词) + country_iso_code = Keyword() # 国家ISO代码(如CN、US) + country_name = Keyword() # 国家名称 + location = GeoPoint() # 经纬度坐标(Elasticsearch的地理点类型) +# 内部文档类:存储用户代理中的浏览器信息(嵌套在UserAgent中) class UserAgentBrowser(InnerDoc): - Family = Keyword() - Version = Keyword() + Family = Keyword() # 浏览器家族(如Chrome、Firefox) + Version = Keyword() # 浏览器版本 +# 内部文档类:存储用户代理中的操作系统信息(继承浏览器信息结构) class UserAgentOS(UserAgentBrowser): - pass + pass # 结构与浏览器一致,包含Family(系统家族)和Version(系统版本) +# 内部文档类:存储用户代理中的设备信息(嵌套在UserAgent中) class UserAgentDevice(InnerDoc): - Family = Keyword() - Brand = Keyword() - Model = Keyword() + Family = Keyword() # 设备家族(如iPhone、Windows) + Brand = Keyword() # 设备品牌(如Apple、Samsung) + Model = Keyword() # 设备型号(如iPhone 13) +# 内部文档类:存储用户代理(User-Agent)完整信息(嵌套在ElapsedTimeDocument中) class UserAgent(InnerDoc): - browser = Object(UserAgentBrowser, required=False) - os = Object(UserAgentOS, required=False) - device = Object(UserAgentDevice, required=False) - string = Text() - is_bot = Boolean() + browser = Object(UserAgentBrowser, required=False) # 浏览器信息(可选) + os = Object(UserAgentOS, required=False) # 操作系统信息(可选) + device = Object(UserAgentDevice, required=False) # 设备信息(可选) + string = Text() # 原始User-Agent字符串 + is_bot = Boolean() # 是否为爬虫机器人 +# Elasticsearch文档类:记录性能耗时信息(如接口响应时间) class ElapsedTimeDocument(Document): - url = Keyword() - time_taken = Long() - log_datetime = Date() - ip = Keyword() - geoip = Object(GeoIp, required=False) - useragent = Object(UserAgent, required=False) + url = Keyword() # 请求URL(精确匹配) + time_taken = Long() # 耗时(毫秒) + log_datetime = Date() # 日志记录时间 + ip = Keyword() # 访问者IP地址 + geoip = Object(GeoIp, required=False) # 地理位置信息(由geoip管道解析,可选) + useragent = Object(UserAgent, required=False) # 用户代理信息(可选) class Index: - name = 'performance' + name = 'performance' # 索引名称:存储性能数据 settings = { - "number_of_shards": 1, - "number_of_replicas": 0 + "number_of_shards": 1, # 主分片数量 + "number_of_replicas": 0 # 副本分片数量(单节点环境设为0) } class Meta: - doc_type = 'ElapsedTime' + doc_type = 'ElapsedTime' # 文档类型(Elasticsearch 7.x后可省略) +# 管理类:处理ElapsedTimeDocument的索引创建、删除和数据插入 class ElaspedTimeDocumentManager: @staticmethod def build_index(): + """创建performance索引(若不存在)""" from elasticsearch import Elasticsearch client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) + # 检查索引是否存在 res = client.indices.exists(index="performance") if not res: + # 初始化索引(根据ElapsedTimeDocument的定义创建映射) ElapsedTimeDocument.init() @staticmethod def delete_index(): + """删除performance索引""" from elasticsearch import Elasticsearch es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) + # 忽略400(索引不存在)和404(请求错误)的错误 es.indices.delete(index='performance', ignore=[400, 404]) @staticmethod def create(url, time_taken, log_datetime, useragent, ip): + """创建一条性能日志文档并保存到Elasticsearch""" + # 确保索引已创建 ElaspedTimeDocumentManager.build_index() + + # 构建用户代理信息对象 ua = UserAgent() ua.browser = UserAgentBrowser() - ua.browser.Family = useragent.browser.family - ua.browser.Version = useragent.browser.version_string + ua.browser.Family = useragent.browser.family # 浏览器家族 + ua.browser.Version = useragent.browser.version_string # 浏览器版本 ua.os = UserAgentOS() - ua.os.Family = useragent.os.family - ua.os.Version = useragent.os.version_string + ua.os.Family = useragent.os.family # 操作系统家族 + ua.os.Version = useragent.os.version_string # 操作系统版本 ua.device = UserAgentDevice() - ua.device.Family = useragent.device.family - ua.device.Brand = useragent.device.brand - ua.device.Model = useragent.device.model - ua.string = useragent.ua_string - ua.is_bot = useragent.is_bot + ua.device.Family = useragent.device.family # 设备家族 + ua.device.Brand = useragent.device.brand # 设备品牌 + ua.device.Model = useragent.device.model # 设备型号 + ua.string = useragent.ua_string # 原始User-Agent字符串 + ua.is_bot = useragent.is_bot # 是否为爬虫 + # 创建性能日志文档 doc = ElapsedTimeDocument( meta={ - 'id': int( - round( - time.time() * - 1000)) + # 用当前时间戳(毫秒级)作为文档ID,确保唯一性 + 'id': int(round(time.time() * 1000)) }, - url=url, - time_taken=time_taken, - log_datetime=log_datetime, - useragent=ua, ip=ip) + url=url, # 请求URL + time_taken=time_taken, # 耗时 + log_datetime=log_datetime, # 记录时间 + useragent=ua, # 用户代理信息 + ip=ip # 访问IP + ) + # 保存文档时应用'geoip'管道,自动解析IP的地理位置 doc.save(pipeline="geoip") +# Elasticsearch文档类:存储博客文章信息(用于全文搜索) class ArticleDocument(Document): + # 文章内容(使用IK分词器:ik_max_word最大粒度分词,ik_smart智能分词) body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') + # 文章标题(同上,支持中文分词搜索) title = Text(analyzer='ik_max_word', search_analyzer='ik_smart') + # 作者信息(嵌套对象) author = Object(properties={ - 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), - 'id': Integer() + 'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 作者昵称 + 'id': Integer() # 作者ID }) + # 分类信息(嵌套对象) category = Object(properties={ - 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), - 'id': Integer() + 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 分类名称 + 'id': Integer() # 分类ID }) + # 标签信息(嵌套对象列表) tags = Object(properties={ - 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), - 'id': Integer() + 'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'), # 标签名称 + 'id': Integer() # 标签ID }) - pub_time = Date() - status = Text() - comment_status = Text() - type = Text() - views = Integer() - article_order = Integer() + pub_time = Date() # 发布时间 + status = Text() # 文章状态(如发布、草稿) + comment_status = Text() # 评论状态(如允许、关闭) + type = Text() # 文章类型(如原创、转载) + views = Integer() # 浏览量 + article_order = Integer() # 文章排序权重 class Index: - name = 'blog' + name = 'blog' # 索引名称:存储博客文章数据 settings = { "number_of_shards": 1, "number_of_replicas": 0 } class Meta: - doc_type = 'Article' + doc_type = 'Article' # 文档类型 +# 管理类:处理ArticleDocument的索引创建、删除、数据同步 class ArticleDocumentManager(): def __init__(self): + """初始化时创建blog索引(若不存在)""" self.create_index() def create_index(self): + """创建blog索引(根据ArticleDocument的定义)""" ArticleDocument.init() def delete_index(self): + """删除blog索引""" from elasticsearch import Elasticsearch es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) es.indices.delete(index='blog', ignore=[400, 404]) def convert_to_doc(self, articles): + """将Django的Article模型对象列表转换为ArticleDocument列表""" return [ ArticleDocument( - meta={ - 'id': article.id}, - body=article.body, - title=article.title, + meta={'id': article.id}, # 用文章ID作为文档ID + body=article.body, # 文章内容 + title=article.title, # 文章标题 author={ - 'nickname': article.author.username, - 'id': article.author.id}, + 'nickname': article.author.username, # 作者用户名 + 'id': article.author.id # 作者ID + }, category={ - 'name': article.category.name, - 'id': article.category.id}, - tags=[ - { - 'name': t.name, - 'id': t.id} for t in article.tags.all()], - pub_time=article.pub_time, - status=article.status, - comment_status=article.comment_status, - type=article.type, - views=article.views, - article_order=article.article_order) for article in articles] + 'name': article.category.name, # 分类名称 + 'id': article.category.id # 分类ID + }, + # 标签列表(遍历文章的tags多对多字段) + tags=[{'name': t.name, 'id': t.id} for t in article.tags.all()], + pub_time=article.pub_time, # 发布时间 + status=article.status, # 文章状态 + comment_status=article.comment_status, # 评论状态 + type=article.type, # 文章类型 + views=article.views, # 浏览量 + article_order=article.article_order # 排序权重 + ) for article in articles + ] def rebuild(self, articles=None): - ArticleDocument.init() + """重建blog索引:将文章数据同步到Elasticsearch(默认同步所有文章)""" + ArticleDocument.init() # 确保索引结构正确 + # 若未指定文章列表,则同步所有文章 articles = articles if articles else Article.objects.all() + # 转换为文档列表 docs = self.convert_to_doc(articles) + # 批量保存文档 for doc in docs: doc.save() def update_docs(self, docs): + """更新文档列表(批量保存)""" for doc in docs: - doc.save() + doc.save() \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/forms.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/forms.py index 8d97eee..715be76 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/forms.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/forms.py @@ -17,22 +17,3 @@ class BlogSearchForm(SearchForm): if self.cleaned_data['querydata']: logger.info(self.cleaned_data['querydata']) return datas -# blog/forms.py - -from django import forms -from .models import UserProfile - -class UserProfileUpdateForm(forms.ModelForm): - """ - 用户资料更新表单 - """ - class Meta: - model = UserProfile - fields = ['avatar', 'bio', 'website', 'github', 'twitter', 'weibo'] - widgets = { - 'bio': forms.Textarea(attrs={'rows': 5, 'placeholder': 'Tell us about yourself...'}), - 'website': forms.URLInput(attrs={'placeholder': 'https://'}), - 'github': forms.URLInput(attrs={'placeholder': 'https://github.com/'}), - 'twitter': forms.URLInput(attrs={'placeholder': 'https://twitter.com/'}), - 'weibo': forms.URLInput(attrs={'placeholder': 'https://weibo.com/'}), - } \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/middleware.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/middleware.py index 94dd70c..898afbc 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/middleware.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/middleware.py @@ -1,42 +1,88 @@ import logging import time +# 用于获取客户端IP地址的工具 from ipware import get_client_ip +# 用于解析用户代理(浏览器/设备信息)的工具 from user_agents import parse +# 导入Elasticsearch相关配置和文档管理器 from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager +# 初始化日志记录器,用于记录中间件运行过程中的信息和错误 logger = logging.getLogger(__name__) class OnlineMiddleware(object): + """ + 自定义Django中间件,用于: + 1. 计算页面渲染耗时 + 2. 收集访问日志(IP、用户代理、访问时间等) + 3. 当启用Elasticsearch时,将访问性能数据存入搜索引擎 + 4. 在响应内容中替换特定标记为页面加载时间 + """ + def __init__(self, get_response=None): + """ + 中间件初始化方法 + :param get_response: Django框架传入的下一个响应处理函数,用于构建中间件链 + """ self.get_response = get_response + # 调用父类初始化方法(兼容Python 2.x,在Python 3中可省略) super().__init__() def __call__(self, request): - ''' page render time ''' + """ + 中间件核心处理方法,在请求到达视图前和响应返回客户端前执行 + :param request: Django请求对象,包含客户端请求的所有信息 + :return: 经过处理的Django响应对象 + """ + # 记录请求处理开始时间(用于计算页面渲染耗时) start_time = time.time() + + # 调用下一个中间件或视图函数,获取响应对象 response = self.get_response(request) + + # 从请求头中获取用户代理字符串(包含浏览器、设备等信息) http_user_agent = request.META.get('HTTP_USER_AGENT', '') + # 通过ipware工具获取客户端IP地址(返回元组:(ip地址, 是否为公开IP)) ip, _ = get_client_ip(request) + # 解析用户代理字符串,生成结构化的用户代理对象(方便提取浏览器) user_agent = parse(http_user_agent) + + # 非流式响应(如普通HTML页面,排除文件下载等流式响应)才进行处理 if not response.streaming: try: + # 计算页面渲染总耗时(当前时间 - 开始时间) cast_time = time.time() - start_time + + # 如果启用了Elasticsearch,将访问性能数据存入搜索引擎 if ELASTICSEARCH_ENABLED: + # 转换耗时为毫秒并保留两位小数 time_taken = round((cast_time) * 1000, 2) + # 获取当前请求的URL路径 url = request.path + # 导入Django时区工具,获取当前时间 from django.utils import timezone + # 通过文档管理器创建并保存访问记录 ElaspedTimeDocumentManager.create( - url=url, - time_taken=time_taken, - log_datetime=timezone.now(), - useragent=user_agent, - ip=ip) + url=url, # 访问的URL + time_taken=time_taken, # 页面加载耗时(毫秒) + log_datetime=timezone.now(), # 访问时间 + useragent=user_agent, # 用户代理信息(浏览器/设备) + ip=ip # 客户端IP地址 + ) + + # 将响应内容中的标记替换为实际渲染耗时(保留前5位字符) + # 注意:仅适用于文本类型响应(如HTML),二进制响应会跳过 response.content = response.content.replace( - b'', str.encode(str(cast_time)[:5])) + b'', # 待替换的二进制标记 + str.encode(str(cast_time)[:5]) # 转换为二进制的耗时字符串 + ) + + # 捕获处理过程中的所有异常,避免中间件错误导致请求失败 except Exception as e: - logger.error("Error OnlineMiddleware: %s" % e) + logger.error("Error in OnlineMiddleware: %s" % e) + # 返回处理后的响应对象 return response diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/migrations/0007_favorite.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/migrations/0007_favorite.py deleted file mode 100644 index 58b31f9..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/migrations/0007_favorite.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.2.6 on 2025-11-22 23:35 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('blog', '0006_alter_blogsettings_options'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Favorite', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to='blog.article')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorite_articles', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': '收藏', - 'verbose_name_plural': '收藏', - 'unique_together': {('article', 'user')}, - }, - ), - ] diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/migrations/0008_userprofile.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/migrations/0008_userprofile.py deleted file mode 100644 index 1bd47d4..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/migrations/0008_userprofile.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 5.2.6 on 2025-11-23 00:03 - -import blog.models -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('blog', '0007_favorite'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='UserProfile', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('avatar', models.ImageField(blank=True, help_text='Upload your avatar image (recommended size: 100x100px)', null=True, upload_to=blog.models.user_avatar_path, verbose_name='Avatar')), - ('bio', models.TextField(blank=True, help_text='Tell us a little about yourself', null=True, verbose_name='Biography')), - ('website', models.URLField(blank=True, null=True, verbose_name='Website')), - ('github', models.URLField(blank=True, null=True, verbose_name='GitHub')), - ('twitter', models.URLField(blank=True, null=True, verbose_name='Twitter')), - ('weibo', models.URLField(blank=True, null=True, verbose_name='Weibo')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='User')), - ], - options={ - 'verbose_name': 'User Profile', - 'verbose_name_plural': 'User Profiles', - 'ordering': ['-created_at'], - }, - ), - ] diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/models.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/models.py index e797ed7..edbe4e1 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/models.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/models.py @@ -1,6 +1,5 @@ import logging import re -import os from abc import abstractmethod from django.conf import settings from django.core.exceptions import ValidationError @@ -14,139 +13,110 @@ from uuslug import slugify from djangoblog.utils import cache_decorator, cache from djangoblog.utils import get_current_site -# 确保导入了 post_save 信号和 receiver 装饰器 -from django.db.models.signals import post_save -from django.dispatch import receiver - logger = logging.getLogger(__name__) -def user_avatar_path(instance, filename): - """ - 定义用户头像的上传路径 - """ - # 文件将被上传到 MEDIA_ROOT/user_/avatar/ - return os.path.join(f'user_{instance.user.id}', 'avatar', filename) class LinkShowType(models.TextChoices): - # 定义友情链接显示类型的枚举类,分别对应首页、列表页、文章页、所有页面、轮播 I = ('i', _('index')) L = ('l', _('list')) P = ('p', _('post')) A = ('a', _('all')) S = ('s', _('slide')) + class BaseModel(models.Model): - """ - 基础模型类,为其他模型提供通用的字段和方法 - """ - id = models.AutoField(primary_key=True) # 自增主键 - creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间 - last_modify_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间 + id = models.AutoField(primary_key=True) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_modify_time = models.DateTimeField(_('modify time'), default=now) def save(self, *args, **kwargs): - """ - 重写save方法,处理slug字段(如果模型有slug和title/name字段),并调用父类save方法 - 同时处理仅更新views字段的特殊情况 - """ is_update_views = isinstance( self, Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] if is_update_views: Article.objects.filter(pk=self.pk).update(views=self.views) else: - # 如果模型有slug字段,生成slug(基于title或name字段) if 'slug' in self.__dict__: - slug_source = getattr(self, 'title') if 'title' in self.__dict__ else getattr(self, 'name') - setattr(self, 'slug', slugify(slug_source)) + slug = getattr( + self, 'title') if 'title' in self.__dict__ else getattr( + self, 'name') + setattr(self, 'slug', slugify(slug)) super().save(*args, **kwargs) def get_full_url(self): - """ - 获取模型对象的完整URL(包含域名) - """ site = get_current_site().domain url = "https://{site}{path}".format(site=site, path=self.get_absolute_url()) return url class Meta: - abstract = True # 抽象模型,不生成数据库表 + abstract = True @abstractmethod def get_absolute_url(self): - """ - 抽象方法,子类必须实现,用于获取模型对象的绝对URL - """ pass + class Article(BaseModel): - """ - 文章模型类,存储文章的相关信息 - """ - # 文章状态:草稿、已发布 + """文章""" STATUS_CHOICES = ( ('d', _('Draft')), ('p', _('Published')), ) - # 评论状态:开启、关闭 COMMENT_STATUS = ( ('o', _('Open')), ('c', _('Close')), ) - # 文章类型:文章、页面 TYPE = ( ('a', _('Article')), ('p', _('Page')), ) - title = models.CharField(_('title'), max_length=200, unique=True) # 文章标题,唯一 - body = MDTextField(_('body')) # 文章内容,使用MDTextField支持markdown + title = models.CharField(_('title'), max_length=200, unique=True) + body = MDTextField(_('body')) pub_time = models.DateTimeField( - _('publish time'), blank=False, null=False, default=now) # 发布时间 + _('publish time'), blank=False, null=False, default=now) status = models.CharField( _('status'), max_length=1, choices=STATUS_CHOICES, - default='p') # 文章状态 + default='p') comment_status = models.CharField( _('comment status'), max_length=1, choices=COMMENT_STATUS, - default='o') # 评论状态 - type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # 文章类型 - views = models.PositiveIntegerField(_('views'), default=0) # 文章浏览量 + default='o') + type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') + views = models.PositiveIntegerField(_('views'), default=0) author = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('author'), blank=False, null=False, - on_delete=models.CASCADE) # 文章作者,外键关联用户模型 + on_delete=models.CASCADE) article_order = models.IntegerField( - _('order'), blank=False, null=False, default=0) # 文章排序序号 - show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # 是否显示目录 + _('order'), blank=False, null=False, default=0) + show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) category = models.ForeignKey( 'Category', verbose_name=_('category'), on_delete=models.CASCADE, blank=False, - null=False) # 文章分类,外键关联Category模型 - tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # 文章标签,多对多关联Tag模型 + null=False) + tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) def body_to_string(self): - """将文章内容转换为字符串返回""" return self.body def __str__(self): - """自定义字符串表示,返回文章标题""" return self.title class Meta: - ordering = ['-article_order', '-pub_time'] # 排序规则:先按article_order降序,再按pub_time降序 + ordering = ['-article_order', '-pub_time'] verbose_name = _('article') verbose_name_plural = verbose_name get_latest_by = 'id' def get_absolute_url(self): - """获取文章的绝对URL,用于生成文章详情页链接""" return reverse('blog:detailbyid', kwargs={ 'article_id': self.id, 'year': self.creation_time.year, @@ -156,33 +126,18 @@ class Article(BaseModel): @cache_decorator(60 * 60 * 10) def get_category_tree(self): - """ - 获取文章分类的树形结构(包含当前分类及其所有父级分类),并缓存 - """ tree = self.category.get_category_tree() names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) return names - # 保留 save 方法 + def save(self, *args, **kwargs): - """重写save方法,调用父类save方法""" - # 如果你之前在这个方法里加了其他逻辑,比如处理slug等, - # 请确保保留下来。如果只是单纯调用父类,可以暂时保留, - # 或者也可以选择删除它,因为默认的save方法就是这样做的。 super().save(*args, **kwargs) - # 添加 viewed 方法 def viewed(self): - """文章被浏览时,浏览量加1并保存""" self.views += 1 self.save(update_fields=['views']) - - - def comment_list(self): - """ - 获取文章的评论列表,优先从缓存获取,缓存不存在则查询数据库并缓存 - """ cache_key = 'article_comments_{id}'.format(id=self.id) value = cache.get(cache_key) if value: @@ -195,25 +150,24 @@ class Article(BaseModel): return comments def get_admin_url(self): - """获取文章在admin后台的编辑URL""" info = (self._meta.app_label, self._meta.model_name) return reverse('admin:%s_%s_change' % info, args=(self.pk,)) @cache_decorator(expiration=60 * 100) def next_article(self): - """获取下一篇文章(id大于当前文章且已发布的第一篇),并缓存""" + # 下一篇 return Article.objects.filter( id__gt=self.id, status='p').order_by('id').first() @cache_decorator(expiration=60 * 100) def prev_article(self): - """获取前一篇文章(id小于当前文章且已发布的第一篇),并缓存""" + # 前一篇 return Article.objects.filter(id__lt=self.id, status='p').first() def get_first_image_url(self): """ - 从文章内容中提取第一张图片的URL - 通过正则表达式匹配markdown图片语法中的图片链接 + Get the first image url from article.body. + :return: """ match = re.search(r'!\[.*?\]\((.+?)\)', self.body) if match: @@ -256,45 +210,37 @@ class Article(BaseModel): return related_articles - # 新增:获取文章的收藏数 - def get_favorite_count(self): - """获取文章的收藏数""" - return self.favorites.count() - class Category(BaseModel): - """ - 文章分类模型类 - """ - name = models.CharField(_('category name'), max_length=30, unique=True) # 分类名称,唯一 + """文章分类""" + name = models.CharField(_('category name'), max_length=30, unique=True) parent_category = models.ForeignKey( 'self', verbose_name=_('parent category'), blank=True, null=True, - on_delete=models.CASCADE) # 父分类,自关联 - slug = models.SlugField(default='no-slug', max_length=60, blank=True) # 分类的slug,用于URL - index = models.IntegerField(default=0, verbose_name=_('index')) # 分类排序序号 + on_delete=models.CASCADE) + slug = models.SlugField(default='no-slug', max_length=60, blank=True) + index = models.IntegerField(default=0, verbose_name=_('index')) class Meta: - ordering = ['-index'] # 按index降序排序 + ordering = ['-index'] verbose_name = _('category') verbose_name_plural = verbose_name def get_absolute_url(self): - """获取分类的绝对URL,用于生成分类页链接""" return reverse( 'blog:category_detail', kwargs={ 'category_name': self.slug}) def __str__(self): - """自定义字符串表示,返回分类名称""" return self.name @cache_decorator(60 * 60 * 10) def get_category_tree(self): """ - 递归获取分类的树形结构(当前分类及其所有父级分类),并缓存 + 递归获得分类目录的父级 + :return: """ categorys = [] @@ -309,7 +255,8 @@ class Category(BaseModel): @cache_decorator(60 * 60 * 10) def get_sub_categorys(self): """ - 递归获取当前分类的所有子分类,包括子分类的子分类等,并缓存 + 获得当前分类目录所有子集 + :return: """ categorys = [] all_categorys = Category.objects.all() @@ -326,229 +273,138 @@ class Category(BaseModel): parse(self) return categorys + class Tag(BaseModel): - """ - 文章标签模型类 - """ - name = models.CharField(_('tag name'), max_length=30, unique=True) # 标签名称,唯一 - slug = models.SlugField(default='no-slug', max_length=60, blank=True) # 标签的slug,用于URL + """文章标签""" + name = models.CharField(_('tag name'), max_length=30, unique=True) + slug = models.SlugField(default='no-slug', max_length=60, blank=True) def __str__(self): - """自定义字符串表示,返回标签名称""" return self.name def get_absolute_url(self): - """获取标签的绝对URL,用于生成标签页链接""" return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) @cache_decorator(60 * 60 * 10) def get_article_count(self): - """获取该标签下的文章数量,并缓存""" return Article.objects.filter(tags__name=self.name).distinct().count() class Meta: - ordering = ['name'] # 按名称排序 + ordering = ['name'] verbose_name = _('tag') verbose_name_plural = verbose_name + class Links(models.Model): - """ - 友情链接模型类 - """ - name = models.CharField(_('link name'), max_length=30, unique=True) # 链接名称,唯一 - link = models.URLField(_('link')) # 链接URL - sequence = models.IntegerField(_('order'), unique=True) # 排序序号,唯一 + """友情链接""" + + name = models.CharField(_('link name'), max_length=30, unique=True) + link = models.URLField(_('link')) + sequence = models.IntegerField(_('order'), unique=True) is_enable = models.BooleanField( - _('is show'), default=True, blank=False, null=False) # 是否显示 + _('is show'), default=True, blank=False, null=False) show_type = models.CharField( _('show type'), max_length=1, choices=LinkShowType.choices, - default=LinkShowType.I) # 显示类型,关联LinkShowType枚举 - creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间 - last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间 + default=LinkShowType.I) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_mod_time = models.DateTimeField(_('modify time'), default=now) class Meta: - ordering = ['sequence'] # 按sequence排序 + ordering = ['sequence'] verbose_name = _('link') verbose_name_plural = verbose_name def __str__(self): - """自定义字符串表示,返回链接名称""" return self.name + class SideBar(models.Model): - """ - 侧边栏模型类,用于展示自定义HTML内容 - """ - name = models.CharField(_('title'), max_length=100) # 侧边栏标题 - content = models.TextField(_('content')) # 侧边栏内容(HTML) - sequence = models.IntegerField(_('order'), unique=True) # 排序序号,唯一 - is_enable = models.BooleanField(_('is enable'), default=True) # 是否启用 - creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间 - last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间 + """侧边栏,可以展示一些html内容""" + name = models.CharField(_('title'), max_length=100) + content = models.TextField(_('content')) + sequence = models.IntegerField(_('order'), unique=True) + is_enable = models.BooleanField(_('is enable'), default=True) + creation_time = models.DateTimeField(_('creation time'), default=now) + last_mod_time = models.DateTimeField(_('modify time'), default=now) class Meta: - ordering = ['sequence'] # 按sequence排序 + ordering = ['sequence'] verbose_name = _('sidebar') verbose_name_plural = verbose_name def __str__(self): - """自定义字符串表示,返回侧边栏标题""" return self.name + class BlogSettings(models.Model): - """ - 博客配置模型类,存储网站的各种配置信息 - """ + """blog的配置""" site_name = models.CharField( _('site name'), max_length=200, null=False, blank=False, - default='') # 网站名称 + default='') site_description = models.TextField( _('site description'), max_length=1000, null=False, blank=False, - default='') # 网站描述 + default='') site_seo_description = models.TextField( - _('site seo description'), max_length=1000, null=False, blank=False, default='') # 网站SEO描述 + _('site seo description'), max_length=1000, null=False, blank=False, default='') site_keywords = models.TextField( _('site keywords'), max_length=1000, null=False, blank=False, - default='') # 网站关键词 - article_sub_length = models.IntegerField(_('article sub length'), default=300) # 文章摘要长度 - sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # 侧边栏文章数量 - sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # 侧边栏评论数量 - article_comment_count = models.IntegerField(_('article comment count'), default=5) # 文章评论数量 - show_google_adsense = models.BooleanField(_('show adsense'), default=False) # 是否显示Google广告 + default='') + article_sub_length = models.IntegerField(_('article sub length'), default=300) + sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) + sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) + article_comment_count = models.IntegerField(_('article comment count'), default=5) + show_google_adsense = models.BooleanField(_('show adsense'), default=False) google_adsense_codes = models.TextField( - _('adsense code'), max_length=2000, null=True, blank=True, default='') # Google广告代码 - open_site_comment = models.BooleanField(_('open site comment'), default=True) # 是否开启网站评论 - global_header = models.TextField("公共头部", null=True, blank=True, default='') # 公共头部HTML - global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # 公共尾部HTML + _('adsense code'), max_length=2000, null=True, blank=True, default='') + open_site_comment = models.BooleanField(_('open site comment'), default=True) + global_header = models.TextField("公共头部", null=True, blank=True, default='') + global_footer = models.TextField("公共尾部", null=True, blank=True, default='') beian_code = models.CharField( '备案号', max_length=2000, null=True, blank=True, - default='') # 网站备案号 + default='') analytics_code = models.TextField( "网站统计代码", max_length=1000, null=False, blank=False, - default='') # 网站统计代码 + default='') show_gongan_code = models.BooleanField( - '是否显示公安备案号', default=False, null=False) # 是否显示公安备案号 + '是否显示公安备案号', default=False, null=False) gongan_beiancode = models.TextField( '公安备案号', max_length=2000, null=True, blank=True, - default='') # 公安备案号 + default='') comment_need_review = models.BooleanField( - '评论是否需要审核', default=False, null=False) # 评论是否需要审核 + '评论是否需要审核', default=False, null=False) class Meta: verbose_name = _('Website configuration') verbose_name_plural = verbose_name def __str__(self): - """自定义字符串表示,返回网站名称""" return self.site_name def clean(self): - """ - 模型验证方法,确保只能有一个配置实例 - 如果存在其他配置实例(排除当前实例),则抛出验证错误 - """ if BlogSettings.objects.exclude(id=self.id).count(): raise ValidationError(_('There can only be one configuration')) def save(self, *args, **kwargs): - """ - 重写save方法,保存后清除缓存(使配置变更立即生效) - """ super().save(*args, **kwargs) from djangoblog.utils import cache - cache.clear() - -class Favorite(models.Model): - """ - 文章收藏模型 - """ - article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='favorites') - # 使用 settings.AUTH_USER_MODEL - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='favorite_articles') - created_at = models.DateTimeField(auto_now_add=True) - - class Meta: - unique_together = ('article', 'user') # 一个用户只能收藏同一篇文章一次 - verbose_name = "收藏" - verbose_name_plural = "收藏" - - def __str__(self): - return f'{self.user.username} favorites {self.article.title}' - -class UserProfile(models.Model): - """ - 用户资料扩展模型 - 用于存储用户头像、个人简介等额外信息 - """ - user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='profile', verbose_name=_('User')) - - # 基本信息 - avatar = models.ImageField( - upload_to=user_avatar_path, - blank=True, - null=True, - verbose_name=_('Avatar'), - help_text=_('Upload your avatar image (recommended size: 100x100px)') - ) - bio = models.TextField( - blank=True, - null=True, - verbose_name=_('Biography'), - help_text=_('Tell us a little about yourself') - ) - - # 可选的社交链接 - website = models.URLField(blank=True, null=True, verbose_name=_('Website')) - github = models.URLField(blank=True, null=True, verbose_name=_('GitHub')) - twitter = models.URLField(blank=True, null=True, verbose_name=_('Twitter')) - weibo = models.URLField(blank=True, null=True, verbose_name=_('Weibo')) - - # 时间戳 - created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created At')) - updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated At')) - - class Meta: - verbose_name = _('User Profile') - verbose_name_plural = _('User Profiles') - ordering = ['-created_at'] - - def __str__(self): - return f"{self.user.username}'s Profile" - - def get_absolute_url(self): - """ - 定义用户资料页的绝对URL - """ - return reverse('blog:user_profile', kwargs={'username': self.user.username}) - -# 信号:当一个新的自定义用户(settings.AUTH_USER_MODEL)被创建时,自动创建一个关联的UserProfile -@receiver(post_save, sender=settings.AUTH_USER_MODEL) -def create_user_profile(sender, instance, created, **kwargs): - if created: - UserProfile.objects.create(user=instance) - -@receiver(post_save, sender=settings.AUTH_USER_MODEL) -def save_user_profile(sender, instance, **kwargs): - # 使用 get_or_create 防止在某些情况下(如手动创建用户时)profile不存在 - UserProfile.objects.get_or_create(user=instance) - instance.profile.save() + cache.clear() \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/search_indexes.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/search_indexes.py index f5d90bc..7f1dfac 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/search_indexes.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/search_indexes.py @@ -1,42 +1,13 @@ -<<<<<<< HEAD -# 从haystack框架导入索引相关的模块,用于实现全文搜索功能 from haystack import indexes -# 导入当前项目中blog应用的Article模型,该模型对应需要被索引的文章数据 from blog.models import Article -# 定义ArticleIndex类,继承自SearchIndex和Indexable,用于配置Article模型的搜索索引 -# SearchIndex:提供索引的核心功能,定义了如何从模型中提取数据构建索引 -# Indexable:标识该类可被索引,要求实现get_model方法来指定关联的模型 class ArticleIndex(indexes.SearchIndex, indexes.Indexable): - # 定义一个text字段作为文档的主要索引字段(document=True表示这是主要搜索字段) - # use_template=True表示使用模板来定义该字段需要索引的内容(通常在templates/search/indexes/blog/article_text.txt中配置) - # 该字段会聚合模型中需要被搜索的字段(如标题、正文等),作为全文搜索的基础 text = indexes.CharField(document=True, use_template=True) - # 实现Indexable接口的方法,返回当前索引关联的模型类 - # 作用:告诉haystack该索引对应的数据来自哪个模型 def get_model(self): return Article - # 定义需要被索引的查询集(即哪些数据会被纳入搜索范围) - # using参数用于指定搜索引擎(多引擎场景下使用),默认None表示使用默认引擎 - # 这里返回状态为'p'(假设表示"已发布")的文章,确保只有已发布的内容可被搜索 def index_queryset(self, using=None): return self.get_model().objects.filter(status='p') -======= -from haystack import indexes - -from blog.models import Article - - -class ArticleIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True) - - def get_model(self): - return Article - - def index_queryset(self, using=None): - return self.get_model().objects.filter(status='p') ->>>>>>> ccy_branch diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/templatetags/blog_tags.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/templatetags/blog_tags.py index 53f457d..d6cd5d5 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/templatetags/blog_tags.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/templatetags/blog_tags.py @@ -341,4 +341,4 @@ def query(qs, **kwargs): @register.filter def addstr(arg1, arg2): """concatenate arg1 & arg2""" - return str(arg1) + str(arg2) \ No newline at end of file + return str(arg1) + str(arg2) diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/urls.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/urls.py index 5badaca..e1c78e4 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/urls.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/urls.py @@ -1,197 +1,108 @@ -<<<<<<< HEAD -<<<<<<< HEAD -======= -# blog/urls.py - ->>>>>>> djq_branch # 导入 Django 内置的路径配置工具和缓存装饰器 from django.urls import path from django.views.decorators.cache import cache_page -# 导入当前应用(blog)的视图模块 +# 导入当前应用(blog)的视图模块,用于关联路由与视图逻辑 from . import views -# 定义应用命名空间 +# 定义应用命名空间(namespace),用于在模板或反向解析时区分不同应用的路由 +# 例如:在模板中使用 {% url 'blog:index' %} 生成首页链接 app_name = "blog" -# 路由配置列表 +# 路由配置列表,每个 path 对应一个 URL 规则与视图的映射 urlpatterns = [ - # 首页路由 + # 首页路由:匹配根路径(网站域名/) path( - '', - views.IndexView.as_view(), - name='index' + r'', # URL 路径表达式,空字符串表示根路径 + views.IndexView.as_view(), # 关联的视图类(IndexView),通过 as_view() 转换为可调用视图 + name='index' # 路由名称,用于反向解析(如 reverse('blog:index')) ), + + # 分页首页路由:匹配带页码的首页(如 /page/2/) path( - 'page//', - views.IndexView.as_view(), + r'page//', # 是路径参数,int 表示接收整数类型,page 是参数名 + views.IndexView.as_view(), # 复用首页视图类,视图中会通过 page 参数处理分页 name='index_page' ), - # 文章详情页路由 - path( - 'article//', - views.ArticleDetailView.as_view(), - name='article_detail' - ), - # 原始的带日期的URL保持不变 + # 文章详情页路由:按日期和文章ID匹配(如 /article/2023/10/20/100.html) path( - 'article////.html', - views.ArticleDetailView.as_view(), + r'article////.html', + # 路径参数:year(年)、month(月)、day(日)、article_id(文章ID),均为整数 + views.ArticleDetailView.as_view(), # 文章详情视图类,处理文章展示逻辑 name='detailbyid' ), - # 文章收藏功能路由 - path( - 'article//favorite/', - views.ArticleFavoriteView.as_view(), - name='article_favorite' - ), - - # 分类详情页路由 + # 分类详情页路由:按分类名匹配(如 /category/tech.html) path( - 'category/.html', - views.CategoryDetailView.as_view(), + r'category/.html', + # :slug 类型表示接收字母、数字、下划线和连字符组成的字符串(适合URL友好的名称) + views.CategoryDetailView.as_view(), # 分类详情视图类,展示该分类下的文章 name='category_detail' ), + + # 分类详情分页路由:带页码的分类页(如 /category/tech/2.html) path( - 'category//.html', - views.CategoryDetailView.as_view(), + r'category//.html', + views.CategoryDetailView.as_view(), # 复用分类视图类,通过 page 参数分页 name='category_detail_page' ), - # 作者详情页路由 + # 作者详情页路由:按作者名匹配(如 /author/alice.html) path( - 'author/.html', - views.AuthorDetailView.as_view(), + r'author/.html', + # :未指定类型,默认接收字符串(除特殊字符外) + views.AuthorDetailView.as_view(), # 作者详情视图类,展示该作者的文章 name='author_detail' ), + + # 作者详情分页路由:带页码的作者页(如 /author/alice/2.html) path( - 'author//.html', - views.AuthorDetailView.as_view(), + r'author//.html', + views.AuthorDetailView.as_view(), # 复用作者视图类,通过 page 参数分页 name='author_detail_page' ), - # 标签详情页路由 + # 标签详情页路由:按标签名匹配(如 /tag/python.html) path( - 'tag/.html', - views.TagDetailView.as_view(), + r'tag/.html', + views.TagDetailView.as_view(), # 标签详情视图类,展示该标签下的文章 name='tag_detail' ), + + # 标签详情分页路由:带页码的标签页(如 /tag/python/2.html) path( - 'tag//.html', - views.TagDetailView.as_view(), + r'tag//.html', + views.TagDetailView.as_view(), # 复用标签视图类,通过 page 参数分页 name='tag_detail_page' ), - # 归档页路由 + # 归档页路由:匹配 /archives.html path( 'archives.html', + # 缓存装饰器:cache_page(60*60) 表示缓存该页面1小时(60秒*60),减轻服务器压力 cache_page(60 * 60)(views.ArchivesView.as_view()), - name='archives' + name='archives' # 归档视图,通常展示按日期分组的文章列表 ), - # 友情链接页路由 + # 友情链接页路由:匹配 /links.html path( 'links.html', - views.LinkListView.as_view(), + views.LinkListView.as_view(), # 友情链接视图类,展示网站链接列表 name='links' ), - # ==================== 关键修正:调整 URL 顺序 ==================== - # 用户资料相关路由 - # 将具体的路径放在通用路径之前 - path( - 'profile/edit/', - views.UserProfileUpdateView.as_view(), - name='user_profile_update' - ), - # 我的收藏页面路由 + # 文件上传路由:匹配 /upload path( - 'profile/favorites/', - views.UserFavoritesView.as_view(), - name='user_favorites' - ), - # 通用的用户资料路径放在最后 - path( - 'profile//', - views.UserProfileDetailView.as_view(), - name='user_profile' - ), - # ================================================================= - - # 其他功能路由 - path( - 'upload', - views.fileupload, + r'upload', + views.fileupload, # 关联函数视图(非类视图),处理文件上传逻辑 name='upload' ), - path( - 'clean', - views.clean_cache_view, - name='clean' - ), -] -from django.urls import path -from django.views.decorators.cache import cache_page -from . import views - -app_name = "blog" -urlpatterns = [ - path( - r'', - views.IndexView.as_view(), - name='index'), - path( - r'page//', - views.IndexView.as_view(), - name='index_page'), - path( - r'article////.html', - views.ArticleDetailView.as_view(), - name='detailbyid'), - path( - r'category/.html', - views.CategoryDetailView.as_view(), - name='category_detail'), - path( - r'category//.html', - views.CategoryDetailView.as_view(), - name='category_detail_page'), - path( - r'author/.html', - views.AuthorDetailView.as_view(), - name='author_detail'), - path( - r'author//.html', - views.AuthorDetailView.as_view(), - name='author_detail_page'), - path( - r'tag/.html', - views.TagDetailView.as_view(), - name='tag_detail'), - path( - r'tag//.html', - views.TagDetailView.as_view(), - name='tag_detail_page'), - path( - 'archives.html', - cache_page( - 60 * 60)( - views.ArchivesView.as_view()), - name='archives'), - path( - 'links.html', - views.LinkListView.as_view(), - name='links'), - path( - r'upload', - views.fileupload, - name='upload'), + # 缓存清理路由:匹配 /clean path( r'clean', - views.clean_cache_view, - name='clean'), -] - + views.clean_cache_view, # 关联缓存清理视图,用于手动触发缓存清理 + name='clean' + ), +] \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/views.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/views.py index b0c38f9..b207192 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/views.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/views.py @@ -4,26 +4,18 @@ import uuid from django.conf import settings from django.core.paginator import Paginator -from django.http import HttpResponse, HttpResponseForbidden, JsonResponse +from django.http import HttpResponse, HttpResponseForbidden from django.shortcuts import get_object_or_404 -from django.shortcuts import render, redirect +from django.shortcuts import render from django.templatetags.static import static from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.views.decorators.csrf import csrf_exempt from django.views.generic.detail import DetailView from django.views.generic.list import ListView -from django.views.generic import View, TemplateView -from django.contrib.auth.mixins import LoginRequiredMixin from haystack.views import SearchView -from django.contrib.auth.decorators import login_required -from django.contrib import messages -from django.http import HttpResponseRedirect - -# 导入自定义用户模型 -from accounts.models import BlogUser -from blog.models import Article, Category, LinkShowType, Links, Tag, Favorite, UserProfile +from blog.models import Article, Category, LinkShowType, Links, Tag from comments.forms import CommentForm from djangoblog.plugin_manage import hooks from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME @@ -31,6 +23,7 @@ from djangoblog.utils import cache, get_blog_setting, get_sha256 logger = logging.getLogger(__name__) + class ArticleListView(ListView): # template_name属性用于指定使用哪个模板进行渲染 template_name = 'blog/article_index.html' @@ -103,6 +96,7 @@ class ArticleListView(ListView): kwargs['hot_articles'] = hot_articles return super(ArticleListView, self).get_context_data(** kwargs) + class IndexView(ArticleListView): ''' 首页 @@ -118,6 +112,7 @@ class IndexView(ArticleListView): cache_key = 'index_{page}'.format(page=self.page_number) return cache_key + class ArticleDetailView(DetailView): ''' 文章详情页面 @@ -176,10 +171,6 @@ class ArticleDetailView(DetailView): logger.info('set hot articles cache') kwargs['hot_articles'] = hot_articles - # 新增:判断当前用户是否收藏了该文章 - if self.request.user.is_authenticated: - kwargs['is_favorited'] = Favorite.objects.filter(article=self.object, user=self.request.user).exists() - context = super(ArticleDetailView, self).get_context_data(** kwargs) article = self.object # Action Hook, 通知插件"文章详情已获取" @@ -190,6 +181,7 @@ class ArticleDetailView(DetailView): return context + class CategoryDetailView(ArticleListView): ''' 分类目录列表 @@ -227,6 +219,7 @@ class CategoryDetailView(ArticleListView): kwargs['tag_name'] = categoryname return super(CategoryDetailView, self).get_context_data(** kwargs) + class AuthorDetailView(ArticleListView): ''' 作者详情页 @@ -242,7 +235,6 @@ class AuthorDetailView(ArticleListView): def get_queryset_data(self): author_name = self.kwargs['author_name'] - # 这里使用 BlogUser 或 settings.AUTH_USER_MODEL 都是安全的 article_list = Article.objects.filter( author__username=author_name, type='a', status='p') return article_list @@ -253,6 +245,7 @@ class AuthorDetailView(ArticleListView): kwargs['tag_name'] = author_name return super(AuthorDetailView, self).get_context_data(** kwargs) + class TagDetailView(ArticleListView): ''' 标签列表页面 @@ -283,6 +276,7 @@ class TagDetailView(ArticleListView): kwargs['tag_name'] = tag_name return super(TagDetailView, self).get_context_data(** kwargs) + class ArchivesView(ArticleListView): ''' 文章归档页面 @@ -299,7 +293,7 @@ class ArchivesView(ArticleListView): cache_key = 'archives' return cache_key - def get_context_data(self, **kwargs): + def get_context_data(self,** kwargs): # 归档页单独添加热门文章(因继承自ArticleListView但需确保显示) hot_articles_cache_key = 'hot_articles' hot_articles = cache.get(hot_articles_cache_key) @@ -308,7 +302,8 @@ class ArchivesView(ArticleListView): cache.set(hot_articles_cache_key, hot_articles, 60 * 60) logger.info('set hot articles cache') kwargs['hot_articles'] = hot_articles - return super(ArchivesView, self).get_context_data(** kwargs) + return super(ArchivesView, self).get_context_data(**kwargs) + class LinkListView(ListView): model = Links @@ -317,7 +312,7 @@ class LinkListView(ListView): def get_queryset(self): return Links.objects.filter(is_enable=True) - def get_context_data(self, **kwargs): + def get_context_data(self,** kwargs): # 链接页添加热门文章 hot_articles_cache_key = 'hot_articles' hot_articles = cache.get(hot_articles_cache_key) @@ -326,7 +321,8 @@ class LinkListView(ListView): cache.set(hot_articles_cache_key, hot_articles, 60 * 60) logger.info('set hot articles cache') kwargs['hot_articles'] = hot_articles - return super(LinkListView, self).get_context_data(** kwargs) + return super(LinkListView, self).get_context_data(**kwargs) + class EsSearchView(SearchView): def get_context(self): @@ -341,6 +337,7 @@ class EsSearchView(SearchView): context['hot_articles'] = hot_articles return context + @csrf_exempt def fileupload(request): """ @@ -382,6 +379,7 @@ def fileupload(request): else: return HttpResponse("only for post") + def page_not_found_view( request, exception, @@ -395,6 +393,7 @@ def page_not_found_view( 'statuscode': '404'}, status=404) + def server_error_view(request, template_name='blog/error_page.html'): return render(request, template_name, @@ -402,6 +401,7 @@ def server_error_view(request, template_name='blog/error_page.html'): 'statuscode': '500'}, status=500) + def permission_denied_view( request, exception, @@ -413,6 +413,7 @@ def permission_denied_view( 'message': _('Sorry, you do not have permission to access this page?'), 'statuscode': '403'}, status=403) + def clean_cache_view(request): cache.clear() return HttpResponse('ok') @@ -476,118 +477,4 @@ class DjangoBlogFeed(Feed): 返回单个项目(文章)的发布日期。 这是可选的,但推荐添加,以符合 RSS 规范。 """ - return item.creation_time - -# ====================================================================== -# 收藏功能视图 (修正和统一) -# ====================================================================== - -class ArticleFavoriteView(LoginRequiredMixin, View): - """ - 处理文章收藏/取消收藏的视图 (统一处理) - """ - http_method_names = ['post'] # 只允许 POST 请求 - - def post(self, request, *args, **kwargs): - article_id = kwargs.get('article_id') - article = get_object_or_404(Article, pk=article_id) - - # get_or_create 是一个原子操作,能有效防止并发问题 - favorite, created = Favorite.objects.get_or_create(article=article, user=request.user) - - if not created: - # 如果记录已存在,则删除(取消收藏) - favorite.delete() - is_favorite = False - else: - # 如果是新创建,则表示收藏成功 - is_favorite = True - - # 返回更新后的收藏数和状态 - favorite_count = article.get_favorite_count() - return JsonResponse({'is_favorite': is_favorite, 'favorite_count': favorite_count}) - -class UserFavoritesView(LoginRequiredMixin, ListView): - """ - 展示用户收藏的所有文章 (基于类的视图) - """ - model = Article - template_name = 'blog/user_favorites.html' - context_object_name = 'favorite_articles' - paginate_by = 10 # 每页显示10篇 - - def get_queryset(self): - # 获取当前用户的所有收藏,并按收藏时间倒序排列 - # 使用 select_related 和 prefetch_related 优化数据库查询 - return Article.objects.filter( - favorites__user=self.request.user - ).select_related('author', 'category').prefetch_related('tags').order_by('-favorites__created_at') - - def get_context_data(self,** kwargs): - context = super().get_context_data(**kwargs) - # 添加 profile_user 用于复用个人资料页面的侧边栏 - context['profile_user'] = self.request.user - return context - -# ====================================================================== -# 用户资料视图 -# ====================================================================== - -from django.views.generic import UpdateView -from django.urls import reverse_lazy -from .forms import UserProfileUpdateForm # 确保你有这个表单文件 - -class UserProfileDetailView(DetailView): - """ - 显示用户公开资料的视图 - """ - model = UserProfile - template_name = 'blog/user_profile_detail.html' - context_object_name = 'profile' - - def get_object(self, queryset=None): - """通过用户名查找用户,然后返回其 profile""" - username = self.kwargs.get('username') - # 修正:使用自定义的 BlogUser 模型进行查询 - user = get_object_or_404(BlogUser, username=username) - return get_object_or_404(UserProfile, user=user) - - def get_context_data(self, **kwargs): - """添加额外的上下文数据,例如用户发布的文章""" - context = super().get_context_data(** kwargs) - user = self.object.user # 从 profile 对象获取 user - # 获取该用户发布的所有公开文章,并按发布时间排序 - context['user_articles'] = Article.objects.filter(author=user, status='p').order_by('-pub_time')[:10] - return context - -class UserProfileUpdateView(LoginRequiredMixin, UpdateView): - """ - 用户编辑自己资料的视图 - """ - model = UserProfile - form_class = UserProfileUpdateForm - template_name = 'blog/user_profile_update.html' - - def get_queryset(self): - """ - 重写此方法以确保用户只能编辑自己的资料。 - 这是对象级权限控制的最佳实践。 - """ - # 只返回与当前登录用户关联的 UserProfile 对象 - return UserProfile.objects.filter(user=self.request.user) - - def get_object(self, queryset=None): - """ - 用户只能编辑自己的 profile。 - 此方法与 get_queryset 结合,提供了双重保障。 - """ - return self.request.user.profile - - def get_success_url(self): - """编辑成功后重定向到自己的资料页""" - return reverse_lazy('blog:user_profile', kwargs={'username': self.request.user.username}) - - def form_valid(self, form): - """表单验证成功后,显示成功消息""" - messages.success(self.request, 'Your profile has been updated successfully!') - return super().form_valid(form) \ No newline at end of file + return item.creation_time \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/comments/views.py b/src/DjangoBlog-master(1)/DjangoBlog-master/comments/views.py index cc2ec6b..ad9b2b9 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/comments/views.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/comments/views.py @@ -13,66 +13,51 @@ from .models import Comment class CommentPostView(FormView): - """处理评论提交的视图类,继承自Django的FormView""" + form_class = CommentForm + template_name = 'blog/article_detail.html' - form_class = CommentForm # 指定使用的表单类 - template_name = 'blog/article_detail.html' # 指定渲染的模板 - - @method_decorator(csrf_protect) # 添加CSRF保护装饰器,防止跨站请求伪造 + @method_decorator(csrf_protect) def dispatch(self, *args, **kwargs): - """重写dispatch方法,在请求处理前后执行额外逻辑""" return super(CommentPostView, self).dispatch(*args, **kwargs) def get(self, request, *args, **kwargs): - """处理GET请求 - 重定向到文章详情页的评论区域""" - article_id = self.kwargs['article_id'] # 从URL参数获取文章ID - article = get_object_or_404(Article, pk=article_id) # 获取文章对象,不存在则返回404 - url = article.get_absolute_url() # 获取文章的绝对URL - return HttpResponseRedirect(url + "#comments") # 重定向到文章页的评论锚点 + 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'] # 获取文章ID - article = get_object_or_404(Article, pk=article_id) # 获取文章对象 + article_id = self.kwargs['article_id'] + article = get_object_or_404(Article, pk=article_id) - # 重新渲染页面,显示表单错误信息 return self.render_to_response({ - 'form': form, # 包含错误信息的表单 - 'article': article # 文章对象 + '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'] # 从URL参数获取文章ID - article = get_object_or_404(Article, pk=article_id) # 获取文章对象 + 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 # 设置评论关联的文章 - + raise ValidationError("该文章评论已关闭.") + comment = form.save(False) + comment.article = article from djangoblog.utils import get_blog_setting - settings = get_blog_setting() # 获取博客设置 - - # 如果博客设置不需要审核评论,则直接启用评论 + settings = get_blog_setting() if not settings.comment_need_review: comment.is_enable = True + comment.author = author - 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) # 保存评论到数据库 + 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)) \ No newline at end of file + "%s#div-comment-%d" % + (article.get_absolute_url(), comment.pk)) diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/admin_site.py b/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/admin_site.py index 40b03fc..f120405 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/admin_site.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/admin_site.py @@ -40,7 +40,7 @@ class DjangoBlogAdminSite(AdminSite): admin_site = DjangoBlogAdminSite(name='admin') -admin_site.register(Article, ArticleAdmin) +admin_site.register(Article, ArticlelAdmin) admin_site.register(Category, CategoryAdmin) admin_site.register(Tag, TagAdmin) admin_site.register(Links, LinksAdmin) diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/settings.py b/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/settings.py index 232cc2b..153c0ef 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/settings.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/settings.py @@ -12,8 +12,15 @@ 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 @@ -23,17 +30,18 @@ BASE_DIR = Path(__file__).resolve().parent.parent # 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 = os.environ.get('DJANGO_DEBUG', 'True') == 'True' +DEBUG = env_to_bool('DJANGO_DEBUG', True) +# DEBUG = False TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' # ALLOWED_HOSTS = [] -ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '*').split(',') +ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] # django 4.0新增配置 -CSRF_TRUSTED_ORIGINS = os.environ.get('DJANGO_CSRF_TRUSTED_ORIGINS', 'http://localhost,http://127.0.0.1').split(',') - +CSRF_TRUSTED_ORIGINS = ['http://example.com'] # Application definition + + INSTALLED_APPS = [ # 'django.contrib.admin', 'django.contrib.admin.apps.SimpleAdminConfig', @@ -57,6 +65,7 @@ INSTALLED_APPS = [ ] MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', @@ -95,6 +104,8 @@ WSGI_APPLICATION = 'djangoblog.wsgi.application' # Database # https://docs.djangoproject.com/en/1.10/ref/settings/#databases + + DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', @@ -102,16 +113,15 @@ DATABASES = { 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or '123456', 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', - 'PORT': int(os.environ.get('DJANGO_MYSQL_PORT') or 3306), + 'PORT': int( + os.environ.get('DJANGO_MYSQL_PORT') or 3306), 'OPTIONS': { - 'charset': 'utf8mb4', - 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", - }, - } -} + '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', @@ -137,31 +147,18 @@ LOCALE_PATHS = ( ) 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/ -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') -# --- MODIFICATION: 自动创建静态文件目录 --- -STATIC_DIR = os.path.join(BASE_DIR, 'static') -STATICFILES_DIRS = [STATIC_DIR] -# 如果 static 目录不存在,则自动创建 -if not os.path.exists(STATIC_DIR): - os.makedirs(STATIC_DIR) -# --- MODIFICATION END --- - -# 媒体文件配置 (用户上传的文件,如头像) -# 确保此配置已正确设置 -MEDIA_URL = '/media/' -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') -# 同样,自动创建媒体文件目录 -if not os.path.exists(MEDIA_ROOT): - os.makedirs(MEDIA_ROOT) HAYSTACK_CONNECTIONS = { 'default': { @@ -171,11 +168,15 @@ HAYSTACK_CONNECTIONS = { } # 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/' @@ -191,7 +192,6 @@ BOOTSTRAP_COLOR_TYPES = [ PAGINATE_BY = 10 # http cache timeout CACHE_CONTROL_MAX_AGE = 2592000 - # cache setting CACHES = { 'default': { @@ -215,18 +215,16 @@ BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \ # Email: EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_USE_TLS = os.environ.get('DJANGO_EMAIL_TLS', 'False') == 'True' -EMAIL_USE_SSL = os.environ.get('DJANGO_EMAIL_SSL', 'True') == 'True' +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' @@ -302,14 +300,10 @@ STATICFILES_FINDERS = ( # other 'compressor.finders.CompressorFinder', ) - -# --- MODIFICATION: 动态设置压缩开关 --- -# 在开发环境(DEBUG=True)关闭压缩,在生产环境(DEBUG=False)开启压缩 -COMPRESS_ENABLED = not DEBUG -# --- MODIFICATION END --- - +COMPRESS_ENABLED = True # COMPRESS_OFFLINE = True + COMPRESS_CSS_FILTERS = [ # creates absolute urls from relative ones 'compressor.filters.css_default.CssAbsoluteFilter', @@ -320,6 +314,8 @@ 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' @@ -345,16 +341,3 @@ ACTIVE_PLUGINS = [ 'view_count', 'seo_optimizer' ] -import os -from pathlib import Path - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - -# ... 其他配置 ... - -# 媒体文件(用户上传的文件)的存储根目录 -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') - -# 媒体文件的 URL 前缀。浏览器通过这个 URL 来访问媒体文件。 -MEDIA_URL = '/media/' \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/tests.py b/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/tests.py index 4be42d4..01237d9 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/tests.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/tests.py @@ -1,7 +1,6 @@ from django.test import TestCase from djangoblog.utils import * -<<<<<<< HEAD class DjangoBlogTest(TestCase): @@ -11,21 +10,6 @@ class DjangoBlogTest(TestCase): def test_utils(self): md5 = get_sha256('test') self.assertIsNotNone(md5) -======= -# 定义测试类,继承自TestCase,用于测试Django博客项目中的工具类/函数 -class DjangoBlogTest(TestCase): - # 本测试用例无需前置初始化操作,故保持空实现 - def setUp(self): - pass - - # 1. 测试SHA256加密工具函数get_sha256 - # 对字符串'test'进行SHA256加密,获取加密结果 - def test_utils(self): - md5 = get_sha256('test') - self.assertIsNotNone(md5) - # 2. 测试Markdown解析工具类CommonMarkdown - # 调用get_markdown方法,解析一段包含多种元素的Markdown文本 ->>>>>>> ccy_branch c = CommonMarkdown.get_markdown(''' # Title1 diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/media/user_2/avatar/173167639100-95311.jpg b/src/DjangoBlog-master(1)/DjangoBlog-master/media/user_2/avatar/173167639100-95311.jpg deleted file mode 100644 index 68d3f17..0000000 Binary files a/src/DjangoBlog-master(1)/DjangoBlog-master/media/user_2/avatar/173167639100-95311.jpg and /dev/null differ diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/media/user_2/avatar/173167639100-95311_6MV3mFh.jpg b/src/DjangoBlog-master(1)/DjangoBlog-master/media/user_2/avatar/173167639100-95311_6MV3mFh.jpg deleted file mode 100644 index 68d3f17..0000000 Binary files a/src/DjangoBlog-master(1)/DjangoBlog-master/media/user_2/avatar/173167639100-95311_6MV3mFh.jpg and /dev/null differ diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/media/user_2/avatar/IMG_24891.jpg b/src/DjangoBlog-master(1)/DjangoBlog-master/media/user_2/avatar/IMG_24891.jpg deleted file mode 100644 index 1fd9c6b..0000000 Binary files a/src/DjangoBlog-master(1)/DjangoBlog-master/media/user_2/avatar/IMG_24891.jpg and /dev/null differ diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/admin.py b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/admin.py index a272f3a..57eab5f 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/admin.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/admin.py @@ -5,72 +5,10 @@ from django.contrib import admin from django.urls import reverse from django.utils.html import format_html -<<<<<<< HEAD -# 获取当前模块的日志记录器 -======= ->>>>>>> ccy_branch logger = logging.getLogger(__name__) class OAuthUserAdmin(admin.ModelAdmin): -<<<<<<< HEAD - """ - OAuth用户管理后台配置类 - 用于自定义Django Admin中OAuth用户模型的显示和行为 - """ - - # 搜索字段配置,支持按昵称和邮箱搜索 - search_fields = ('nickname', 'email') - - # 每页显示的项目数量 - list_per_page = 20 - - # 列表页显示的字段 - list_display = ( - 'id', - 'nickname', - 'link_to_usermodel', # 自定义方法:链接到用户模型 - 'show_user_image', # 自定义方法:显示用户头像 - 'type', # OAuth类型 - 'email', - ) - - # 可点击进入编辑页的字段 - list_display_links = ('id', 'nickname') - - # 右侧筛选器配置 - list_filter = ('author', 'type',) - - # 只读字段列表,开始为空 - readonly_fields = [] - - def get_readonly_fields(self, request, obj=None): - """ - 动态获取只读字段 - 将所有模型字段和多对多字段设为只读 - """ - return list(self.readonly_fields) + \ - [field.name for field in obj._meta.fields] + \ - [field.name for field in obj._meta.many_to_many] - - def has_add_permission(self, request): - """ - 禁用添加权限 - OAuth用户应该通过OAuth流程自动创建,而不是手动添加 - """ - return False - - def link_to_usermodel(self, obj): - """ - 自定义方法:生成指向关联用户模型的链接 - """ - if obj.author: - # 获取用户模型的app和模型名称信息 - info = (obj.author._meta.app_label, obj.author._meta.model_name) - # 生成用户编辑页面的URL - link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) - # 返回HTML格式的链接 -======= search_fields = ('nickname', 'email') list_per_page = 20 list_display = ( @@ -97,44 +35,20 @@ class OAuthUserAdmin(admin.ModelAdmin): if obj.author: info = (obj.author._meta.app_label, obj.author._meta.model_name) link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) ->>>>>>> ccy_branch return format_html( u'%s' % (link, obj.author.nickname if obj.author.nickname else obj.author.email)) def show_user_image(self, obj): -<<<<<<< HEAD - """ - 自定义方法:在管理后台显示用户头像 - """ - img = obj.picture # 获取头像图片URL -======= img = obj.picture ->>>>>>> ccy_branch return format_html( u'' % (img)) -<<<<<<< HEAD - # 设置自定义方法在管理后台的显示名称 -======= ->>>>>>> ccy_branch link_to_usermodel.short_description = '用户' show_user_image.short_description = '用户头像' class OAuthConfigAdmin(admin.ModelAdmin): -<<<<<<< HEAD - """ - OAuth配置管理后台配置类 - 用于管理不同OAuth服务的配置信息 - """ - - # 列表页显示的字段 - list_display = ('type', 'appkey', 'appsecret', 'is_enable') - - # 右侧筛选器配置 -======= list_display = ('type', 'appkey', 'appsecret', 'is_enable') ->>>>>>> ccy_branch list_filter = ('type',) diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/apps.py b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/apps.py index 637f7a3..17fcea2 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/apps.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/apps.py @@ -2,18 +2,4 @@ from django.apps import AppConfig class OauthConfig(AppConfig): -<<<<<<< HEAD - """ - OAuth应用配置类 - 用于配置Django中OAuth应用的元数据和启动行为 - - 这个类继承自Django的AppConfig基类,用于定义 - OAuth应用在Django项目中的配置信息 - """ - - # 指定应用的Python路径,Django使用这个名称来识别应用 - # 这应该与应用目录的名称保持相同 - name = 'oauth' -======= name = 'oauth' ->>>>>>> ccy_branch diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/forms.py b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/forms.py index e59d538..0e4ede3 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/forms.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/forms.py @@ -3,37 +3,6 @@ from django.forms import widgets class RequireEmailForm(forms.Form): -<<<<<<< HEAD - """ - 邮箱验证表单类 - 用于在OAuth登录过程中要求用户提供邮箱地址 - 通常在第三方OAuth服务没有返回邮箱信息时使用 - """ - - # 邮箱字段,标签显示为'电子邮箱' - email = forms.EmailField(label='电子邮箱', required=True) - - # OAuth用户ID隐藏字段,用于关联OAuth用户记录 - oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False) - - def __init__(self, *args, **kwargs): - """ - 初始化方法,自定义表单字段的显示属性 - - Args: - *args: 位置参数 - **kwargs: 关键字参数 - """ - # 调用父类的初始化方法 - super(RequireEmailForm, self).__init__(*args, **kwargs) - - # 自定义邮箱字段的输入控件,添加占位符和CSS类 - self.fields['email'].widget = widgets.EmailInput( - attrs={ - 'placeholder': "email", # 输入框内的提示文字 - "class": "form-control" # Bootstrap样式类 - }) -======= email = forms.EmailField(label='电子邮箱', required=True) oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False) @@ -41,4 +10,3 @@ class RequireEmailForm(forms.Form): super(RequireEmailForm, self).__init__(*args, **kwargs) self.fields['email'].widget = widgets.EmailInput( attrs={'placeholder': "email", "class": "form-control"}) ->>>>>>> ccy_branch diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/models.py b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/models.py index bd12f4b..be838ed 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/models.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/models.py @@ -7,87 +7,11 @@ from django.utils.translation import gettext_lazy as _ class OAuthUser(models.Model): -<<<<<<< HEAD - """ - OAuth用户模型 - 用于存储通过第三方OAuth服务登录的用户信息 - """ - - # 关联到系统的本地用户,允许为空 -======= ->>>>>>> ccy_branch author = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('author'), blank=True, null=True, -<<<<<<< HEAD - on_delete=models.CASCADE) # 删除用户时级联删除OAuth记录 - - # OAuth服务提供的用户唯一标识 - openid = models.CharField(max_length=50) - - # 用户在第三方平台的昵称 - nickname = models.CharField(max_length=50, verbose_name=_('nick name')) - - # OAuth访问令牌,用于后续API调用 - token = models.CharField(max_length=150, null=True, blank=True) - - # 用户在第三方平台的头像URL - picture = models.CharField(max_length=350, blank=True, null=True) - - # OAuth服务类型(如github、weibo等) - type = models.CharField(blank=False, null=False, max_length=50) - - # 用户在第三方平台的邮箱 - email = models.CharField(max_length=50, null=True, blank=True) - - # 存储额外的OAuth返回数据(JSON格式) - metadata = models.TextField(null=True, blank=True) - - # 记录创建时间 - creation_time = models.DateTimeField(_('creation time'), default=now) - - # 最后修改时间 - last_modify_time = models.DateTimeField(_('last modify time'), default=now) - - def __str__(self): - """返回对象的字符串表示,显示用户昵称""" - return self.nickname - - class Meta: - """模型元数据配置""" - verbose_name = _('oauth user') # 单数名称 - verbose_name_plural = verbose_name # 复数名称 - ordering = ['-creation_time'] # 按创建时间降序排列 - - -class OAuthConfig(models.Model): - """ - OAuth服务配置模型 - 用于存储不同OAuth服务的应用配置信息 - """ - - # OAuth服务类型选择项 - TYPE = ( - ('weibo', _('weibo')), # 微博 - ('google', _('google')), # 谷歌 - ('github', 'GitHub'), # GitHub - ('facebook', 'FaceBook'), # Facebook - ('qq', 'QQ'), # QQ - ) - - # OAuth服务类型,从预定义选项中选择 - type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a') - - # OAuth应用的AppKey/Client ID - appkey = models.CharField(max_length=200, verbose_name='AppKey') - - # OAuth应用的AppSecret/Client Secret - appsecret = models.CharField(max_length=200, verbose_name='AppSecret') - - # OAuth回调URL -======= on_delete=models.CASCADE) openid = models.CharField(max_length=50) nickname = models.CharField(max_length=50, verbose_name=_('nick name')) @@ -119,56 +43,25 @@ class OAuthConfig(models.Model): type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a') appkey = models.CharField(max_length=200, verbose_name='AppKey') appsecret = models.CharField(max_length=200, verbose_name='AppSecret') ->>>>>>> ccy_branch callback_url = models.CharField( max_length=200, verbose_name=_('callback url'), blank=False, default='') -<<<<<<< HEAD - - # 是否启用该OAuth服务 - is_enable = models.BooleanField( - _('is enable'), default=True, blank=False, null=False) - - # 配置创建时间 - creation_time = models.DateTimeField(_('creation time'), default=now) - - # 配置最后修改时间 - last_modify_time = models.DateTimeField(_('last modify time'), default=now) - - def clean(self): - """ - 数据验证方法 - 确保同类型的OAuth配置只能有一个 - """ -======= is_enable = models.BooleanField( _('is enable'), default=True, blank=False, null=False) creation_time = models.DateTimeField(_('creation time'), default=now) last_modify_time = models.DateTimeField(_('last modify time'), default=now) def clean(self): ->>>>>>> ccy_branch if OAuthConfig.objects.filter( type=self.type).exclude(id=self.id).count(): raise ValidationError(_(self.type + _('already exists'))) def __str__(self): -<<<<<<< HEAD - """返回对象的字符串表示,显示OAuth服务类型""" - return self.type - - class Meta: - """模型元数据配置""" - verbose_name = 'oauth配置' # 单数名称(中文) - verbose_name_plural = verbose_name # 复数名称 - ordering = ['-creation_time'] # 按创建时间降序排列 -======= return self.type class Meta: verbose_name = 'oauth配置' verbose_name_plural = verbose_name ordering = ['-creation_time'] ->>>>>>> ccy_branch diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/oauthmanager.py b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/oauthmanager.py index 23f4315..2e7ceef 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/oauthmanager.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/oauthmanager.py @@ -9,115 +9,86 @@ import requests from djangoblog.utils import cache_decorator from oauth.models import OAuthUser, OAuthConfig -import logging -import requests -import json -import urllib.parse -import os -from abc import ABCMeta, abstractmethod -from django.core.cache import cache -from cache_decorator import cache_decorator - -# 获取logger实例 logger = logging.getLogger(__name__) class OAuthAccessTokenException(Exception): ''' - OAuth授权失败异常类 + oauth授权失败异常 ''' class BaseOauthManager(metaclass=ABCMeta): - """OAuth授权管理器基类""" - - # 授权URL + """获取用户授权""" AUTH_URL = None - # 获取token的URL + """获取token""" TOKEN_URL = None - # 获取用户信息的API URL + """获取用户信息""" API_URL = None - # icon图标名 + '''icon图标名''' ICON_NAME = None def __init__(self, access_token=None, openid=None): - """ - 初始化OAuth管理器 - - Args: - access_token: 访问令牌 - openid: 用户唯一标识 - """ self.access_token = access_token self.openid = openid @property def is_access_token_set(self): - """检查access_token是否已设置""" return self.access_token is not None @property def is_authorized(self): - """检查是否已授权(既有access_token又有openid)""" return self.is_access_token_set and self.access_token is not None and self.openid is not None @abstractmethod def get_authorization_url(self, nexturl='/'): - """获取授权URL(抽象方法)""" pass @abstractmethod def get_access_token_by_code(self, code): - """通过授权码获取访问令牌(抽象方法)""" pass @abstractmethod def get_oauth_userinfo(self): - """获取用户信息(抽象方法)""" pass @abstractmethod def get_picture(self, metadata): - """从元数据中获取用户头像(抽象方法)""" pass def do_get(self, url, params, headers=None): - """执行GET请求""" rsp = requests.get(url=url, params=params, headers=headers) logger.info(rsp.text) return rsp.text def do_post(self, url, params, headers=None): - """执行POST请求""" rsp = requests.post(url, params, headers=headers) logger.info(rsp.text) return rsp.text def get_config(self): - """获取OAuth配置""" value = OAuthConfig.objects.filter(type=self.ICON_NAME) return value[0] if value else None class WBOauthManager(BaseOauthManager): - """微博OAuth管理器""" - - # 微博OAuth相关URL AUTH_URL = 'https://api.weibo.com/oauth2/authorize' TOKEN_URL = 'https://api.weibo.com/oauth2/access_token' API_URL = 'https://api.weibo.com/2/users/show.json' ICON_NAME = 'weibo' def __init__(self, access_token=None, openid=None): - """初始化微博OAuth管理器""" config = self.get_config() - self.client_id = config.appkey if config else '' # 应用Key - self.client_secret = config.appsecret if config else '' # 应用Secret - self.callback_url = config.callback_url if config else '' # 回调URL - super(WBOauthManager, self).__init__(access_token=access_token, openid=openid) + self.client_id = config.appkey if config else '' + self.client_secret = config.appsecret if config else '' + self.callback_url = config.callback_url if config else '' + super( + WBOauthManager, + self).__init__( + access_token=access_token, + openid=openid) def get_authorization_url(self, nexturl='/'): - """获取微博授权URL""" params = { 'client_id': self.client_id, 'response_type': 'code', @@ -127,7 +98,7 @@ class WBOauthManager(BaseOauthManager): return url def get_access_token_by_code(self, code): - """通过授权码获取访问令牌""" + params = { 'client_id': self.client_id, 'client_secret': self.client_secret, @@ -146,7 +117,6 @@ class WBOauthManager(BaseOauthManager): raise OAuthAccessTokenException(rsp) def get_oauth_userinfo(self): - """获取微博用户信息""" if not self.is_authorized: return None params = { @@ -157,14 +127,14 @@ class WBOauthManager(BaseOauthManager): try: datas = json.loads(rsp) user = OAuthUser() - user.metadata = rsp # 原始元数据 - user.picture = datas['avatar_large'] # 用户头像 - user.nickname = datas['screen_name'] # 用户昵称 - user.openid = datas['id'] # 用户OpenID - user.type = 'weibo' # 用户类型 - user.token = self.access_token # 访问令牌 + user.metadata = rsp + user.picture = datas['avatar_large'] + user.nickname = datas['screen_name'] + user.openid = datas['id'] + user.type = 'weibo' + user.token = self.access_token if 'email' in datas and datas['email']: - user.email = datas['email'] # 用户邮箱 + user.email = datas['email'] return user except Exception as e: logger.error(e) @@ -172,16 +142,12 @@ class WBOauthManager(BaseOauthManager): return None def get_picture(self, metadata): - """从元数据中获取用户头像""" datas = json.loads(metadata) return datas['avatar_large'] class ProxyManagerMixin: - """代理管理器混入类,用于处理网络代理""" - def __init__(self, *args, **kwargs): - """初始化代理设置""" if os.environ.get("HTTP_PROXY"): self.proxies = { "http": os.environ.get("HTTP_PROXY"), @@ -191,52 +157,50 @@ class ProxyManagerMixin: self.proxies = None def do_get(self, url, params, headers=None): - """使用代理执行GET请求""" rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies) logger.info(rsp.text) return rsp.text def do_post(self, url, params, headers=None): - """使用代理执行POST请求""" rsp = requests.post(url, params, headers=headers, proxies=self.proxies) logger.info(rsp.text) return rsp.text class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager): - """Google OAuth管理器""" - AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth' TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token' API_URL = 'https://www.googleapis.com/oauth2/v3/userinfo' ICON_NAME = 'google' def __init__(self, access_token=None, openid=None): - """初始化Google OAuth管理器""" config = self.get_config() self.client_id = config.appkey if config else '' self.client_secret = config.appsecret if config else '' self.callback_url = config.callback_url if config else '' - super(GoogleOauthManager, self).__init__(access_token=access_token, openid=openid) + super( + GoogleOauthManager, + self).__init__( + access_token=access_token, + openid=openid) def get_authorization_url(self, nexturl='/'): - """获取Google授权URL""" params = { 'client_id': self.client_id, 'response_type': 'code', 'redirect_uri': self.callback_url, - 'scope': 'openid email', # 请求的权限范围 + 'scope': 'openid email', } url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) return url def get_access_token_by_code(self, code): - """通过授权码获取访问令牌""" params = { 'client_id': self.client_id, 'client_secret': self.client_secret, 'grant_type': 'authorization_code', 'code': code, + 'redirect_uri': self.callback_url } rsp = self.do_post(self.TOKEN_URL, params) @@ -252,7 +216,6 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager): raise OAuthAccessTokenException(rsp) def get_oauth_userinfo(self): - """获取Google用户信息""" if not self.is_authorized: return None params = { @@ -260,16 +223,17 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager): } rsp = self.do_get(self.API_URL, params) try: + datas = json.loads(rsp) user = OAuthUser() user.metadata = rsp - user.picture = datas['picture'] # 用户头像 - user.nickname = datas['name'] # 用户昵称 - user.openid = datas['sub'] # 用户唯一标识 + user.picture = datas['picture'] + user.nickname = datas['name'] + user.openid = datas['sub'] user.token = self.access_token user.type = 'google' if datas['email']: - user.email = datas['email'] # 用户邮箱 + user.email = datas['email'] return user except Exception as e: logger.error(e) @@ -277,50 +241,48 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager): return None def get_picture(self, metadata): - """从元数据中获取用户头像""" datas = json.loads(metadata) return datas['picture'] class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager): - """GitHub OAuth管理器""" - AUTH_URL = 'https://github.com/login/oauth/authorize' TOKEN_URL = 'https://github.com/login/oauth/access_token' API_URL = 'https://api.github.com/user' ICON_NAME = 'github' def __init__(self, access_token=None, openid=None): - """初始化GitHub OAuth管理器""" config = self.get_config() self.client_id = config.appkey if config else '' self.client_secret = config.appsecret if config else '' self.callback_url = config.callback_url if config else '' - super(GitHubOauthManager, self).__init__(access_token=access_token, openid=openid) + super( + GitHubOauthManager, + self).__init__( + access_token=access_token, + openid=openid) def get_authorization_url(self, next_url='/'): - """获取GitHub授权URL""" params = { 'client_id': self.client_id, 'response_type': 'code', 'redirect_uri': f'{self.callback_url}&next_url={next_url}', - 'scope': 'user' # 请求的用户权限 + 'scope': 'user' } url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) return url def get_access_token_by_code(self, code): - """通过授权码获取访问令牌""" params = { 'client_id': self.client_id, 'client_secret': self.client_secret, 'grant_type': 'authorization_code', 'code': code, + 'redirect_uri': self.callback_url } rsp = self.do_post(self.TOKEN_URL, params) - # 解析URL编码的响应 from urllib import parse r = parse.parse_qs(rsp) if 'access_token' in r: @@ -330,21 +292,21 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager): raise OAuthAccessTokenException(rsp) def get_oauth_userinfo(self): - """获取GitHub用户信息""" + rsp = self.do_get(self.API_URL, params={}, headers={ - "Authorization": "token " + self.access_token # 使用token进行认证 + "Authorization": "token " + self.access_token }) try: datas = json.loads(rsp) user = OAuthUser() - user.picture = datas['avatar_url'] # 用户头像 - user.nickname = datas['name'] # 用户昵称 - user.openid = datas['id'] # 用户ID + user.picture = datas['avatar_url'] + user.nickname = datas['name'] + user.openid = datas['id'] user.type = 'github' user.token = self.access_token user.metadata = rsp if 'email' in datas and datas['email']: - user.email = datas['email'] # 用户邮箱 + user.email = datas['email'] return user except Exception as e: logger.error(e) @@ -352,44 +314,44 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager): return None def get_picture(self, metadata): - """从元数据中获取用户头像""" datas = json.loads(metadata) return datas['avatar_url'] class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager): - """Facebook OAuth管理器""" - AUTH_URL = 'https://www.facebook.com/v16.0/dialog/oauth' TOKEN_URL = 'https://graph.facebook.com/v16.0/oauth/access_token' API_URL = 'https://graph.facebook.com/me' ICON_NAME = 'facebook' def __init__(self, access_token=None, openid=None): - """初始化Facebook OAuth管理器""" config = self.get_config() self.client_id = config.appkey if config else '' self.client_secret = config.appsecret if config else '' self.callback_url = config.callback_url if config else '' - super(FaceBookOauthManager, self).__init__(access_token=access_token, openid=openid) + super( + FaceBookOauthManager, + self).__init__( + access_token=access_token, + openid=openid) def get_authorization_url(self, next_url='/'): - """获取Facebook授权URL""" params = { 'client_id': self.client_id, 'response_type': 'code', 'redirect_uri': self.callback_url, - 'scope': 'email,public_profile' # 请求的权限范围 + 'scope': 'email,public_profile' } url = self.AUTH_URL + "?" + urllib.parse.urlencode(params) return url def get_access_token_by_code(self, code): - """通过授权码获取访问令牌""" params = { 'client_id': self.client_id, 'client_secret': self.client_secret, + # 'grant_type': 'authorization_code', 'code': code, + 'redirect_uri': self.callback_url } rsp = self.do_post(self.TOKEN_URL, params) @@ -403,54 +365,52 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager): raise OAuthAccessTokenException(rsp) def get_oauth_userinfo(self): - """获取Facebook用户信息""" params = { 'access_token': self.access_token, - 'fields': 'id,name,picture,email' # 请求的用户字段 + 'fields': 'id,name,picture,email' } try: rsp = self.do_get(self.API_URL, params) datas = json.loads(rsp) user = OAuthUser() - user.nickname = datas['name'] # 用户昵称 - user.openid = datas['id'] # 用户ID + user.nickname = datas['name'] + user.openid = datas['id'] user.type = 'facebook' user.token = self.access_token user.metadata = rsp if 'email' in datas and datas['email']: - user.email = datas['email'] # 用户邮箱 + user.email = datas['email'] if 'picture' in datas and datas['picture'] and datas['picture']['data'] and datas['picture']['data']['url']: - user.picture = str(datas['picture']['data']['url']) # 用户头像 + user.picture = str(datas['picture']['data']['url']) return user except Exception as e: logger.error(e) return None def get_picture(self, metadata): - """从元数据中获取用户头像""" datas = json.loads(metadata) return str(datas['picture']['data']['url']) class QQOauthManager(BaseOauthManager): - """QQ OAuth管理器""" - AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize' TOKEN_URL = 'https://graph.qq.com/oauth2.0/token' API_URL = 'https://graph.qq.com/user/get_user_info' - OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me' # 获取OpenID的URL + OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me' ICON_NAME = 'qq' def __init__(self, access_token=None, openid=None): - """初始化QQ OAuth管理器""" config = self.get_config() self.client_id = config.appkey if config else '' self.client_secret = config.appsecret if config else '' self.callback_url = config.callback_url if config else '' - super(QQOauthManager, self).__init__(access_token=access_token, openid=openid) + super( + QQOauthManager, + self).__init__( + access_token=access_token, + openid=openid) def get_authorization_url(self, next_url='/'): - """获取QQ授权URL""" params = { 'response_type': 'code', 'client_id': self.client_id, @@ -460,7 +420,6 @@ class QQOauthManager(BaseOauthManager): return url def get_access_token_by_code(self, code): - """通过授权码获取访问令牌""" params = { 'grant_type': 'authorization_code', 'client_id': self.client_id, @@ -470,7 +429,6 @@ class QQOauthManager(BaseOauthManager): } rsp = self.do_get(self.TOKEN_URL, params) if rsp: - # 解析URL编码的响应 d = urllib.parse.parse_qs(rsp) if 'access_token' in d: token = d['access_token'] @@ -480,22 +438,22 @@ class QQOauthManager(BaseOauthManager): raise OAuthAccessTokenException(rsp) def get_open_id(self): - """获取用户的OpenID""" if self.is_access_token_set: params = { 'access_token': self.access_token } rsp = self.do_get(self.OPEN_ID_URL, params) if rsp: - # 清理响应格式(JSONP格式) - rsp = rsp.replace('callback(', '').replace(')', '').replace(';', '') + rsp = rsp.replace( + 'callback(', '').replace( + ')', '').replace( + ';', '') obj = json.loads(rsp) openid = str(obj['openid']) self.openid = openid return openid def get_oauth_userinfo(self): - """获取QQ用户信息""" openid = self.get_open_id() if openid: params = { @@ -507,26 +465,24 @@ class QQOauthManager(BaseOauthManager): logger.info(rsp) obj = json.loads(rsp) user = OAuthUser() - user.nickname = obj['nickname'] # 用户昵称 + user.nickname = obj['nickname'] user.openid = openid user.type = 'qq' user.token = self.access_token user.metadata = rsp if 'email' in obj: - user.email = obj['email'] # 用户邮箱 + user.email = obj['email'] if 'figureurl' in obj: - user.picture = str(obj['figureurl']) # 用户头像 + user.picture = str(obj['figureurl']) return user def get_picture(self, metadata): - """从元数据中获取用户头像""" datas = json.loads(metadata) return str(datas['figureurl']) @cache_decorator(expiration=100 * 60) def get_oauth_apps(): - """获取所有启用的OAuth应用(带缓存)""" configs = OAuthConfig.objects.filter(is_enable=True).all() if not configs: return [] @@ -537,10 +493,12 @@ def get_oauth_apps(): def get_manager_by_type(type): - """根据类型获取对应的OAuth管理器""" applications = get_oauth_apps() if applications: - finds = list(filter(lambda x: x.ICON_NAME.lower() == type.lower(), applications)) + finds = list( + filter( + lambda x: x.ICON_NAME.lower() == type.lower(), + applications)) if finds: return finds[0] - return None \ No newline at end of file + return None diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/tests.py b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/tests.py index e6c4513..bb23b9b 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/tests.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/tests.py @@ -13,78 +13,17 @@ from oauth.oauthmanager import BaseOauthManager # Create your tests here. class OAuthConfigTest(TestCase): -<<<<<<< HEAD - """ - OAuth配置基础测试类 - 测试OAuth登录流程的基本功能 - """ - - def setUp(self): - """ - 测试初始化方法,在每个测试方法执行前运行 - """ - self.client = Client() # Django测试客户端,用于模拟HTTP请求 - self.factory = RequestFactory() # 用于创建请求对象的工厂 - - def test_oauth_login_test(self): - """ - 测试OAuth登录流程 - 验证微博OAuth登录的跳转和授权流程 - """ - # 创建微博OAuth配置 -======= def setUp(self): self.client = Client() self.factory = RequestFactory() def test_oauth_login_test(self): ->>>>>>> ccy_branch c = OAuthConfig() c.type = 'weibo' c.appkey = 'appkey' c.appsecret = 'appsecret' c.save() -<<<<<<< HEAD - # 测试OAuth登录请求,应该重定向到微博授权页面 - response = self.client.get('/oauth/oauthlogin?type=weibo') - self.assertEqual(response.status_code, 302) # 验证重定向状态码 - self.assertTrue("api.weibo.com" in response.url) # 验证跳转到微博授权页面 - - # 测试授权回调处理,应该重定向到首页 - response = self.client.get('/oauth/authorize?type=weibo&code=code') - self.assertEqual(response.status_code, 302) # 验证重定向状态码 - self.assertEqual(response.url, '/') # 验证跳转到首页 - - -class OauthLoginTest(TestCase): - """ - OAuth登录详细测试类 - 测试各种OAuth服务提供商的登录流程 - """ - - def setUp(self) -> None: - """ - 测试初始化方法 - """ - self.client = Client() # Django测试客户端 - self.factory = RequestFactory() # 请求工厂 - self.apps = self.init_apps() # 初始化所有OAuth应用配置 - - def init_apps(self): - """ - 初始化所有支持的OAuth应用配置 - 为每种OAuth服务创建测试配置 - """ - # 获取所有OAuth管理器的子类实例 - applications = [p() for p in BaseOauthManager.__subclasses__()] - for application in applications: - # 为每个OAuth服务创建配置 - c = OAuthConfig() - c.type = application.ICON_NAME.lower() # 服务类型(小写) - c.appkey = 'appkey' # 测试用的AppKey - c.appsecret = 'appsecret' # 测试用的AppSecret -======= response = self.client.get('/oauth/oauthlogin?type=weibo') self.assertEqual(response.status_code, 302) self.assertTrue("api.weibo.com" in response.url) @@ -107,23 +46,10 @@ class OauthLoginTest(TestCase): c.type = application.ICON_NAME.lower() c.appkey = 'appkey' c.appsecret = 'appsecret' ->>>>>>> ccy_branch c.save() return applications def get_app_by_type(self, type): -<<<<<<< HEAD - """ - 根据类型获取对应的OAuth应用实例 - - Args: - type: OAuth服务类型 - - Returns: - 对应的OAuth管理器实例 - """ -======= ->>>>>>> ccy_branch for app in self.apps: if app.ICON_NAME.lower() == type: return app @@ -131,87 +57,32 @@ class OauthLoginTest(TestCase): @patch("oauth.oauthmanager.WBOauthManager.do_post") @patch("oauth.oauthmanager.WBOauthManager.do_get") def test_weibo_login(self, mock_do_get, mock_do_post): -<<<<<<< HEAD - """ - 测试微博OAuth登录流程 - 使用mock模拟API调用 - """ - weibo_app = self.get_app_by_type('weibo') - assert weibo_app # 确保获取到微博应用实例 - - # 获取授权URL - url = weibo_app.get_authorization_url() - - # 设置mock返回值 - 获取access token - mock_do_post.return_value = json.dumps({"access_token": "access_token", - "uid": "uid" - }) - # 设置mock返回值 - 获取用户信息 -======= weibo_app = self.get_app_by_type('weibo') assert weibo_app url = weibo_app.get_authorization_url() mock_do_post.return_value = json.dumps({"access_token": "access_token", "uid": "uid" }) ->>>>>>> ccy_branch mock_do_get.return_value = json.dumps({ "avatar_large": "avatar_large", "screen_name": "screen_name", "id": "id", "email": "email", }) -<<<<<<< HEAD - - # 执行获取access token的操作 userinfo = weibo_app.get_access_token_by_code('code') - - # 验证返回的用户信息 -======= - userinfo = weibo_app.get_access_token_by_code('code') ->>>>>>> ccy_branch self.assertEqual(userinfo.token, 'access_token') self.assertEqual(userinfo.openid, 'id') @patch("oauth.oauthmanager.GoogleOauthManager.do_post") @patch("oauth.oauthmanager.GoogleOauthManager.do_get") def test_google_login(self, mock_do_get, mock_do_post): -<<<<<<< HEAD - """ - 测试Google OAuth登录流程 - """ google_app = self.get_app_by_type('google') assert google_app - url = google_app.get_authorization_url() - - # 模拟Google OAuth的token响应 -======= - google_app = self.get_app_by_type('google') - assert google_app - url = google_app.get_authorization_url() ->>>>>>> ccy_branch mock_do_post.return_value = json.dumps({ "access_token": "access_token", "id_token": "id_token", }) -<<<<<<< HEAD - - # 模拟Google用户信息响应 - mock_do_get.return_value = json.dumps({ - "picture": "picture", - "name": "name", - "sub": "sub", # Google的用户ID字段 - "email": "email", - }) - - token = google_app.get_access_token_by_code('code') - userinfo = google_app.get_oauth_userinfo() - - # 验证用户信息 - self.assertEqual(userinfo.token, 'access_token') - self.assertEqual(userinfo.openid, 'sub') # Google使用sub作为用户ID -======= mock_do_get.return_value = json.dumps({ "picture": "picture", "name": "name", @@ -222,74 +93,30 @@ class OauthLoginTest(TestCase): userinfo = google_app.get_oauth_userinfo() self.assertEqual(userinfo.token, 'access_token') self.assertEqual(userinfo.openid, 'sub') ->>>>>>> ccy_branch @patch("oauth.oauthmanager.GitHubOauthManager.do_post") @patch("oauth.oauthmanager.GitHubOauthManager.do_get") def test_github_login(self, mock_do_get, mock_do_post): -<<<<<<< HEAD - """ - 测试GitHub OAuth登录流程 - """ github_app = self.get_app_by_type('github') assert github_app - url = github_app.get_authorization_url() - # 验证GitHub授权URL包含必要信息 self.assertTrue("github.com" in url) self.assertTrue("client_id" in url) - - # 模拟GitHub的token响应(字符串格式) mock_do_post.return_value = "access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer" - - # 模拟GitHub用户信息响应 -======= - github_app = self.get_app_by_type('github') - assert github_app - url = github_app.get_authorization_url() - self.assertTrue("github.com" in url) - self.assertTrue("client_id" in url) - mock_do_post.return_value = "access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer" ->>>>>>> ccy_branch mock_do_get.return_value = json.dumps({ "avatar_url": "avatar_url", "name": "name", "id": "id", "email": "email", }) -<<<<<<< HEAD - token = github_app.get_access_token_by_code('code') userinfo = github_app.get_oauth_userinfo() - - # 验证用户信息 -======= - token = github_app.get_access_token_by_code('code') - userinfo = github_app.get_oauth_userinfo() ->>>>>>> ccy_branch self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a') self.assertEqual(userinfo.openid, 'id') @patch("oauth.oauthmanager.FaceBookOauthManager.do_post") @patch("oauth.oauthmanager.FaceBookOauthManager.do_get") def test_facebook_login(self, mock_do_get, mock_do_post): -<<<<<<< HEAD - """ - 测试Facebook OAuth登录流程 - """ - facebook_app = self.get_app_by_type('facebook') - assert facebook_app - - url = facebook_app.get_authorization_url() - self.assertTrue("facebook.com" in url) # 验证Facebook授权URL - - # 模拟Facebook token响应 - mock_do_post.return_value = json.dumps({ - "access_token": "access_token", - }) - - # 模拟Facebook用户信息响应(嵌套结构) -======= facebook_app = self.get_app_by_type('facebook') assert facebook_app url = facebook_app.get_authorization_url() @@ -297,7 +124,6 @@ class OauthLoginTest(TestCase): mock_do_post.return_value = json.dumps({ "access_token": "access_token", }) ->>>>>>> ccy_branch mock_do_get.return_value = json.dumps({ "name": "name", "id": "id", @@ -308,18 +134,6 @@ class OauthLoginTest(TestCase): } } }) -<<<<<<< HEAD - - token = facebook_app.get_access_token_by_code('code') - userinfo = facebook_app.get_oauth_userinfo() - - self.assertEqual(userinfo.token, 'access_token') - - @patch("oauth.oauthmanager.QQOauthManager.do_get", side_effect=[ - 'access_token=access_token&expires_in=3600', # 第一次调用:获取token - 'callback({"client_id":"appid","openid":"openid"} );', # 第二次调用:获取openid - json.dumps({ # 第三次调用:获取用户信息 -======= token = facebook_app.get_access_token_by_code('code') userinfo = facebook_app.get_oauth_userinfo() self.assertEqual(userinfo.token, 'access_token') @@ -328,7 +142,6 @@ class OauthLoginTest(TestCase): 'access_token=access_token&expires_in=3600', 'callback({"client_id":"appid","openid":"openid"} );', json.dumps({ ->>>>>>> ccy_branch "nickname": "nickname", "email": "email", "figureurl": "figureurl", @@ -336,49 +149,21 @@ class OauthLoginTest(TestCase): }) ]) def test_qq_login(self, mock_do_get): -<<<<<<< HEAD - """ - 测试QQ OAuth登录流程 - 使用side_effect模拟多次不同的API响应 - """ - qq_app = self.get_app_by_type('qq') - assert qq_app - - url = qq_app.get_authorization_url() - self.assertTrue("qq.com" in url) # 验证QQ授权URL - - token = qq_app.get_access_token_by_code('code') - userinfo = qq_app.get_oauth_userinfo() - -======= qq_app = self.get_app_by_type('qq') assert qq_app url = qq_app.get_authorization_url() self.assertTrue("qq.com" in url) token = qq_app.get_access_token_by_code('code') userinfo = qq_app.get_oauth_userinfo() ->>>>>>> ccy_branch self.assertEqual(userinfo.token, 'access_token') @patch("oauth.oauthmanager.WBOauthManager.do_post") @patch("oauth.oauthmanager.WBOauthManager.do_get") def test_weibo_authoriz_login_with_email(self, mock_do_get, mock_do_post): -<<<<<<< HEAD - """ - 测试包含邮箱的微博授权登录完整流程 - 验证用户认证和会话管理 - """ - # 模拟获取access token - mock_do_post.return_value = json.dumps({"access_token": "access_token", - "uid": "uid" - }) - # 模拟用户信息(包含邮箱) -======= mock_do_post.return_value = json.dumps({"access_token": "access_token", "uid": "uid" }) ->>>>>>> ccy_branch mock_user_info = { "avatar_large": "avatar_large", "screen_name": "screen_name1", @@ -387,46 +172,25 @@ class OauthLoginTest(TestCase): } mock_do_get.return_value = json.dumps(mock_user_info) -<<<<<<< HEAD - # 测试登录跳转 -======= ->>>>>>> ccy_branch response = self.client.get('/oauth/oauthlogin?type=weibo') self.assertEqual(response.status_code, 302) self.assertTrue("api.weibo.com" in response.url) -<<<<<<< HEAD - # 测试授权回调 -======= ->>>>>>> ccy_branch response = self.client.get('/oauth/authorize?type=weibo&code=code') self.assertEqual(response.status_code, 302) self.assertEqual(response.url, '/') -<<<<<<< HEAD - # 验证用户认证状态 -======= ->>>>>>> ccy_branch user = auth.get_user(self.client) assert user.is_authenticated self.assertTrue(user.is_authenticated) self.assertEqual(user.username, mock_user_info['screen_name']) self.assertEqual(user.email, mock_user_info['email']) -<<<<<<< HEAD - - # 登出后再次测试 -======= ->>>>>>> ccy_branch self.client.logout() response = self.client.get('/oauth/authorize?type=weibo&code=code') self.assertEqual(response.status_code, 302) self.assertEqual(response.url, '/') -<<<<<<< HEAD - # 再次验证用户认证状态 -======= ->>>>>>> ccy_branch user = auth.get_user(self.client) assert user.is_authenticated self.assertTrue(user.is_authenticated) @@ -436,22 +200,10 @@ class OauthLoginTest(TestCase): @patch("oauth.oauthmanager.WBOauthManager.do_post") @patch("oauth.oauthmanager.WBOauthManager.do_get") def test_weibo_authoriz_login_without_email(self, mock_do_get, mock_do_post): -<<<<<<< HEAD - """ - 测试不包含邮箱的微博授权登录流程 - 验证邮箱补充流程 - """ - # 模拟获取access token - mock_do_post.return_value = json.dumps({"access_token": "access_token", - "uid": "uid" - }) - # 模拟用户信息(不包含邮箱) -======= mock_do_post.return_value = json.dumps({"access_token": "access_token", "uid": "uid" }) ->>>>>>> ccy_branch mock_user_info = { "avatar_large": "avatar_large", "screen_name": "screen_name1", @@ -459,33 +211,10 @@ class OauthLoginTest(TestCase): } mock_do_get.return_value = json.dumps(mock_user_info) -<<<<<<< HEAD - # 测试登录跳转 -======= ->>>>>>> ccy_branch response = self.client.get('/oauth/oauthlogin?type=weibo') self.assertEqual(response.status_code, 302) self.assertTrue("api.weibo.com" in response.url) -<<<<<<< HEAD - # 测试授权回调 - 应该重定向到邮箱补充页面 - response = self.client.get('/oauth/authorize?type=weibo&code=code') - self.assertEqual(response.status_code, 302) - - # 解析OAuth用户ID - oauth_user_id = int(response.url.split('/')[-1].split('.')[0]) - self.assertEqual(response.url, f'/oauth/requireemail/{oauth_user_id}.html') - - # 提交邮箱信息 - response = self.client.post(response.url, {'email': 'test@gmail.com', 'oauthid': oauth_user_id}) - self.assertEqual(response.status_code, 302) - - # 生成邮箱验证签名 - sign = get_sha256(settings.SECRET_KEY + - str(oauth_user_id) + settings.SECRET_KEY) - - # 验证绑定成功URL -======= response = self.client.get('/oauth/authorize?type=weibo&code=code') self.assertEqual(response.status_code, 302) @@ -499,16 +228,11 @@ class OauthLoginTest(TestCase): sign = get_sha256(settings.SECRET_KEY + str(oauth_user_id) + settings.SECRET_KEY) ->>>>>>> ccy_branch url = reverse('oauth:bindsuccess', kwargs={ 'oauthid': oauth_user_id, }) self.assertEqual(response.url, f'{url}?type=email') -<<<<<<< HEAD - # 验证邮箱确认流程 -======= ->>>>>>> ccy_branch path = reverse('oauth:email_confirm', kwargs={ 'id': oauth_user_id, 'sign': sign @@ -516,19 +240,10 @@ class OauthLoginTest(TestCase): response = self.client.get(path) self.assertEqual(response.status_code, 302) self.assertEqual(response.url, f'/oauth/bindsuccess/{oauth_user_id}.html?type=success') -<<<<<<< HEAD - - # 验证最终用户状态 -======= ->>>>>>> ccy_branch user = auth.get_user(self.client) from oauth.models import OAuthUser oauth_user = OAuthUser.objects.get(author=user) self.assertTrue(user.is_authenticated) self.assertEqual(user.username, mock_user_info['screen_name']) self.assertEqual(user.email, 'test@gmail.com') -<<<<<<< HEAD - self.assertEqual(oauth_user.pk, oauth_user_id) -======= self.assertEqual(oauth_user.pk, oauth_user_id) ->>>>>>> ccy_branch diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/urls.py b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/urls.py index 180fe81..c4a12a0 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/urls.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/urls.py @@ -2,36 +2,24 @@ from django.urls import path from . import views -# OAuth应用的路由配置 -app_name = "oauth" # 定义应用命名空间,用于URL反向解析 - +app_name = "oauth" urlpatterns = [ - # OAuth授权入口点 - 启动第三方登录流程 path( - r'oauth/authorize', # 授权URL路径 - views.authorize), # 对应的视图函数,处理授权逻辑 - - # 需要邮箱地址页面 - 当第三方登录未返回邮箱时要求用户输入 + r'oauth/authorize', + views.authorize), path( - r'oauth/requireemail/.html', # 带oauthid参数的URL - views.RequireEmailView.as_view(), # 类视图,处理邮箱输入 - name='require_email'), # URL名称,用于反向解析 - - # 邮箱确认 - 验证用户输入的邮箱地址 + r'oauth/requireemail/.html', + views.RequireEmailView.as_view(), + name='require_email'), path( - r'oauth/emailconfirm//.html', # 带id和签名参数的URL - views.emailconfirm, # 视图函数,处理邮箱确认 - name='email_confirm'), # URL名称,用于反向解析 - - # 绑定成功页面 - 显示第三方账号绑定成功信息 + r'oauth/emailconfirm//.html', + views.emailconfirm, + name='email_confirm'), path( - r'oauth/bindsuccess/.html', # 带oauthid参数的URL - views.bindsuccess, # 视图函数,显示绑定成功页面 - name='bindsuccess'), # URL名称,用于反向解析 - - # OAuth登录处理 - 处理第三方登录回调 + r'oauth/bindsuccess/.html', + views.bindsuccess, + name='bindsuccess'), path( - r'oauth/oauthlogin', # OAuth登录回调URL路径 - views.oauthlogin, # 视图函数,处理登录回调逻辑 - name='oauthlogin') # URL名称,用于反向解析 -] + r'oauth/oauthlogin', + views.oauthlogin, + name='oauthlogin')] diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/views.py b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/views.py index e08c39b..12e3a6e 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/views.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/views.py @@ -23,32 +23,17 @@ from oauth.forms import RequireEmailForm from .models import OAuthUser from .oauthmanager import get_manager_by_type, OAuthAccessTokenException -# 获取logger实例用于记录日志 logger = logging.getLogger(__name__) def get_redirecturl(request): - """ - 获取重定向URL,并进行安全验证 - - Args: - request: HTTP请求对象 - - Returns: - str: 安全的重定向URL - """ - # 从请求参数获取next_url,默认为None nexturl = request.GET.get('next_url', None) - # 如果nexturl为空或是登录页面,则重定向到首页 if not nexturl or nexturl == '/login/' or nexturl == '/login': nexturl = '/' return nexturl - - # 解析URL,检查域名安全性 p = urlparse(nexturl) if p.netloc: site = get_current_site().domain - # 检查域名是否匹配当前站点,防止开放重定向攻击 if not p.netloc.replace('www.', '') == site.replace('www.', ''): logger.info('非法url:' + nexturl) return "/" @@ -56,54 +41,26 @@ def get_redirecturl(request): def oauthlogin(request): - """ - OAuth登录入口 - 重定向到第三方授权页面 - - Args: - request: HTTP请求对象 - - Returns: - HttpResponseRedirect: 重定向响应 - """ - # 获取OAuth类型(如weibo、github等) type = request.GET.get('type', None) if not type: return HttpResponseRedirect('/') - - # 获取对应的OAuth管理器 manager = get_manager_by_type(type) if not manager: return HttpResponseRedirect('/') - - # 获取安全的重定向URL nexturl = get_redirecturl(request) - # 获取第三方授权URL并重定向 authorizeurl = manager.get_authorization_url(nexturl) return HttpResponseRedirect(authorizeurl) def authorize(request): - """ - OAuth授权回调处理 - 处理第三方登录回调 - - Args: - request: HTTP请求对象 - - Returns: - HttpResponseRedirect: 重定向响应 - """ type = request.GET.get('type', None) if not type: return HttpResponseRedirect('/') - manager = get_manager_by_type(type) if not manager: return HttpResponseRedirect('/') - - # 获取授权码 code = request.GET.get('code', None) try: - # 使用授权码获取访问令牌 rsp = manager.get_access_token_by_code(code) except OAuthAccessTokenException as e: logger.warning("OAuthAccessTokenException:" + str(e)) @@ -111,139 +68,101 @@ def authorize(request): except Exception as e: logger.error(e) rsp = None - nexturl = get_redirecturl(request) if not rsp: - # 如果获取token失败,重新跳转到授权页面 return HttpResponseRedirect(manager.get_authorization_url(nexturl)) - - # 获取用户信息 user = manager.get_oauth_userinfo() if user: - # 处理昵称为空的情况 if not user.nickname or not user.nickname.strip(): user.nickname = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') - try: - # 检查是否已存在该OAuth用户 temp = OAuthUser.objects.get(type=type, openid=user.openid) - # 更新用户信息 temp.picture = user.picture temp.metadata = user.metadata temp.nickname = user.nickname user = temp except ObjectDoesNotExist: pass - - # Facebook的token过长,清空处理 + # facebook的token过长 if type == 'facebook': user.token = '' - - # 如果用户有邮箱,直接处理登录 if user.email: - with transaction.atomic(): # 使用事务保证数据一致性 + with transaction.atomic(): author = None try: author = get_user_model().objects.get(id=user.author_id) except ObjectDoesNotExist: pass - if not author: - # 创建或获取用户 result = get_user_model().objects.get_or_create(email=user.email) author = result[0] - if result[1]: # 如果是新创建的用户 + if result[1]: try: get_user_model().objects.get(username=user.nickname) except ObjectDoesNotExist: author.username = user.nickname else: - # 用户名冲突时生成唯一用户名 author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') author.source = 'authorize' author.save() - # 关联OAuth用户和系统用户 user.author = author user.save() - # 发送OAuth用户登录信号 oauth_user_login_signal.send( sender=authorize.__class__, id=user.id) - # 登录用户 login(request, author) return HttpResponseRedirect(nexturl) else: - # 没有邮箱,保存OAuth用户信息并跳转到邮箱输入页面 user.save() url = reverse('oauth:require_email', kwargs={ 'oauthid': user.id }) + return HttpResponseRedirect(url) else: return HttpResponseRedirect(nexturl) def emailconfirm(request, id, sign): - """ - 邮箱确认处理 - 验证邮箱并完成用户绑定 - - Args: - request: HTTP请求对象 - id: OAuth用户ID - sign: 安全签名 - - Returns: - HttpResponseRedirect: 重定向响应 - """ if not sign: return HttpResponseForbidden() - - # 验证签名安全性 if not get_sha256(settings.SECRET_KEY + str(id) + settings.SECRET_KEY).upper() == sign.upper(): return HttpResponseForbidden() - - # 获取OAuth用户 oauthuser = get_object_or_404(OAuthUser, pk=id) with transaction.atomic(): if oauthuser.author: author = get_user_model().objects.get(pk=oauthuser.author_id) else: - # 创建或获取系统用户 result = get_user_model().objects.get_or_create(email=oauthuser.email) author = result[0] - if result[1]: # 新创建的用户 + if result[1]: author.source = 'emailconfirm' author.username = oauthuser.nickname.strip() if oauthuser.nickname.strip( ) else "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S') author.save() - - # 关联用户 oauthuser.author = author oauthuser.save() - - # 发送登录信号并登录用户 oauth_user_login_signal.send( sender=emailconfirm.__class__, id=oauthuser.id) login(request, author) - # 发送绑定成功邮件 site = 'http://' + get_current_site().domain content = _(''' -

恭喜您,您已成功绑定邮箱。您可以使用%(oauthuser_type)s直接登录本站,无需密码。

- 欢迎您继续关注本站,地址是%(site)s - 再次感谢! +

Congratulations, you have successfully bound your email address. You can use + %(oauthuser_type)s to directly log in to this website without a password.

+ You are welcome to continue to follow this site, the address is + %(site)s + Thank you again!
- 如果上面的链接无法打开,请将此链接复制到浏览器。 + If the link above cannot be opened, please copy this link to your browser. %(site)s ''') % {'oauthuser_type': oauthuser.type, 'site': site} - send_email(emailto=[oauthuser.email, ], title=_('恭喜您绑定成功!'), content=content) - - # 跳转到绑定成功页面 + send_email(emailto=[oauthuser.email, ], title=_('Congratulations on your successful binding!'), content=content) url = reverse('oauth:bindsuccess', kwargs={ 'oauthid': id }) @@ -252,17 +171,12 @@ def emailconfirm(request, id, sign): class RequireEmailView(FormView): - """ - 需要邮箱视图 - 处理用户输入邮箱地址 - """ - form_class = RequireEmailForm # 使用的表单类 - template_name = 'oauth/require_email.html' # 模板名称 + form_class = RequireEmailForm + template_name = 'oauth/require_email.html' def get(self, request, *args, **kwargs): - """GET请求处理""" oauthid = self.kwargs['oauthid'] oauthuser = get_object_or_404(OAuthUser, pk=oauthid) - # 如果已有邮箱,直接跳过(这里注释掉了重定向逻辑) if oauthuser.email: pass # return HttpResponseRedirect('/') @@ -270,7 +184,6 @@ class RequireEmailView(FormView): return super(RequireEmailView, self).get(request, *args, **kwargs) def get_initial(self): - """获取表单初始数据""" oauthid = self.kwargs['oauthid'] return { 'email': '', @@ -278,49 +191,41 @@ class RequireEmailView(FormView): } def get_context_data(self, **kwargs): - """获取模板上下文数据""" oauthid = self.kwargs['oauthid'] oauthuser = get_object_or_404(OAuthUser, pk=oauthid) - # 添加用户头像到上下文 if oauthuser.picture: kwargs['picture'] = oauthuser.picture return super(RequireEmailView, self).get_context_data(**kwargs) def form_valid(self, form): - """表单验证通过后的处理""" email = form.cleaned_data['email'] oauthid = form.cleaned_data['oauthid'] oauthuser = get_object_or_404(OAuthUser, pk=oauthid) oauthuser.email = email oauthuser.save() - - # 生成安全签名 sign = get_sha256(settings.SECRET_KEY + str(oauthuser.id) + settings.SECRET_KEY) site = get_current_site().domain if settings.DEBUG: site = '127.0.0.1:8000' - - # 构建邮箱确认URL path = reverse('oauth:email_confirm', kwargs={ 'id': oauthid, 'sign': sign }) url = "http://{site}{path}".format(site=site, path=path) - # 发送确认邮件 content = _(""" -

请点击下面的链接完成邮箱绑定

+

Please click the link below to bind your email

+ %(url)s - 再次感谢! -
- 如果上面的链接无法打开,请将此链接复制到浏览器。 + + Thank you again!
+ If the link above cannot be opened, please copy this link to your browser. +
%(url)s """) % {'url': url} - send_email(emailto=[email, ], title=_('绑定邮箱'), content=content) - - # 跳转到绑定成功提示页面 + send_email(emailto=[email, ], title=_('Bind your email'), content=content) url = reverse('oauth:bindsuccess', kwargs={ 'oauthid': oauthid }) @@ -329,31 +234,20 @@ class RequireEmailView(FormView): def bindsuccess(request, oauthid): - """ - 绑定成功页面 - 显示绑定状态信息 - - Args: - request: HTTP请求对象 - oauthid: OAuth用户ID - - Returns: - HttpResponse: 渲染的响应 - """ type = request.GET.get('type', None) oauthuser = get_object_or_404(OAuthUser, pk=oauthid) - - # 根据类型显示不同的提示信息 if type == 'email': - title = _('绑定邮箱') + title = _('Bind your email') content = _( - '恭喜您,绑定只差一步之遥。请登录您的邮箱查看邮件完成绑定。谢谢。') + 'Congratulations, the binding is just one step away. ' + 'Please log in to your email to check the email to complete the binding. Thank you.') else: - title = _('绑定成功') + title = _('Binding successful') content = _( - "恭喜您,您已成功绑定邮箱地址。您可以使用%(oauthuser_type)s直接登录本站,无需密码。欢迎您继续关注本站。" % { + "Congratulations, you have successfully bound your email address. You can use %(oauthuser_type)s" + " to directly log in to this website without a password. You are welcome to continue to follow this site." % { 'oauthuser_type': oauthuser.type}) - return render(request, 'oauth/bindsuccess.html', { 'title': title, 'content': content - }) \ No newline at end of file + }) diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/models.py b/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/models.py index f883bb5..760942c 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/models.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/models.py @@ -5,41 +5,16 @@ from django.utils.timezone import now # Create your models here. class OwnTrackLog(models.Model): - """ - OwnTracks位置数据记录模型 - 用于存储移动设备通过OwnTracks应用上报的地理位置信息 - """ - - # 设备标识符字段,必填,最大长度100字符 tid = models.CharField(max_length=100, null=False, verbose_name='用户') - - # 纬度坐标,浮点数类型 lat = models.FloatField(verbose_name='纬度') - - # 经度坐标,浮点数类型 lon = models.FloatField(verbose_name='经度') - - # 记录创建时间,自动设置为当前时间 creation_time = models.DateTimeField('创建时间', default=now) def __str__(self): - """ - 定义模型的字符串表示形式 - 在Admin后台和其他显示场合使用设备ID作为标识 - """ return self.tid class Meta: - """模型元数据配置""" - - # 默认按创建时间升序排列 ordering = ['creation_time'] - - # 在Admin后台中显示的单数名称 verbose_name = "OwnTrackLogs" - - # 在Admin后台中显示的复数名称 verbose_name_plural = verbose_name - - # 指定获取最新记录时使用的字段 - get_latest_by = 'creation_time' \ No newline at end of file + get_latest_by = 'creation_time' diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/tests.py b/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/tests.py index d1fceac..3b4b9d8 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/tests.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/tests.py @@ -9,83 +9,56 @@ from .models import OwnTrackLog # Create your tests here. class OwnTrackLogTest(TestCase): - """测试OwnTrackLog位置追踪功能""" - def setUp(self): - """测试初始化设置""" - self.client = Client() # Django测试客户端 - self.factory = RequestFactory() # 请求工厂,用于创建请求对象 + self.client = Client() + self.factory = RequestFactory() def test_own_track_log(self): - """测试位置数据记录功能""" - # 测试用例1:正常的位置数据提交 o = { - 'tid': 12, # 设备ID - 'lat': 123.123, # 纬度 - 'lon': 134.341 # 经度 + 'tid': 12, + 'lat': 123.123, + 'lon': 134.341 } - # 发送POST请求提交位置数据 self.client.post( '/owntracks/logtracks', - json.dumps(o), # 将数据转换为JSON格式 - content_type='application/json' # 设置内容类型为JSON - ) - - # 验证数据是否成功保存到数据库 + json.dumps(o), + content_type='application/json') length = len(OwnTrackLog.objects.all()) - self.assertEqual(length, 1) # 应该有一条记录 + self.assertEqual(length, 1) - # 测试用例2:缺少经度的不完整数据提交 o = { 'tid': 12, 'lat': 123.123 - # 缺少lon字段 } - # 发送不完整数据的POST请求 self.client.post( '/owntracks/logtracks', json.dumps(o), - content_type='application/json' - ) - - # 验证不完整数据不应该被保存(记录数仍为1) + content_type='application/json') length = len(OwnTrackLog.objects.all()) self.assertEqual(length, 1) - # 测试用例3:未登录用户访问地图页面 rsp = self.client.get('/owntracks/show_maps') - self.assertEqual(rsp.status_code, 302) # 应该重定向到登录页面 + self.assertEqual(rsp.status_code, 302) - # 创建超级用户用于登录测试 user = BlogUser.objects.create_superuser( email="liangliangyy1@gmail.com", username="liangliangyy1", password="liangliangyy1") - # 使用新创建的用户登录 self.client.login(username='liangliangyy1', password='liangliangyy1') - - # 创建一条位置记录用于后续测试 s = OwnTrackLog() s.tid = 12 s.lon = 123.234 s.lat = 34.234 s.save() - # 测试用例4:登录用户访问日期列表页面 rsp = self.client.get('/owntracks/show_dates') - self.assertEqual(rsp.status_code, 200) # 应该成功返回 - - # 测试用例5:登录用户访问地图页面 + self.assertEqual(rsp.status_code, 200) rsp = self.client.get('/owntracks/show_maps') - self.assertEqual(rsp.status_code, 200) # 应该成功返回 - - # 测试用例6:登录用户获取位置数据(默认日期) + self.assertEqual(rsp.status_code, 200) rsp = self.client.get('/owntracks/get_datas') - self.assertEqual(rsp.status_code, 200) # 应该成功返回 - - # 测试用例7:登录用户获取指定日期的位置数据 + self.assertEqual(rsp.status_code, 200) rsp = self.client.get('/owntracks/get_datas?date=2018-02-26') - self.assertEqual(rsp.status_code, 200) # 应该成功返回 + self.assertEqual(rsp.status_code, 200) diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/urls.py b/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/urls.py index 7852b43..c19ada8 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/urls.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/urls.py @@ -2,32 +2,11 @@ from django.urls import path from . import views -# 定义应用命名空间,用于URL反向解析时区分不同应用的同名URL app_name = "owntracks" -# URL模式配置列表 urlpatterns = [ - # 接收OwnTracks位置数据上报的端点 - # 路径:/owntracks/logtracks - # 视图:manage_owntrack_log - 处理位置数据存储 - # 名称:logtracks - 用于URL反向解析 path('owntracks/logtracks', views.manage_owntrack_log, name='logtracks'), - - # 显示轨迹地图页面的端点 - # 路径:/owntracks/show_maps - # 视图:show_maps - 渲染地图展示页面 - # 名称:show_maps - 用于URL反向解析 path('owntracks/show_maps', views.show_maps, name='show_maps'), - - # 获取位置数据API端点 - # 路径:/owntracks/get_datas - # 视图:get_datas - 返回JSON格式的位置轨迹数据 - # 名称:get_datas - 用于URL反向解析 path('owntracks/get_datas', views.get_datas, name='get_datas'), - - # 显示有记录的日期列表端点 - # 路径:/owntracks/show_dates - # 视图:show_log_dates - 显示所有有位置记录的日期 - # 名称:show_dates - 用于URL反向解析 path('owntracks/show_dates', views.show_log_dates, name='show_dates') ] diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/views.py b/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/views.py index bf29efa..4c72bdd 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/views.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/views.py @@ -16,181 +16,112 @@ from django.views.decorators.csrf import csrf_exempt from .models import OwnTrackLog -# 获取logger实例,用于记录日志 logger = logging.getLogger(__name__) -# 处理OwnTracks位置数据的POST请求,不需要CSRF验证 @csrf_exempt def manage_owntrack_log(request): - """ - 接收并存储OwnTracks移动端发送的位置数据 - 支持格式:JSON格式的位置信息,包含tid(设备ID)、lat(纬度)、lon(经度) - """ try: - # 解析请求中的JSON数据 s = json.loads(request.read().decode('utf-8')) - tid = s['tid'] # 设备标识符 - lat = s['lat'] # 纬度 - lon = s['lon'] # 经度 + tid = s['tid'] + lat = s['lat'] + lon = s['lon'] - # 记录接收到的位置信息日志 logger.info( 'tid:{tid}.lat:{lat}.lon:{lon}'.format( tid=tid, lat=lat, lon=lon)) - - # 验证必需字段是否存在 if tid and lat and lon: - # 创建新的位置记录对象 m = OwnTrackLog() m.tid = tid m.lat = lat m.lon = lon - m.save() # 保存到数据库 - return HttpResponse('ok') # 返回成功响应 + m.save() + return HttpResponse('ok') else: - return HttpResponse('data error') # 数据不完整错误 + return HttpResponse('data error') except Exception as e: - # 记录异常信息 logger.error(e) - return HttpResponse('error') # 返回错误响应 + return HttpResponse('error') -# 显示地图页面,需要用户登录 @login_required def show_maps(request): - """ - 显示位置轨迹地图页面 - 仅超级用户可以访问,支持按日期筛选显示轨迹 - """ if request.user.is_superuser: - # 设置默认日期为当前UTC日期 defaultdate = str(datetime.datetime.now(timezone.utc).date()) - # 从GET参数获取日期,如未提供则使用默认日期 date = request.GET.get('date', defaultdate) context = { 'date': date } - # 渲染地图显示页面 return render(request, 'owntracks/show_maps.html', context) else: - # 非超级用户返回403禁止访问 from django.http import HttpResponseForbidden return HttpResponseForbidden() -# 显示有位置记录的日期列表,需要用户登录 @login_required def show_log_dates(request): - """ - 获取所有有位置记录的日期列表 - 用于在地图页面中选择查看特定日期的轨迹 - """ - # 从数据库获取所有记录的时间戳 dates = OwnTrackLog.objects.values_list('creation_time', flat=True) - # 提取日期部分,去重并排序 results = list(sorted(set(map(lambda x: x.strftime('%Y-%m-%d'), dates)))) context = { - 'results': results # 日期列表 + 'results': results } - # 渲染日期列表页面 return render(request, 'owntracks/show_log_dates.html', context) def convert_to_amap(locations): - """ - 将GPS坐标批量转换为高德地图坐标 - 由于高德API限制,每次最多转换30个坐标点 - - 参数: - locations: OwnTrackLog对象的查询集 - - 返回: - 转换后的坐标字符串,格式为"经度,纬度;经度,纬度;..." - """ convert_result = [] - # 创建迭代器用于分批处理 it = iter(locations) - # 每次处理30个坐标点 item = list(itertools.islice(it, 30)) while item: - # 将坐标格式化为高德API要求的格式 datas = ';'.join( set(map(lambda x: str(x.lon) + ',' + str(x.lat), item))) - # 高德地图API密钥 key = '8440a376dfc9743d8924bf0ad141f28e' api = 'http://restapi.amap.com/v3/assistant/coordinate/convert' query = { 'key': key, - 'locations': datas, # 要转换的坐标 - 'coordsys': 'gps' # 源坐标系为GPS + 'locations': datas, + 'coordsys': 'gps' } - # 调用高德坐标转换API rsp = requests.get(url=api, params=query) result = json.loads(rsp.text) if "locations" in result: - convert_result.append(result['locations']) # 添加转换结果 - item = list(itertools.islice(it, 30)) # 获取下一批坐标 + convert_result.append(result['locations']) + item = list(itertools.islice(it, 30)) - # 合并所有批次的转换结果 return ";".join(convert_result) -# 获取特定日期的位置数据,需要用户登录 @login_required def get_datas(request): - """ - 根据日期参数获取该日期所有设备的位置轨迹数据 - 返回JSON格式,包含每个设备的轨迹路径 - - 支持两种坐标格式: - 1. 原始GPS坐标(当前使用) - 2. 高德转换坐标(注释状态) - """ - # 设置默认查询时间为当前日期的开始时间(UTC) now = django.utils.timezone.now().replace(tzinfo=timezone.utc) querydate = django.utils.timezone.datetime( now.year, now.month, now.day, 0, 0, 0) - - # 如果提供了日期参数,解析并设置查询日期 if request.GET.get('date', None): date = list(map(lambda x: int(x), request.GET.get('date').split('-'))) querydate = django.utils.timezone.datetime( date[0], date[1], date[2], 0, 0, 0) - - # 查询日期范围:从查询日期的00:00到次日00:00 nextdate = querydate + datetime.timedelta(days=1) - - # 获取该日期范围内的所有位置记录 models = OwnTrackLog.objects.filter( creation_time__range=(querydate, nextdate)) - result = list() if models and len(models): - # 按设备ID分组处理 for tid, item in groupby( sorted(models, key=lambda k: k.tid), key=lambda k: k.tid): d = dict() - d["name"] = tid # 设备ID作为轨迹名称 - + d["name"] = tid paths = list() - - # 使用高德转换后的经纬度(当前注释掉,使用原始GPS坐标) + # 使用高德转换后的经纬度 # locations = convert_to_amap( # sorted(item, key=lambda x: x.creation_time)) # for i in locations.split(';'): # paths.append(i.split(',')) - - # 使用GPS原始经纬度,按时间排序 + # 使用GPS原始经纬度 for location in sorted(item, key=lambda x: x.creation_time): paths.append([str(location.lon), str(location.lat)]) - - d["path"] = paths # 设备的轨迹路径 - result.append(d) # 添加到结果列表 - - # 返回JSON格式的轨迹数据 - return JsonResponse(result, safe=False) \ No newline at end of file + d["path"] = paths + result.append(d) + return JsonResponse(result, safe=False) diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/account/forget_password.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/account/forget_password.html deleted file mode 100644 index 3384531..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/account/forget_password.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends 'share_layout/base_account.html' %} -{% load i18n %} -{% load static %} -{% block content %} -
- - - - - -

- Home Page - | - login page -

- -
-{% endblock %} \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/account/login.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/account/login.html deleted file mode 100644 index cff8d33..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/account/login.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends 'share_layout/base_account.html' %} -{% load static %} -{% load i18n %} -{% block content %} -
- - - - - -

- - {% trans 'Create Account' %} - - | - Home Page - | - - {% trans 'Forget Password' %} - -

- -
-{% endblock %} \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/account/registration_form.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/account/registration_form.html deleted file mode 100644 index 65e7549..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/account/registration_form.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends 'share_layout/base_account.html' %} -{% load static %} -{% block content %} -
- - - - - -

- Sign In -

- -
-{% endblock %} \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/account/result.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/account/result.html deleted file mode 100644 index 23c9094..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/account/result.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends 'share_layout/base.html' %} -{% load i18n %} -{% block header %} - {{ title }} -{% endblock %} -{% block content %} -
- -
-{% endblock %} \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/article_archives.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/article_archives.html deleted file mode 100644 index 959319e..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/article_archives.html +++ /dev/null @@ -1,60 +0,0 @@ -{% extends 'share_layout/base.html' %} -{% load blog_tags %} -{% load cache %} -{% load i18n %} -{% block header %} - - {% trans 'article archive' %} | {{ SITE_DESCRIPTION }} - - - - - - - - - -{% endblock %} -{% block content %} -
-
- -
- -

{% trans 'article archive' %}

-
- -
- - {% regroup article_list by pub_time.year as year_post_group %} -
    - {% for year in year_post_group %} -
  • {{ year.grouper }} {% trans 'year' %} - {% regroup year.list by pub_time.month as month_post_group %} -
      - {% for month in month_post_group %} -
    • {{ month.grouper }} {% trans 'month' %} - -
    • - {% endfor %} -
    -
  • - {% endfor %} -
-
-
-
- -{% endblock %} - - -{% block sidebar %} - {% load_sidebar user 'i' %} -{% endblock %} - - diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/article_detail.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/article_detail.html deleted file mode 100644 index 0c10c6f..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/article_detail.html +++ /dev/null @@ -1,166 +0,0 @@ -{% extends 'share_layout/base.html' %} -{% load blog_tags %} -{% load static %} - -{% block header %} -{% endblock %} - -{% block content %} -
-
- -
- {% load_article_detail article False user %} -
- - {% if article.type == 'a' %} - - {% endif %} - - -
- {% if user.is_authenticated %} - {# 为按钮添加一个 data-action 属性,用于JS判断当前操作 #} - - {% else %} - - - 收藏 - ({{ article.get_favorite_count }}) - -

- 请 登录 后收藏文章 -

- {% endif %} -
- - - {% if related_articles %} - - {% endif %} - -
- - {% if article.comment_status == "o" and OPEN_SITE_COMMENT %} - {% include 'comments/tags/comment_list.html' %} - {% if user.is_authenticated %} - {% include 'comments/tags/post_comment.html' %} - {% else %} -
-

您还没有登录,请您登录后发表评论。 -

- {% load oauth_tags %} - {% load_oauth_applications request %} -
- {% endif %} - {% endif %} -
-{% endblock %} - -{% block sidebar %} - {% load_sidebar user "p" %} -{% endblock %} - -{% block footer %} - - - -{% endblock %} \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/article_index.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/article_index.html deleted file mode 100644 index 0ee6150..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/article_index.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends 'share_layout/base.html' %} -{% load blog_tags %} -{% load cache %} -{% block header %} - {% if tag_name %} - {{ page_type }}:{{ tag_name }} | {{ SITE_DESCRIPTION }} - {% comment %}{% endcomment %} - {% else %} - {{ SITE_NAME }} | {{ SITE_DESCRIPTION }} - {% endif %} - - - - - - - -{% endblock %} -{% block content %} -
-
- {% if page_type and tag_name %} -
- -

{{ page_type }}:{{ tag_name }}

-
- {% endif %} - - {% for article in article_list %} - {% load_article_detail article True user %} - {% endfor %} - {% if is_paginated %} - {% load_pagination_info page_obj page_type tag_name %} - - {% endif %} -
-
- -{% endblock %} -{% block sidebar %} - {% load_sidebar user linktype %} -{% endblock %} \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/error_page.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/error_page.html deleted file mode 100644 index d41cfb6..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/error_page.html +++ /dev/null @@ -1,45 +0,0 @@ -{% extends 'share_layout/base.html' %} -{% load blog_tags %} -{% load cache %} -{% block header %} - {% if tag_name %} - {% if statuscode == '404' %} - 404 NotFound - {% elif statuscode == '403' %} - Permission Denied - {% elif statuscode == '500' %} - 500 Error - {% else %} - - {% endif %} - {% comment %}{% endcomment %} - {% else %} - {{ SITE_NAME }} | {{ SITE_DESCRIPTION }} - {% endif %} - - - - - - - -{% endblock %} -{% block content %} -
-
- -
-

{{ message }}

-
- -
-
- -{% endblock %} - - -{% block sidebar %} - {% load_sidebar user 'i' %} -{% endblock %} - - diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/links_list.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/links_list.html deleted file mode 100644 index ccecbea..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/links_list.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends 'share_layout/base.html' %} -{% load blog_tags %} -{% load cache %} -{% block header %} - - 友情链接 | {{ SITE_DESCRIPTION }} - - - - - - - - - -{% endblock %} -{% block content %} -
-
- -
- -

友情链接

-
- -
- -
-
-
- -{% endblock %} - - -{% block sidebar %} - {% load_sidebar user 'i' %} -{% endblock %} - - diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/article_info.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/article_info.html deleted file mode 100644 index 0eb030f..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/article_info.html +++ /dev/null @@ -1,86 +0,0 @@ -{% load blog_tags %} -{% load cache %} -{% load i18n %} -
-
- -

- {% if isindex %} - {% if article.article_order > 0 %} - 【{% trans 'pin to top' %}】{{ article.title }} - {% else %} - {{ article.title }} - {% endif %} - - {% else %} - {{ article.title }} - {% endif %} -

- -
- {% if article.type == 'a' %} - {% if not isindex %} - {% cache 36000 breadcrumb article.pk %} - {% load_breadcrumb article %} - {% endcache %} - {% endif %} - {% endif %} -
- -
- {% if isindex %} - {% if isindex %} - {# 如果是列表页(isindex=True),只显示摘要 #} - {{ article.summary|default:article.body|truncatechars:200|safe }} -{% else %} - {# 如果是详情页(isindex=False),显示完整内容 #} - {{ article.body|safe }} -{% endif %} -

Read more

- {% else %} - - {% if article.show_toc %} - {% get_markdown_toc article.body as toc %} - {% trans 'toc' %}: - {{ toc|safe }} - -
- {% endif %} -
- - {% if isindex %} - {# 列表页显示摘要或正文前200字符 #} - {{ article.summary|default:article.body|truncatechars:200|safe }} -{% else %} - {# 详情页显示完整正文(因传入的是 False,对应详情页) #} - {{ article.body|safe }} -{% endif %} - -
- {% endif %} - -
- - {% load_article_metas article user %} - -
\ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/article_meta_info.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/article_meta_info.html deleted file mode 100644 index 977c55a..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/article_meta_info.html +++ /dev/null @@ -1,54 +0,0 @@ -{% load i18n %} -{% load blog_tags %} - - \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/article_pagination.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/article_pagination.html deleted file mode 100644 index 95514ff..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/article_pagination.html +++ /dev/null @@ -1,17 +0,0 @@ -{% load i18n %} - \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/article_tag_list.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/article_tag_list.html deleted file mode 100644 index c8ba474..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/article_tag_list.html +++ /dev/null @@ -1,19 +0,0 @@ -{% load i18n %} -{% if article_tags_list %} -
-
- {% trans 'tags' %} -
-
- - {% for url,count,tag,color in article_tags_list %} - - {{ tag.name }} - {{ count }} - - {% endfor %} - -
-
-{% endif %} diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/breadcrumb.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/breadcrumb.html deleted file mode 100644 index 67087d5..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/breadcrumb.html +++ /dev/null @@ -1,19 +0,0 @@ - - diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/sidebar.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/sidebar.html deleted file mode 100644 index 8d5c524..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/tags/sidebar.html +++ /dev/null @@ -1,129 +0,0 @@ -{% load blog_tags %} -{% load i18n %} - \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/user_favorites.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/user_favorites.html deleted file mode 100644 index 8aac53c..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/user_favorites.html +++ /dev/null @@ -1,97 +0,0 @@ - -{% extends 'share_layout/base.html' %} -{% load static %} -{% load blog_tags %} - -{% block header %} - 我的收藏 | {{ SITE_NAME }} -{% endblock %} - -{% block content %} -
-
- - - -
-
-{% endblock %} - -{% block sidebar %} - {% load_sidebar profile_user "p" %} -{% endblock %} - -{% block extra_footer %} - -{% endblock %} \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/user_profile_detail.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/user_profile_detail.html deleted file mode 100644 index f3a64c2..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/user_profile_detail.html +++ /dev/null @@ -1,230 +0,0 @@ - -{% extends 'share_layout/base.html' %} -{% load static %} -{% load blog_tags %} - -{% block header %} - {{ profile.user.username }}'s Profile | {{ SITE_NAME }} -{% endblock %} - -{% block content %} -
-
- -
-
-
- {% if profile.avatar %} - {{ profile.user.username }}'s avatar - {% else %} - Default Avatar - {% endif %} -
-
-

{{ profile.user.username }}

-
- Joined on {{ profile.created_at|date:"F j, Y" }} - {% if user.is_authenticated and user == profile.user %} - - Edit Profile - - {% endif %} -
-
-
- -
- {% if profile.bio %} -
-

About Me

-

{{ profile.bio|linebreaks }}

-
- {% endif %} - - {% if profile.website or profile.github or profile.twitter or profile.weibo %} - - {% endif %} - - - {% if user.is_authenticated and user == profile.user %} -
- - 我的收藏 ({{ user.favorite_articles.count }}) - -
- {% endif %} - - -
-
- - {% if user_articles %} -
-

Articles by {{ profile.user.username }} ({{ user_articles|length }})

-
    - {% for article in user_articles %} -
  • - {{ article.title }} - -
  • - {% endfor %} -
-
- {% endif %} - -
-
-{% endblock %} - -{% block sidebar %} - {% load_sidebar user "p" %} -{% endblock %} - -{% block extra_footer %} - - -{% endblock %} \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/user_profile_update.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/user_profile_update.html deleted file mode 100644 index 45c1a5e..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/user_profile_update.html +++ /dev/null @@ -1,104 +0,0 @@ - -{% extends 'share_layout/base.html' %} -{% load static %} -{% load blog_tags %} - -{% block header %} - Edit Profile | {{ SITE_NAME }} -{% endblock %} - -{% block content %} -
-
- -
-
-

Edit Your Profile

-
- -
-
- {% csrf_token %} - - {% if form.errors %} -
- Please correct the errors below. -
- {% endif %} - -
-
- {% if user.profile.avatar %} - Current Avatar - {% else %} - Default Avatar - {% endif %} -
- {{ form.avatar.label_tag }} {{ form.avatar }} - {{ form.avatar.help_text }} - {{ form.avatar.errors }} -
- -
- {{ form.bio.label_tag }} - {{ form.bio }} - {{ form.bio.errors }} -
- -
- {{ form.website.label_tag }} - {{ form.website }} - {{ form.website.errors }} -
- -
- {{ form.github.label_tag }} - {{ form.github }} - {{ form.github.errors }} -
- -
- {{ form.twitter.label_tag }} - {{ form.twitter }} - {{ form.twitter.errors }} -
- -
- {{ form.weibo.label_tag }} - {{ form.weibo }} - {{ form.weibo.errors }} -
- - - Cancel -
-
-
- -
-
-{% endblock %} - -{% block sidebar %} - {% load_sidebar user "p" %} -{% endblock %} - -{% block extra_footer %} - -{% endblock %} \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/comments/tags/comment_item.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/comments/tags/comment_item.html deleted file mode 100644 index 0693649..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/comments/tags/comment_item.html +++ /dev/null @@ -1,37 +0,0 @@ -{% load blog_tags %} -
  • -
    - - - -

    {{ comment_item.body|escape|comment_markdown }}

    - -
    - -
  • \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/comments/tags/comment_item_tree.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/comments/tags/comment_item_tree.html deleted file mode 100644 index a407d76..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/comments/tags/comment_item_tree.html +++ /dev/null @@ -1,57 +0,0 @@ -{% load blog_tags %} -
  • -
    - - - -

    - {% if comment_item.parent_comment %} -

    - {% endif %} -

    - -

    {{ comment_item.body|escape|comment_markdown }}

    - - -
    - -
  • -{% query article_comments parent_comment=comment_item as cc_comments %} -{% for cc in cc_comments %} - {% with comment_item=cc template_name="comments/tags/comment_item_tree.html" %} - {% if depth >= 1 %} - {% include template_name %} - {% else %} - {% with depth=depth|add:1 %} - {% include template_name %} - {% endwith %} - {% endif %} - {% endwith %} -{% endfor %} \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/comments/tags/comment_list.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/comments/tags/comment_list.html deleted file mode 100644 index 4092161..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/comments/tags/comment_list.html +++ /dev/null @@ -1,45 +0,0 @@ - -
    - {% load blog_tags %} - {% load comments_tags %} - {% load cache %} - - - {% if article_comments %} -
    -
      - {# {% query article_comments parent_comment=None as parent_comments %}#} - {% for comment_item in p_comments %} - - {% with 0 as depth %} - {% include "comments/tags/comment_item_tree.html" %} - {% endwith %} - {% endfor %} - -
    - -
    -
    - {% endif %} -
    - -
    \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/comments/tags/post_comment.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/comments/tags/post_comment.html deleted file mode 100644 index 3ae5a27..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/comments/tags/post_comment.html +++ /dev/null @@ -1,33 +0,0 @@ -
    - -
    -

    发表评论 - -

    -
    {% csrf_token %} -

    - {{ form.body.label_tag }} - - {{ form.body }} - {{ form.body.errors }} -

    - {{ form.parent_comment_id }} -
    - {% if COMMENT_NEED_REVIEW %} - 支持markdown,评论经审核后才会显示。 - {% else %} - 支持markdown。 - {% endif %} - - -
    -
    -
    - -
    - - diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/oauth/bindsuccess.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/oauth/bindsuccess.html deleted file mode 100644 index 4bee77c..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/oauth/bindsuccess.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends 'share_layout/base.html' %} -{% block header %} - {{ title }} -{% endblock %} -{% block content %} -
    -
    - -
    - -

    {{ content }}

    -
    -
    -
    - - 登录 - | - 回到首页 -
    -
    -
    -{% endblock %} \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/oauth/oauth_applications.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/oauth/oauth_applications.html deleted file mode 100644 index a841ad2..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/oauth/oauth_applications.html +++ /dev/null @@ -1,13 +0,0 @@ -{% load i18n %} - diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/oauth/require_email.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/oauth/require_email.html deleted file mode 100644 index 3adef12..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/oauth/require_email.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends 'share_layout/base_account.html' %} - -{% load static %} -{% block content %} -
    - - - - - -

    - 登录 -

    - -
    -{% endblock %} \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/owntracks/show_log_dates.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/owntracks/show_log_dates.html deleted file mode 100644 index 7dbba21..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/owntracks/show_log_dates.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - 记录日期 - - - -
      - {% for date in results %} -
    • - {{ date }} -
    • - {% endfor %} -
    - - \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/owntracks/show_maps.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/owntracks/show_maps.html deleted file mode 100644 index 3aeda36..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/owntracks/show_maps.html +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - 运动轨迹 - - - -
    - - - - - - - - \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/search/indexes/blog/article_text.txt b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/search/indexes/blog/article_text.txt deleted file mode 100644 index 4f9ca76..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/search/indexes/blog/article_text.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ object.title }} -{{ object.author.username }} -{{ object.body }} \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/search/search.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/search/search.html deleted file mode 100644 index 1404c60..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/search/search.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends 'share_layout/base.html' %} -{% load blog_tags %} -{% block header %} - {{ SITE_NAME }} | {{ SITE_DESCRIPTION }} - - - - - - - -{% endblock %} -{% block content %} -
    -
    - {% if query %} -
    - {% if suggestion %} -

    - 已显示 “{{ suggestion }}” 的搜索结果。   - 仍然搜索:{{ query }}
    -

    - {% else %} -

    - 搜索:{{ query }}    -

    - {% endif %} -
    - {% endif %} - {% if query and page.object_list %} - {% for article in page.object_list %} - {% load_article_detail article.object True user %} - {% endfor %} - {% if page.has_previous or page.has_next %} - - - {% endif %} - {% else %} -
    - -

    哎呀,关键字:{{ query }}没有找到结果,要不换个词再试试?

    -
    - {% endif %} -
    -
    -{% endblock %} - - -{% block sidebar %} - {% load_sidebar request.user 'i' %} -{% endblock %} - - diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/adsense.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/adsense.html deleted file mode 100644 index 8f99c55..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/adsense.html +++ /dev/null @@ -1,6 +0,0 @@ - \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/base.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/base.html deleted file mode 100644 index 5da4913..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/base.html +++ /dev/null @@ -1,157 +0,0 @@ -{% load static %} -{% load cache %} -{% load i18n %} -{% load compress %} - - - - - - - - - - - - {% load blog_tags %} - {% head_meta %} - {% block header %} - - {% endblock %} - - - - - - - - - - - - - - - - - - - - - - {% compress css %} - - - - {% comment %}{% endcomment %} - - - - {% block compress_css %} - {% endblock %} - {% endcompress %} - - {% if GLOBAL_HEADER %} - {{ GLOBAL_HEADER|safe }} - {% endif %} - - - - -
    - -
    - - -
    - {% block content %} - {% endblock %} - - {% block sidebar %} - {% endblock %} -
    - - {% include 'share_layout/footer.html' %} -
    - - - {% compress js %} - - - - - {% block compress_js %} - {% endblock %} - {% endcompress %} - - - - - - - - {% block footer %} - {% endblock %} - - \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/base_account.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/base_account.html deleted file mode 100644 index c00d842..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/base_account.html +++ /dev/null @@ -1,47 +0,0 @@ - - - - {% load static %} - - - - - - - - - {{ SITE_NAME }} | {{ SITE_DESCRIPTION }} - - {% load compress %} - {% compress css %} - - - - - - - - - - {% endcompress %} - {% compress js %} - - - {% endcompress %} - - - - - -{% block content %} -{% endblock %} - - - - - - - \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/footer.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/footer.html deleted file mode 100644 index cd86a29..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/footer.html +++ /dev/null @@ -1,56 +0,0 @@ - - - diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/nav.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/nav.html deleted file mode 100644 index 2146f91..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/nav.html +++ /dev/null @@ -1,131 +0,0 @@ -{% load i18n %} -{% load blog_tags %} - - - - - - - - \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/nav_node.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/nav_node.html deleted file mode 100644 index c266880..0000000 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/nav_node.html +++ /dev/null @@ -1,19 +0,0 @@ - - -