add calendar grid view

This commit is contained in:
Your Name 2023-04-20 13:36:12 -04:00 committed by Henri Dickson
parent c796c462a1
commit cbb36c3ea4
17 changed files with 431 additions and 107 deletions

View file

@ -116,7 +116,7 @@
{% include "partial/_footer.html" %}
</div>
{% if unread_announcements %}
{% if request.user.unread_announcements %}
{% include "partial/_announcement.html" %}
{% endif %}
</body>

View file

@ -306,19 +306,9 @@ def discover(request):
if user.is_authenticated:
layout = user.get_preference().discover_layout
top_tags = user.tag_manager.all_tags[:10]
unread_announcements = Announcement.objects.filter(
pk__gt=request.user.read_announcement_index
).order_by("-pk")
try:
user.read_announcement_index = Announcement.objects.latest("pk").pk
user.save(update_fields=["read_announcement_index"])
except ObjectDoesNotExist:
# when there is no annoucenment
pass
else:
layout = []
top_tags = []
unread_announcements = []
cache_key = "public_gallery_list"
gallery_list = cache.get(cache_key, [])
@ -374,6 +364,5 @@ def discover(request):
"top_tags": top_tags,
"gallery_list": gallery_list,
"layout": layout,
"unread_announcements": unread_announcements,
},
)

View file

@ -0,0 +1,31 @@
.svg-tip {
padding: 10px;
background: #191919;
opacity: 0.8;
color: #eee;
font-size: 12px;
position: absolute;
z-index: 99999;
text-align: center;
border-radius: 3px;
}
.svg-tip:after {
-moz-box-sizing: border-box;
box-sizing: border-box;
position: absolute;
left: 50%;
height: 5px;
width: 5px;
bottom: -10px;
margin: 0 0 0 -5px;
content: " ";
border: 5px solid transparent;
border-top-color: rgba(0,0,0,0.8);
}
.wday, .month {
font-variant: small-caps;
color: #222222;
font-size: 12px;
}

View file

