Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions blog/migrations/0007_favorite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Generated by Django 5.2.1 on 2025-05-17 10:30

import django.db.models.deletion
import django.utils.timezone
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.AutoField(primary_key=True, serialize=False)),
(
"creation_time",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="creation time"
),
),
(
"last_modify_time",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="modify time"
),
),
(
"article",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="blog.article",
verbose_name="article",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="user",
),
),
],
options={
"verbose_name": "favorite",
"verbose_name_plural": "favorite",
"unique_together": {("user", "article")},
},
),
]
23 changes: 23 additions & 0 deletions blog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,3 +363,26 @@ def save(self, *args, **kwargs):
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()


class Favorite(BaseModel):
"""文章收藏"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('user'),
on_delete=models.CASCADE)
article = models.ForeignKey(
'Article',
verbose_name=_('article'),
on_delete=models.CASCADE)

class Meta:
verbose_name = _('favorite')
verbose_name_plural = verbose_name
unique_together = ('user', 'article') # 防止重复收藏

def __str__(self):
return f'{self.user.username} - {self.article.title}'

def get_absolute_url(self):
return self.article.get_absolute_url()
91 changes: 90 additions & 1 deletion blog/static/blog/js/blog.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,93 @@ window.onload = function () {
// selector.on('change', function () {
// form.submit();
// });
// });
// });

// 获取CSRF Token的函数
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}

// 文章详情页收藏功能
function initArticleFavorite() {
const favoriteBtn = document.getElementById('favoriteBtn');
if (!favoriteBtn) return;

const favoriteText = document.getElementById('favoriteText');
const articleId = favoriteBtn.dataset.articleId;

// 检查是否已收藏
fetch(`/favorite/check/${articleId}/`)
.then(response => response.json())
.then(data => {
if (data.is_favorite) {
favoriteBtn.classList.remove('btn-primary');
favoriteBtn.classList.add('btn-danger');
favoriteText.textContent = '取消收藏';
}
});

favoriteBtn.addEventListener('click', function() {
const isFavorite = favoriteBtn.classList.contains('btn-danger');
const url = isFavorite ? `/favorite/remove/${articleId}/` : `/favorite/add/${articleId}/`;

fetch(url, {
Copy link

Copilot AI May 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding error handling (e.g. catch blocks) for fetch requests will make the client-side interactions more robust in case of network failures.

Copilot uses AI. Check for mistakes.
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
if (isFavorite) {
favoriteBtn.classList.remove('btn-danger');
favoriteBtn.classList.add('btn-primary');
favoriteText.textContent = '收藏文章';
} else {
favoriteBtn.classList.remove('btn-primary');
favoriteBtn.classList.add('btn-danger');
favoriteText.textContent = '取消收藏';
}
}
});
});
}

// 收藏列表页功能
function initFavoriteList() {
document.querySelectorAll('.remove-favorite').forEach(button => {
button.addEventListener('click', function() {
const articleId = this.dataset.articleId;
fetch(`/favorite/remove/${articleId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
this.closest('.article-item').remove();
}
});
});
});
}

// 初始化所有功能
document.addEventListener('DOMContentLoaded', function() {
initArticleFavorite();
initFavoriteList();
});
4 changes: 4 additions & 0 deletions blog/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,8 @@
r'clean',
views.clean_cache_view,
name='clean'),
path('favorite/add/<int:article_id>/', views.add_favorite, name='add_favorite'),
path('favorite/remove/<int:article_id>/', views.remove_favorite, name='remove_favorite'),
path('favorite/check/<int:article_id>/', views.check_favorite, name='check_favorite'),
path('favorites/', views.favorite_list, name='favorite_list'),
]
51 changes: 47 additions & 4 deletions blog/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@

from django.conf import settings
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
from django.shortcuts import get_object_or_404, 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 haystack.views import SearchView
from django.contrib.auth.decorators import login_required

from blog.models import Article, Category, LinkShowType, Links, Tag
from blog.models import Article, Category, LinkShowType, Links, Tag, Favorite
from comments.forms import CommentForm
from djangoblog.utils import cache, get_blog_setting, get_sha256

Expand Down Expand Up @@ -373,3 +373,46 @@ def permission_denied_view(
def clean_cache_view(request):
cache.clear()
return HttpResponse('ok')


@login_required
def add_favorite(request, article_id):
article = get_object_or_404(Article, id=article_id)
favorite, created = Favorite.objects.get_or_create(
user=request.user,
article=article
)
return JsonResponse({
'status': 'success',
'message': '收藏成功' if created else '已经收藏过了'
})

@login_required
def remove_favorite(request, article_id):
article = get_object_or_404(Article, id=article_id)
Favorite.objects.filter(
user=request.user,
article=article
).delete()
return JsonResponse({
'status': 'success',
'message': '取消收藏成功'
})

@login_required
def favorite_list(request):
favorites = Favorite.objects.filter(user=request.user).select_related('article')
return render(request, 'blog/favorite_list.html', {
'favorites': favorites
})

@login_required
def check_favorite(request, article_id):
article = get_object_or_404(Article, id=article_id)
is_favorite = Favorite.objects.filter(
user=request.user,
article=article
).exists()
return JsonResponse({
'is_favorite': is_favorite
})
4 changes: 2 additions & 2 deletions deploy/k8s/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ data:
DJANGO_MYSQL_PORT: db_port
DJANGO_REDIS_URL: "redis:6379"
DJANGO_DEBUG: "False"
MYSQL_ROOT_PASSWORD: DjAnGoBlOg!2!Q@W#E
MYSQL_ROOT_PASSWORD: db_password
MYSQL_DATABASE: djangoblog
MYSQL_PASSWORD: DjAnGoBlOg!2!Q@W#E
MYSQL_PASSWORD: db_password
DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx

17 changes: 16 additions & 1 deletion templates/blog/article_detail.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{% extends 'share_layout/base.html' %}
{% load blog_tags %}
{% load static %}

{% block header %}
<title>{{ article.title }} | {{ SITE_DESCRIPTION }}</title>
Expand Down Expand Up @@ -32,6 +33,15 @@
<div id="content" role="main">
{% load_article_detail article False user %}

{% if user.is_authenticated %}
<div class="favorite-container" style="margin: 20px 0;">
<button id="favoriteBtn" class="btn btn-primary" data-article-id="{{ article.id }}">
<i class="fa fa-star"></i>
<span id="favoriteText">收藏文章</span>
Copy link

Copilot AI May 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider wrapping '收藏文章' in a translation function to ensure consistent internationalization support as seen elsewhere in the application.

Suggested change
<span id="favoriteText">收藏文章</span>
<span id="favoriteText">{% trans "收藏文章" %}</span>

Copilot uses AI. Check for mistakes.
</button>
</div>
{% endif %}

{% if article.type == 'a' %}
<nav class="nav-single">
<h3 class="assistive-text">文章导航</h3>
Expand Down Expand Up @@ -69,8 +79,13 @@ <h3 class="comment-meta">您还没有登录,请您<a
{% endif %}
</div><!-- #primary -->

{% if user.is_authenticated %}
{% block extra_js %}
<script src="{% static 'blog/js/blog.js' %}"></script>
{% endblock %}
{% endif %}
{% endblock %}

{% block sidebar %}
{% load_sidebar user "p" %}
{% endblock %}
{% endblock %}
57 changes: 57 additions & 0 deletions templates/blog/favorite_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{% extends 'share_layout/base.html' %}
{% load blog_tags %}
{% load i18n %}
{% load static %}

{% block title %}
我的收藏 - {{ SITE_NAME }}
{% endblock %}

{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h3 class="card-title">我的收藏</h3>
</div>
<div class="card-body">
{% if favorites %}
<div class="article-list">
{% for favorite in favorites %}
<div class="article-item">
<h2 class="article-title">
<a href="{{ favorite.article.get_absolute_url }}">
{{ favorite.article.title }}
</a>
</h2>
<div class="article-meta">
<span class="article-date">
收藏于: {{ favorite.creation_time|date:"Y-m-d H:i" }}
</span>
<span class="article-category">
分类: <a href="{{ favorite.article.category.get_absolute_url }}">
{{ favorite.article.category.name }}
</a>
</span>
<button class="btn btn-sm btn-danger remove-favorite"
data-article-id="{{ favorite.article.id }}">
取消收藏
</button>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-center">还没有收藏任何文章</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

{% block extra_js %}
<script src="{% static 'blog/js/blog.js' %}"></script>
{% endblock %}
6 changes: 6 additions & 0 deletions templates/share_layout/nav.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
class="menu-item menu-item-type-custom menu-item-object-custom current-menu-item current_page_item menu-item-home menu-item-3498">
<a href="/">{% trans 'index' %}</a></li>

<li class="menu-item">
<a href="{% url 'blog:favorite_list' %}">
<i class="fa fa-star"></i> 我的收藏
Copy link

Copilot AI May 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider wrapping the text '我的收藏' with a translation tag (e.g. {% trans '我的收藏' %}) for consistency with other localized strings.

Suggested change
<i class="fa fa-star"></i> 我的收藏
<i class="fa fa-star"></i> {% trans '我的收藏' %}

Copilot uses AI. Check for mistakes.
</a>
</li>

{% load blog_tags %}
{% query nav_category_list parent_category=None as root_categorys %}
{% for node in root_categorys %}
Expand Down
Loading