new data model: timeline page
This commit is contained in:
parent
dc75a730d1
commit
6a42dc9247
15 changed files with 532 additions and 22 deletions
|
@ -87,6 +87,14 @@
|
||||||
</h5>
|
</h5>
|
||||||
<a href="{% url 'users:tag_list' user.mastodon_username %}">{% trans '更多' %}</a>
|
<a href="{% url 'users:tag_list' user.mastodon_username %}">{% trans '更多' %}</a>
|
||||||
<div class="tag-collection" style="margin-left: 0;">
|
<div class="tag-collection" style="margin-left: 0;">
|
||||||
|
{% if tags %}
|
||||||
|
{% for t in tags %}
|
||||||
|
<span class="tag-collection__tag">
|
||||||
|
<a href="/users/{{ user.mastodon_username }}/tag/{{ t }}/">{{ t }}</a>
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
{% endif %}
|
||||||
{% if book_tags %}
|
{% if book_tags %}
|
||||||
<div>{% trans '书籍' %}</div>
|
<div>{% trans '书籍' %}</div>
|
||||||
{% for v in book_tags %}
|
{% for v in book_tags %}
|
||||||
|
|
|
@ -17,6 +17,8 @@ from django.db.models import Count, Avg
|
||||||
import django.dispatch
|
import django.dispatch
|
||||||
import math
|
import math
|
||||||
import uuid
|
import uuid
|
||||||
|
from catalog.common.utils import DEFAULT_ITEM_COVER, item_cover_path
|
||||||
|
from django.utils.baseconv import base62
|
||||||
|
|
||||||
|
|
||||||
class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||||
|
@ -28,6 +30,10 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||||
metadata = models.JSONField(default=dict)
|
metadata = models.JSONField(default=dict)
|
||||||
attached_to = models.ForeignKey(User, null=True, default=None, on_delete=models.SET_NULL, related_name="attached_with")
|
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):
|
class Content(Piece):
|
||||||
item = models.ForeignKey(Item, on_delete=models.PROTECT)
|
item = models.ForeignKey(Item, on_delete=models.PROTECT)
|
||||||
|
@ -42,6 +48,15 @@ class Content(Piece):
|
||||||
class Like(Piece):
|
class Like(Piece):
|
||||||
target = models.ForeignKey(Piece, on_delete=models.CASCADE, related_name='likes')
|
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):
|
class Note(Content):
|
||||||
pass
|
pass
|
||||||
|
@ -176,7 +191,7 @@ class List(Piece):
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
ml = self.ordered_members
|
ml = self.ordered_members
|
||||||
p = {'_' + self.__class__.__name__.lower(): self}
|
p = {'parent': self}
|
||||||
p.update(params)
|
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)
|
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)
|
list_add.send(sender=self.__class__, instance=self, item=item, member=member)
|
||||||
|
@ -267,7 +282,7 @@ ShelfTypeNames = [
|
||||||
|
|
||||||
|
|
||||||
class ShelfMember(ListMember):
|
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):
|
class Shelf(List):
|
||||||
|
@ -322,7 +337,7 @@ class ShelfManager:
|
||||||
Shelf.objects.create(owner=self.owner, item_category=ic, shelf_type=qt)
|
Shelf.objects.create(owner=self.owner, item_category=ic, shelf_type=qt)
|
||||||
|
|
||||||
def _shelf_member_for_item(self, item):
|
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):
|
def _shelf_for_item_and_type(item, shelf_type):
|
||||||
if not item or not shelf_type:
|
if not item or not shelf_type:
|
||||||
|
@ -331,7 +346,7 @@ class ShelfManager:
|
||||||
|
|
||||||
def locate_item(self, item):
|
def locate_item(self, item):
|
||||||
member = ShelfMember.objects.filter(owner=self.owner, item=item).first()
|
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):
|
def move_item(self, item, shelf_type, visibility=0, metadata=None):
|
||||||
# shelf_type=None means remove from current shelf
|
# shelf_type=None means remove from current shelf
|
||||||
|
@ -340,7 +355,7 @@ class ShelfManager:
|
||||||
raise ValueError('empty item')
|
raise ValueError('empty item')
|
||||||
new_shelfmember = None
|
new_shelfmember = None
|
||||||
last_shelfmember = self._shelf_member_for_item(item)
|
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_metadata = last_shelfmember.metadata if last_shelfmember else None
|
||||||
last_visibility = last_shelfmember.visibility 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
|
shelf = self.get_shelf(item.category, shelf_type) if shelf_type else None
|
||||||
|
@ -393,7 +408,7 @@ Collection
|
||||||
|
|
||||||
|
|
||||||
class CollectionMember(ListMember):
|
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):
|
class Collection(List):
|
||||||
|
@ -401,6 +416,7 @@ class Collection(List):
|
||||||
catalog_item = models.OneToOneField(CatalogCollection, on_delete=models.PROTECT)
|
catalog_item = models.OneToOneField(CatalogCollection, on_delete=models.PROTECT)
|
||||||
title = models.CharField(_("title in primary language"), max_length=1000, default="")
|
title = models.CharField(_("title in primary language"), max_length=1000, default="")
|
||||||
brief = models.TextField(_("简介"), blank=True, 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")
|
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
|
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:
|
if self.catalog_item.title != self.title or self.catalog_item.brief != self.brief:
|
||||||
self.catalog_item.title = self.title
|
self.catalog_item.title = self.title
|
||||||
self.catalog_item.brief = self.brief
|
self.catalog_item.brief = self.brief
|
||||||
|
self.catalog_item.cover = self.cover
|
||||||
self.catalog_item.save()
|
self.catalog_item.save()
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -425,7 +442,7 @@ Tag
|
||||||
|
|
||||||
|
|
||||||
class TagMember(ListMember):
|
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)]
|
TagValidators = [RegexValidator(regex=r'\s+', inverse_match=True)]
|
||||||
|
@ -460,7 +477,7 @@ class TagManager:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def tag_item_by_user(item, user, tag_titles, default_visibility=0):
|
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])
|
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:
|
for title in titles - current_titles:
|
||||||
tag = Tag.objects.filter(owner=user, title=title).first()
|
tag = Tag.objects.filter(owner=user, title=title).first()
|
||||||
if not tag:
|
if not tag:
|
||||||
|
@ -485,6 +502,7 @@ class TagManager:
|
||||||
def __init__(self, user):
|
def __init__(self, user):
|
||||||
self.owner = user
|
self.owner = user
|
||||||
|
|
||||||
|
@property
|
||||||
def all_tags(self):
|
def all_tags(self):
|
||||||
return TagManager.all_tags_for_user(self.owner)
|
return TagManager.all_tags_for_user(self.owner)
|
||||||
|
|
||||||
|
@ -493,7 +511,7 @@ class TagManager:
|
||||||
TagManager.add_tag_by_user(item, tag, self.owner, visibility)
|
TagManager.add_tag_by_user(item, tag, self.owner, visibility)
|
||||||
|
|
||||||
def get_item_tags(self, item):
|
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)
|
Item.tags = property(TagManager.public_tags_for_item)
|
||||||
|
@ -519,11 +537,11 @@ class Mark:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def shelf_type(self):
|
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
|
@property
|
||||||
def shelf_label(self):
|
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
|
@property
|
||||||
def created_time(self):
|
def created_time(self):
|
||||||
|
|
0
journal/templatetags/__init__.py
Normal file
0
journal/templatetags/__init__.py
Normal file
27
journal/templatetags/user_actions.py
Normal file
27
journal/templatetags/user_actions.py
Normal file
|
@ -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
|
9
journal/urls.py
Normal file
9
journal/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from django.urls import path, re_path
|
||||||
|
from .views import *
|
||||||
|
|
||||||
|
|
||||||
|
app_name = 'journal'
|
||||||
|
urlpatterns = [
|
||||||
|
path('wish/<str:item_uuid>', wish, name='wish'),
|
||||||
|
path('like/<str:piece_uuid>', like, name='like'),
|
||||||
|
]
|
|
@ -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")
|
||||||
|
|
|
@ -97,6 +97,7 @@ class Command(BaseCommand):
|
||||||
owner_id=entity.owner_id,
|
owner_id=entity.owner_id,
|
||||||
title=entity.title,
|
title=entity.title,
|
||||||
brief=entity.description,
|
brief=entity.description,
|
||||||
|
cover=entity.cover,
|
||||||
collaborative=entity.collaborative,
|
collaborative=entity.collaborative,
|
||||||
created_time=entity.created_time,
|
created_time=entity.created_time,
|
||||||
edited_time=entity.edited_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)
|
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}']
|
shelf = shelf_cache[f'{user_id}_{item.category}_{entity.status}']
|
||||||
ShelfMember.objects.create(
|
ShelfMember.objects.create(
|
||||||
_shelf_id=shelf,
|
parent_id=shelf,
|
||||||
owner_id=user_id,
|
owner_id=user_id,
|
||||||
position=0,
|
position=0,
|
||||||
item_id=item_id,
|
item_id=item_id,
|
||||||
|
@ -212,7 +213,7 @@ class Command(BaseCommand):
|
||||||
else:
|
else:
|
||||||
tag = tag_cache[tag_key]
|
tag = tag_cache[tag_key]
|
||||||
TagMember.objects.create(
|
TagMember.objects.create(
|
||||||
_tag_id=tag,
|
parent_id=tag,
|
||||||
owner_id=user_id,
|
owner_id=user_id,
|
||||||
position=0,
|
position=0,
|
||||||
item_id=item_id,
|
item_id=item_id,
|
||||||
|
|
|
@ -36,6 +36,9 @@ class LocalActivity(models.Model, UserOwnedObjectMixin):
|
||||||
action_object = models.ForeignKey(Piece, on_delete=models.CASCADE)
|
action_object = models.ForeignKey(Piece, on_delete=models.CASCADE)
|
||||||
created_time = models.DateTimeField(default=timezone.now, db_index=True)
|
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:
|
class ActivityManager:
|
||||||
def __init__(self, user):
|
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)
|
q = Q(owner_id__in=self.owner.following, visibility__lt=2) | Q(owner=self.owner)
|
||||||
if before_time:
|
if before_time:
|
||||||
q = q & Q(created_time__lt=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
|
@staticmethod
|
||||||
def get_manager_for_user(user):
|
def get_manager_for_user(user):
|
||||||
|
@ -105,6 +108,7 @@ class DefaultActivityProcessor:
|
||||||
'template': self.template,
|
'template': self.template,
|
||||||
'action_object': self.action_object,
|
'action_object': self.action_object,
|
||||||
}
|
}
|
||||||
|
print(params)
|
||||||
LocalActivity.objects.create(**params)
|
LocalActivity.objects.create(**params)
|
||||||
|
|
||||||
def updated(self):
|
def updated(self):
|
||||||
|
@ -140,9 +144,9 @@ class LikeCollectionProcessor(DefaultActivityProcessor):
|
||||||
template = ActivityTemplate.LikeCollection
|
template = ActivityTemplate.LikeCollection
|
||||||
|
|
||||||
def created(self):
|
def created(self):
|
||||||
if isinstance(self.action_object, Collection):
|
if isinstance(self.action_object.target, Collection):
|
||||||
super.created()
|
super().created()
|
||||||
|
|
||||||
def updated(self):
|
def updated(self):
|
||||||
if isinstance(self.action_object, Collection):
|
if isinstance(self.action_object.target, Collection):
|
||||||
super.update()
|
super().update()
|
||||||
|
|
60
social/templates/activity/create_collection.html
Normal file
60
social/templates/activity/create_collection.html
Normal file
|
@ -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 %}
|
||||||
|
<div class="entity-list__entity-img-wrapper">
|
||||||
|
<a href="{{ activity.action_object.url }}">
|
||||||
|
<img src="{{ activity.action_object.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img" style="min-width:80px;max-width:80px">
|
||||||
|
</a>
|
||||||
|
{% if not action.take %}
|
||||||
|
<a class="entity-list__entity-action-icon" hx-post="{{ action.url }}">➕</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="entity-list__entity-text">
|
||||||
|
<div class="collection-item-position-edit">
|
||||||
|
<span class="entity-marks__mark-time">
|
||||||
|
{% if activity.action_object.metadata.shared_link %}
|
||||||
|
<a href="{{ activity.action_object.metadata.shared_link }}" action_object="_blank">
|
||||||
|
<img src="{% static 'img/fediverse.svg' %}" style="filter: invert(93%) sepia(1%) saturate(53%) hue-rotate(314deg) brightness(95%) contrast(80%); vertical-align:text-top; max-width:14px; margin-right:6px;" />
|
||||||
|
<span class="entity-marks__mark-time">{{ activity.action_object.created_time|prettydate }}</span></a>
|
||||||
|
{% else %}
|
||||||
|
<a><span class="entity-marks__mark-time">{{ activity.action_object.created_time|prettydate }}</span></a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="entity-list__entity-info" style="top:0px;">
|
||||||
|
<a href="{% url 'users:home' activity.owner.mastodon_username %}">{{ activity.owner.display_name }}</a> {% trans '创建了收藏单' %}
|
||||||
|
</span>
|
||||||
|
<div class="entity-list__entity-title">
|
||||||
|
<a href="{{ activity.action_object.url }}" class="entity-list__entity-link" style="font-weight:bold;">{{ activity.action_object.title }}
|
||||||
|
{% if activity.action_object.year %}<small style="font-weight: lighter">({{ activity.action_object.year }})</small>{% endif %}
|
||||||
|
</a>
|
||||||
|
{% for res in activity.action_object.external_resources.all %}
|
||||||
|
<a href="{{ res.url }}">
|
||||||
|
<span class="source-label source-label__{{ res.site_name }}">{{ res.site_name.label }}</span>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<p class="entity-list__entity-brief">
|
||||||
|
{% if activity.review %}
|
||||||
|
<a href="{{ activity.review.url }}">{{ activity.review.title }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if activity.mark %}
|
||||||
|
{% if activity.mark.rating %}
|
||||||
|
<span class="entity-marks__rating-star rating-star" data-rating-score="{{ activity.mark.rating | floatformat:"0" }}" style=""></span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if activity.mark.text %}
|
||||||
|
<p class="entity-marks__mark-content">{{ activity.mark.text }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
57
social/templates/activity/like_collection.html
Normal file
57
social/templates/activity/like_collection.html
Normal file
|
@ -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 %}
|
||||||
|
<div class="entity-list__entity-img-wrapper">
|
||||||
|
<a href="{{ activity.action_object.target.url }}">
|
||||||
|
<img src="{{ activity.action_object.target.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img" style="min-width:80px;max-width:80px">
|
||||||
|
</a>
|
||||||
|
{% if not action.take %}
|
||||||
|
<a class="entity-list__entity-action-icon" hx-post="{{ action.url }}">➕</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="entity-list__entity-text">
|
||||||
|
<div class="collection-item-position-edit">
|
||||||
|
<span class="entity-marks__mark-time">
|
||||||
|
{% if activity.action_object.metadata.shared_link %}
|
||||||
|
<a href="{{ activity.action_object.metadata.shared_link }}" action_object="_blank">
|
||||||
|
<img src="{% static 'img/fediverse.svg' %}" style="filter: invert(93%) sepia(1%) saturate(53%) hue-rotate(314deg) brightness(95%) contrast(80%); vertical-align:text-top; max-width:14px; margin-right:6px;" />
|
||||||
|
<span class="entity-marks__mark-time">{{ activity.action_object.created_time|prettydate }}</span></a>
|
||||||
|
{% else %}
|
||||||
|
<a><span class="entity-marks__mark-time">{{ activity.action_object.created_time|prettydate }}</span></a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="entity-list__entity-info" style="top:0px;">
|
||||||
|
<a href="{% url 'users:home' activity.owner.mastodon_username %}">{{ activity.owner.display_name }}</a> 关注了
|
||||||
|
<a href="{% url 'users:home' activity.action_object.target.owner.mastodon_username %}">{{ activity.action_object.target.owner.display_name }}</a>
|
||||||
|
的收藏单
|
||||||
|
</span>
|
||||||
|
<div class="entity-list__entity-title">
|
||||||
|
<a href="{{ activity.action_object.target.url }}" class="entity-list__entity-link" style="font-weight:bold;">{{ activity.action_object.target.title }}
|
||||||
|
{% if activity.action_object.target.year %}<small style="font-weight: lighter">({{ activity.action_object.target.year }})</small>{% endif %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p class="entity-list__entity-brief">
|
||||||
|
{% if activity.review %}
|
||||||
|
<a href="{{ activity.review.url }}">{{ activity.review.title }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if activity.mark %}
|
||||||
|
{% if activity.mark.rating %}
|
||||||
|
<span class="entity-marks__rating-star rating-star" data-rating-score="{{ activity.mark.rating | floatformat:"0" }}" style=""></span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if activity.mark.text %}
|
||||||
|
<p class="entity-marks__mark-content">{{ activity.mark.text }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
61
social/templates/activity/mark_item.html
Normal file
61
social/templates/activity/mark_item.html
Normal file
|
@ -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 %}
|
||||||
|
|
||||||
|
<div class="entity-list__entity-img-wrapper">
|
||||||
|
<a href="{{ activity.action_object.item.url }}">
|
||||||
|
<img src="{{ activity.action_object.item.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img" style="min-width:80px;max-width:80px">
|
||||||
|
</a>
|
||||||
|
{% if not action.take %}
|
||||||
|
<a class="entity-list__entity-action-icon" hx-post="{{ action.url }}">➕</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="entity-list__entity-text">
|
||||||
|
<div class="collection-item-position-edit">
|
||||||
|
<span class="entity-marks__mark-time">
|
||||||
|
{% if activity.action_object.metadata.shared_link %}
|
||||||
|
<a href="{{ activity.action_object.metadata.shared_link }}" action_object="_blank">
|
||||||
|
<img src="{% static 'img/fediverse.svg' %}" style="filter: invert(93%) sepia(1%) saturate(53%) hue-rotate(314deg) brightness(95%) contrast(80%); vertical-align:text-top; max-width:14px; margin-right:6px;" />
|
||||||
|
<span class="entity-marks__mark-time">{{ activity.action_object.created_time|prettydate }}</span></a>
|
||||||
|
{% else %}
|
||||||
|
<a><span class="entity-marks__mark-time">{{ activity.action_object.created_time|prettydate }}</span></a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="entity-list__entity-info" style="top:0px;">
|
||||||
|
<a href="{% url 'users:home' activity.owner.mastodon_username %}">{{ activity.owner.display_name }}</a> {{ activity.action_object.parent.shelf_label }}
|
||||||
|
</span>
|
||||||
|
<div class="entity-list__entity-title">
|
||||||
|
<a href="{{ activity.action_object.item.url }}" class="entity-list__entity-link" style="font-weight:bold;">{{ activity.action_object.item.title }}
|
||||||
|
{% if activity.action_object.item.year %}<small style="font-weight: lighter">({{ activity.action_object.item.year }})</small>{% endif %}
|
||||||
|
</a>
|
||||||
|
{% for res in activity.action_object.item.external_resources.all %}
|
||||||
|
<a href="{{ res.url }}">
|
||||||
|
<span class="source-label source-label__{{ res.site_name }}">{{ res.site_name.label }}</span>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<p class="entity-list__entity-brief">
|
||||||
|
{% if activity.review %}
|
||||||
|
<a href="{{ activity.review.url }}">{{ activity.review.title }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if activity.mark %}
|
||||||
|
{% if activity.mark.rating %}
|
||||||
|
<span class="entity-marks__rating-star rating-star" data-rating-score="{{ activity.mark.rating | floatformat:"0" }}" style=""></span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if activity.mark.text %}
|
||||||
|
<p class="entity-marks__mark-content">{{ activity.mark.text }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
83
social/templates/feed.html
Normal file
83
social/templates/feed.html
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load l10n %}
|
||||||
|
{% load admin_url %}
|
||||||
|
{% load mastodon %}
|
||||||
|
{% load oauth_token %}
|
||||||
|
{% load truncate %}
|
||||||
|
{% load thumb %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ site_name }}</title>
|
||||||
|
|
||||||
|
{% include "partial/_common_libs.html" with jquery=1 %}
|
||||||
|
|
||||||
|
<script src="{% static 'lib/js/rating-star.js' %}"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready( function() {
|
||||||
|
let render = function() {
|
||||||
|
let ratingLabels = $(".rating-star");
|
||||||
|
$(ratingLabels).each( function(index, value) {
|
||||||
|
let ratingScore = $(this).data("rating-score") / 2;
|
||||||
|
$(this).starRating({
|
||||||
|
initialRating: ratingScore,
|
||||||
|
readOnly: true,
|
||||||
|
starSize: 16,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
document.body.addEventListener('htmx:load', function(evt) {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script src="{% static 'js/mastodon.js' %}"></script>
|
||||||
|
<script src="{% static 'js/home.js' %}"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="page-wrapper">
|
||||||
|
<div id="content-wrapper">
|
||||||
|
{% include "partial/_navbar.html" with current="timeline" %}
|
||||||
|
|
||||||
|
<section id="content" class="container">
|
||||||
|
<div class="grid grid--reverse-order">
|
||||||
|
<div class="grid__main grid__main--reverse-order">
|
||||||
|
<div class="main-section-wrapper">
|
||||||
|
<div class="entity-list">
|
||||||
|
|
||||||
|
<!-- <div class="set">
|
||||||
|
<h5 class="entity-list__title">
|
||||||
|
我的时间轴
|
||||||
|
</h5>
|
||||||
|
</div> -->
|
||||||
|
<ul class="entity-list__entities">
|
||||||
|
<div hx-get="{% url 'social:data' %}" hx-trigger="revealed" hx-swap="outerHTML"></div>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "partial/_sidebar.html" %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
{% include "partial/_footer.html" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.body.addEventListener('htmx:configRequest', (event) => {
|
||||||
|
event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% if unread_announcements %}
|
||||||
|
{% include "partial/_announcement.html" %}
|
||||||
|
{% endif %}
|
||||||
|
</body>
|
||||||
|
</html>
|
80
social/templates/feed_data.html
Normal file
80
social/templates/feed_data.html
Normal file
|
@ -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 %}
|
||||||
|
|
||||||
|
<li class="entity-list__entity">
|
||||||
|
{% with "activity/"|add:activity.template|add:".html" as template %}
|
||||||
|
{% include template %}
|
||||||
|
{% endwith %}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% if forloop.last %}
|
||||||
|
<div class="htmx-indicator" style="margin-left: 60px;"
|
||||||
|
hx-get="{% url 'social:data' %}?last={{ activity.created_time|date:'Y-m-d H:i:s.uO'|urlencode }}"
|
||||||
|
hx-trigger="revealed"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill="#ccc">
|
||||||
|
<rect y="10" width="15" height="120" rx="6">
|
||||||
|
<animate attributeName="height"
|
||||||
|
begin="0.5s" dur="1s"
|
||||||
|
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
|
||||||
|
repeatCount="indefinite" />
|
||||||
|
<animate attributeName="y"
|
||||||
|
begin="0.5s" dur="1s"
|
||||||
|
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
|
||||||
|
repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
<rect x="30" y="10" width="15" height="120" rx="6">
|
||||||
|
<animate attributeName="height"
|
||||||
|
begin="0.25s" dur="1s"
|
||||||
|
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
|
||||||
|
repeatCount="indefinite" />
|
||||||
|
<animate attributeName="y"
|
||||||
|
begin="0.25s" dur="1s"
|
||||||
|
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
|
||||||
|
repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
<rect x="60" width="15" height="140" rx="6">
|
||||||
|
<animate attributeName="height"
|
||||||
|
begin="0s" dur="1s"
|
||||||
|
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
|
||||||
|
repeatCount="indefinite" />
|
||||||
|
<animate attributeName="y"
|
||||||
|
begin="0s" dur="1s"
|
||||||
|
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
|
||||||
|
repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
<rect x="90" y="10" width="15" height="120" rx="6">
|
||||||
|
<animate attributeName="height"
|
||||||
|
begin="0.25s" dur="1s"
|
||||||
|
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
|
||||||
|
repeatCount="indefinite" />
|
||||||
|
<animate attributeName="y"
|
||||||
|
begin="0.25s" dur="1s"
|
||||||
|
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
|
||||||
|
repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
<rect x="120" y="10" width="15" height="120" rx="6">
|
||||||
|
<animate attributeName="height"
|
||||||
|
begin="0.5s" dur="1s"
|
||||||
|
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
|
||||||
|
repeatCount="indefinite" />
|
||||||
|
<animate attributeName="y"
|
||||||
|
begin="0.5s" dur="1s"
|
||||||
|
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
|
||||||
|
repeatCount="indefinite" />
|
||||||
|
</rect>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
<div>{% trans '目前没有更多内容了' %}</div>
|
||||||
|
{% endfor %}
|
9
social/urls.py
Normal file
9
social/urls.py
Normal file
|
@ -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'),
|
||||||
|
]
|
|
@ -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],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Reference in a new issue