@ -0,0 +1,246 @@
(function ($) {
$.fn.calendar_yearview_blocks = function (options) {
// Format string
if (!String.prototype.formatString) {
String.prototype.formatString = function () {
var args = arguments;
return this.replace(/{(\d+)}/g, function (match, number) {
return typeof args[number] !== 'undefined'
? args[number]
: match
;
});
};
}
// If the number less than 10, add a zero before it
var prettyNumber = function (number) {
return number < 10 ? '0' + number.toString() : number = number.toString();
};
var getDisplayDate = function (date_obj) {
var pretty_month = prettyNumber(date_obj.getMonth() + 1);
var pretty_date = prettyNumber(date_obj.getDate());
return "{0}-{1}-{2}".formatString(date_obj.getFullYear(), pretty_month, pretty_date);
};
var start = function () {
obj_timestamp = JSON.parse(settings.data);
var wrap_chart = _this;
var end_date = new Date(settings.final_date);
end_date.setDate(end_date.getDate()+1);
var current_date = new Date();
var start_date = new Date();
start_date.setMonth(end_date.getMonth() - 12);
var start_weekday = settings.start_monday === true?1:0;
for (var i = 0; i < 7; i++) {
var day = start_date.getDay();
if (day === start_weekday) {
break;
}
else {
// Loop until start_weekday
start_date.setDate(start_date.getDate() + 1);
}
}
var loop_html = "";
// One year has 52 weeks
var step = 13; // Amount of pixels to move
var month_position = [];
month_position.push({month_index: start_date.getMonth(), x: 0});
var using_month = start_date.getMonth();
for (var i = 0; i <= 52; i++) { // For each week, generate a column
var g_x = i * step;
var item_html = '<g transform="translate(' + g_x.toString() + ',0)">';
for (var j = 0; j < 7; j++) { // For each weekday, generate a row
if (start_date > end_date) {
// Break the loop when today's date is found
break;
}
var y = j * step;
var month_in_day = start_date.getMonth();
var data_date = getDisplayDate(start_date);
// Check first day in week
if (j === start_weekday && month_in_day !== using_month) {
using_month = month_in_day;
month_position.push({month_index: using_month, x: g_x});
}
// Put a box around today's date
if (settings.stylize_today) {
var match_today = current_date.getTime() === start_date.getTime() ? '" style="stroke:black;stroke-width:2;opacity:0.5"' : '';
} else {
var match_today = "";
}
var items = [];
var legend = '', items_str = '';
if (obj_timestamp[data_date]) {
if (obj_timestamp[data_date].items) {
items = obj_timestamp[data_date].items;
items_str = items.join(", ")
items_str = items_str.replaceAll('&', '&amp;');
items_str = items_str.replaceAll('"', '&quot;');
}
if (obj_timestamp[data_date].legend) {
legend = obj_timestamp[data_date].legend;
legend = legend.replaceAll('&', '&amp;');
legend = legend.replaceAll('"', '&quot;');
}
}
var item_name = items[0]?items[0]:false;
var color = settings.colors[item_name]?settings.colors[item_name]:settings.colors['default'];
// Fill a square for the 1st item
item_html += '<rect class="day" width="11" height="11" y="' + y + '" fill="' + color + match_today + '" data-items="' + items_str + '" data-legend="' + legend + '" data-date="' + data_date + '"/>';
if (items.length === 2) { // Fill a trangle for the 2nd
var item_name_1 = items[1]?items[1]:false;
var color_1 = settings.colors[item_name_1]?settings.colors[item_name_1]:settings.colors['default'];
item_html += '<polygon points="' + 0 + ',' + (y+11) + ' ' + 0 + ',' + y + ' ' + 11 + ',' + y + '" fill="' + color_1 + '" data-items="' + items_str + '" data-legend="' + legend + '" data-date="' + data_date + '"/>';
} else if (items.length === 3) { // Fill 2 rectangles for 2nd and 3rd
var item_name_1 = items[1]?items[1]:false;
var color_1 = settings.colors[item_name_1]?settings.colors[item_name_1]:settings.colors['default'];
var item_name_2 = items[2]?items[2]:false;
var color_2 = settings.colors[item_name_2]?settings.colors[item_name_2]:settings.colors['default'];
item_html += '<polygon points="' + 0 + ',' + (y+8) + ' ' + 0 + ',' + y + ' ' + 11 + ',' + y + ' ' + 11 + ',' + (y+8) + '" fill="' + color_1 + '" data-items="' + items_str + '" data-legend="' + legend + '" data-date="' + data_date + '"/>';
item_html += '<polygon points="' + 0 + ',' + (y+4) + ' ' + 0 + ',' + y + ' ' + 11 + ',' + y + ' ' + 11 + ',' + (y+4) + '" fill="' + color_2 + '" data-items="' + items_str + '" data-legend="' + legend + '" data-date="' + data_date + '"/>';
} else if (items.length === 4) { // Fill 3 cubes for 2nd, 3rd and 4th
var item_name_1 = items[1]?items[1]:false;
var color_1 = settings.colors[item_name_1]?settings.colors[item_name_1]:settings.colors['default'];
var item_name_2 = items[2]?items[2]:false;
var color_2 = settings.colors[item_name_2]?settings.colors[item_name_2]:settings.colors['default'];
var item_name_3 = items[3]?items[3]:false;
var color_3 = settings.colors[item_name_3]?settings.colors[item_name_3]:settings.colors['default'];
item_html += '<polygon points="' + 0 + ',' + (y+11) + ' ' + 0 + ',' + (y+6) + ' ' + 6 + ',' + (y+6) + ' ' + 6 + ',' + (y+11) + '" fill="' + color_1 + '" data-items="' + items_str + '" data-legend="' + legend + '" data-date="' + data_date + '"/>';
item_html += '<polygon points="' + 0 + ',' + (y+6) + ' ' + 0 + ',' + y + ' ' + 6 + ',' + y + ' ' + 6 + ',' + (y+6) + '" fill="' + color_2 + '" data-items="' + items_str + '" data-legend="' + legend + '" data-date="' + data_date + '"/>';
item_html += '<polygon points="' + 6 + ',' + (y+6) + ' ' + 6 + ',' + y + ' ' + 11 + ',' + y + ' ' + 11 + ',' + (y+6) + '" fill="' + color_3 + '" data-items="' + items_str + '" data-legend="' + legend + '" data-date="' + data_date + '"/>';
}
// Move on to the next day
start_date.setDate(start_date.getDate() + 1);
}
item_html += "</g>";
loop_html += item_html;
}
// Trick
if (month_position[1].x - month_position[0].x < 40) {
// Fix ugly graph by removing the first item
month_position.shift(0);
}
// Add labels for Months
for (var i = 0; i < month_position.length; i++) {
var item = month_position[i];
var month_name = settings.month_names[item.month_index];
loop_html += '<text x="' + item.x + '" y="-5" class="month">' + month_name + '</text>';
}
// Add labels for Weekdays
if (settings.start_monday === true) {
loop_html += '<text text-anchor="middle" class="wday" dx="-12" dy="11">{0}</text>'.formatString(settings.day_names[0]) +
'<text text-anchor="middle" class="wday" dx="-12" dy="36">{0}</text>'.formatString(settings.day_names[1]) +
'<text text-anchor="middle" class="wday" dx="-12" dy="61">{0}</text>'.formatString(settings.day_names[2]) +
'<text text-anchor="middle" class="wday" dx="-12" dy="86">{0}</text>'.formatString(settings.day_names[3]);
} else {
loop_html += '<text text-anchor="middle" class="wday" dx="-10" dy="22">{0}</text>'.formatString(settings.day_names[0]) +
'<text text-anchor="middle" class="wday" dx="-10" dy="48">{0}</text>'.formatString(settings.day_names[1]) +
'<text text-anchor="middle" class="wday" dx="-10" dy="74">{0}</text>'.formatString(settings.day_names[2]);
}
// Fixed size with width= 721 and height = 110
var wire_html =
'<svg width="721" height="110">' +
'<g transform="translate(25, 20)">' +
loop_html +
'</g>' + '"Your browser does not support inline SVG."' +
'</svg>';
wrap_chart.html(wire_html);
_this.find('rect, polygon').on("mouseenter", mouseEnter);
_this.find('rect, polygon').on("mouseleave", mouseLeave);
_this.find('rect, polygon').on("click", mouseClick);
appendTooltip();
};
var mouseLeave = function (evt) {
$('.svg-tip').hide();
};
var mouseClick = function (evt) {
var items = $(evt.target).attr('data-items');
var date = $(evt.target).attr('data-date');
$("#moreinfo").text("On {0}, following items are seen: {1}".formatString(date, items))
};
// Handle mouseEnter event when entering into rect element
var mouseEnter = function (evt) {
var target_offset = $(evt.target).offset();
var items = $(evt.target).attr('data-items');
var legend = $(evt.target).attr('data-legend');
var date = $(evt.target).attr('data-date');
var text = settings.tooltip_style === 'default' ? "{0}: <br />{1}".formatString(date, legend ? legend : items) : (legend ? legend : items);
// Depending on settings, only show a tooltip when there's something to be shown
if (items.length >= 1 || settings.always_show_tooltip === true) {
var svg_tip = $('.svg-tip').show();
svg_tip.html(text);
var svg_width = Math.round(svg_tip.width() / 2 + 5);
var svg_height = svg_tip.height() * 2 + 10;
svg_tip.css({top: target_offset.top - svg_height - 5});
svg_tip.css({left: target_offset.left - svg_width});
}
};
// Append tooltip to display when the mouse enters the rect element
// Default is display:none
var appendTooltip = function () {
if ($('.svg-tip').length === 0) {
$('body').append('<div class="svg-tip svg-tip-one-line" style="display:none" ></div>');
}
};
// Default settings which can be overridden by the user
var settings = $.extend({
colors: {
'default': '#eeeeee'
},
month_names: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
day_names: ['M', 'W', 'F', 'S'],
start_monday: true,
always_show_tooltip: false,
stylize_today: true,
final_date: new Date().toISOString().slice(0, 10),
tooltip_style: 'default', // or 'custom'
data: []
}, options);
var _this = $(this);
start();
};
}(jQuery));

