new data model: user profile pages
This commit is contained in:
parent
f36ec360e3
commit
a45fdf9edf
15 changed files with 731 additions and 36 deletions
|
@ -3,4 +3,3 @@ from catalog.common import *
|
|||
|
||||
class Collection(Item):
|
||||
category = ItemCategory.Collection
|
||||
url_path = 'collection'
|
||||
|
|
|
@ -274,7 +274,7 @@
|
|||
{% if collection_list %}
|
||||
{% for c in collection_list %}
|
||||
<p>
|
||||
<a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
|
||||
<a href="{{ c.url }}">{{ c.title }}</a>
|
||||
</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.core.exceptions import ValidationError
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
import json
|
||||
from .models import *
|
||||
from common.forms import PreviewImageInput
|
||||
|
||||
|
||||
class ReviewForm(forms.ModelForm):
|
||||
|
@ -33,3 +34,44 @@ class ReviewForm(forms.ModelForm):
|
|||
choices=VisibilityType.choices,
|
||||
widget=forms.RadioSelect
|
||||
)
|
||||
|
||||
|
||||
COLLABORATIVE_CHOICES = [
|
||||
(0, _("仅限创建者")),
|
||||
(1, _("创建者及其互关用户")),
|
||||
]
|
||||
|
||||
|
||||
class CollectionForm(forms.ModelForm):
|
||||
# id = forms.IntegerField(required=False, widget=forms.HiddenInput())
|
||||
title = forms.CharField(label=_("标题"))
|
||||
brief = MarkdownxFormField(label=_("介绍 (Markdown)"))
|
||||
# share_to_mastodon = forms.BooleanField(label=_("分享到联邦网络"), initial=True, required=False)
|
||||
visibility = forms.TypedChoiceField(
|
||||
label=_("可见性"),
|
||||
initial=0,
|
||||
coerce=int,
|
||||
choices=VisibilityType.choices,
|
||||
widget=forms.RadioSelect
|
||||
)
|
||||
collaborative = forms.TypedChoiceField(
|
||||
label=_("协作整理权限"),
|
||||
initial=0,
|
||||
coerce=int,
|
||||
choices=COLLABORATIVE_CHOICES,
|
||||
widget=forms.RadioSelect
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Collection
|
||||
fields = [
|
||||
'title',
|
||||
'cover',
|
||||
'visibility',
|
||||
'collaborative',
|
||||
'brief',
|
||||
]
|
||||
|
||||
widgets = {
|
||||
'cover': PreviewImageInput(),
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ class UserOwnedObjectMixin:
|
|||
return True
|
||||
|
||||
def is_editable_by(self, viewer):
|
||||
return True if viewer.is_staff or viewer.is_superuser or viewer == self.owner else False
|
||||
return viewer.is_authenticated and (viewer.is_staff or viewer.is_superuser or viewer == self.owner)
|
||||
|
||||
@classmethod
|
||||
def get_available(cls, entity, request_user, following_only=False):
|
||||
|
|
|
@ -23,6 +23,7 @@ from django.db.models import Q
|
|||
from catalog.models import *
|
||||
import mistune
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from markdown import markdown
|
||||
|
||||
|
||||
class VisibilityType(models.IntegerChoices):
|
||||
|
@ -31,8 +32,19 @@ class VisibilityType(models.IntegerChoices):
|
|||
Private = 2, _('仅自己')
|
||||
|
||||
|
||||
def q_visible_to(viewer, owner):
|
||||
if viewer == owner:
|
||||
return Q()
|
||||
# elif viewer.is_blocked_by(owner):
|
||||
# return Q(pk__in=[])
|
||||
elif viewer.is_following(owner):
|
||||
return Q(visibility__ne=2)
|
||||
else:
|
||||
return Q(visibility=0)
|
||||
|
||||
|
||||
def query_visible(user):
|
||||
return Q(visibility=0) | Q(owner_id__in=user.following, visibility__lt=2) | Q(owner_id=user.id)
|
||||
return Q(visibility=0) | Q(owner_id__in=user.following, visibility=1) | Q(owner_id=user.id)
|
||||
|
||||
|
||||
def query_following(user):
|
||||
|
@ -231,14 +243,22 @@ class List(Piece):
|
|||
# subclass must add this:
|
||||
# items = models.ManyToManyField(Item, through='ListMember')
|
||||
|
||||
@ property
|
||||
@property
|
||||
def ordered_members(self):
|
||||
return self.members.all().order_by('position', 'item_id')
|
||||
|
||||
@ property
|
||||
@property
|
||||
def ordered_items(self):
|
||||
return self.items.all().order_by(self.MEMBER_CLASS.__name__.lower() + '__position')
|
||||
|
||||
@property
|
||||
def recent_items(self):
|
||||
return self.items.all().order_by('-' + self.MEMBER_CLASS.__name__.lower() + '__created_time')
|
||||
|
||||
@property
|
||||
def recent_members(self):
|
||||
return self.members.all().order_by('-created_time')
|
||||
|
||||
def has_item(self, item):
|
||||
return self.members.filter(item=item).count() > 0
|
||||
|
||||
|
@ -356,14 +376,19 @@ class Shelf(List):
|
|||
def __str__(self):
|
||||
return f'{self.id} {self.title}'
|
||||
|
||||
@ cached_property
|
||||
@cached_property
|
||||
def item_category_label(self):
|
||||
return ItemCategory(self.item_category).label
|
||||
|
||||
@cached_property
|
||||
def shelf_label(self):
|
||||
return next(iter([n[2] for n in iter(ShelfTypeNames) if n[0] == self.item_category and n[1] == self.shelf_type]), self.shelf_type)
|
||||
|
||||
@ cached_property
|
||||
@cached_property
|
||||
def title(self):
|
||||
q = _("{item_category} {shelf_label} list").format(shelf_label=self.shelf_label, item_category=self.item_category)
|
||||
return _("{user}'s {shelf_name}").format(user=self.owner.mastodon_username, shelf_name=q)
|
||||
q = _("{shelf_label}的{item_category}").format(shelf_label=self.shelf_label, item_category=self.item_category_label)
|
||||
return q
|
||||
# return _("{user}'s {shelf_name}").format(user=self.owner.mastodon_username, shelf_name=q)
|
||||
|
||||
|
||||
class ShelfLogEntry(models.Model):
|
||||
|
@ -452,6 +477,10 @@ class ShelfManager:
|
|||
def get_shelf(self, item_category, shelf_type):
|
||||
return self.owner.shelf_set.all().filter(item_category=item_category, shelf_type=shelf_type).first()
|
||||
|
||||
def get_items_on_shelf(self, item_category, shelf_type):
|
||||
shelf = self.owner.shelf_set.all().filter(item_category=item_category, shelf_type=shelf_type).first()
|
||||
return shelf.members.all().order_by
|
||||
|
||||
@ staticmethod
|
||||
def get_manager_for_user(user):
|
||||
return ShelfManager(user)
|
||||
|
@ -469,8 +498,13 @@ Collection
|
|||
class CollectionMember(ListMember):
|
||||
parent = models.ForeignKey('Collection', related_name='members', on_delete=models.CASCADE)
|
||||
|
||||
@property
|
||||
def note(self):
|
||||
return self.metadata.get('comment')
|
||||
|
||||
|
||||
class Collection(List):
|
||||
url_path = 'collection'
|
||||
MEMBER_CLASS = CollectionMember
|
||||
catalog_item = models.OneToOneField(CatalogCollection, on_delete=models.PROTECT)
|
||||
title = models.CharField(_("title in primary language"), max_length=1000, default="")
|
||||
|
@ -479,9 +513,14 @@ class Collection(List):
|
|||
items = models.ManyToManyField(Item, through='CollectionMember', related_name="collections")
|
||||
collaborative = models.PositiveSmallIntegerField(default=0) # 0: Editable by owner only / 1: Editable by bi-direction followers
|
||||
|
||||
@ property
|
||||
@property
|
||||
def html(self):
|
||||
html = markdown(self.brief)
|
||||
return html
|
||||
|
||||
@property
|
||||
def plain_description(self):
|
||||
html = markdown(self.description)
|
||||
html = markdown(self.brief)
|
||||
return RE_HTML_TAG.sub(' ', html)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
|
142
journal/templates/collection.html
Normal file
142
journal/templates/collection.html
Normal file
|
@ -0,0 +1,142 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load humanize %}
|
||||
{% 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">
|
||||
<meta property="og:title" content="{{ site_name }} {% trans '收藏单' %} - {{ collection.title }}">
|
||||
<meta property="og:description" content="{{ collection.description }}">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:article:author" content="{{ collection.owner.username }}">
|
||||
<meta property="og:url" content="{{ request.build_absolute_uri }}">
|
||||
<meta property="og:image" content="{{ collection.cover|thumb:'normal' }}">
|
||||
|
||||
<title>{{ site_name }} {% trans '收藏单' %} - {{ collection.title }}</title>
|
||||
|
||||
{% include "partial/_common_libs.html" with jquery=1 %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
{% include "partial/_navbar.html" %}
|
||||
|
||||
<section id="content">
|
||||
<div class="grid">
|
||||
<div class="grid__main" id="main">
|
||||
<div class="main-section-wrapper">
|
||||
<div class="review-head">
|
||||
<h5 class="review-head__title">
|
||||
{{ collection.title }}
|
||||
</h5>
|
||||
{% if collection.visibility > 0 %}
|
||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||
</svg></span>
|
||||
{% endif %}
|
||||
<div class="review-head__body">
|
||||
<div class="review-head__info">
|
||||
|
||||
<a href="{% url 'users:home' collection.owner.mastodon_username %}" class="review-head__owner-link">{{ collection.owner.mastodon_username }}</a>
|
||||
|
||||
|
||||
<span class="review-head__time">{{ collection.edited_time }}</span>
|
||||
|
||||
</div>
|
||||
<div class="review-head__actions">
|
||||
{% if request.user == collection.owner %}
|
||||
<a class="review-head__action-link" href="{% url 'journal:collection_edit' collection.uuid %}">{% trans '编辑' %}</a>
|
||||
<a class="review-head__action-link" href="{% url 'journal:collection_delete' collection.uuid %}">{% trans '删除' %}</a>
|
||||
{% elif editable %}
|
||||
<span class="review-head__time">可协作整理</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="dividing-line"></div> --> <!-- <div class="entity-card__img-wrapper" style="text-align: center;"> <img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img"> </div> -->
|
||||
{{ collection.html | safe }}
|
||||
|
||||
</div>
|
||||
<div class="entity-list" hx-get="{% url 'journal:collection_retrieve_items' collection.uuid %}" hx-trigger="load">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid__aside" id="aside">
|
||||
<div class="aside-section-wrapper">
|
||||
<div class="entity-card">
|
||||
<div class="entity-card__img-wrapper">
|
||||
<a href="{% url 'collection:retrieve' collection.id %}">
|
||||
<img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img">
|
||||
</a>
|
||||
</div>
|
||||
<div class="entity-card__info-wrapper">
|
||||
<h5 class="entity-card__title">
|
||||
<a href="{{ collection.url }}">
|
||||
{{ collection.title }}
|
||||
</a>
|
||||
</h5>
|
||||
{% if follower_count %}
|
||||
被 {{ follower_count }} 人关注
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.user != collection.owner %}
|
||||
<div class="aside-section-wrapper">
|
||||
<div class="action-panel">
|
||||
<div class="action-panel__button-group action-panel__button-group--center">
|
||||
{% if following %}
|
||||
<form action="{% url 'collection:unfollow' collection.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="action-panel__button">{% trans '取消关注' %}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="{% url 'collection:follow' collection.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="action-panel__button">{% trans '关注' %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="aside-section-wrapper">
|
||||
<div class="action-panel">
|
||||
<div class="action-panel__button-group action-panel__button-group--center">
|
||||
<form>
|
||||
<button class="action-panel__button add-to-list" hx-get="{% url 'collection:share' collection.id %}" hx-target="body" hx-swap="beforeend">{% trans '分享到联邦网络' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% include "partial/_footer.html" %}
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
$(".markdownx textarea").hide();
|
||||
</script>
|
||||
<script>
|
||||
document.body.addEventListener('htmx:configRequest', (event) => {
|
||||
event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
45
journal/templates/collection_edit.html
Normal file
45
journal/templates/collection_edit.html
Normal file
|
@ -0,0 +1,45 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ site_name }} - {{ title }}</title>
|
||||
{% include "partial/_common_libs.html" with jquery=1 %}
|
||||
<style type="text/css">
|
||||
#id_collaborative li, #id_visibility li {display: inline-block !important;}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
{% include "partial/_navbar.html" %}
|
||||
<div id="content-wrapper">
|
||||
<section id="content" class="container">
|
||||
<div class="grid">
|
||||
<div class="single-section-wrapper" id="main">
|
||||
<form class="entity-form" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<input class="button" type="submit" value="{% trans '提交' %}">
|
||||
</form>
|
||||
{{ form.media }}
|
||||
<div class="dividing-line"></div>
|
||||
</div>
|
||||
<div class="single-section-wrapper">
|
||||
<div class="entity-list" hx-get="{% url 'journal:collection_retrieve_items' collection.uuid %}?edit=1" hx-trigger="load"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% include "partial/_footer.html" %}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
22
journal/templates/collection_items.html
Normal file
22
journal/templates/collection_items.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
{% load thumb %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
<ul class="entity-list__entities">
|
||||
{% for member in collection.members.all %}
|
||||
{% with "list_item_"|add:member.item.class_name|add:".html" as template %}
|
||||
{% include template with item=member.item mark=None collection_member=member %}
|
||||
{% endwith %}
|
||||
{% empty %}
|
||||
暂无条目
|
||||
{% endfor %}
|
||||
{% if collection_edit %}
|
||||
<li>
|
||||
<form class="entity-form" hx-target=".entity-list" hx-post="{% url 'journal:collection_append_item' collection.uuid %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="url" name="url" placeholder="{{ request.scheme }}://{{ request.get_host }}/item/abcd123" style="min-width:24rem" required>
|
||||
<input type="text" name="comment" placeholder="{% trans '备注' %}" style="min-width:24rem">
|
||||
<input class="button" type="submit" value="{% trans '添加' %}" >
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
|
@ -19,12 +19,12 @@
|
|||
{% if collection_edit %}
|
||||
<div class="collection-item-position-edit">
|
||||
{% if not forloop.first %}
|
||||
<a hx-target=".entity-list" hx-post="{% url 'collection:move_up_item' form.instance.id collectionitem.id %}">▲</a>
|
||||
<a hx-target=".entity-list" hx-post="{% url 'journal:collection_move_up_item' form.instance.uuid collection_member.id %}">▲</a>
|
||||
{% endif %}
|
||||
{% if not forloop.last %}
|
||||
<a hx-target=".entity-list" hx-post="{% url 'collection:move_down_item' form.instance.id collectionitem.id %}">▼</a>
|
||||
<a hx-target=".entity-list" hx-post="{% url 'journal:collection_move_down_item' form.instance.uuid collection_member.id %}">▼</a>
|
||||
{% endif %}
|
||||
<a hx-target=".entity-list" hx-post="{% url 'collection:delete_item' form.instance.id collectionitem.id %}">✖</a>
|
||||
<a hx-target=".entity-list" hx-post="{% url 'journal:collection_delete_item' form.instance.uuid collection_member.id %}">✖</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -119,14 +119,19 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if collectionitem %}
|
||||
{% if collection_member %}
|
||||
<div class="clearfix"></div>
|
||||
<div class="dividing-line dividing-line--dashed"></div>
|
||||
<div class="entity-marks" style="margin-bottom: 0;">
|
||||
<ul class="entity-marks__mark-list">
|
||||
<li class="entity-marks__mark">
|
||||
<p class="entity-marks__mark-content" hx-target="this" hx-swap="innerHTML">
|
||||
{% include "show_item_comment.html" %}
|
||||
|
||||
{{ collection_member.note }}
|
||||
{% if collection_edit %}
|
||||
<a class="action-icon" hx-get="{% url 'journal:collection_update_item_note' collection.uuid collection_member.uuid %}"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M19,20H5a1,1,0,0,0,0,2H19a1,1,0,0,0,0-2Z"/><path d="M5,18h.09l4.17-.38a2,2,0,0,0,1.21-.57l9-9a1.92,1.92,0,0,0-.07-2.71h0L16.66,2.6A2,2,0,0,0,14,2.53l-9,9a2,2,0,0,0-.57,1.21L4,16.91a1,1,0,0,0,.29.8A1,1,0,0,0,5,18ZM15.27,4,18,6.73,16,8.68,13.32,6Zm-8.9,8.91L12,7.32l2.7,2.7-5.6,5.6-3,.28Z"/></g></svg></a>
|
||||
{% endif %}
|
||||
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
211
journal/templates/profile.html
Normal file
211
journal/templates/profile.html
Normal file
|
@ -0,0 +1,211 @@
|
|||
{% 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">
|
||||
{% if user == request.user %}
|
||||
<title>{{ site_name }} - {% trans '我的个人主页' %}</title>
|
||||
{% else %}
|
||||
<title>{{ site_name }} - {{user.display_name}}</title>
|
||||
{% endif %}
|
||||
<link rel="alternate" type="application/rss+xml" title="{{ site_name }} - {{ user.mastodon_username }}的评论" href="{{ request.build_absolute_uri }}feed/reviews/">
|
||||
|
||||
{% include "partial/_common_libs.html" with jquery=1 %}
|
||||
|
||||
<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="home" %}
|
||||
|
||||
<section id="content">
|
||||
<div class="grid grid--reverse-order">
|
||||
<div class="grid__main grid__main--reverse-order">
|
||||
|
||||
<div class="main-section-wrapper sortable">
|
||||
|
||||
{% for category, category_shelves in shelf_list.items %}
|
||||
{% for shelf_type, shelf in category_shelves.items %}
|
||||
|
||||
<div class="entity-sort" id="{{ category }}_{{ shelf_type }}">
|
||||
<h5 class="entity-sort__label">
|
||||
{{ shelf.title }}
|
||||
</h5>
|
||||
<span class="entity-sort__count">
|
||||
{{ shelf.count }}
|
||||
</span>
|
||||
{% if shelf.count > 5 %}
|
||||
<a href="{% url 'journal:user_mark_list' user.mastodon_username shelf_type category %}"
|
||||
class="entity-sort__more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
<ul class="entity-sort__entity-list">
|
||||
{% for member in shelf.members %}
|
||||
<li class="entity-sort__entity">
|
||||
|
||||
<a href="{{ member.item.url }}">
|
||||
<img src="{{ member.item.cover|thumb:'normal' }}"
|
||||
alt="{{ member.item.title }}" class="entity-sort__entity-img">
|
||||
<div class="entity-sort__entity-name" title="{{ member.item.title }}">
|
||||
{{ member.item.title }}</div>
|
||||
</a>
|
||||
</li>
|
||||
{% empty %}
|
||||
<div>暂无记录</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
<div class="entity-sort" id="collection_created">
|
||||
<h5 class="entity-sort__label">
|
||||
{% trans '创建的收藏单' %}
|
||||
</h5>
|
||||
<span class="entity-sort__count">
|
||||
{{ collections_count }}
|
||||
</span>
|
||||
{% if collections_count > 5 %}
|
||||
<a href="{% url 'journal:user_collection_list' user.mastodon_username %}"
|
||||
class="entity-sort__more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
{% if user == request.user %}
|
||||
<a href="{% url 'journal:collection_create' %}"class="entity-sort__more-link">{% trans '新建' %}</a>
|
||||
{% endif %}
|
||||
|
||||
<ul class="entity-sort__entity-list">
|
||||
{% for collection in collections %}
|
||||
<li class="entity-sort__entity">
|
||||
<a href="{{ collection.url }}">
|
||||
<img src="{{ collection.cover|thumb:'normal' }}"
|
||||
alt="{{collection.title}}" class="entity-sort__entity-img">
|
||||
<span class="entity-sort__entity-name"
|
||||
title="{{collection.title}}">{{ collection.title }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% empty %}
|
||||
<div>暂无记录</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="entity-sort" id="collection_marked">
|
||||
<h5 class="entity-sort__label">
|
||||
{% trans '关注的收藏单' %}
|
||||
</h5>
|
||||
<span class="entity-sort__count">
|
||||
{{ liked_collections_count }}
|
||||
</span>
|
||||
{% if liked_collections_count > 5 %}
|
||||
<a href="{% url 'journal:user_liked_collection_list' user.mastodon_username %}"
|
||||
class="entity-sort__more-link">{% trans '更多' %}</a>
|
||||
{% endif %}
|
||||
|
||||
<ul class="entity-sort__entity-list">
|
||||
{% for collection in liked_collections %}
|
||||
<li class="entity-sort__entity">
|
||||
<a href="{{ collection.url }}">
|
||||
<img src="{{ collection.cover|thumb:'normal' }}"
|
||||
alt="{{collection.title}}" class="entity-sort__entity-img">
|
||||
<span class="entity-sort__entity-name"
|
||||
title="{{collection.title}}">{{ collection.title }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% empty %}
|
||||
<div>暂无记录</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 383.947 383.947">
|
||||
<polygon points="0,303.947 0,383.947 80,383.947 316.053,147.893 236.053,67.893 " />
|
||||
<path
|
||||
d="M377.707,56.053L327.893,6.24c-8.32-8.32-21.867-8.32-30.187,0l-39.04,39.04l80,80l39.04-39.04 C386.027,77.92,386.027,64.373,377.707,56.053z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="icon-save" id="sortSaveIcon" style="display: none;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 384 384" >
|
||||
<path
|
||||
d="M298.667,0h-256C19.093,0,0,19.093,0,42.667v298.667C0,364.907,19.093,384,42.667,384h298.667 C364.907,384,384,364.907,384,341.333v-256L298.667,0z M192,341.333c-35.307,0-64-28.693-64-64c0-35.307,28.693-64,64-64 s64,28.693,64,64C256,312.64,227.307,341.333,192,341.333z M256,128H42.667V42.667H256V128z" />
|
||||
</svg>
|
||||
</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="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 %}
|
||||
<script>
|
||||
const initialLayoutData = JSON.parse("{{ layout|escapejs }}");
|
||||
// initialize sort element visibility and order
|
||||
initialLayoutData.forEach(elem => {
|
||||
// False to false, True to true
|
||||
if (elem.visibility === "False") {
|
||||
elem.visibility = false;
|
||||
} else {
|
||||
elem.visibility = true;
|
||||
}
|
||||
// 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>
|
|
@ -26,7 +26,7 @@
|
|||
<div class="grid__main" id="main">
|
||||
<div class="single-section-wrapper">
|
||||
|
||||
<form action="{{ submit_url }}" method="post" class="review-form">
|
||||
<form method="post" class="review-form">
|
||||
{% csrf_token %}
|
||||
{{ form.item }}
|
||||
<div>
|
||||
|
|
|
@ -110,7 +110,7 @@ class TagTest(TestCase):
|
|||
self.assertEqual(self.user2.tags, [t1, t3])
|
||||
TagManager.add_tag_by_user(self.book2, t3, self.user2)
|
||||
TagManager.add_tag_by_user(self.movie1, t3, self.user2)
|
||||
self.assertEqual(self.user2.tags, [t1, t3])
|
||||
self.assertEqual(sorted(self.user2.tags), [t1, t3])
|
||||
|
||||
|
||||
class MarkTest(TestCase):
|
||||
|
|
|
@ -26,11 +26,24 @@ urlpatterns = [
|
|||
path('review/edit/<str:item_uuid>/<str:review_uuid>', review_edit, name='review_edit'),
|
||||
path('review/delete/<str:review_uuid>', review_delete, name='review_delete'),
|
||||
|
||||
re_path(r'^user/(?P<user_name>[A-Za-z0-0_\-.@]+)/(?P<shelf_type>' + _get_all_shelf_types() + ')/(?P<item_category>' + _get_all_categories() + ')/$', user_mark_list, name='user_mark_list'),
|
||||
re_path(r'^user/(?P<user_name>[A-Za-z0-0_\-.@]+)/reviews/(?P<item_category>' + _get_all_categories() + ')/$', user_review_list, name='user_review_list'),
|
||||
re_path(r'^user/(?P<user_name>[A-Za-z0-0_\-.@]+)/tags/(?P<tag_title>[^/]+)/$', user_tag_member_list, name='user_tag_member_list'),
|
||||
re_path(r'^user/(?P<user_name>[A-Za-z0-0_\-.@]+)/collections/$', user_collection_list, name='user_collection_list'),
|
||||
re_path(r'^user/(?P<user_name>[A-Za-z0-0_\-.@]+)/like/collections/$', user_liked_collection_list, name='user_liked_collection_list'),
|
||||
re_path(r'^user/(?P<user_name>[A-Za-z0-0_\-.@]+)/tags/$', user_tag_list, name='user_tag_list'),
|
||||
path('collection/<str:collection_uuid>', collection_retrieve, name='collection_retrieve'),
|
||||
path('collection/create/', collection_edit, name='collection_create'),
|
||||
path('collection/edit/<str:collection_uuid>', collection_edit, name='collection_edit'),
|
||||
path('collection/delete/<str:collection_uuid>', collection_delete, name='collection_delete'),
|
||||
path('collection/<str:collection_uuid>/items', collection_retrieve_items, name='collection_retrieve_items'),
|
||||
path('collection/<str:collection_uuid>/append_item', collection_append_item, name='collection_append_item'),
|
||||
path('collection/<str:collection_uuid>/delete_item/<str:collection_member_uuid>', collection_delete_item, name='collection_delete_item'),
|
||||
path('collection/<str:collection_uuid>/move_up_item/<str:collection_member_uuid>', collection_move_up_item, name='collection_move_up_item'),
|
||||
path('collection/<str:collection_uuid>/move_down_item/<str:collection_member_uuid>', collection_move_down_item, name='collection_move_down_item'),
|
||||
path('collection/<str:collection_uuid>/update_item_note/<str:collection_member_uuid>', collection_update_item_note, name='collection_update_item_note'),
|
||||
|
||||
re_path(r'^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/(?P<shelf_type>' + _get_all_shelf_types() + ')/(?P<item_category>' + _get_all_categories() + ')/$', user_mark_list, name='user_mark_list'),
|
||||
re_path(r'^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/reviews/(?P<item_category>' + _get_all_categories() + ')/$', user_review_list, name='user_review_list'),
|
||||
re_path(r'^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/tags/(?P<tag_title>[^/]+)/$', user_tag_member_list, name='user_tag_member_list'),
|
||||
re_path(r'^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/collections/$', user_collection_list, name='user_collection_list'),
|
||||
re_path(r'^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/like/collections/$', user_liked_collection_list, name='user_liked_collection_list'),
|
||||
re_path(r'^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/tags/$', user_tag_list, name='user_tag_list'),
|
||||
|
||||
re_path(r'^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/$', home, name='user_profile'),
|
||||
|
||||
]
|
||||
|
|
197
journal/views.py
197
journal/views.py
|
@ -11,7 +11,6 @@ from django.core.paginator import Paginator
|
|||
from .models import *
|
||||
from django.conf import settings
|
||||
import re
|
||||
from users.models import User
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.db.models import Q
|
||||
import time
|
||||
|
@ -20,7 +19,7 @@ from django.utils.baseconv import base62
|
|||
from .forms import *
|
||||
from mastodon.api import share_review
|
||||
from users.views import render_user_blocked, render_user_not_found
|
||||
|
||||
from users.models import User, Report, Preference
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
PAGE_SIZE = 10
|
||||
|
@ -115,6 +114,101 @@ def mark(request, item_uuid):
|
|||
return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
|
||||
|
||||
|
||||
def collection_retrieve(request, collection_uuid):
|
||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||
if not collection.is_visible_to(request.user):
|
||||
raise PermissionDenied()
|
||||
return render(request, 'collection.html', {'collection': collection})
|
||||
|
||||
|
||||
def collection_retrieve_items(request, collection_uuid):
|
||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||
if not collection.is_visible_to(request.user):
|
||||
raise PermissionDenied()
|
||||
form = CollectionForm(instance=collection)
|
||||
return render(
|
||||
request,
|
||||
'collection_items.html',
|
||||
{
|
||||
'collection': collection,
|
||||
'form': form,
|
||||
'collection_edit': request.GET.get('edit'), # collection.is_editable_by(request.user),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def collection_update_item_note(request, collection_uuid, collection_member_uuid):
|
||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||
if not collection.is_editable_by(request.user):
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
@login_required
|
||||
def collection_append_item(request, collection_uuid):
|
||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||
if not collection.is_editable_by(request.user):
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
@login_required
|
||||
def collection_delete_item(request, collection_uuid, collection_member_uuid):
|
||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||
if not collection.is_editable_by(request.user):
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
@login_required
|
||||
def collection_move_up_item(request, collection_uuid, collection_member_uuid):
|
||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||
if not collection.is_editable_by(request.user):
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
@login_required
|
||||
def collection_move_down_item(request, collection_uuid, collection_member_uuid):
|
||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||
if not collection.is_editable_by(request.user):
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
@login_required
|
||||
def collection_edit(request, collection_uuid=None):
|
||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid)) if collection_uuid else None
|
||||
if collection and not collection.is_editable_by(request.user):
|
||||
raise PermissionDenied()
|
||||
if request.method == 'GET':
|
||||
form = CollectionForm(instance=collection) if collection else CollectionForm()
|
||||
return render(request, 'collection_edit.html', {'form': form, 'collection': collection})
|
||||
elif request.method == 'POST':
|
||||
form = CollectionForm(request.POST, instance=collection) if collection else CollectionForm(request.POST)
|
||||
if form.is_valid():
|
||||
if not collection:
|
||||
form.instance.owner = request.user
|
||||
form.instance.edited_time = timezone.now()
|
||||
form.save()
|
||||
return redirect(reverse("journal:collection_retrieve", args=[form.instance.uuid]))
|
||||
else:
|
||||
return HttpResponseBadRequest(form.errors)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
def collection_delete(request, collection_uuid):
|
||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||
if not collection.is_editable_by(request.user):
|
||||
raise PermissionDenied()
|
||||
if request.method == 'GET':
|
||||
collection_form = CollectionForm(instance=collection)
|
||||
return render(request, 'collection_delete.html', {'form': collection_form, 'collection': collection})
|
||||
elif request.method == 'POST':
|
||||
collection.delete()
|
||||
return redirect(reverse("users:home"))
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
def review_retrieve(request, review_uuid):
|
||||
piece = get_object_or_404(Review, uid=base62.decode(review_uuid))
|
||||
if not piece.is_visible_to(request.user):
|
||||
|
@ -198,11 +292,7 @@ def _render_list(request, user_name, type, shelf_type=None, item_category=None,
|
|||
queryset = queryset.filter(query_item_category(item_category))
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
if user != request.user:
|
||||
if request.user.is_following(user):
|
||||
queryset = queryset.filter(visibility__ne=2)
|
||||
else:
|
||||
queryset = queryset.filter(visibility=0)
|
||||
queryset = queryset.filter(q_visible_to(request.user, user))
|
||||
paginator = Paginator(queryset, PAGE_SIZE)
|
||||
page_number = request.GET.get('page', default=1)
|
||||
members = paginator.get_page(page_number)
|
||||
|
@ -239,7 +329,7 @@ def user_tag_list(request, user_name):
|
|||
if user != request.user:
|
||||
tags = tags.filter(visibility=0)
|
||||
tags = tags.values('title').annotate(total=Count('members')).order_by('-total')
|
||||
return render(request, f'user_tag_list.html', {
|
||||
return render(request, 'user_tag_list.html', {
|
||||
'user': user,
|
||||
'tags': tags,
|
||||
})
|
||||
|
@ -258,7 +348,7 @@ def user_collection_list(request, user_name):
|
|||
collections = collections.filter(visibility__ne=2)
|
||||
else:
|
||||
collections = collections.filter(visibility=0)
|
||||
return render(request, f'user_collection_list.html', {
|
||||
return render(request, 'user_collection_list.html', {
|
||||
'user': user,
|
||||
'collections': collections,
|
||||
})
|
||||
|
@ -274,7 +364,94 @@ def user_liked_collection_list(request, user_name):
|
|||
collections = Collection.objects.filter(likes__owner=user)
|
||||
if user != request.user:
|
||||
collections = collections.filter(query_visible(request.user))
|
||||
return render(request, f'user_collection_list.html', {
|
||||
return render(request, 'user_collection_list.html', {
|
||||
'user': user,
|
||||
'collections': collections,
|
||||
})
|
||||
|
||||
|
||||
def home_anonymous(request, id):
|
||||
login_url = settings.LOGIN_URL + "?next=" + request.get_full_path()
|
||||
try:
|
||||
username = id.split('@')[0]
|
||||
site = id.split('@')[1]
|
||||
return render(request, 'users/home_anonymous.html', {
|
||||
'login_url': login_url,
|
||||
'username': username,
|
||||
'site': site,
|
||||
})
|
||||
except Exception:
|
||||
return redirect(login_url)
|
||||
|
||||
|
||||
def home(request, user_name):
|
||||
if not request.user.is_authenticated:
|
||||
return home_anonymous(request, user_name)
|
||||
if request.method != 'GET':
|
||||
return HttpResponseBadRequest()
|
||||
user = User.get(user_name)
|
||||
if user is None:
|
||||
return render_user_not_found(request)
|
||||
|
||||
# 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 request.user.is_blocked_by(user) or request.user.is_blocking(user):
|
||||
return render_user_blocked(request)
|
||||
# no these value on other's home page
|
||||
reports = None
|
||||
unread_announcements = None
|
||||
|
||||
qv = q_visible_to(request.user, user)
|
||||
shelf_list = {}
|
||||
visbile_categories = [ItemCategory.Book, ItemCategory.Movie, ItemCategory.TV, ItemCategory.Music, ItemCategory.Game]
|
||||
for category in visbile_categories:
|
||||
shelf_list[category] = {}
|
||||
for shelf_type in ShelfType:
|
||||
shelf = user.shelf_manager.get_shelf(category, shelf_type)
|
||||
members = shelf.recent_members.filter(qv)
|
||||
shelf_list[category][shelf_type] = {
|
||||
'title': shelf.title,
|
||||
'count': members.count(),
|
||||
'members': members[:5].prefetch_related('item'),
|
||||
}
|
||||
reviews = Review.objects.filter(owner=user).filter(qv)
|
||||
shelf_list[category]['reviewed'] = {
|
||||
'title': '评论过的' + category.label,
|
||||
'count': reviews.count(),
|
||||
'members': reviews[:5].prefetch_related('item'),
|
||||
}
|
||||
collections = Collection.objects.filter(owner=user).filter(qv).order_by("-edited_time")
|
||||
liked_collections = Collection.objects.filter(likes__owner=user).order_by("-edited_time")
|
||||
if user != request.user:
|
||||
liked_collections = liked_collections.filter(query_visible(request.user))
|
||||
|
||||
layout = user.get_preference().get_serialized_home_layout()
|
||||
|
||||
return render(
|
||||
request,
|
||||
'profile.html',
|
||||
{
|
||||
'user': user,
|
||||
'shelf_list': shelf_list,
|
||||
'collections': collections[:5],
|
||||
'collections_count': collections.count(),
|
||||
'liked_collections': liked_collections.order_by("-edited_time")[:5],
|
||||
'liked_collections_count': liked_collections.count(),
|
||||
'layout': layout,
|
||||
'reports': reports,
|
||||
'unread_announcements': unread_announcements,
|
||||
}
|
||||
)
|
||||
|
|
|
@ -48,7 +48,7 @@ class ActivityManager:
|
|||
q = Q(owner_id__in=self.owner.following, visibility__lt=2) | Q(owner=self.owner)
|
||||
if before_time:
|
||||
q = q & Q(created_time__lt=before_time)
|
||||
return LocalActivity.objects.filter(q).order_by('-created_time') # .select_related() https://github.com/django-polymorphic/django-polymorphic/pull/531
|
||||
return LocalActivity.objects.filter(q).order_by('-created_time').prefetch_related('action_object') # .select_related() https://github.com/django-polymorphic/django-polymorphic/pull/531
|
||||
|
||||
@staticmethod
|
||||
def get_manager_for_user(user):
|
||||
|
|
Loading…
Add table
Reference in a new issue