From 6a42dc92474685d435647fa707de2a08bc82d2fc Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 21 Dec 2022 14:34:36 -0500 Subject: [PATCH] new data model: timeline page --- common/templates/partial/_sidebar.html | 8 ++ journal/models.py | 40 ++++++--- journal/templatetags/__init__.py | 0 journal/templatetags/user_actions.py | 27 ++++++ journal/urls.py | 9 ++ journal/views.py | 47 ++++++++++- legacy/management/commands/migrate_journal.py | 5 +- social/models.py | 14 ++-- .../templates/activity/create_collection.html | 60 ++++++++++++++ .../templates/activity/like_collection.html | 57 +++++++++++++ social/templates/activity/mark_item.html | 61 ++++++++++++++ social/templates/feed.html | 83 +++++++++++++++++++ social/templates/feed_data.html | 80 ++++++++++++++++++ social/urls.py | 9 ++ social/views.py | 54 +++++++++++- 15 files changed, 532 insertions(+), 22 deletions(-) create mode 100644 journal/templatetags/__init__.py create mode 100644 journal/templatetags/user_actions.py create mode 100644 journal/urls.py create mode 100644 social/templates/activity/create_collection.html create mode 100644 social/templates/activity/like_collection.html create mode 100644 social/templates/activity/mark_item.html create mode 100644 social/templates/feed.html create mode 100644 social/templates/feed_data.html create mode 100644 social/urls.py diff --git a/common/templates/partial/_sidebar.html b/common/templates/partial/_sidebar.html index 455bdbac..bfe5a335 100644 --- a/common/templates/partial/_sidebar.html +++ b/common/templates/partial/_sidebar.html @@ -87,6 +87,14 @@ {% trans '更多' %}
+ {% if tags %} + {% for t in tags %} + + {{ t }} + + {% endfor %} +
+ {% endif %} {% if book_tags %}
{% trans '书籍' %}
{% for v in book_tags %} diff --git a/journal/models.py b/journal/models.py index 04dc422c..38b3cf0c 100644 --- a/journal/models.py +++ b/journal/models.py @@ -17,6 +17,8 @@ from django.db.models import Count, Avg import django.dispatch import math import uuid +from catalog.common.utils import DEFAULT_ITEM_COVER, item_cover_path +from django.utils.baseconv import base62 class Piece(PolymorphicModel, UserOwnedObjectMixin): @@ -28,6 +30,10 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin): metadata = models.JSONField(default=dict) attached_to = models.ForeignKey(User, null=True, default=None, on_delete=models.SET_NULL, related_name="attached_with") + @property + def uuid(self): + return base62.encode(self.uid.int) + class Content(Piece): item = models.ForeignKey(Item, on_delete=models.PROTECT) @@ -42,6 +48,15 @@ class Content(Piece): class Like(Piece): target = models.ForeignKey(Piece, on_delete=models.CASCADE, related_name='likes') + @staticmethod + def user_like_piece(user, piece): + if not piece or piece.__class__ not in [Collection]: + return + like = Like.objects.filter(owner=user, target=piece).first() + if not like: + like = Like.objects.create(owner=user, target=piece) + return like + class Note(Content): pass @@ -176,7 +191,7 @@ class List(Piece): return None else: ml = self.ordered_members - p = {'_' + self.__class__.__name__.lower(): self} + p = {'parent': self} p.update(params) member = self.MEMBER_CLASS.objects.create(owner=self.owner, position=ml.last().position + 1 if ml.count() else 1, item=item, **p) list_add.send(sender=self.__class__, instance=self, item=item, member=member) @@ -267,7 +282,7 @@ ShelfTypeNames = [ class ShelfMember(ListMember): - _shelf = models.ForeignKey('Shelf', related_name='members', on_delete=models.CASCADE) + parent = models.ForeignKey('Shelf', related_name='members', on_delete=models.CASCADE) class Shelf(List): @@ -322,7 +337,7 @@ class ShelfManager: Shelf.objects.create(owner=self.owner, item_category=ic, shelf_type=qt) def _shelf_member_for_item(self, item): - return ShelfMember.objects.filter(item=item, _shelf__in=self.owner.shelf_set.all()).first() + return ShelfMember.objects.filter(item=item, parent__in=self.owner.shelf_set.all()).first() def _shelf_for_item_and_type(item, shelf_type): if not item or not shelf_type: @@ -331,7 +346,7 @@ class ShelfManager: def locate_item(self, item): member = ShelfMember.objects.filter(owner=self.owner, item=item).first() - return member # ._shelf if member else None + return member # .parent if member else None def move_item(self, item, shelf_type, visibility=0, metadata=None): # shelf_type=None means remove from current shelf @@ -340,7 +355,7 @@ class ShelfManager: raise ValueError('empty item') new_shelfmember = None last_shelfmember = self._shelf_member_for_item(item) - last_shelf = last_shelfmember._shelf if last_shelfmember else None + last_shelf = last_shelfmember.parent if last_shelfmember else None last_metadata = last_shelfmember.metadata if last_shelfmember else None last_visibility = last_shelfmember.visibility if last_shelfmember else None shelf = self.get_shelf(item.category, shelf_type) if shelf_type else None @@ -393,7 +408,7 @@ Collection class CollectionMember(ListMember): - _collection = models.ForeignKey('Collection', related_name='members', on_delete=models.CASCADE) + parent = models.ForeignKey('Collection', related_name='members', on_delete=models.CASCADE) class Collection(List): @@ -401,6 +416,7 @@ class Collection(List): catalog_item = models.OneToOneField(CatalogCollection, on_delete=models.PROTECT) title = models.CharField(_("title in primary language"), max_length=1000, default="") brief = models.TextField(_("简介"), blank=True, default="") + cover = models.ImageField(upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True) 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 @@ -415,6 +431,7 @@ class Collection(List): if self.catalog_item.title != self.title or self.catalog_item.brief != self.brief: self.catalog_item.title = self.title self.catalog_item.brief = self.brief + self.catalog_item.cover = self.cover self.catalog_item.save() super().save(*args, **kwargs) @@ -425,7 +442,7 @@ Tag class TagMember(ListMember): - _tag = models.ForeignKey('Tag', related_name='members', on_delete=models.CASCADE) + parent = models.ForeignKey('Tag', related_name='members', on_delete=models.CASCADE) TagValidators = [RegexValidator(regex=r'\s+', inverse_match=True)] @@ -460,7 +477,7 @@ class TagManager: @staticmethod def tag_item_by_user(item, user, tag_titles, default_visibility=0): titles = set([Tag.cleanup_title(tag_title) for tag_title in tag_titles]) - current_titles = set([m._tag.title for m in TagMember.objects.filter(owner=user, item=item)]) + current_titles = set([m.parent.title for m in TagMember.objects.filter(owner=user, item=item)]) for title in titles - current_titles: tag = Tag.objects.filter(owner=user, title=title).first() if not tag: @@ -485,6 +502,7 @@ class TagManager: def __init__(self, user): self.owner = user + @property def all_tags(self): return TagManager.all_tags_for_user(self.owner) @@ -493,7 +511,7 @@ class TagManager: TagManager.add_tag_by_user(item, tag, self.owner, visibility) def get_item_tags(self, item): - return sorted([m['_tag__title'] for m in TagMember.objects.filter(_tag__owner=self.owner, item=item).values('_tag__title')]) + return sorted([m['parent__title'] for m in TagMember.objects.filter(parent__owner=self.owner, item=item).values('parent__title')]) Item.tags = property(TagManager.public_tags_for_item) @@ -519,11 +537,11 @@ class Mark: @property def shelf_type(self): - return self.shelfmember._shelf.shelf_type if self.shelfmember else None + return self.shelfmember.parent.shelf_type if self.shelfmember else None @property def shelf_label(self): - return self.shelfmember._shelf.shelf_label if self.shelfmember else None + return self.shelfmember.parent.shelf_label if self.shelfmember else None @property def created_time(self): diff --git a/journal/templatetags/__init__.py b/journal/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/journal/templatetags/user_actions.py b/journal/templatetags/user_actions.py new file mode 100644 index 00000000..3c08ae1c --- /dev/null +++ b/journal/templatetags/user_actions.py @@ -0,0 +1,27 @@ +from django import template +from journal.models import Collection, Like +from django.shortcuts import reverse + +register = template.Library() + + +@register.simple_tag(takes_context=True) +def wish_item_action(context, item): + user = context['request'].user + if user and user.is_authenticated: + action = { + 'taken': user.shelf_manager.locate_item(item) is not None, + 'url': reverse("journal:wish", args=[item.uuid]), + } + return action + + +@register.simple_tag(takes_context=True) +def like_piece_action(context, piece): + user = context['request'].user + if user and user.is_authenticated: + action = { + 'taken': Like.objects.filter(target=piece, owner=user).first() is not None, + 'url': reverse("journal:like", args=[piece.uuid]), + } + return action diff --git a/journal/urls.py b/journal/urls.py new file mode 100644 index 00000000..0e1f79d8 --- /dev/null +++ b/journal/urls.py @@ -0,0 +1,9 @@ +from django.urls import path, re_path +from .views import * + + +app_name = 'journal' +urlpatterns = [ + path('wish/', wish, name='wish'), + path('like/', like, name='like'), +] diff --git a/journal/views.py b/journal/views.py index 91ea44a2..123f052e 100644 --- a/journal/views.py +++ b/journal/views.py @@ -1,3 +1,46 @@ -from django.shortcuts import render +import logging +from django.shortcuts import render, get_object_or_404, redirect, reverse +from django.contrib.auth.decorators import login_required, permission_required +from django.utils.translation import gettext_lazy as _ +from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseServerError, HttpResponseNotFound +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.db import IntegrityError, transaction +from django.db.models import Count +from django.utils import timezone +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 +from management.models import Announcement +from django.utils.baseconv import base62 -# Create your views here. + +PAGE_SIZE = 10 + + +@login_required +def wish(request, item_uuid): + if request.method == 'POST': + item = get_object_or_404(Item, uid=base62.decode(item_uuid)) + if not item: + return HttpResponseNotFound("item not found") + request.user.shelf_manager.move_item(item, ShelfType.WISHLIST) + return HttpResponse("✔️") + else: + return HttpResponseBadRequest("invalid request") + + +@login_required +def like(request, piece_uuid): + if request.method == 'POST': + piece = get_object_or_404(Collection, uid=base62.decode(piece_uuid)) + if not piece: + return HttpResponseNotFound("piece not found") + Like.user_like_piece(request.user, piece) + return HttpResponse("✔️") + else: + return HttpResponseBadRequest("invalid request") diff --git a/legacy/management/commands/migrate_journal.py b/legacy/management/commands/migrate_journal.py index fbca5047..427e5130 100644 --- a/legacy/management/commands/migrate_journal.py +++ b/legacy/management/commands/migrate_journal.py @@ -97,6 +97,7 @@ class Command(BaseCommand): owner_id=entity.owner_id, title=entity.title, brief=entity.description, + cover=entity.cover, collaborative=entity.collaborative, created_time=entity.created_time, edited_time=entity.edited_time, @@ -197,7 +198,7 @@ class Command(BaseCommand): Comment.objects.create(owner_id=user_id, item_id=item_id, text=entity.text, visibility=visibility) shelf = shelf_cache[f'{user_id}_{item.category}_{entity.status}'] ShelfMember.objects.create( - _shelf_id=shelf, + parent_id=shelf, owner_id=user_id, position=0, item_id=item_id, @@ -212,7 +213,7 @@ class Command(BaseCommand): else: tag = tag_cache[tag_key] TagMember.objects.create( - _tag_id=tag, + parent_id=tag, owner_id=user_id, position=0, item_id=item_id, diff --git a/social/models.py b/social/models.py index fcc0f49c..fb9c7447 100644 --- a/social/models.py +++ b/social/models.py @@ -36,6 +36,9 @@ class LocalActivity(models.Model, UserOwnedObjectMixin): action_object = models.ForeignKey(Piece, on_delete=models.CASCADE) created_time = models.DateTimeField(default=timezone.now, db_index=True) + def __str__(self): + return f'Activity [{self.owner}:{self.template}:{self.action_object}]' + class ActivityManager: def __init__(self, user): @@ -45,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) + return LocalActivity.objects.filter(q).order_by('-created_time') # .select_related() https://github.com/django-polymorphic/django-polymorphic/pull/531 @staticmethod def get_manager_for_user(user): @@ -105,6 +108,7 @@ class DefaultActivityProcessor: 'template': self.template, 'action_object': self.action_object, } + print(params) LocalActivity.objects.create(**params) def updated(self): @@ -140,9 +144,9 @@ class LikeCollectionProcessor(DefaultActivityProcessor): template = ActivityTemplate.LikeCollection def created(self): - if isinstance(self.action_object, Collection): - super.created() + if isinstance(self.action_object.target, Collection): + super().created() def updated(self): - if isinstance(self.action_object, Collection): - super.update() + if isinstance(self.action_object.target, Collection): + super().update() diff --git a/social/templates/activity/create_collection.html b/social/templates/activity/create_collection.html new file mode 100644 index 00000000..b9d31f3e --- /dev/null +++ b/social/templates/activity/create_collection.html @@ -0,0 +1,60 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load thumb %} +{% load prettydate %} +{% load user_actions %} + +{% like_piece_action activity.action_object as action %} +
+ + + + {% if not action.take %} + + {% endif %} +
+
+
+ + {% if activity.action_object.metadata.shared_link %} + + + {{ activity.action_object.created_time|prettydate }} + {% else %} + {{ activity.action_object.created_time|prettydate }} + {% endif %} + +
+ + {{ activity.owner.display_name }} {% trans '创建了收藏单' %} + + +

+ {% if activity.review %} + {{ activity.review.title }} + {% endif %} + {% if activity.mark %} + {% if activity.mark.rating %} + + {% endif %} + + {% if activity.mark.text %} +

{{ activity.mark.text }}

+ {% endif %} + {% endif %} +

+
\ No newline at end of file diff --git a/social/templates/activity/like_collection.html b/social/templates/activity/like_collection.html new file mode 100644 index 00000000..3efae399 --- /dev/null +++ b/social/templates/activity/like_collection.html @@ -0,0 +1,57 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load thumb %} +{% load prettydate %} +{% load user_actions %} + +{% like_piece_action activity.action_object.target as action %} +
+ + + + {% if not action.take %} + + {% endif %} +
+
+
+ + {% if activity.action_object.metadata.shared_link %} + + + {{ activity.action_object.created_time|prettydate }} + {% else %} + {{ activity.action_object.created_time|prettydate }} + {% endif %} + +
+ + {{ activity.owner.display_name }} 关注了 + {{ activity.action_object.target.owner.display_name }} + 的收藏单 + + +