View file

@ -36,7 +36,7 @@ $sub-section-title-margin: 8px
$width: 130px
min-width: $width
max-width: $width
& &__entity-text
margin-left: 20px
overflow: hidden
@ -47,7 +47,7 @@ $sub-section-title-margin: 8px
& &__entity-link
font-size: 1.2em
& &__entity-title
display: block
@ -55,7 +55,7 @@ $sub-section-title-margin: 8px
color: $color-tertiary
margin-left: 5px
position: relative;
top: -1px;
top: -1px;
& &__entity-info
max-width: 73%
@ -107,7 +107,7 @@ $sub-section-title-margin: 8px
object-fit: contain
max-width: 150px
object-position: top
& &__img-origin
cursor: zoom-in
@ -123,7 +123,7 @@ $sub-section-title-margin: 8px
& &__title--secondary
color: $color-tertiary
& &__fields
display: inline-block
vertical-align: top
@ -192,7 +192,7 @@ $mark-review-padding-wider: 6px 0
& &__more-link
margin-left: 5px
& &__mark-list
& &__mark
margin: 0
padding: $mark-review-padding
@ -218,7 +218,7 @@ $mark-review-padding-wider: 6px 0
& &__empty
// includes reviews of an entity
// sub section
// sub section
.entity-reviews
// when used alone
&:first-child
@ -229,7 +229,7 @@ $mark-review-padding-wider: 6px 0
& > a
margin-right: 5px
&--stand-alone
margin-bottom: $section-title-margin
margin-bottom: $section-title-margin
& &__more-link
margin-left: 5px
& &__review-list
@ -241,7 +241,7 @@ $mark-review-padding-wider: 6px 0
&:last-child
border: none
&--wider
padding: $mark-review-padding-wider
padding: $mark-review-padding-wider
& &__review-time
color: $color-light
@ -250,7 +250,7 @@ $mark-review-padding-wider: 6px 0
& &__owner-link
& &__empty
& &__empty
.dividing-line
height: 0
@ -312,7 +312,7 @@ $mark-review-padding-wider: 6px 0
-webkit-line-clamp: 2
& &__empty
// for drag and sort purpose
&--placeholder
border: dashed $color-tertiary 4px
@ -328,7 +328,6 @@ $mark-review-padding-wider: 6px 0
&--hidden
opacity: 0.4
.entity-sort-control
display: flex
justify-content: flex-end
@ -367,7 +366,7 @@ $mark-review-padding-wider: 6px 0
overflow: auto
& &__user-name
& &__user-bio
& &__user-avatar
@ -384,7 +383,7 @@ $mark-review-padding-wider: 6px 0
margin-bottom: 10px
&::after
@include clear
& &__info
float: left
@ -400,7 +399,7 @@ $mark-review-padding-wider: 6px 0
position: relative
top: 3px
left: -1px
& &__actions
float: right
@ -526,7 +525,7 @@ $mark-review-padding-wider: 6px 0
.entity-marks
& &__mark-list
& &__mark
& &__rating-star
@ -550,7 +549,7 @@ $mark-review-padding-wider: 6px 0
float: unset
& &__actions
float: unset
.track-carousel
&__content
padding-bottom: 10px
@ -560,18 +559,23 @@ $mark-review-padding-wider: 6px 0
margin-right: 4.5%
.calendar_view
left: -20px
position: relative
// Medium devices (tablets, 768px and up)
@media (max-width: $medium-devices)
pass
.calendar_view
overflow-x: scroll
// Large devices (desktops, 992px and up)
@media (max-width: $large-devices)
.main-section-wrapper
padding: $main-section-padding-mobile
.entity-detail
display: flex
& &__info
// display: flex
// Extra large devices (large desktops, 1200px and up)
@media (max-width: $x-large-devices)
pass
pass

