diff --git a/catalog/templates/discover.html b/catalog/templates/discover.html index 144df49f..1468e3f2 100644 --- a/catalog/templates/discover.html +++ b/catalog/templates/discover.html @@ -116,7 +116,7 @@ {% include "partial/_footer.html" %} - {% if unread_announcements %} + {% if request.user.unread_announcements %} {% include "partial/_announcement.html" %} {% endif %} diff --git a/catalog/views.py b/catalog/views.py index ba3edc5b..cec71849 100644 --- a/catalog/views.py +++ b/catalog/views.py @@ -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, }, ) diff --git a/common/static/lib/css/calendar_yearview_blocks.css b/common/static/lib/css/calendar_yearview_blocks.css new file mode 100644 index 00000000..d5785d0e --- /dev/null +++ b/common/static/lib/css/calendar_yearview_blocks.css @@ -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; +} diff --git a/common/static/lib/js/calendar_yearview_blocks.js b/common/static/lib/js/calendar_yearview_blocks.js new file mode 100644 index 00000000..7c9934ec --- /dev/null +++ b/common/static/lib/js/calendar_yearview_blocks.js @@ -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 = ''; + + 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('&', '&'); + items_str = items_str.replaceAll('"', '"'); + } + if (obj_timestamp[data_date].legend) { + legend = obj_timestamp[data_date].legend; + legend = legend.replaceAll('&', '&'); + legend = legend.replaceAll('"', '"'); + } + } + + 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 += ''; + 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 += ''; + } 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 += ''; + item_html += ''; + } 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 += ''; + item_html += ''; + item_html += ''; + } + + // Move on to the next day + start_date.setDate(start_date.getDate() + 1); + + } + + item_html += ""; + + 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 += '' + month_name + ''; + } + + // Add labels for Weekdays + if (settings.start_monday === true) { + loop_html += '{0}'.formatString(settings.day_names[0]) + + '{0}'.formatString(settings.day_names[1]) + + '{0}'.formatString(settings.day_names[2]) + + '{0}'.formatString(settings.day_names[3]); + } else { + loop_html += '{0}'.formatString(settings.day_names[0]) + + '{0}'.formatString(settings.day_names[1]) + + '{0}'.formatString(settings.day_names[2]); + } + + // Fixed size with width= 721 and height = 110 + var wire_html = + '' + + '' + + loop_html + + '' + '"Your browser does not support inline 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}:
{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(''); + } + }; + + // 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)); diff --git a/common/static/sass/_MainSection.sass b/common/static/sass/_MainSection.sass index 935409a8..159feb7a 100644 --- a/common/static/sass/_MainSection.sass +++ b/common/static/sass/_MainSection.sass @@ -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 \ No newline at end of file + pass diff --git a/common/templates/partial/_announcement.html b/common/templates/partial/_announcement.html index f98a4cb8..c8aecf90 100644 --- a/common/templates/partial/_announcement.html +++ b/common/templates/partial/_announcement.html @@ -23,7 +23,7 @@
- -
- {% if request.user.is_staff and request.user == user%} -
-
{% trans '投诉信息' %}
-
全部投诉 -
- -
-
- {% endif %} -
{% endif %} diff --git a/journal/models.py b/journal/models.py index 31bdc01b..fd229263 100644 --- a/journal/models.py +++ b/journal/models.py @@ -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") diff --git a/journal/templates/calendar_data.html b/journal/templates/calendar_data.html new file mode 100644 index 00000000..211544d9 --- /dev/null +++ b/journal/templates/calendar_data.html @@ -0,0 +1 @@ +{{ calendar_data|json_script:"calendar_data" }} diff --git a/journal/templates/profile.html b/journal/templates/profile.html index 160a9c92..e906e78a 100644 --- a/journal/templates/profile.html +++ b/journal/templates/profile.html @@ -18,9 +18,11 @@ {% endif %} - {% include "common_libs.html" with jquery=0 %} + {% include "common_libs.html" with jquery=1 %} + + @@ -34,6 +36,38 @@
+ {% if request.user.is_staff %} +
+
书影音日历
+
+

+ +

+
+ +
+ {% 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" %}
- {% if unread_announcements %} + {% if request.user.unread_announcements %} {% include "partial/_announcement.html" %} {% endif %} diff --git a/journal/urls.py b/journal/urls.py index 2d0f89cc..2eb1cdf6 100644 --- a/journal/urls.py +++ b/journal/urls.py @@ -123,5 +123,10 @@ urlpatterns = [ name="user_tag_list", ), re_path(r"^users/(?P[A-Za-z0-9_\-.@]+)/$", profile, name="user_profile"), + re_path( + r"^users/(?P[A-Za-z0-9_\-.@]+)/calendar_data$", + user_calendar_data, + name="user_calendar_data", + ), path("users//feed/reviews/", ReviewFeed(), name="review_feed"), ] diff --git a/journal/views.py b/journal/views.py index 0cbe7246..bbc220ae 100644 --- a/journal/views.py +++ b/journal/views.py @@ -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, }, ) diff --git a/management/templates/management/detail.html b/management/templates/management/detail.html index 60142c14..1695a352 100644 --- a/management/templates/management/detail.html +++ b/management/templates/management/detail.html @@ -6,8 +6,7 @@ - - + {% include "common_libs.html" with jquery=0 %} {{ site_name }} - {{ object.title }} @@ -44,4 +43,4 @@ - \ No newline at end of file + diff --git a/management/templates/management/list.html b/management/templates/management/list.html index fad80c22..1727f0d2 100644 --- a/management/templates/management/list.html +++ b/management/templates/management/list.html @@ -7,8 +7,7 @@ - - + {% include "common_libs.html" with jquery=0 %} {{ site_name }} - {% trans '公告栏' %} @@ -58,10 +57,10 @@ {% trans '删除' %} {% endif %} - +

{{ announcement.get_plain_content }}

- + {% if not forloop.last %}
{% endif %} @@ -71,16 +70,16 @@ {% endfor %} diff --git a/social/templates/feed.html b/social/templates/feed.html index e5a183b7..8ef15750 100644 --- a/social/templates/feed.html +++ b/social/templates/feed.html @@ -58,7 +58,7 @@ }) -{% if unread_announcements %} +{% if request.user.unread_announcements %} {% include "partial/_announcement.html" %} {% endif %} diff --git a/social/views.py b/social/views.py index fb3a3431..44e7d389 100644 --- a/social/views.py +++ b/social/views.py @@ -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], }, ) diff --git a/users/models.py b/users/models.py index 9ac5f653..70e5dcc8 100644 --- a/users/models.py +++ b/users/models.py @@ -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):