+ {% if activity.review %} + {{ activity.review.title }} + {% endif %} + {% if activity.mark %} + {% if activity.mark.rating %} + + {% endif %} + + {% if activity.mark.text %} +

{{ activity.mark.text }}

+ {% endif %} + {% endif %} +

+
\ No newline at end of file diff --git a/social/templates/activity/mark_item.html b/social/templates/activity/mark_item.html new file mode 100644 index 00000000..6694951d --- /dev/null +++ b/social/templates/activity/mark_item.html @@ -0,0 +1,61 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load thumb %} +{% load prettydate %} +{% load user_actions %} + +{% wish_item_action activity.action_object.item as action %} + +
+ + + + {% if not action.take %} + + {% endif %} +
+
+
+ + {% if activity.action_object.metadata.shared_link %} + + + {{ activity.action_object.created_time|prettydate }} + {% else %} + {{ activity.action_object.created_time|prettydate }} + {% endif %} + +
+ + {{ activity.owner.display_name }} {{ activity.action_object.parent.shelf_label }} + + +

+ {% if activity.review %} + {{ activity.review.title }} + {% endif %} + {% if activity.mark %} + {% if activity.mark.rating %} + + {% endif %} + + {% if activity.mark.text %} +

{{ activity.mark.text }}

+ {% endif %} + {% endif %} +