View file

@ -23,7 +23,7 @@
</div>
<div class="announcement-modal__body">
<ul>
{% for ann in unread_announcements %}
{% for ann in request.user.unread_announcements %}
<li class="announcement">
<a href="{% url 'management:retrieve' ann.pk %}">
<h5 class="announcement__title">{{ ann.title }}</h5>

View file

@ -124,32 +124,6 @@
{% endif %}
</div>
<div
class="aside-section-wrapper aside-section-wrapper--transparent aside-section-wrapper--collapse">
{% if request.user.is_staff and request.user == user%}
<div class="report-panel">
<h5 class="report-panel__label">{% trans '投诉信息' %}</h5>
<a class="report-panel__all-link"
href="{% url 'users:manage_report' %}">全部投诉</a>
<div class="report-panel__body">
<ul class="report-panel__report-list">
{% for report in reports %}
<li class="report-panel__report">
<a href="{% url 'journal:user_profile' report.submit_user.mastodon_username %}"
class="report-panel__user-link">{{ report.submit_user }}</a>{% trans '已投诉' %}<a
href="{% url 'journal:user_profile' report.reported_user.mastodon_username %}"
class="report-panel__user-link">{{ report.reported_user }}</a>
</li>
{% empty %}
<div>{% trans '暂无新投诉' %}</div>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>

