new data model: user profile pages

This commit is contained in:
Your Name 2022-12-28 01:09:55 -05:00
parent f36ec360e3
commit a45fdf9edf
15 changed files with 731 additions and 36 deletions

View file

@ -3,4 +3,3 @@ from catalog.common import *
class Collection(Item):
category = ItemCategory.Collection
url_path = 'collection'

View file

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

View file

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

View file

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

View file

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

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

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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