+
\ No newline at end of file diff --git a/social/templates/feed.html b/social/templates/feed.html new file mode 100644 index 00000000..b9c31d81 --- /dev/null +++ b/social/templates/feed.html @@ -0,0 +1,83 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load thumb %} + + + + + + + {{ site_name }} + + {% include "partial/_common_libs.html" with jquery=1 %} + + + + + + + + +
+
+ {% include "partial/_navbar.html" with current="timeline" %} + +
+
+
+
+
+ + +
    +
    +
+
+
+
+ + {% include "partial/_sidebar.html" %} +
+
+
+ {% include "partial/_footer.html" %} +
+ + + +{% if unread_announcements %} +{% include "partial/_announcement.html" %} +{% endif %} + + diff --git a/social/templates/feed_data.html b/social/templates/feed_data.html new file mode 100644 index 00000000..61310301 --- /dev/null +++ b/social/templates/feed_data.html @@ -0,0 +1,80 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load thumb %} +{% load prettydate %} +{% load user_actions %} +{% for activity in activities %} + +
  • + {% with "activity/"|add:activity.template|add:".html" as template %} + {% include template %} + {% endwith %} +
  • + +{% if forloop.last %} +
    + + + + + + + + + + + + + + + + + + + + + + +
    +{% endif %} +{% empty %} +
    {% trans '目前没有更多内容了' %}
    +{% endfor %} diff --git a/social/urls.py b/social/urls.py new file mode 100644 index 00000000..75a2664f --- /dev/null +++ b/social/urls.py @@ -0,0 +1,9 @@ +from django.urls import path, re_path +from .views import * + + +app_name = 'social' +urlpatterns = [ + path('', feed, name='feed'), + path('data', data, name='data'), +] diff --git a/social/views.py b/social/views.py index 91ea44a2..412e507e 100644 --- a/social/views.py +++ b/social/views.py @@ -1,3 +1,53 @@ -from django.shortcuts import render +import logging +from django.shortcuts import render, get_object_or_404, redirect, reverse +from django.contrib.auth.decorators import login_required, permission_required +from django.utils.translation import gettext_lazy as _ +from django.http import HttpResponseBadRequest, HttpResponseServerError +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.db import IntegrityError, transaction +from django.db.models import Count +from django.utils import timezone +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 +from management.models import Announcement -# Create your views here. + +PAGE_SIZE = 10 + + +@login_required +def feed(request): + if request.method != 'GET': + return + user = request.user + unread = Announcement.objects.filter(pk__gt=user.read_announcement_index).order_by('-pk') + if unread: + user.read_announcement_index = Announcement.objects.latest('pk').pk + user.save(update_fields=['read_announcement_index']) + return render( + request, + 'feed.html', + { + 'tags': user.tag_manager.all_tags[:10], + 'unread_announcements': unread, + } + ) + + +@login_required +def data(request): + if request.method != 'GET': + return + return render( + request, + 'feed_data.html', + { + 'activities': ActivityManager(request.user).get_timeline(before_time=request.GET.get('last'))[:PAGE_SIZE], + } + )