View file

@ -1,5 +1,4 @@
from django.db import models
from django.db.models.fields import related
from polymorphic.models import PolymorphicModel
from users.models import User
from catalog.common.models import Item, ItemCategory
@ -24,8 +23,7 @@ from catalog.models import *
from django.contrib.contenttypes.models import ContentType
from .renderers import render_md, render_text
from catalog.common import jsondata
from journal import renderers
from django.db import connection
_logger = logging.getLogger(__name__)
@ -47,6 +45,17 @@ def q_visible_to(viewer, owner):
return Q(visibility=0)
def max_visiblity_to(viewer, owner):
if viewer == owner:
return 2
# elif viewer.is_blocked_by(owner):
# return Q(pk__in=[])
elif viewer.is_authenticated and viewer.is_following(owner):
return 1
else:
return 0
def query_visible(user):
return (
Q(visibility=0)
@ -691,6 +700,31 @@ class ShelfManager:
def get_manager_for_user(user):
return ShelfManager(user)
def get_calendar_data(self, max_visiblity):
shelf_id = self.get_shelf(ShelfType.COMPLETE).pk
timezone_offset = timezone.now().strftime("%z")
calendar_data = {}
sql = "SELECT DATE_TRUNC('day', journal_shelfmember.created_time AT TIME ZONE %s) date, django_content_type.model type, COUNT(1) count FROM journal_shelfmember, catalog_item, django_content_type WHERE journal_shelfmember.item_id = catalog_item.id AND django_content_type.id = catalog_item.polymorphic_ctype_id AND parent_id = %s AND journal_shelfmember.created_time >= NOW() - INTERVAL '366 days' AND journal_shelfmember.visibility <= %s GROUP BY item_id, date, type;"
with connection.cursor() as cursor:
cursor.execute(sql, [timezone_offset, shelf_id, max_visiblity])
data = cursor.fetchall()
for line in data:
date = line[0].strftime("%Y-%m-%d")
typ = line[1]
if date not in calendar_data:
calendar_data[date] = {"items": []}
if typ[:2] == "tv":
typ = "movie"
elif typ == "album":
typ = "music"
elif typ == "edition":
typ = "book"
elif typ not in ["book", "movie", "music", "game"]:
typ = "other"
if typ not in calendar_data[date]["items"]:
calendar_data[date]["items"].append(typ)
return calendar_data
User.shelf_manager = cached_property(ShelfManager.get_manager_for_user)
User.shelf_manager.__set_name__(User, "shelf_manager")

View file

@ -0,0 +1 @@
{{ calendar_data|json_script:"calendar_data" }}

View file

@ -18,9 +18,11 @@
{% endif %}
<link rel="alternate" type="application/rss+xml" title="{{ site_name }} - {{ user.mastodon_username }}的评论" href="{{ request.build_absolute_uri }}feed/reviews/">
{% include "common_libs.html" with jquery=0 %}
{% include "common_libs.html" with jquery=1 %}
<script src="{% static 'js/mastodon.js' %}" defer></script>
<script src="{% static 'js/home.js' %}" defer></script>
<script src="{% static 'lib/js/calendar_yearview_blocks.js' %}" defer></script>
<link href="{% static 'lib/css/calendar_yearview_blocks.css' %}" media="all" rel="stylesheet"/>
</head>
<body>
@ -34,6 +36,38 @@
<div class="main-section-wrapper sortable">
{% if request.user.is_staff %}
<div class="entity-sort" id="calendar_grid">
<h5 class="entity-sort__label">书影音日历</h5>
<div class="calendar_view" hx-get="{% url 'journal:user_calendar_data' user.mastodon_username %}" hx-trigger="load" hx-swap="innerHTML">
<p style="text-align: center;">
<i class="fas fa-fan fa-spin"></i>
</p>
</div>
<script type="text/javascript">
document.body.addEventListener('htmx:afterSettle', function(evt) {
if ($(evt.detail.elt).hasClass('calendar_view')) {
$(evt.detail.elt).calendar_yearview_blocks({
data: $(evt.detail.elt).text(),
start_monday: false,
always_show_tooltip: false,
month_names: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
day_names: ['', '', ''],
colors: {
'default': '#eeeeee', // Default color
'book': '#B4D2A5',
'movie': '#7CBDFE',
'music': '#FEA36D',
'game': '#C5A290',
'other': '#9D6AB0'
}
});
}
});
</script>
</div>
{% endif %}
{% for category, category_shelves in shelf_list.items %}
{% for shelf_type, shelf in category_shelves.items %}
@ -189,7 +223,7 @@
{% include "partial/_footer.html" %}
</div>
{% if unread_announcements %}
{% if request.user.unread_announcements %}
{% include "partial/_announcement.html" %}
{% endif %}
</body>

View file

@ -123,5 +123,10 @@ urlpatterns = [
name="user_tag_list",
),
re_path(r"^users/(?P<user_name>[A-Za-z0-9_\-.@]+)/$", profile, name="user_profile"),
re_path(
r"^users/(?P<user_name>[A-Za-z0-9_\-.@]+)/calendar_data$",
user_calendar_data,
name="user_calendar_data",
),
path("users/<str:id>/feed/reviews/", ReviewFeed(), name="review_feed"),
]

