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>
|
||||
<a href="{% url 'users:tag_list' user.mastodon_username %}">{% trans '更多' %}</a>
|
||||
<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 %}
|
||||
<div>{% trans '书籍' %}</div>
|
||||
{% for v in book_tags %}
|
||||
|
|
|
@ -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):
|
||||
|
|
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,
|
||||
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,
|
||||
|
|
|
@ -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()
|
||||
|
|
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