catalog discover page
This commit is contained in:
parent
fcb32b7c51
commit
0b46193e7d
14 changed files with 360 additions and 31 deletions
50
catalog/management/commands/discover.py
Normal file
50
catalog/management/commands/discover.py
Normal 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."))
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
123
catalog/templates/discover.html
Normal file
123
catalog/templates/discover.html
Normal 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>
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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>
|
||||
|
|
17
users/migrations/0003_preference_discover_layout.py
Normal file
17
users/migrations/0003_preference_discover_layout.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue