catalog discover page

This commit is contained in:
Your Name 2023-04-19 22:31:27 -04:00 committed by Henri Dickson
parent fcb32b7c51
commit 0b46193e7d
14 changed files with 360 additions and 31 deletions

View file

@ -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."))

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,123 @@
{% load static %}
{% load i18n %}
{% load admin_url %}
{% load mastodon %}
{% load oauth_token %}
{% load truncate %}
{% load thumb %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ site_name }} - {% trans '发现' %}</title>
<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 %}
<script src="{% static 'js/mastodon.js' %}" defer></script>
<script src="{% static 'js/home.js' %}" defer></script>
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" with current="discover" %}
<section id="content">
<div class="grid grid--reverse-order">
<div class="grid__main grid__main--reverse-order">
<div class="main-section-wrapper sortable">
{% for gallery in gallery_list %}
<div class="entity-sort" id="{{ gallery.name }}" {% if not gallery.items %}style="display:none;"{% endif %}>
<h5 class="entity-sort__label">
{{ gallery.title }}
</h5>
<ul class="entity-sort__entity-list">
{% for item in gallery.items %}
<li class="entity-sort__entity">
<a href="{{ item.url }}">
<img src="{{ item.cover_image_url | default:item.cover.url }}" alt="{{ item.title }}" class="entity-sort__entity-img">
<div class="entity-sort__entity-name" title="{{ item.title }}"> {{ item.title }}</div>
</a>
</li>
{% empty %}
<div>暂无记录</div>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
{% if user == request.user %}
<div class="entity-sort-control">
<div class="entity-sort-control__button" id="sortEditButton">
<span class="entity-sort-control__text" id="sortEditText">
{% trans '编辑布局' %}
</span>
<span class="entity-sort-control__text" id="sortSaveText" style="display: none;">
{% trans '保存' %}
</span>
<span class="icon-edit" id="sortEditIcon">
<i class="fa-solid fa-pencil"></i>
</span>
<span class="icon-save" id="sortSaveIcon" style="display: none;">
<i class="fa-regular fa-floppy-disk"></i>
</span>
</div>
<div class="entity-sort-control__button" id="sortExitButton" style="display: none;">
<span class="entity-sort-control__text">
{% trans '取消' %}
</span>
</div>
</div>
<div class="entity-sort-control__button entity-sort-control__button--float-right" id="toggleDisplayButtonTemplate" style="display: none;">
<span class="showText" style="display: none;">
{% trans '显示' %}
</span>
<span class="hideText" style="display: none;">
{% trans '隐藏' %}
</span>
</div>
<form action="{% url 'users:set_layout' %}" method="post" id="sortForm">
{% csrf_token %}
<input type="hidden" name="name" value="discover">
<input type="hidden" name="layout">
</form>
<script src="https://cdn.staticfile.org/html5sortable/0.13.3/html5sortable.min.js" crossorigin="anonymous"></script>
<script src="{% static 'js/sort_layout.js' %}"></script>
{% endif %}
{{ layout|json_script:"layout-data" }}
<script>
const initialLayoutData = JSON.parse(document.getElementById('layout-data').textContent);
// initialize sort element visibility and order
initialLayoutData.forEach(elem => {
// set visiblity
$('#' + elem.id).data('visibility', elem.visibility);
if (!elem.visibility) {
$('#' + elem.id).hide();
}
// order
$('#' + elem.id).appendTo('.main-section-wrapper');
});
</script>
</div>
{% include "partial/_sidebar.html" %}
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
{% if unread_announcements %}
{% include "partial/_announcement.html" %}
{% endif %}
</body>
</html>

View file

@ -84,4 +84,5 @@ urlpatterns = [
path("fetch_refresh/<str:job_id>", fetch_refresh, name="fetch_refresh"),
path("refetch", refetch, name="refetch"),
path("unlink", unlink, name="unlink"),
path("discover/", discover, name="discover"),
]

View file

@ -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,
},
)

View file

@ -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

View file