View file

@ -787,24 +787,10 @@ def profile(request, user_name):
if not request.user.is_authenticated and user.get_preference().no_anonymous_view:
return profile_anonymous(request, user_name)
# access one's own home page
if user == request.user:
reports = Report.objects.order_by("-submitted_time").filter(is_read=False)
unread_announcements = Announcement.objects.filter(
pk__gt=request.user.read_announcement_index
).order_by("-pk")
try:
request.user.read_announcement_index = Announcement.objects.latest("pk").pk
request.user.save(update_fields=["read_announcement_index"])
except ObjectDoesNotExist:
# when there is no annoucenment
pass
# visit other's home page
else:
if user.is_blocked_by(request.user) or user.is_blocking(request.user):
return render_user_blocked(request)
# no these value on other's home page
reports = None
unread_announcements = None
if user != request.user and (
user.is_blocked_by(request.user) or user.is_blocking(request.user)
):
return render_user_blocked(request)
qv = q_visible_to(request.user, user)
shelf_list = {}
@ -866,7 +852,22 @@ def profile(request, user_name):
],
"liked_collections_count": liked_collections.count(),
"layout": user.get_preference().profile_layout,
"reports": reports,
"unread_announcements": unread_announcements,
},
)
def user_calendar_data(request, user_name):
if request.method != "GET":
raise BadRequest()
user = User.get(user_name)
if user is None or not request.user.is_authenticated:
return HttpResponse("")
max_visiblity = max_visiblity_to(request.user, user)
calendar_data = user.shelf_manager.get_calendar_data(max_visiblity)
return render(
request,
"calendar_data.html",
{
"calendar_data": calendar_data,
},
)

