new data model: timeline page

This commit is contained in:
Your Name 2022-12-21 14:34:36 -05:00
parent dc75a730d1
commit 6a42dc9247
15 changed files with 532 additions and 22 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

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

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

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

View file

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