@ -29,14 +29,20 @@
{% if request.user.is_authenticated %}
<a class="navbar__link {% if current == 'home' %}current{% endif %}" href="{% url 'journal:user_profile' request.user.mastodon_username %}">{% trans '主页' %}</a>
<a class="navbar__link {% if current == 'discover' %}current{% endif %}" href="{% url 'catalog:discover' %}">{% trans '发现' %}</a>
<a class="navbar__link {% if current == 'timeline' %}current{% endif %}" href="{% url 'social:feed' %}">{% trans '动态' %}</a>
<a class="navbar__link {% if current == 'data' %}current{% endif %}" href="{% url 'users:data' %}">{% trans '数据' %}</a>
<a class="navbar__link {% if current == 'preferences' %}current{% endif %}" href="{% url 'users:preferences' %}">{% trans '设置' %}</a>
<a class="navbar__link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
{% if request.user.is_superuser %}
<a class="navbar__link" href="{% admin_url %}">{% trans '后台' %}</a>
{% endif %}
<a class="navbar__link {% if current == 'home' %}current{% endif %}" href="{% url 'journal:user_profile' request.user.mastodon_username %}">{% trans '个人主页' %}</a>
<div class="navbar__link dropdown">
<a class="dropbtn"><i class="fa-solid fa-gear" title="{% trans '更多' %}"></i></a>
<div class="dropdown-content">
<a class="navbar__link {% if current == 'data' %}current{% endif %}" href="{% url 'users:data' %}">{% trans '数据' %}</a>
<a class="navbar__link {% if current == 'preferences' %}current{% endif %}" href="{% url 'users:preferences' %}">{% trans '设置' %}</a>
<a class="navbar__link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
{% if request.user.is_superuser %}
<a class="navbar__link" href="{% admin_url %}">{% trans '后台' %}</a>
{% endif %}
</div>
</div>
{% else %}
<a class="navbar__link" href="{% url 'users:login' %}?next={{ request.path }}">{% trans '登录' %}</a>

View file

@ -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):

View file

@ -37,7 +37,7 @@
{% for category, category_shelves in shelf_list.items %}
{% for shelf_type, shelf in category_shelves.items %}
<div class="entity-sort" id="{{ category }}_{{ shelf_type }}">
<div class="entity-sort" id="{{ category }}_{{ shelf_type }}" {% if not shelf.count %}style="display:none;"{% endif %}>
<h5 class="entity-sort__label">
{{ shelf.title }}
</h5>
@ -64,7 +64,7 @@
{% endfor %}
<div class="entity-sort" id="collection_created">
<div class="entity-sort" id="collection_created" {% if not collections_count %}style="display:none;"{% endif %}>
<h5 class="entity-sort__label">
{% trans '创建的收藏单' %}
</h5>
@ -95,7 +95,7 @@
</ul>
</div>
<div class="entity-sort" id="collection_marked">
<div class="entity-sort" id="collection_marked" {% if not liked_collections_count %}style="display:none;"{% endif %}>
<h5 class="entity-sort__label">
{% trans '关注的收藏单' %}
</h5>
@ -158,6 +158,7 @@
</div>
<form action="{% url 'users:set_layout' %}" method="post" id="sortForm">
{% csrf_token %}
<input type="hidden" name="name" value="profile">
<input type="hidden" name="layout">
</form>
<script src="https://cdn.staticfile.org/html5sortable/0.13.3/html5sortable.min.js" crossorigin="anonymous"></script>

View file

@ -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),
),
]

View file

@ -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
)

View file

@ -45,7 +45,7 @@
<span>{% trans '登录后显示个人主页:' %}</span>
<div class="import-panel__checkbox import-panel__checkbox--last">
<input type="checkbox" name="classic_homepage" id="classic_homepage" {%if request.user.preference.classic_homepage %}checked{% endif %}>
<label for="classic_homepage">{% trans '默认登录后显示好友动态,如果希望登录后显示本人主页可选中此处' %}</label>
<label for="classic_homepage">{% trans '默认登录后显示内容发现,如果希望登录后显示本人主页可选中此处' %}</label>
</div>
<br style="margin-bottom:1.5em">
<span>{% trans '不允许未登录用户访问个人主页:' %}</span>

View file

@ -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()