From 0b46193e7dcace9398ab24b6626c9c97a7d85850 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 19 Apr 2023 22:31:27 -0400 Subject: [PATCH] catalog discover page --- catalog/management/commands/discover.py | 50 +++++++ catalog/models.py | 2 +- catalog/podcast/models.py | 4 + catalog/templates/discover.html | 123 ++++++++++++++++++ catalog/urls.py | 1 + catalog/views.py | 91 ++++++++++++- common/static/sass/_Navbar.sass | 53 ++++++-- common/templates/partial/_navbar.html | 20 ++- common/views.py | 2 +- journal/templates/profile.html | 7 +- .../0003_preference_discover_layout.py | 17 +++ users/models.py | 4 + users/templates/users/preferences.html | 2 +- users/views.py | 15 ++- 14 files changed, 360 insertions(+), 31 deletions(-) create mode 100644 catalog/management/commands/discover.py create mode 100644 catalog/templates/discover.html create mode 100644 users/migrations/0003_preference_discover_layout.py diff --git a/catalog/management/commands/discover.py b/catalog/management/commands/discover.py new file mode 100644 index 00000000..e60d1897 --- /dev/null +++ b/catalog/management/commands/discover.py @@ -0,0 +1,50 @@ +from django.core.management.base import BaseCommand +from django.core.cache import cache +import pprint +from catalog.models import * +from journal.models import ShelfMember, query_item_category, ItemCategory +from datetime import timedelta +from django.utils import timezone +from django.db.models import Count + + +class Command(BaseCommand): + help = "catalog app utilities" + + def add_arguments(self, parser): + parser.add_argument( + "--generate", + action="store_true", + help="generate discover data", + ) + + def handle(self, *args, **options): + if options["generate"]: + cache_key = "public_gallery_list" + gallery_categories = [ + ItemCategory.Book, + ItemCategory.Movie, + ItemCategory.TV, + ItemCategory.Game, + ItemCategory.Music, + ItemCategory.Podcast, + ] + gallery_list = [] + for category in gallery_categories: + item_ids = [ + m.item_id + for m in ShelfMember.objects.filter(query_item_category(category)) + .filter(created_time__gt=timezone.now() - timedelta(days=180)) + .annotate(num=Count("item_id")) + .order_by("-num")[:100] + ] + gallery_list.append( + { + "name": "popular_" + category.value, + "title": "热门" + category.label, + "item_ids": item_ids, + } + ) + cache.set(cache_key, gallery_list, timeout=None) + + self.stdout.write(self.style.SUCCESS(f"Done.")) diff --git a/catalog/models.py b/catalog/models.py index 5657da5e..c541088c 100644 --- a/catalog/models.py +++ b/catalog/models.py @@ -12,7 +12,7 @@ from .tv.models import ( ) from .music.models import Album, AlbumSchema, AlbumInSchema from .game.models import Game, GameSchema, GameInSchema -from .podcast.models import Podcast, PodcastSchema, PodcastInSchema +from .podcast.models import Podcast, PodcastSchema, PodcastInSchema, PodcastEpisode from .performance.models import Performance from .collection.models import Collection as CatalogCollection from django.contrib.contenttypes.models import ContentType diff --git a/catalog/podcast/models.py b/catalog/podcast/models.py index 781f5d15..158f1ca3 100644 --- a/catalog/podcast/models.py +++ b/catalog/podcast/models.py @@ -89,6 +89,10 @@ class PodcastEpisode(Item): def parent_item(self): return self.program + @property + def cover_image_url(self): + return self.cover_url or self.program.cover_image_url + def get_absolute_url_with_position(self, position=None): return ( self.absolute_url diff --git a/catalog/templates/discover.html b/catalog/templates/discover.html new file mode 100644 index 00000000..144df49f --- /dev/null +++ b/catalog/templates/discover.html @@ -0,0 +1,123 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load thumb %} + + + + + + + {{ site_name }} - {% trans '发现' %} + + + {% include "common_libs.html" with jquery=0 %} + + + + + +
+
+ {% include "partial/_navbar.html" with current="discover" %} + +
+
+
+ +
+ + {% for gallery in gallery_list %} +
+
+ {{ gallery.title }} +
+ +
+ {% endfor %} + +
+ + {% if user == request.user %} + +
+
+ + {% trans '编辑布局' %} + + + + + + +
+ +
+ +
+ {% csrf_token %} + + +
+ + + {% endif %} + + {{ layout|json_script:"layout-data" }} + + +
+ + {% include "partial/_sidebar.html" %} +
+
+
+ {% include "partial/_footer.html" %} +
+ + {% if unread_announcements %} + {% include "partial/_announcement.html" %} + {% endif %} + + diff --git a/catalog/urls.py b/catalog/urls.py index a8a16ac0..835a9cce 100644 --- a/catalog/urls.py +++ b/catalog/urls.py @@ -84,4 +84,5 @@ urlpatterns = [ path("fetch_refresh/", fetch_refresh, name="fetch_refresh"), path("refetch", refetch, name="refetch"), path("unlink", unlink, name="unlink"), + path("discover/", discover, name="discover"), ] diff --git a/catalog/views.py b/catalog/views.py index 9f7725ae..ba3edc5b 100644 --- a/catalog/views.py +++ b/catalog/views.py @@ -3,14 +3,14 @@ from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required, permission_required from django.utils.translation import gettext_lazy as _ from django.http import HttpResponseRedirect -from django.core.exceptions import BadRequest, PermissionDenied +from django.core.exceptions import BadRequest, PermissionDenied, ObjectDoesNotExist from django.db.models import Count from django.utils import timezone from django.core.paginator import Paginator from catalog.common.models import ExternalResource from .models import * from django.views.decorators.clickjacking import xframe_options_exempt -from journal.models import Mark, ShelfMember, Review +from journal.models import Mark, ShelfMember, Review, query_item_category from journal.models import ( query_visible, query_following, @@ -18,10 +18,14 @@ from journal.models import ( ) from common.utils import PageLinksGenerator, get_uuid_or_404 from common.config import PAGE_LINK_NUMBER -from journal.models import ShelfTypeNames +from journal.models import ShelfTypeNames, ShelfType, ItemCategory from .forms import * from .search.views import * from django.http import Http404 +from management.models import Announcement +from django.core.cache import cache +import random + _logger = logging.getLogger(__name__) @@ -292,3 +296,84 @@ def review_list(request, item_path, item_uuid): "item": item, }, ) + + +@login_required +def discover(request): + if request.method != "GET": + raise BadRequest() + user = request.user + 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, []) + + for gallery in gallery_list: + ids = ( + random.sample(gallery["item_ids"], 10) + if len(gallery["item_ids"]) > 10 + else gallery["item_ids"] + ) + gallery["items"] = Item.objects.filter(id__in=ids) + + if user.is_authenticated: + podcast_ids = [ + p.item_id + for p in user.shelf_manager.get_members( + ShelfType.PROGRESS, ItemCategory.Podcast + ) + ] + episodes = PodcastEpisode.objects.filter(program_id__in=podcast_ids).order_by( + "-pub_date" + )[:10] + gallery_list.insert( + 0, + { + "name": "my_recent_podcasts", + "title": "在听播客的近期更新", + "items": episodes, + }, + ) + # books = Edition.objects.filter( + # id__in=[ + # p.item_id + # for p in user.shelf_manager.get_members( + # ShelfType.PROGRESS, ItemCategory.Book + # ).order_by("-created_time")[:10] + # ] + # ) + # gallery_list.insert( + # 0, + # { + # "name": "my_books_inprogress", + # "title": "正在读的书", + # "items": books, + # }, + # ) + + return render( + request, + "discover.html", + { + "user": user, + "top_tags": top_tags, + "gallery_list": gallery_list, + "layout": layout, + "unread_announcements": unread_announcements, + }, + ) diff --git a/common/static/sass/_Navbar.sass b/common/static/sass/_Navbar.sass index f03a12a6..dc1751ec 100644 --- a/common/static/sass/_Navbar.sass +++ b/common/static/sass/_Navbar.sass @@ -22,17 +22,17 @@ margin: 0 display: flex justify-content: space-around - + & &__link margin: 9px color: $color-secondary - + &:active, &:hover, &:hover:visited color: $color-primary - &:visited + &:visited color: $color-secondary .current @@ -56,12 +56,12 @@ margin: 0 margin-left: -1px padding: 0 - padding-left: 10px + padding-left: 10px color: $color-secondary appearance: auto background-color: white - height: $widget-height - width: 80px + height: $widget-height + width: 80px border-top-left-radius: 0 border-bottom-left-radius: 0 @@ -70,13 +70,13 @@ padding: 0 margin: 0 border: none - + background-color: transparent color: $color-primary &:focus, &:hover background-color: transparent - color: $color-secondary + color: $color-secondary // Small devices (landscape phones, 576px and up) @media (max-width: $small-devices) @@ -102,7 +102,7 @@ position: absolute right: 5px top: 3px - + transform: scale(0.7) &:hover + .navbar__link-list max-height: 500px @@ -119,8 +119,14 @@ & .navbar__search-dropdown cursor: pointer height: $widget-height - width: 80px + width: 80px padding-left: 5px + .dropdown + display: inline + margin-left: 0px + margin-right: 0px + .dropbtn + display: none // Medium devices (tablets, 768px and up) @media (max-width: $medium-devices) pass @@ -131,3 +137,30 @@ // Extra large devices (large desktops, 1200px and up) @media (max-width: $x-large-devices) pass + +@media (min-width: $small-devices) + .navbar + .dropbtn + color: #606c76 + cursor: pointer + .dropdown + position: relative + display: inline-block + float: right + .dropdown-content + display: none + position: absolute + right: 0 + background-color: #f9f9f9 + min-width: 80px + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2) + z-index: 1 + .dropdown-content a + color: black + padding: 4px 8px + text-decoration: none + display: block + .dropdown-content a:hover + background-color: #eeeeee + .dropdown:hover .dropdown-content + display: block diff --git a/common/templates/partial/_navbar.html b/common/templates/partial/_navbar.html index 222d0206..c96ab5d4 100644 --- a/common/templates/partial/_navbar.html +++ b/common/templates/partial/_navbar.html @@ -29,14 +29,20 @@ {% if request.user.is_authenticated %} - {% trans '主页' %} + {% trans '发现' %} {% trans '动态' %} - {% trans '数据' %} - {% trans '设置' %} - {% trans '登出' %} - {% if request.user.is_superuser %} - {% trans '后台' %} - {% endif %} + {% trans '个人主页' %} + {% else %} {% trans '登录' %} diff --git a/common/views.py b/common/views.py index b9ffffbd..da3fbe83 100644 --- a/common/views.py +++ b/common/views.py @@ -11,7 +11,7 @@ def home(request): reverse("journal:user_profile", args=[request.user.mastodon_username]) ) else: - return redirect(reverse("social:feed")) + return redirect(reverse("catalog:discover")) def error_400(request, exception=None): diff --git a/journal/templates/profile.html b/journal/templates/profile.html index 77800680..160a9c92 100644 --- a/journal/templates/profile.html +++ b/journal/templates/profile.html @@ -37,7 +37,7 @@ {% for category, category_shelves in shelf_list.items %} {% for shelf_type, shelf in category_shelves.items %} -
+
{{ shelf.title }}
@@ -64,7 +64,7 @@ {% endfor %} -
+
{% trans '创建的收藏单' %}
@@ -95,7 +95,7 @@
-
+
{% trans '关注的收藏单' %}
@@ -158,6 +158,7 @@
{% csrf_token %} +
diff --git a/users/migrations/0003_preference_discover_layout.py b/users/migrations/0003_preference_discover_layout.py new file mode 100644 index 00000000..4a510505 --- /dev/null +++ b/users/migrations/0003_preference_discover_layout.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.18 on 2023-04-19 21:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0002_preference_default_no_share"), + ] + + operations = [ + migrations.AddField( + model_name="preference", + name="discover_layout", + field=models.JSONField(blank=True, default=list), + ), + ] diff --git a/users/models.py b/users/models.py index 21264dbe..be4d26ad 100644 --- a/users/models.py +++ b/users/models.py @@ -208,6 +208,10 @@ class Preference(models.Model): blank=True, default=list, ) + discover_layout = models.JSONField( + blank=True, + default=list, + ) export_status = models.JSONField( blank=True, null=True, encoder=DjangoJSONEncoder, default=dict ) diff --git a/users/templates/users/preferences.html b/users/templates/users/preferences.html index 96023589..0122c4db 100644 --- a/users/templates/users/preferences.html +++ b/users/templates/users/preferences.html @@ -45,7 +45,7 @@ {% trans '登录后显示个人主页:' %}
- +

{% trans '不允许未登录用户访问个人主页:' %} diff --git a/users/views.py b/users/views.py index 200033ca..f94c2653 100644 --- a/users/views.py +++ b/users/views.py @@ -76,11 +76,16 @@ def following(request, id): def set_layout(request): if request.method == "POST": layout = json.loads(request.POST.get("layout")) - request.user.preference.profile_layout = layout - request.user.preference.save() - return redirect( - reverse("journal:user_profile", args=[request.user.mastodon_username]) - ) + if request.POST.get("name") == "profile": + request.user.preference.profile_layout = layout + request.user.preference.save(update_fields=["profile_layout"]) + return redirect( + reverse("journal:user_profile", args=[request.user.mastodon_username]) + ) + elif request.POST.get("name") == "discover": + request.user.preference.discover_layout = layout + request.user.preference.save(update_fields=["discover_layout"]) + return redirect(reverse("catalog:discover")) else: raise BadRequest()