diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/admin.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/admin.py index 46c3420..e5232f2 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/admin.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/admin.py @@ -4,109 +4,192 @@ 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 -# Register your models here. -from .models import Article - +# 导入 blog 应用下的所有模型 +from .models import ( + Article, Category, Tag, Links, SideBar, BlogSettings, + UserProfile, Favorite # 确保导入了 UserProfile 和 Favorite +) +# ------------------------------------------------------------------------------ +# Article Admin +# ------------------------------------------------------------------------------ class ArticleForm(forms.ModelForm): - # body = forms.CharField(widget=AdminPagedownWidget()) - class Meta: model = Article fields = '__all__' - -def makr_article_publish(modeladmin, request, queryset): +def make_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') - -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): +@admin.register(Article) +class ArticleAdmin(admin.ModelAdmin): list_per_page = 20 search_fields = ('body', 'title') form = ArticleForm list_display = ( - 'id', - 'title', - 'author', - 'link_to_category', - 'creation_time', - 'views', - 'status', - 'type', - 'article_order') + 'id', 'title', 'author', 'link_to_category', + 'pub_time', 'views', 'status', 'type', 'article_order' + ) list_display_links = ('id', 'title') - list_filter = ('status', 'type', 'category') + list_filter = ('status', 'type', 'category', 'tags') 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 = [ - makr_article_publish, - draft_article, - close_article_commentstatus, - open_article_commentstatus] + make_article_publish, draft_article, + close_article_commentstatus, open_article_commentstatus + ] def link_to_category(self, obj): - 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') + 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') def get_form(self, request, obj=None, **kwargs): - form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs) - form.base_fields['author'].queryset = get_user_model( - ).objects.filter(is_superuser=True) + form = super().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: - url = obj.get_full_url() - return url - else: - from djangoblog.utils import get_current_site - site = get_current_site().domain - return site - - -class TagAdmin(admin.ModelAdmin): - exclude = ('slug', 'last_mod_time', 'creation_time') - + return obj.get_full_url() + return super().get_view_on_site_url(obj) +# ------------------------------------------------------------------------------ +# Category Admin +# ------------------------------------------------------------------------------ +@admin.register(Category) class CategoryAdmin(admin.ModelAdmin): - list_display = ('name', 'parent_category', 'index') + 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 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 = ('name', 'content', 'is_enable', 'sequence') + 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): - pass + list_display = ('id', 'site_name') + 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 diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/forms.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/forms.py index 715be76..8d97eee 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/forms.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/forms.py @@ -17,3 +17,22 @@ 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/migrations/0007_favorite.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/migrations/0007_favorite.py new file mode 100644 index 0000000..58b31f9 --- /dev/null +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/migrations/0007_favorite.py @@ -0,0 +1,30 @@ +# 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 new file mode 100644 index 0000000..1bd47d4 --- /dev/null +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/migrations/0008_userprofile.py @@ -0,0 +1,37 @@ +# 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 edbe4e1..c8547f4 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/models.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/models.py @@ -1,5 +1,6 @@ import logging import re +import os from abc import abstractmethod from django.conf import settings from django.core.exceptions import ValidationError @@ -13,8 +14,18 @@ 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')) @@ -23,7 +34,6 @@ class LinkShowType(models.TextChoices): A = ('a', _('all')) S = ('s', _('slide')) - class BaseModel(models.Model): id = models.AutoField(primary_key=True) creation_time = models.DateTimeField(_('creation time'), default=now) @@ -56,7 +66,6 @@ class BaseModel(models.Model): def get_absolute_url(self): pass - class Article(BaseModel): """文章""" STATUS_CHOICES = ( @@ -130,9 +139,6 @@ class Article(BaseModel): names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) return names - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - def viewed(self): self.views += 1 self.save(update_fields=['views']) @@ -210,6 +216,11 @@ class Article(BaseModel): return related_articles + # 新增:获取文章的收藏数 + def get_favorite_count(self): + """获取文章的收藏数""" + return self.favorites.count() + class Category(BaseModel): """文章分类""" @@ -273,7 +284,6 @@ class Category(BaseModel): parse(self) return categorys - class Tag(BaseModel): """文章标签""" name = models.CharField(_('tag name'), max_length=30, unique=True) @@ -294,7 +304,6 @@ class Tag(BaseModel): verbose_name = _('tag') verbose_name_plural = verbose_name - class Links(models.Model): """友情链接""" @@ -319,7 +328,6 @@ class Links(models.Model): def __str__(self): return self.name - class SideBar(models.Model): """侧边栏,可以展示一些html内容""" name = models.CharField(_('title'), max_length=100) @@ -337,7 +345,6 @@ class SideBar(models.Model): def __str__(self): return self.name - class BlogSettings(models.Model): """blog的配置""" site_name = models.CharField( @@ -407,4 +414,79 @@ class BlogSettings(models.Model): def save(self, *args, **kwargs): super().save(*args, **kwargs) from djangoblog.utils import cache - cache.clear() \ No newline at end of file + 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() 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 d6cd5d5..53f457d 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) + return str(arg1) + str(arg2) \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/urls.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/urls.py index e1c78e4..e1dbaec 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/urls.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/urls.py @@ -1,108 +1,130 @@ +# blog/urls.py + # 导入 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( - r'', # URL 路径表达式,空字符串表示根路径 - views.IndexView.as_view(), # 关联的视图类(IndexView),通过 as_view() 转换为可调用视图 - name='index' # 路由名称,用于反向解析(如 reverse('blog:index')) + '', + views.IndexView.as_view(), + name='index' ), - - # 分页首页路由:匹配带页码的首页(如 /page/2/) path( - r'page//', # 是路径参数,int 表示接收整数类型,page 是参数名 - views.IndexView.as_view(), # 复用首页视图类,视图中会通过 page 参数处理分页 + 'page//', + views.IndexView.as_view(), name='index_page' ), - # 文章详情页路由:按日期和文章ID匹配(如 /article/2023/10/20/100.html) + # 文章详情页路由 path( - r'article////.html', - # 路径参数:year(年)、month(月)、day(日)、article_id(文章ID),均为整数 - views.ArticleDetailView.as_view(), # 文章详情视图类,处理文章展示逻辑 + 'article//', + views.ArticleDetailView.as_view(), + name='article_detail' + ), + # 原始的带日期的URL保持不变 + path( + 'article////.html', + views.ArticleDetailView.as_view(), name='detailbyid' ), - # 分类详情页路由:按分类名匹配(如 /category/tech.html) + # 文章收藏功能路由 path( - r'category/.html', - # :slug 类型表示接收字母、数字、下划线和连字符组成的字符串(适合URL友好的名称) - views.CategoryDetailView.as_view(), # 分类详情视图类,展示该分类下的文章 - name='category_detail' + 'article//favorite/', + views.ArticleFavoriteView.as_view(), + name='article_favorite' ), - # 分类详情分页路由:带页码的分类页(如 /category/tech/2.html) + # 分类详情页路由 path( - r'category//.html', - views.CategoryDetailView.as_view(), # 复用分类视图类,通过 page 参数分页 + 'category/.html', + views.CategoryDetailView.as_view(), + name='category_detail' + ), + path( + 'category//.html', + views.CategoryDetailView.as_view(), name='category_detail_page' ), - # 作者详情页路由:按作者名匹配(如 /author/alice.html) + # 作者详情页路由 path( - r'author/.html', - # :未指定类型,默认接收字符串(除特殊字符外) - views.AuthorDetailView.as_view(), # 作者详情视图类,展示该作者的文章 + 'author/.html', + views.AuthorDetailView.as_view(), name='author_detail' ), - - # 作者详情分页路由:带页码的作者页(如 /author/alice/2.html) path( - r'author//.html', - views.AuthorDetailView.as_view(), # 复用作者视图类,通过 page 参数分页 + 'author//.html', + views.AuthorDetailView.as_view(), name='author_detail_page' ), - # 标签详情页路由:按标签名匹配(如 /tag/python.html) + # 标签详情页路由 path( - r'tag/.html', - views.TagDetailView.as_view(), # 标签详情视图类,展示该标签下的文章 + 'tag/.html', + views.TagDetailView.as_view(), name='tag_detail' ), - - # 标签详情分页路由:带页码的标签页(如 /tag/python/2.html) path( - r'tag//.html', - views.TagDetailView.as_view(), # 复用标签视图类,通过 page 参数分页 + 'tag//.html', + views.TagDetailView.as_view(), 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' ), - # 文件上传路由:匹配 /upload + # ==================== 关键修正:调整 URL 顺序 ==================== + # 用户资料相关路由 + # 将具体的路径放在通用路径之前 path( - r'upload', - views.fileupload, # 关联函数视图(非类视图),处理文件上传逻辑 - name='upload' + 'profile/edit/', + views.UserProfileUpdateView.as_view(), + name='user_profile_update' ), + # 我的收藏页面路由 + path( + 'profile/favorites/', + views.UserFavoritesView.as_view(), + name='user_favorites' + ), + # 通用的用户资料路径放在最后 + path( + 'profile//', + views.UserProfileDetailView.as_view(), + name='user_profile' + ), + # ================================================================= - # 缓存清理路由:匹配 /clean + # 其他功能路由 + path( + 'upload', + views.fileupload, + name='upload' + ), path( - r'clean', - views.clean_cache_view, # 关联缓存清理视图,用于手动触发缓存清理 + '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 b207192..b0c38f9 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/views.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/views.py @@ -4,18 +4,26 @@ import uuid from django.conf import settings from django.core.paginator import Paginator -from django.http import HttpResponse, HttpResponseForbidden +from django.http import HttpResponse, HttpResponseForbidden, JsonResponse from django.shortcuts import get_object_or_404 -from django.shortcuts import render +from django.shortcuts import render, redirect 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 blog.models import Article, Category, LinkShowType, Links, Tag +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 comments.forms import CommentForm from djangoblog.plugin_manage import hooks from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME @@ -23,7 +31,6 @@ 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' @@ -96,7 +103,6 @@ class ArticleListView(ListView): kwargs['hot_articles'] = hot_articles return super(ArticleListView, self).get_context_data(** kwargs) - class IndexView(ArticleListView): ''' 首页 @@ -112,7 +118,6 @@ class IndexView(ArticleListView): cache_key = 'index_{page}'.format(page=self.page_number) return cache_key - class ArticleDetailView(DetailView): ''' 文章详情页面 @@ -171,6 +176,10 @@ 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, 通知插件"文章详情已获取" @@ -181,7 +190,6 @@ class ArticleDetailView(DetailView): return context - class CategoryDetailView(ArticleListView): ''' 分类目录列表 @@ -219,7 +227,6 @@ class CategoryDetailView(ArticleListView): kwargs['tag_name'] = categoryname return super(CategoryDetailView, self).get_context_data(** kwargs) - class AuthorDetailView(ArticleListView): ''' 作者详情页 @@ -235,6 +242,7 @@ 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 @@ -245,7 +253,6 @@ class AuthorDetailView(ArticleListView): kwargs['tag_name'] = author_name return super(AuthorDetailView, self).get_context_data(** kwargs) - class TagDetailView(ArticleListView): ''' 标签列表页面 @@ -276,7 +283,6 @@ class TagDetailView(ArticleListView): kwargs['tag_name'] = tag_name return super(TagDetailView, self).get_context_data(** kwargs) - class ArchivesView(ArticleListView): ''' 文章归档页面 @@ -293,7 +299,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) @@ -302,8 +308,7 @@ 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 @@ -312,7 +317,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) @@ -321,8 +326,7 @@ 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): @@ -337,7 +341,6 @@ class EsSearchView(SearchView): context['hot_articles'] = hot_articles return context - @csrf_exempt def fileupload(request): """ @@ -379,7 +382,6 @@ def fileupload(request): else: return HttpResponse("only for post") - def page_not_found_view( request, exception, @@ -393,7 +395,6 @@ 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, @@ -401,7 +402,6 @@ def server_error_view(request, template_name='blog/error_page.html'): 'statuscode': '500'}, status=500) - def permission_denied_view( request, exception, @@ -413,7 +413,6 @@ 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') @@ -477,4 +476,118 @@ class DjangoBlogFeed(Feed): 返回单个项目(文章)的发布日期。 这是可选的,但推荐添加,以符合 RSS 规范。 """ - return item.creation_time \ No newline at end of file + 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 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 f120405..40b03fc 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, ArticlelAdmin) +admin_site.register(Article, ArticleAdmin) 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 30f9ac5..232cc2b 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/settings.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/settings.py @@ -12,15 +12,8 @@ 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 @@ -30,18 +23,17 @@ 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 = env_to_bool('DJANGO_DEBUG', True) -# DEBUG = False +DEBUG = os.environ.get('DJANGO_DEBUG', 'True') == 'True' TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' # ALLOWED_HOSTS = [] -ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] +ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '*').split(',') # django 4.0新增配置 -CSRF_TRUSTED_ORIGINS = ['http://example.com'] -# Application definition - +CSRF_TRUSTED_ORIGINS = os.environ.get('DJANGO_CSRF_TRUSTED_ORIGINS', 'http://localhost,http://127.0.0.1').split(',') +# Application definition INSTALLED_APPS = [ # 'django.contrib.admin', 'django.contrib.admin.apps.SimpleAdminConfig', @@ -65,7 +57,6 @@ INSTALLED_APPS = [ ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', @@ -104,8 +95,6 @@ WSGI_APPLICATION = 'djangoblog.wsgi.application' # Database # https://docs.djangoproject.com/en/1.10/ref/settings/#databases - - DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', @@ -113,15 +102,16 @@ 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'}, - }} + 'charset': 'utf8mb4', + 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", + }, + } +} # Password validation # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators - AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', @@ -147,18 +137,31 @@ 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': { @@ -168,15 +171,11 @@ 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/' @@ -192,6 +191,7 @@ BOOTSTRAP_COLOR_TYPES = [ PAGINATE_BY = 10 # http cache timeout CACHE_CONTROL_MAX_AGE = 2592000 + # cache setting CACHES = { 'default': { @@ -215,16 +215,18 @@ BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \ # Email: EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) -EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) +EMAIL_USE_TLS = os.environ.get('DJANGO_EMAIL_TLS', 'False') == 'True' +EMAIL_USE_SSL = os.environ.get('DJANGO_EMAIL_SSL', 'True') == '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' @@ -300,9 +302,13 @@ STATICFILES_FINDERS = ( # other 'compressor.finders.CompressorFinder', ) -COMPRESS_ENABLED = True -# COMPRESS_OFFLINE = True +# --- MODIFICATION: 动态设置压缩开关 --- +# 在开发环境(DEBUG=True)关闭压缩,在生产环境(DEBUG=False)开启压缩 +COMPRESS_ENABLED = not DEBUG +# --- MODIFICATION END --- + +# COMPRESS_OFFLINE = True COMPRESS_CSS_FILTERS = [ # creates absolute urls from relative ones @@ -314,8 +320,6 @@ 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' @@ -340,4 +344,17 @@ ACTIVE_PLUGINS = [ 'external_links', 'view_count', 'seo_optimizer' -] \ No newline at end of file +] +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/media/user_2/avatar/173167639100-95311.jpg b/src/DjangoBlog-master(1)/DjangoBlog-master/media/user_2/avatar/173167639100-95311.jpg new file mode 100644 index 0000000..68d3f17 Binary files /dev/null and b/src/DjangoBlog-master(1)/DjangoBlog-master/media/user_2/avatar/173167639100-95311.jpg 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 new file mode 100644 index 0000000..68d3f17 Binary files /dev/null and b/src/DjangoBlog-master(1)/DjangoBlog-master/media/user_2/avatar/173167639100-95311_6MV3mFh.jpg 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 new file mode 100644 index 0000000..1fd9c6b Binary files /dev/null and b/src/DjangoBlog-master(1)/DjangoBlog-master/media/user_2/avatar/IMG_24891.jpg differ 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 index 1361436..0c10c6f 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/article_detail.html +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/article_detail.html @@ -1,5 +1,6 @@ {% extends 'share_layout/base.html' %} {% load blog_tags %} +{% load static %} {% block header %} {% endblock %} @@ -7,7 +8,10 @@ {% block content %}
- {% load_article_detail article False user %} + +
+ {% 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 %}
@@ -52,7 +84,6 @@ {% endif %} -
@@ -75,4 +106,61 @@ {% 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/user_favorites.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/user_favorites.html new file mode 100644 index 0000000..8aac53c --- /dev/null +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/user_favorites.html @@ -0,0 +1,97 @@ + +{% 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 new file mode 100644 index 0000000..f3a64c2 --- /dev/null +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/user_profile_detail.html @@ -0,0 +1,230 @@ + +{% 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 new file mode 100644 index 0000000..45c1a5e --- /dev/null +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/blog/user_profile_update.html @@ -0,0 +1,104 @@ + +{% 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/share_layout/base.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/base.html index cf5f53a..5da4913 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/base.html +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/base.html @@ -43,6 +43,21 @@ + + + + {% compress css %} -
- -
- - {% block content %} - {% endblock %} +
+ +
+ {% block content %} + {% endblock %} - {% block sidebar %} - {% endblock %} + {% block sidebar %} + {% endblock %} +
- -
- {% include 'share_layout/footer.html' %} -
+ {% include 'share_layout/footer.html' %} +
{% compress js %} @@ -99,11 +113,45 @@ {% 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/nav.html b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/nav.html index 24d4da6..2146f91 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/nav.html +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/templates/share_layout/nav.html @@ -1,4 +1,5 @@ {% load i18n %} +{% load blog_tags %} \ No newline at end of file + + + + + + + \ No newline at end of file