View file

@ -6,8 +6,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
{% include "common_libs.html" with jquery=0 %}
<title>{{ site_name }} - {{ object.title }}</title>
</head>
@ -44,4 +43,4 @@
</script>
</body>
</html>
</html>

View file

@ -7,8 +7,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
{% include "common_libs.html" with jquery=0 %}
<title>{{ site_name }} - {% trans '公告栏' %}</title>
</head>
@ -58,10 +57,10 @@
<a href="{% url 'management:delete' announcement.pk %}">{% trans '删除' %}</a>
</span>
{% endif %}
<p>{{ announcement.get_plain_content }}</p>
</li>
{% if not forloop.last %}
<div class="dividing-line"></div>
{% endif %}
@ -71,16 +70,16 @@
{% endfor %}
</ul>
<!-- <div class="pagination">
{% if object_list.pagination.has_prev %}
<a href="?page=1&q={% if request.GET.q %}{{ request.GET.q }}{% endif %}"
class="pagination__nav-link pagination__nav-link">&laquo;</a>
<a href="?page={{ object_list.previous_page_number }}&q={% if request.GET.q %}{{ request.GET.q }}{% endif %}"
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">&lsaquo;</a>
{% endif %}
{% for page in object_list.pagination.page_range %}
{% if page == object_list.pagination.current_page %}
<a href="?page={{ page }}&q={% if request.GET.q %}{{ request.GET.q }}{% endif %}"
class="pagination__page-link pagination__page-link--current">{{ page }}</a>
@ -88,16 +87,16 @@
<a href="?page={{ page }}&q={% if request.GET.q %}{{ request.GET.q }}{% endif %}"
class="pagination__page-link">{{ page }}</a>
{% endif %}
{% endfor %}
{% if object_list.pagination.has_next %}
<a href="?page={{ object_list.next_page_number }}&q={% if request.GET.q %}{{ request.GET.q }}{% endif %}"
class="pagination__nav-link pagination__nav-link--left-margin">&rsaquo;</a>
<a href="?page={{ object_list.pagination.last_page }}&q={% if request.GET.q %}{{ request.GET.q }}{% endif %}"
class="pagination__nav-link">&raquo;</a>
{% endif %}
</div> -->
</div>
</div>

View file

@ -58,7 +58,7 @@
})
</script>
{% if unread_announcements %}
{% if request.user.unread_announcements %}
{% include "partial/_announcement.html" %}
{% endif %}
</body>

View file

@ -17,19 +17,11 @@ PAGE_SIZE = 10
def feed(request):
if request.method != "GET":
raise BadRequest()
user = request.user
unread = Announcement.objects.filter(pk__gt=user.read_announcement_index).order_by(
"-pk"
)
if unread:
user.read_announcement_index = Announcement.objects.latest("pk").pk
user.save(update_fields=["read_announcement_index"])
return render(
request,
"feed.html",
{
"top_tags": user.tag_manager.all_tags[:10],
"unread_announcements": unread,
"top_tags": request.user.tag_manager.all_tags[:10],
},
)

View file

@ -1,5 +1,6 @@
import uuid
import django.contrib.postgres.fields as postgres
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils import timezone
@ -7,6 +8,7 @@ from django.core.serializers.json import DjangoJSONEncoder
from django.utils.translation import gettext_lazy as _
from common.utils import GenerateDateUUIDMediaFilePath
from django.conf import settings
from management.models import Announcement
from mastodon.api import *
from django.urls import reverse
@ -186,6 +188,19 @@ class User(AbstractUser):
else:
return 0
@property
def unread_announcements(self):
unread_announcements = Announcement.objects.filter(
pk__gt=self.read_announcement_index
).order_by("-pk")
try:
self.read_announcement_index = Announcement.objects.latest("pk").pk
self.save(update_fields=["read_announcement_index"])
except ObjectDoesNotExist:
# when there is no annoucenment
pass
return unread_announcements
@classmethod
def get(cls, id):
if isinstance(id, str):