add timeline, etc
This commit is contained in:
parent
eb602ce2b1
commit
cd4d9f64cd
22 changed files with 457 additions and 0 deletions
|
@ -28,6 +28,7 @@ urlpatterns = [
|
|||
path('music/', include('music.urls')),
|
||||
path('games/', include('games.urls')),
|
||||
path('collections/', include('collection.urls')),
|
||||
path('timeline/', include('timeline.urls')),
|
||||
path('sync/', include('sync.urls')),
|
||||
path('announcement/', include('management.urls')),
|
||||
path('hijack/', include('hijack.urls')),
|
||||
|
|
|
@ -167,6 +167,7 @@ def retrieve(request, id):
|
|||
else:
|
||||
mark_form = BookMarkForm(initial={
|
||||
'book': book,
|
||||
'visibility': request.user.preference.default_visibility if request.user.is_authenticated else 0,
|
||||
'tags': mark_tags
|
||||
})
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ from markdownx.models import MarkdownxField
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django.conf import settings
|
||||
from common.utils import ChoicesDictGenerator, GenerateDateUUIDMediaFilePath
|
||||
from django.shortcuts import reverse
|
||||
|
||||
|
||||
def collection_cover_path(instance, filename):
|
||||
|
@ -22,6 +23,10 @@ class Collection(UserOwnedEntity):
|
|||
def __str__(self):
|
||||
return f"Collection({self.id} {self.owner} {self.title})"
|
||||
|
||||
@property
|
||||
def translated_status(self):
|
||||
return '创建了收藏单'
|
||||
|
||||
@property
|
||||
def collectionitem_list(self):
|
||||
return sorted(list(self.collectionitem_set.all()), key=lambda i: i.position)
|
||||
|
@ -48,6 +53,14 @@ class Collection(UserOwnedEntity):
|
|||
i.save()
|
||||
return i
|
||||
|
||||
@property
|
||||
def item(self):
|
||||
return self
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return settings.APP_WEBSITE + reverse("collection:retrieve", args=[self.id])
|
||||
|
||||
|
||||
class CollectionItem(models.Model):
|
||||
movie = models.ForeignKey(Movie, on_delete=models.CASCADE, null=True)
|
||||
|
@ -90,3 +103,7 @@ class CollectionMark(UserOwnedEntity):
|
|||
|
||||
def __str__(self):
|
||||
return f"CollectionMark({self.id} {self.owner} {self.collection})"
|
||||
|
||||
@property
|
||||
def translated_status(self):
|
||||
return '关注了收藏单'
|
||||
|
|
|
@ -323,6 +323,10 @@ class Review(UserOwnedEntity):
|
|||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@property
|
||||
def translated_status(self):
|
||||
return '评论了'
|
||||
|
||||
|
||||
class Tag(models.Model):
|
||||
content = models.CharField(max_length=50)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
from django import template
|
||||
import datetime
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
@ -9,3 +11,19 @@ def current_user_marked_item(context, item):
|
|||
if context['request'].user and context['request'].user.is_authenticated:
|
||||
return context['request'].user.get_mark_for_item(item)
|
||||
return None
|
||||
|
||||
|
||||
@register.filter
|
||||
def prettydate(d):
|
||||
diff = timezone.now() - d
|
||||
s = diff.seconds
|
||||
if diff.days > 14 or diff.days < 0:
|
||||
return d.strftime('%Y年%m月%d日')
|
||||
elif diff.days >= 1:
|
||||
return '{} 天前'.format(diff.days)
|
||||
elif s < 120:
|
||||
return '刚刚'
|
||||
elif s < 3600:
|
||||
return '{} 分钟前'.format(s / 60)
|
||||
else:
|
||||
return '{} 小时前'.format(s / 3600)
|
||||
|
|
|
@ -168,6 +168,7 @@ def retrieve(request, id):
|
|||
else:
|
||||
mark_form = GameMarkForm(initial={
|
||||
'game': game,
|
||||
'visibility': request.user.preference.default_visibility if request.user.is_authenticated else 0,
|
||||
'tags': mark_tags
|
||||
})
|
||||
|
||||
|
|
|
@ -168,6 +168,7 @@ def retrieve(request, id):
|
|||
else:
|
||||
mark_form = MovieMarkForm(initial={
|
||||
'movie': movie,
|
||||
'visibility': request.user.preference.default_visibility if request.user.is_authenticated else 0,
|
||||
'tags': mark_tags
|
||||
})
|
||||
|
||||
|
|
|
@ -186,6 +186,7 @@ def retrieve_song(request, id):
|
|||
else:
|
||||
mark_form = SongMarkForm(initial={
|
||||
'song': song,
|
||||
'visibility': request.user.preference.default_visibility if request.user.is_authenticated else 0,
|
||||
'tags': mark_tags
|
||||
})
|
||||
|
||||
|
@ -729,6 +730,7 @@ def retrieve_album(request, id):
|
|||
else:
|
||||
mark_form = AlbumMarkForm(initial={
|
||||
'album': album,
|
||||
'visibility': request.user.preference.default_visibility if request.user.is_authenticated else 0,
|
||||
'tags': mark_tags
|
||||
})
|
||||
|
||||
|
|
0
timeline/__init__.py
Normal file
0
timeline/__init__.py
Normal file
3
timeline/admin.py
Normal file
3
timeline/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
15
timeline/apps.py
Normal file
15
timeline/apps.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TimelineConfig(AppConfig):
|
||||
name = 'timeline'
|
||||
|
||||
def ready(self):
|
||||
from .models import init_post_save_handler
|
||||
from books.models import BookMark, BookReview
|
||||
from movies.models import MovieMark, MovieReview
|
||||
from games.models import GameMark, GameReview
|
||||
from music.models import AlbumMark, AlbumReview, SongMark, SongReview
|
||||
from collection.models import Collection, CollectionMark
|
||||
for m in [BookMark, BookReview, MovieMark, MovieReview, GameMark, GameReview, AlbumMark, AlbumReview, SongMark, SongReview, Collection, CollectionMark]:
|
||||
init_post_save_handler(m)
|
20
timeline/management/commands/regen_activity.py
Normal file
20
timeline/management/commands/regen_activity.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from users.models import User
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
from timeline.models import Activity
|
||||
from books.models import BookMark, BookReview
|
||||
from movies.models import MovieMark, MovieReview
|
||||
from games.models import GameMark, GameReview
|
||||
from music.models import AlbumMark, AlbumReview, SongMark, SongReview
|
||||
from collection.models import Collection, CollectionMark
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Re-populating activity for timeline'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
for cl in [BookMark, BookReview, MovieMark, MovieReview, GameMark, GameReview, AlbumMark, AlbumReview, SongMark, SongReview, Collection, CollectionMark]:
|
||||
for a in tqdm(cl.objects.all(), desc=f'Populating {cl.__name__}'):
|
||||
Activity.upsert_item(a)
|
63
timeline/models.py
Normal file
63
timeline/models.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
from django.db import models
|
||||
from common.models import UserOwnedEntity
|
||||
from books.models import BookMark, BookReview
|
||||
from movies.models import MovieMark, MovieReview
|
||||
from games.models import GameMark, GameReview
|
||||
from music.models import AlbumMark, AlbumReview, SongMark, SongReview
|
||||
from collection.models import Collection, CollectionMark
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
|
||||
|
||||
class Activity(UserOwnedEntity):
|
||||
bookmark = models.ForeignKey(BookMark, models.CASCADE, null=True)
|
||||
bookreview = models.ForeignKey(BookReview, models.CASCADE, null=True)
|
||||
moviemark = models.ForeignKey(MovieMark, models.CASCADE, null=True)
|
||||
moviereview = models.ForeignKey(MovieReview, models.CASCADE, null=True)
|
||||
gamemark = models.ForeignKey(GameMark, models.CASCADE, null=True)
|
||||
gamereview = models.ForeignKey(GameReview, models.CASCADE, null=True)
|
||||
albummark = models.ForeignKey(AlbumMark, models.CASCADE, null=True)
|
||||
albumreview = models.ForeignKey(AlbumReview, models.CASCADE, null=True)
|
||||
songmark = models.ForeignKey(SongMark, models.CASCADE, null=True)
|
||||
songreview = models.ForeignKey(SongReview, models.CASCADE, null=True)
|
||||
collection = models.ForeignKey(Collection, models.CASCADE, null=True)
|
||||
collectionmark = models.ForeignKey(CollectionMark, models.CASCADE, null=True)
|
||||
|
||||
@property
|
||||
def target(self):
|
||||
items = [self.bookmark, self.bookreview, self.moviemark, self.moviereview, self.gamemark, self.gamereview,
|
||||
self.songmark, self.songreview, self.albummark, self.albumreview, self.collection, self.collectionmark]
|
||||
return next((x for x in items if x is not None), None)
|
||||
|
||||
@property
|
||||
def mark(self):
|
||||
items = [self.bookmark, self.moviemark, self.gamemark, self.songmark, self.albummark]
|
||||
return next((x for x in items if x is not None), None)
|
||||
|
||||
@property
|
||||
def review(self):
|
||||
items = [self.bookreview, self.moviereview, self.gamereview, self.songreview, self.albumreview]
|
||||
return next((x for x in items if x is not None), None)
|
||||
|
||||
@classmethod
|
||||
def upsert_item(self, item):
|
||||
attr = item.__class__.__name__.lower()
|
||||
f = {'owner': item.owner, attr: item}
|
||||
activity = Activity.objects.filter(**f).first()
|
||||
if not activity:
|
||||
activity = Activity.objects.create(**f)
|
||||
activity.created_time = item.created_time
|
||||
activity.visibility = item.visibility
|
||||
activity.save()
|
||||
|
||||
|
||||
def _post_save_handler(sender, instance, created, **kwargs):
|
||||
Activity.upsert_item(instance)
|
||||
|
||||
|
||||
# def activity_post_delete_handler(sender, instance, **kwargs):
|
||||
# pass
|
||||
|
||||
|
||||
def init_post_save_handler(model):
|
||||
post_save.connect(_post_save_handler, sender=model)
|
||||
# post_delete.connect(activity_post_delete_handler, sender=model) # delete handled by database
|
83
timeline/templates/timeline.html
Normal file
83
timeline/templates/timeline.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 }} - {{ user.mastodon_username }} {{ list_title }}</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.6.1"></script>
|
||||
<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>
|
||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'lib/css/neo.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-wrapper">
|
||||
<div id="content-wrapper">
|
||||
{% include "partial/_navbar.html" %}
|
||||
|
||||
<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" hx-indicator=".htmx-indicator">
|
||||
<div hx-get="{% url 'timeline: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>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
116
timeline/templates/timeline_data.html
Normal file
116
timeline/templates/timeline_data.html
Normal file
|
@ -0,0 +1,116 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load admin_url %}
|
||||
{% load mastodon %}
|
||||
{% load oauth_token %}
|
||||
{% load truncate %}
|
||||
{% load thumb %}
|
||||
{% load neo %}
|
||||
|
||||
{% for activity in activities %}
|
||||
<li class="entity-list__entity">
|
||||
<div class="entity-list__entity-img-wrapper">
|
||||
<a href="{{ activity.target.item.url }}">
|
||||
<img src="{{ activity.target.item.cover|thumb:'normal' }}" alt="" class="entity-list__entity-img" style="min-width:80px;max-width:80px">
|
||||
</a>
|
||||
</div>
|
||||
<div class="entity-list__entity-text">
|
||||
<div class="collection-item-position-edit">
|
||||
<span class="entity-marks__mark-time">
|
||||
{% if activity.target.shared_link %}
|
||||
<a href="{{ activity.target.shared_link }}" target="_blank"><span class="entity-marks__mark-time">{{ activity.target.created_time|prettydate }}</span></a>
|
||||
{% else %}
|
||||
<a><span class="entity-marks__mark-time">{{ activity.target.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.mastodon_account.display_name }}</a> {{ activity.target.translated_status }}
|
||||
</span>
|
||||
<div class="entity-list__entity-title">
|
||||
<a href="{{ activity.target.item.url }}" class="entity-list__entity-link" style="font-weight:bold;">{{ activity.target.item.title }}</a>
|
||||
{% if activity.target.item.source_url %}
|
||||
<a href="{{ activity.target.item.source_url }}">
|
||||
<span class="source-label source-label__{{ activity.target.item.source_site }}" style="font-size:xx-small;">{{ activity.target.item.get_source_site_display }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</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>
|
||||
</li>
|
||||
{% if forloop.last %}
|
||||
<div class="htmx-indicator" style="margin-left: 60px;"
|
||||
hx-get="{% url 'timeline: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 %}
|
3
timeline/tests.py
Normal file
3
timeline/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
9
timeline/urls.py
Normal file
9
timeline/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django.urls import path, re_path
|
||||
from .views import *
|
||||
|
||||
|
||||
app_name = 'timeline'
|
||||
urlpatterns = [
|
||||
path('', timeline, name='timeline'),
|
||||
path('data', data, name='data'),
|
||||
]
|
56
timeline/views.py
Normal file
56
timeline/views.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
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 mastodon import mastodon_request_included
|
||||
from mastodon.models import MastodonApplication
|
||||
from mastodon.api import post_toot, TootVisibilityEnum
|
||||
from common.utils import PageLinksGenerator
|
||||
from common.views import PAGE_LINK_NUMBER, jump_or_scrape
|
||||
from common.models import SourceSiteEnum
|
||||
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
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
mastodon_logger = logging.getLogger("django.mastodon")
|
||||
PAGE_SIZE = 20
|
||||
|
||||
@login_required
|
||||
def timeline(request):
|
||||
if request.method != 'GET':
|
||||
return
|
||||
return render(
|
||||
request,
|
||||
'timeline.html',
|
||||
{
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def data(request):
|
||||
if request.method != 'GET':
|
||||
return
|
||||
q = Q(owner_id__in=request.user.following, visibility__lt=2) | Q(owner_id=request.user.id)
|
||||
last = request.GET.get('last')
|
||||
if last:
|
||||
q = q & Q(created_time__lt=last)
|
||||
activities = Activity.objects.filter(q).order_by('-created_time')[:PAGE_SIZE]
|
||||
return render(
|
||||
request,
|
||||
'timeline_data.html',
|
||||
{
|
||||
'activities': activities,
|
||||
}
|
||||
)
|
|
@ -38,6 +38,7 @@ from books.models import BookMark, BookReview
|
|||
from movies.models import MovieMark, MovieReview
|
||||
from games.models import GameMark, GameReview
|
||||
from music.models import AlbumMark, SongMark, AlbumReview, SongReview
|
||||
from timeline.models import Activity
|
||||
from collection.models import Collection
|
||||
from common.importers.goodreads import GoodreadsImporter
|
||||
from common.importers.douban import DoubanImporter
|
||||
|
@ -47,6 +48,7 @@ from common.importers.douban import DoubanImporter
|
|||
@login_required
|
||||
def preferences(request):
|
||||
if request.method == 'POST':
|
||||
request.user.preference.default_visibility = int(request.POST.get('default_visibility'))
|
||||
request.user.preference.mastodon_publish_public = bool(request.POST.get('mastodon_publish_public'))
|
||||
request.user.preference.mastodon_append_tag = request.POST.get('mastodon_append_tag', '').strip()
|
||||
request.user.preference.save()
|
||||
|
@ -110,6 +112,7 @@ def reset_visibility(request):
|
|||
GameMark.objects.filter(owner=request.user).update(visibility=visibility)
|
||||
AlbumMark.objects.filter(owner=request.user).update(visibility=visibility)
|
||||
SongMark.objects.filter(owner=request.user).update(visibility=visibility)
|
||||
Activity.objects.filter(owner=request.user).update(visibility=visibility)
|
||||
messages.add_message(request, messages.INFO, _('已重置。'))
|
||||
return redirect(reverse("users:data"))
|
||||
|
||||
|
|
19
users/management/commands/refresh_following.py
Normal file
19
users/management/commands/refresh_following.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from users.models import User
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Refresh following data for all users'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
count = 0
|
||||
for user in tqdm(User.objects.all()):
|
||||
user.following = user.get_following_ids()
|
||||
if user.following:
|
||||
count += 1
|
||||
user.save(update_fields=['following'])
|
||||
|
||||
print(f'{count} users updated')
|
|
@ -22,6 +22,7 @@ class User(AbstractUser):
|
|||
unique=False,
|
||||
help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
|
||||
)
|
||||
following = models.JSONField(default=list)
|
||||
mastodon_id = models.CharField(max_length=100, blank=False)
|
||||
# mastodon domain name, eg donotban.com
|
||||
mastodon_site = models.CharField(max_length=100, blank=False)
|
||||
|
@ -79,12 +80,21 @@ class User(AbstractUser):
|
|||
self.mastodon_mutes = get_related_acct_list(self.mastodon_site, self.mastodon_token, '/api/v1/mutes')
|
||||
self.mastodon_blocks = get_related_acct_list(self.mastodon_site, self.mastodon_token, '/api/v1/blocks')
|
||||
self.mastodon_domain_blocks = get_related_acct_list(self.mastodon_site, self.mastodon_token, '/api/v1/domain_blocks')
|
||||
self.following = self.get_following_ids()
|
||||
updated = True
|
||||
elif code == 401:
|
||||
print(f'401 {self}')
|
||||
self.mastodon_token = ''
|
||||
return updated
|
||||
|
||||
def get_following_ids(self):
|
||||
fl = []
|
||||
for m in self.mastodon_following:
|
||||
target = User.get(m)
|
||||
if target and ((not target.mastodon_locked) or self.mastodon_username in target.mastodon_followers):
|
||||
fl.append(target.id)
|
||||
return fl
|
||||
|
||||
def is_blocking(self, target):
|
||||
return target.mastodon_username in self.mastodon_blocks or target.mastodon_site in self.mastodon_domain_blocks
|
||||
|
||||
|
@ -142,6 +152,7 @@ class Preference(models.Model):
|
|||
)
|
||||
export_status = models.JSONField(blank=True, null=True, encoder=DjangoJSONEncoder, default=dict)
|
||||
import_status = models.JSONField(blank=True, null=True, encoder=DjangoJSONEncoder, default=dict)
|
||||
default_visibility = models.PositiveSmallIntegerField(default=0)
|
||||
mastodon_publish_public = models.BooleanField(null=False, default=False)
|
||||
mastodon_append_tag = models.CharField(max_length=2048, default='')
|
||||
|
||||
|
|
|
@ -33,6 +33,17 @@
|
|||
<div class="import-panel__body">
|
||||
<form action="{% url 'users:preferences' %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<span>{% trans '新标记默认可见性:' %}</span>
|
||||
<div class="import-panel__checkbox import-panel__checkbox--last">
|
||||
可见性:
|
||||
<label for="id_visibility_0"><input type="radio" name="default_visibility" value="0" required="" id="id_visibility_0" {%if request.user.preference.default_visibility == 0 %}checked{% endif %}>
|
||||
公开</label>
|
||||
<label for="id_visibility_1"><input type="radio" name="default_visibility" value="1" required="" id="id_visibility_1" {%if request.user.preference.default_visibility == 1 %}checked{% endif %}>
|
||||
仅关注者</label>
|
||||
<label for="id_visibility_2"><input type="radio" name="default_visibility" value="2" required="" id="id_visibility_2" {%if request.user.preference.default_visibility == 2 %}checked{% endif %}>
|
||||
仅自己</label>
|
||||
</div>
|
||||
<br>
|
||||
<span>{% trans '在联邦网络上以公开方式分享的帖文是否发布到公共时间轴上:' %}</span>
|
||||
<div class="import-panel__checkbox import-panel__checkbox--last">
|
||||
<input type="checkbox" name="mastodon_publish_public" id="visibility" {%if request.user.preference.mastodon_publish_public%}checked{% endif %}>
|
||||
|
|
Loading…
Add table
Reference in a new issue