Merge remote-tracking branch 'neo/main' into generalization

This commit is contained in:
doubaniux 2022-11-26 19:41:14 +01:00
commit 2645bc05f0
35 changed files with 863 additions and 430 deletions

3
.gitignore vendored
View file

@ -31,3 +31,6 @@ log
# typesense folder # typesense folder
/typesense-data /typesense-data
# test coverage
.coverage

1034
LICENSE

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
# Boofilsic # Boofilsic
An application allows you to mark any books, movies and more things you love. An application allows you to mark any books, movies and more things you love.
Depends on Mastodon. Works with Mastodon API and Twitter API.
## Install ## Install
Please see [doc/GUIDE.md](doc/GUIDE.md) Please see [doc/GUIDE.md](doc/GUIDE.md)

View file

@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/3.0/ref/settings/
""" """
import os import os
import requests
import psycopg2.extensions import psycopg2.extensions
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
@ -296,9 +297,9 @@ TMDB_API3_KEY = "***REMOVED***"
GOOGLE_API_KEY = '***REMOVED***' GOOGLE_API_KEY = '***REMOVED***'
# IGDB # IGDB
IGDB_CLIENT_ID = '***REMOVED***' IGDB_CLIENT_ID = 'deadbeef'
IGDB_SECRET = "***REMOVED***" IGDB_CLIENT_SECRET = 'deadbeef'
IGDB_ACCESS_TOKEN = '***REMOVED***' IGDB_ACCESS_TOKEN = requests.post(f'https://id.twitch.tv/oauth2/token?client_id={IGDB_CLIENT_ID}&client_secret={IGDB_CLIENT_SECRET}&grant_type=client_credentials').json()['access_token']
# Thumbnail setting # Thumbnail setting
# It is possible to optimize the image size even more: https://easy-thumbnails.readthedocs.io/en/latest/ref/optimize/ # It is possible to optimize the image size even more: https://easy-thumbnails.readthedocs.io/en/latest/ref/optimize/

View file

@ -120,6 +120,10 @@ class Book(Entity):
else: else:
return [self] # Book.objects.filter(id=self.id) return [self] # Book.objects.filter(id=self.id)
@property
def year(self):
return self.pub_year
@property @property
def verbose_category_name(self): def verbose_category_name(self):
return _("书籍") return _("书籍")

View file

@ -99,7 +99,7 @@
{% endif %} {% endif %}
{% if book.last_editor %} {% if book.last_editor and book.last_editor.preference.show_last_edit or user.is_staff %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' book.last_editor.mastodon_username %}">{{ book.last_editor | default:"" }}</a></div> <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' book.last_editor.mastodon_username %}">{{ book.last_editor | default:"" }}</a></div>
{% endif %} {% endif %}

View file

@ -210,7 +210,8 @@ def retrieve(request, id):
review_list_more = True if len( review_list_more = True if len(
review_list) > REVIEW_NUMBER else False review_list) > REVIEW_NUMBER else False
review_list = review_list[:REVIEW_NUMBER] review_list = review_list[:REVIEW_NUMBER]
collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(book=book))) all_collections = CollectionItem.objects.filter(book=book).annotate(num_marks=Count('collection__collection_marks')).order_by('-num_marks')[:20]
collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, all_collections))
# def strip_html_tags(text): # def strip_html_tags(text):
# import re # import re
@ -346,7 +347,7 @@ def wish(request, id):
params = { params = {
'owner': request.user, 'owner': request.user,
'status': MarkStatusEnum.WISH, 'status': MarkStatusEnum.WISH,
'visibility': 0, 'visibility': request.user.preference.default_visibility,
'book': book, 'book': book,
} }
try: try:

View file

@ -92,6 +92,9 @@
{{ collection.title }} {{ collection.title }}
</a> </a>
</h5> </h5>
{% if follower_count %}
被 {{ follower_count }} 人关注
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View file

@ -10,7 +10,7 @@ from django.utils import timezone
from django.core.paginator import Paginator from django.core.paginator import Paginator
from mastodon import mastodon_request_included from mastodon import mastodon_request_included
from mastodon.models import MastodonApplication from mastodon.models import MastodonApplication
from mastodon.api import post_toot, TootVisibilityEnum, share_collection from mastodon.api import share_collection
from common.utils import PageLinksGenerator from common.utils import PageLinksGenerator
from common.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin from common.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin
from common.models import SourceSiteEnum from common.models import SourceSiteEnum
@ -153,12 +153,8 @@ def retrieve(request, id):
raise PermissionDenied() raise PermissionDenied()
form = CollectionForm(instance=collection) form = CollectionForm(instance=collection)
if request.user.is_authenticated: following = True if request.user.is_authenticated and CollectionMark.objects.filter(owner=request.user, collection=collection).first() is not None else False
following = True if CollectionMark.objects.filter(owner=request.user, collection=collection).first() is not None else False follower_count = CollectionMark.objects.filter(collection=collection).count()
followers = []
else:
following = False
followers = []
return render( return render(
request, request,
@ -167,7 +163,7 @@ def retrieve(request, id):
'collection': collection, 'collection': collection,
'form': form, 'form': form,
'editable': request.user.is_authenticated and collection.is_editable_by(request.user), 'editable': request.user.is_authenticated and collection.is_editable_by(request.user),
'followers': followers, 'follower_count': follower_count,
'following': following, 'following': following,
} }
) )
@ -184,10 +180,6 @@ def retrieve_entity_list(request, id):
raise PermissionDenied() raise PermissionDenied()
form = CollectionForm(instance=collection) form = CollectionForm(instance=collection)
followers = []
if request.user.is_authenticated:
followers = []
return render( return render(
request, request,
'entity_list.html', 'entity_list.html',
@ -195,8 +187,6 @@ def retrieve_entity_list(request, id):
'collection': collection, 'collection': collection,
'form': form, 'form': form,
'editable': request.user.is_authenticated and collection.is_editable_by(request.user), 'editable': request.user.is_authenticated and collection.is_editable_by(request.user),
'followers': followers,
} }
) )

View file

@ -70,7 +70,7 @@ class GoodreadsImporter:
'rating': book['rating'], 'rating': book['rating'],
'text': book['review'], 'text': book['review'],
'status': status, 'status': status,
'visibility': 0, 'visibility': user.preference.default_visibility,
'book': book['book'], 'book': book['book'],
} }
if book['last_updated']: if book['last_updated']:

View file

@ -154,8 +154,10 @@ class AbstractScraper:
if settings.LUMINATI_USERNAME is None: if settings.LUMINATI_USERNAME is None:
proxies = None proxies = None
if settings.SCRAPESTACK_KEY is not None: if settings.PROXYCRAWL_KEY is not None:
url = f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}' url = f'https://api.proxycrawl.com/?token={settings.PROXYCRAWL_KEY}&url={url}'
# if settings.SCRAPESTACK_KEY is not None:
# url = f'http://api.scrapestack.com/scrape?access_key={settings.SCRAPESTACK_KEY}&url={url}'
else: else:
session_id = random.random() session_id = random.random()
proxy_url = ('http://%s-country-cn-session-%s:%s@zproxy.lum-superproxy.io:%d' % proxy_url = ('http://%s-country-cn-session-%s:%s@zproxy.lum-superproxy.io:%d' %

View file

@ -42,7 +42,7 @@ class GoodreadsScraper(AbstractScraper):
content = self.download_page(url, headers) content = self.download_page(url, headers)
try: try:
title = content.xpath("//h1[@id='bookTitle']/text()")[0].strip() title = content.xpath("//h1/text()")[0].strip()
except IndexError: except IndexError:
raise ValueError("given url contains no book info") raise ValueError("given url contains no book info")

View file

@ -199,10 +199,8 @@ class Indexer:
# print(r) # print(r)
import types import types
results = types.SimpleNamespace() results = types.SimpleNamespace()
results.items = list([x for x in map(lambda i: self.item_to_obj( results.items = list([x for x in map(lambda i: self.item_to_obj(i['document']), r['hits']) if x is not None])
i['document']), r['hits']) if x is not None]) results.num_pages = (r['found'] + SEARCH_PAGE_SIZE - 1) // SEARCH_PAGE_SIZE
results.num_pages = (
r['found'] + SEARCH_PAGE_SIZE - 1) // SEARCH_PAGE_SIZE
# print(results) # print(results)
return results return results

View file

@ -3,10 +3,10 @@
<div class="footer__border"> <div class="footer__border">
<a class="footer__link" target="_blank" href="https://donotban.com/@whitiewhite">原作者</a> <a class="footer__link" target="_blank" href="https://donotban.com/@whitiewhite">原作者</a>
<a class="footer__link" target="_blank" href="{{ support_link }}">报告错误</a> <a class="footer__link" target="_blank" href="{{ support_link }}">报告错误</a>
<a class="footer__link" target="_blank" href="https://github.com/doubaniux/boofilsic" id="githubLink">Github</a> <a class="footer__link" target="_blank" href="https://github.com/neodb-social" id="githubLink">源代码</a>
<a class="footer__link" target="_blank" href="https://patreon.com/tertius" id="sponsor">捐助上游项目</a> <a class="footer__link" target="_blank" href="https://patreon.com/tertius" id="sponsor">捐助上游项目</a>
<a class="footer__link" target="_blank" href="/announcement/supported-sites/" id="supported-sites">支持的网站</a> <a class="footer__link" target="_blank" href="/announcement/supported-sites/" id="supported-sites">支持的网站</a>
<a class="footer__link" target="_blank" href="/announcement/" id="supported-sites">公告栏</a> <a class="footer__link" target="_blank" href="/announcement/" id="supported-sites">公告栏</a>
</div> </div>
</div> </div>
</footer> </footer>

View file

@ -48,11 +48,15 @@ def external_search(request):
category = None category = None
keywords = request.GET.get("q", default='').strip() keywords = request.GET.get("q", default='').strip()
page_number = int(request.GET.get('page', default=1)) page_number = int(request.GET.get('page', default=1))
items = ExternalSources.search(category, keywords, page_number) if keywords else []
dedupe_urls = request.session.get('search_dedupe_urls', [])
items = [i for i in items if i.source_url not in dedupe_urls]
return render( return render(
request, request,
"common/external_search_result.html", "common/external_search_result.html",
{ {
"external_items": ExternalSources.search(category, keywords, page_number) if keywords else [], "external_items": items,
} }
) )
@ -85,18 +89,31 @@ def search(request):
pass pass
result = Indexer.search(keywords, page=page_number, category=category, tag=tag) result = Indexer.search(keywords, page=page_number, category=category, tag=tag)
for item in result.items: keys = []
item.tag_list = item.all_tag_list[:TAG_NUMBER_ON_LIST] items = []
urls = []
for i in result.items:
key = i.isbn if hasattr(i, 'isbn') else (i.imdb_code if hasattr(i, 'imdb_code') else None)
if key is None:
items.append(i)
elif key not in keys:
keys.append(key)
items.append(i)
urls.append(i.source_url)
i.tag_list = i.all_tag_list[:TAG_NUMBER_ON_LIST]
if request.path.endswith('.json/'): if request.path.endswith('.json/'):
return JsonResponse({ return JsonResponse({
'num_pages': result.num_pages, 'num_pages': result.num_pages,
'items':list(map(lambda i:i.get_json(), result.items)) 'items':list(map(lambda i:i.get_json(), items))
}) })
request.session['search_dedupe_urls'] = urls
return render( return render(
request, request,
"common/search_result.html", "common/search_result.html",
{ {
"items": result.items, "items": items,
"pagination": PageLinksGenerator(PAGE_LINK_NUMBER, page_number, result.num_pages), "pagination": PageLinksGenerator(PAGE_LINK_NUMBER, page_number, result.num_pages),
"categories": ['book', 'movie', 'music', 'game'], "categories": ['book', 'movie', 'music', 'game'],
} }

View file

@ -33,7 +33,7 @@ python3 manage.py check
Initialize database Initialize database
``` ```
python3 manage.py makemigrations users books movies games music sync mastodon management collection python3 manage.py makemigrations users books movies games music sync mastodon management collection timeline
python3 manage.py migrate users python3 manage.py migrate users
python3 manage.py migrate python3 manage.py migrate
``` ```
@ -104,3 +104,11 @@ docker-compose build
docker-compose up db && docker exec -it app_db_1 psql -U postgres postgres -c 'CREATE EXTENSION hstore WITH SCHEMA public;' # first time only docker-compose up db && docker exec -it app_db_1 psql -U postgres postgres -c 'CREATE EXTENSION hstore WITH SCHEMA public;' # first time only
docker-compose up docker-compose up
``` ```
Run Tests
```
psql template1 -c 'create extension hstore;' # first time only
coverage run --source='.' manage.py test
coverage report
```

View file

@ -98,6 +98,10 @@ class Game(Entity):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("games:retrieve", args=[self.id]) return reverse("games:retrieve", args=[self.id])
@property
def year(self):
return self.release_date.year if self.release_date else None
@property @property
def wish_url(self): def wish_url(self):
return reverse("games:wish", args=[self.id]) return reverse("games:wish", args=[self.id])

View file

@ -133,7 +133,7 @@
{% if game.last_editor %} {% if game.last_editor and game.last_editor.preference.show_last_edit or user.is_staff %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' game.last_editor.mastodon_username %}">{{ game.last_editor | default:"" }}</a></div> <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' game.last_editor.mastodon_username %}">{{ game.last_editor | default:"" }}</a></div>
{% endif %} {% endif %}

View file

@ -212,7 +212,8 @@ def retrieve(request, id):
review_list_more = True if len( review_list_more = True if len(
review_list) > REVIEW_NUMBER else False review_list) > REVIEW_NUMBER else False
review_list = review_list[:REVIEW_NUMBER] review_list = review_list[:REVIEW_NUMBER]
collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(game=game))) all_collections = CollectionItem.objects.filter(game=game).annotate(num_marks=Count('collection__collection_marks')).order_by('-num_marks')[:20]
collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, all_collections))
# def strip_html_tags(text): # def strip_html_tags(text):
# import re # import re
@ -348,7 +349,7 @@ def wish(request, id):
params = { params = {
'owner': request.user, 'owner': request.user,
'status': MarkStatusEnum.WISH, 'status': MarkStatusEnum.WISH,
'visibility': 0, 'visibility': request.user.preference.default_visibility,
'game': game, 'game': game,
} }
try: try:

View file

@ -9,6 +9,7 @@ from django.shortcuts import reverse
from urllib.parse import quote from urllib.parse import quote
from .models import CrossSiteUserInfo, MastodonApplication from .models import CrossSiteUserInfo, MastodonApplication
from mastodon.utils import rating_to_emoji from mastodon.utils import rating_to_emoji
import re
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -63,6 +64,7 @@ TWITTER_API_TOKEN = 'https://api.twitter.com/2/oauth2/token'
USER_AGENT = f"{settings.CLIENT_NAME}/1.0" USER_AGENT = f"{settings.CLIENT_NAME}/1.0"
get = functools.partial(requests.get, timeout=settings.MASTODON_TIMEOUT) get = functools.partial(requests.get, timeout=settings.MASTODON_TIMEOUT)
put = functools.partial(requests.put, timeout=settings.MASTODON_TIMEOUT)
post = functools.partial(requests.post, timeout=settings.MASTODON_TIMEOUT) post = functools.partial(requests.post, timeout=settings.MASTODON_TIMEOUT)
@ -78,7 +80,7 @@ def get_relationships(site, id_list, token): # no longer in use
return response.json() return response.json()
def post_toot(site, content, visibility, token, local_only=False): def post_toot(site, content, visibility, token, local_only=False, update_id=None):
headers = { headers = {
'User-Agent': USER_AGENT, 'User-Agent': USER_AGENT,
'Authorization': f'Bearer {token}', 'Authorization': f'Bearer {token}',
@ -90,6 +92,10 @@ def post_toot(site, content, visibility, token, local_only=False):
'text': content if len(content) <= 150 else content[0:150] + '...' 'text': content if len(content) <= 150 else content[0:150] + '...'
} }
response = post(url, headers=headers, json=payload) response = post(url, headers=headers, json=payload)
if response.status_code == 201:
response.status_code = 200
if response.status_code != 200:
logger.error(f"Error {url} {response.status_code}")
else: else:
url = 'https://' + site + API_PUBLISH_TOOT url = 'https://' + site + API_PUBLISH_TOOT
payload = { payload = {
@ -99,7 +105,10 @@ def post_toot(site, content, visibility, token, local_only=False):
if local_only: if local_only:
payload['local_only'] = True payload['local_only'] = True
try: try:
response = post(url, headers=headers, data=payload) if update_id:
response = put(url + '/' + update_id, headers=headers, data=payload)
if update_id is None or response.status_code != 200:
response = post(url, headers=headers, data=payload)
if response.status_code == 201: if response.status_code == 201:
response.status_code = 200 response.status_code = 200
if response.status_code != 200: if response.status_code != 200:
@ -402,7 +411,11 @@ def share_mark(mark):
tags = '\n' + user.get_preference().mastodon_append_tag.replace('[category]', str(mark.item.verbose_category_name)) if user.get_preference().mastodon_append_tag else '' tags = '\n' + user.get_preference().mastodon_append_tag.replace('[category]', str(mark.item.verbose_category_name)) if user.get_preference().mastodon_append_tag else ''
stars = rating_to_emoji(mark.rating, MastodonApplication.objects.get(domain_name=user.mastodon_site).star_mode) stars = rating_to_emoji(mark.rating, MastodonApplication.objects.get(domain_name=user.mastodon_site).star_mode)
content = f"{mark.translated_status}{mark.item.title}{stars}\n{mark.item.url}\n{mark.text}{tags}" content = f"{mark.translated_status}{mark.item.title}{stars}\n{mark.item.url}\n{mark.text}{tags}"
response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token) update_id = None
if mark.shared_link: # "https://mastodon.social/@username/1234567890"
r = re.match(r'.+/(\w+)$', mark.shared_link) # might be re.match(r'.+/([^/]+)$', u) if Pleroma supports edit
update_id = r[1] if r else None
response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token, False, update_id)
if response and response.status_code in [200, 201]: if response and response.status_code in [200, 201]:
j = response.json() j = response.json()
if 'url' in j: if 'url' in j:
@ -428,7 +441,11 @@ def share_review(review):
visibility = TootVisibilityEnum.UNLISTED visibility = TootVisibilityEnum.UNLISTED
tags = '\n' + user.get_preference().mastodon_append_tag.replace('[category]', str(review.item.verbose_category_name)) if user.get_preference().mastodon_append_tag else '' tags = '\n' + user.get_preference().mastodon_append_tag.replace('[category]', str(review.item.verbose_category_name)) if user.get_preference().mastodon_append_tag else ''
content = f"发布了关于《{review.item.title}》的评论\n{review.url}\n{review.title}{tags}" content = f"发布了关于《{review.item.title}》的评论\n{review.url}\n{review.title}{tags}"
response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token) update_id = None
if review.shared_link: # "https://mastodon.social/@username/1234567890"
r = re.match(r'.+/(\w+)$', review.shared_link) # might be re.match(r'.+/([^/]+)$', u) if Pleroma supports edit
update_id = r[1] if r else None
response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token, False, update_id)
if response and response.status_code in [200, 201]: if response and response.status_code in [200, 201]:
j = response.json() j = response.json()
if 'url' in j: if 'url' in j:

View file

@ -195,7 +195,7 @@
{% endif %} {% endif %}
{% if movie.last_editor %} {% if movie.last_editor and movie.last_editor.preference.show_last_edit or user.is_staff %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' movie.last_editor.mastodon_username %}">{{ movie.last_editor | default:"" }}</a></div> <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' movie.last_editor.mastodon_username %}">{{ movie.last_editor | default:"" }}</a></div>
{% endif %} {% endif %}

View file

@ -211,7 +211,8 @@ def retrieve(request, id):
review_list_more = True if len( review_list_more = True if len(
review_list) > REVIEW_NUMBER else False review_list) > REVIEW_NUMBER else False
review_list = review_list[:REVIEW_NUMBER] review_list = review_list[:REVIEW_NUMBER]
collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(movie=movie))) all_collections = CollectionItem.objects.filter(movie=movie).annotate(num_marks=Count('collection__collection_marks')).order_by('-num_marks')[:20]
collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, all_collections))
# def strip_html_tags(text): # def strip_html_tags(text):
# import re # import re
@ -347,7 +348,7 @@ def wish(request, id):
params = { params = {
'owner': request.user, 'owner': request.user,
'status': MarkStatusEnum.WISH, 'status': MarkStatusEnum.WISH,
'visibility': 0, 'visibility': request.user.preference.default_visibility,
'movie': movie, 'movie': movie,
} }
try: try:

View file

@ -55,6 +55,10 @@ class Album(Entity):
history = HistoricalRecords() history = HistoricalRecords()
@property
def year(self):
return self.release_date.year if self.release_date else None
def __str__(self): def __str__(self):
return self.title return self.title

View file

@ -125,7 +125,7 @@
{% endif %} {% endif %}
{% if album.last_editor %} {% if album.last_editor and album.last_editor.preference.show_last_edit or user.is_staff %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' album.last_editor.mastodon_username %}">{{ album.last_editor | default:"" }}</a></div> <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' album.last_editor.mastodon_username %}">{{ album.last_editor | default:"" }}</a></div>
{% endif %} {% endif %}

View file

@ -113,7 +113,7 @@
{% endif %} {% endif %}
{% if song.last_editor %} {% if song.last_editor and song.last_editor.preference.show_last_edit or user.is_staff %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' song.last_editor.mastodon_username %}">{{ song.last_editor | default:"" }}</a></div> <div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' song.last_editor.mastodon_username %}">{{ song.last_editor | default:"" }}</a></div>
{% endif %} {% endif %}

View file

@ -218,7 +218,8 @@ def retrieve_song(request, id):
review_list_more = True if len( review_list_more = True if len(
review_list) > REVIEW_NUMBER else False review_list) > REVIEW_NUMBER else False
review_list = review_list[:REVIEW_NUMBER] review_list = review_list[:REVIEW_NUMBER]
collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(song=song))) all_collections = CollectionItem.objects.filter(song=song).annotate(num_marks=Count('collection__collection_marks')).order_by('-num_marks')[:20]
collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, all_collections))
# def strip_html_tags(text): # def strip_html_tags(text):
# import re # import re
@ -354,7 +355,7 @@ def wish_song(request, id):
params = { params = {
'owner': request.user, 'owner': request.user,
'status': MarkStatusEnum.WISH, 'status': MarkStatusEnum.WISH,
'visibility': 0, 'visibility': request.user.preference.default_visibility,
'song': song, 'song': song,
} }
try: try:
@ -780,7 +781,8 @@ def retrieve_album(request, id):
review_list_more = True if len( review_list_more = True if len(
review_list) > REVIEW_NUMBER else False review_list) > REVIEW_NUMBER else False
review_list = review_list[:REVIEW_NUMBER] review_list = review_list[:REVIEW_NUMBER]
collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, CollectionItem.objects.filter(album=album))) all_collections = CollectionItem.objects.filter(album=album).annotate(num_marks=Count('collection__collection_marks')).order_by('-num_marks')[:20]
collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, all_collections))
# def strip_html_tags(text): # def strip_html_tags(text):
# import re # import re
@ -916,7 +918,7 @@ def wish_album(request, id):
params = { params = {
'owner': request.user, 'owner': request.user,
'status': MarkStatusEnum.WISH, 'status': MarkStatusEnum.WISH,
'visibility': 0, 'visibility': request.user.preference.default_visibility,
'album': album, 'album': album,
} }
try: try:

View file

@ -1,5 +1,6 @@
coverage
dateparser dateparser
django~=3.2.14 django~=3.2.16
django-hstore django-hstore
django-markdownx @ git+https://github.com/alphatownsman/django-markdownx.git@e69480c64ad9c5d0499f4a8625da78cf2bb7691b django-markdownx @ git+https://github.com/alphatownsman/django-markdownx.git@e69480c64ad9c5d0499f4a8625da78cf2bb7691b
django-sass django-sass

View file

@ -10,7 +10,6 @@ from django.utils import timezone
from django.core.paginator import Paginator from django.core.paginator import Paginator
from mastodon import mastodon_request_included from mastodon import mastodon_request_included
from mastodon.models import MastodonApplication from mastodon.models import MastodonApplication
from mastodon.api import post_toot, TootVisibilityEnum
from common.utils import PageLinksGenerator from common.utils import PageLinksGenerator
from .models import * from .models import *
from books.models import BookTag from books.models import BookTag

View file

@ -52,8 +52,9 @@ def preferences(request):
preference.default_visibility = int(request.POST.get('default_visibility')) preference.default_visibility = int(request.POST.get('default_visibility'))
preference.classic_homepage = bool(request.POST.get('classic_homepage')) preference.classic_homepage = bool(request.POST.get('classic_homepage'))
preference.mastodon_publish_public = bool(request.POST.get('mastodon_publish_public')) preference.mastodon_publish_public = bool(request.POST.get('mastodon_publish_public'))
preference.show_last_edit = bool(request.POST.get('show_last_edit'))
preference.mastodon_append_tag = request.POST.get('mastodon_append_tag', '').strip() preference.mastodon_append_tag = request.POST.get('mastodon_append_tag', '').strip()
preference.save(update_fields=['default_visibility', 'classic_homepage', 'mastodon_publish_public', 'mastodon_append_tag']) preference.save(update_fields=['default_visibility', 'classic_homepage', 'mastodon_publish_public', 'mastodon_append_tag', 'show_last_edit'])
return render(request, 'users/preferences.html') return render(request, 'users/preferences.html')

72
users/feeds.py Normal file
View file

@ -0,0 +1,72 @@
from django.contrib.syndication.views import Feed
from django.urls import reverse
from books.models import BookReview
from .models import User
from markdown import markdown
import operator
import mimetypes
MAX_ITEM_PER_TYPE = 10
class ReviewFeed(Feed):
def get_object(self, request, id):
return User.get(id)
def title(self, user):
return "%s的评论" % user.display_name
def link(self, user):
return user.url
def description(self, user):
return "%s的评论合集 - NeoDB" % user.display_name
def items(self, user):
if user is None:
return None
book_reviews = list(user.user_bookreviews.filter(visibility=0)[:MAX_ITEM_PER_TYPE])
movie_reviews = list(user.user_moviereviews.filter(visibility=0)[:MAX_ITEM_PER_TYPE])
album_reviews = list(user.user_albumreviews.filter(visibility=0)[:MAX_ITEM_PER_TYPE])
game_reviews = list(user.user_gamereviews.filter(visibility=0)[:MAX_ITEM_PER_TYPE])
all_reviews = sorted(
book_reviews + movie_reviews + album_reviews + game_reviews,
key=operator.attrgetter('created_time'),
reverse=True
)
return all_reviews
def item_title(self, item):
return f"{item.title} - 评论《{item.item.title}"
def item_description(self, item):
target_html = f'<p><a href="{item.item.url}">{item.item.title}</a></p>\n'
html = markdown(item.content)
return target_html + html
# item_link is only needed if NewsItem has no get_absolute_url method.
def item_link(self, item):
return item.url
def item_categories(self, item):
return [item.item.verbose_category_name]
def item_pubdate(self, item):
return item.created_time
def item_updateddate(self, item):
return item.edited_time
def item_enclosure_url(self, item):
return item.item.cover.url
def item_enclosure_mime_type(self, item):
t, _ = mimetypes.guess_type(item.item.cover.url)
return t
def item_enclosure_length(self, item):
return item.item.cover.file.size
def item_comments(self, item):
return item.shared_link

View file

@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _
from common.utils import GenerateDateUUIDMediaFilePath from common.utils import GenerateDateUUIDMediaFilePath
from django.conf import settings from django.conf import settings
from mastodon.api import * from mastodon.api import *
from django.shortcuts import reverse
def report_image_path(instance, filename): def report_image_path(instance, filename):
@ -59,6 +60,10 @@ class User(AbstractUser):
def display_name(self): def display_name(self):
return self.mastodon_account['display_name'] if self.mastodon_account and 'display_name' in self.mastodon_account and self.mastodon_account['display_name'] else self.mastodon_username return self.mastodon_account['display_name'] if self.mastodon_account and 'display_name' in self.mastodon_account and self.mastodon_account['display_name'] else self.mastodon_username
@property
def url(self):
return reverse("users:home", args=[self.mastodon_username])
def __str__(self): def __str__(self):
return self.mastodon_username return self.mastodon_username
@ -166,6 +171,7 @@ class Preference(models.Model):
classic_homepage = models.BooleanField(null=False, default=False) classic_homepage = models.BooleanField(null=False, default=False)
mastodon_publish_public = models.BooleanField(null=False, default=False) mastodon_publish_public = models.BooleanField(null=False, default=False)
mastodon_append_tag = models.CharField(max_length=2048, default='') mastodon_append_tag = models.CharField(max_length=2048, default='')
show_last_edit = models.PositiveSmallIntegerField(default=0)
def get_serialized_home_layout(self): def get_serialized_home_layout(self):
return str(self.home_layout).replace("\'", "\"") return str(self.home_layout).replace("\'", "\"")

View file

@ -16,6 +16,7 @@
{% else %} {% else %}
<title>{{ site_name }} - {{user.display_name}}</title> <title>{{ site_name }} - {{user.display_name}}</title>
{% endif %} {% endif %}
<link rel="alternate" type="application/rss+xml" title="{{ site_name }} - {{ user.mastodon_username }}的评论" href="{{ request.build_absolute_uri }}feed/reviews/">
{% include "partial/_common_libs.html" with jquery=1 %} {% include "partial/_common_libs.html" with jquery=1 %}

View file

@ -6,6 +6,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="refresh" content="0;URL={{ login_url }}" /> <meta http-equiv="refresh" content="0;URL={{ login_url }}" />
<title>{{ site_name }} - {{ username }}@{{ site }}</title> <title>{{ site_name }} - {{ username }}@{{ site }}</title>
<link rel="alternate" type="application/rss+xml" title="{{ site_name }} - {{ username }}@{{ site }}的评论" href="{{ request.build_absolute_uri }}feed/reviews/">
<meta property="og:title" content="{{ site_name }} - {{ username }}@{{ site }}的书影音"> <meta property="og:title" content="{{ site_name }} - {{ username }}@{{ site }}的书影音">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:url" content="{{ request.build_absolute_uri }}"> <meta property="og:url" content="{{ request.build_absolute_uri }}">

View file

@ -44,9 +44,15 @@
<br> <br>
<span>{% trans '登录后显示个人主页:' %}</span> <span>{% trans '登录后显示个人主页:' %}</span>
<div class="import-panel__checkbox import-panel__checkbox--last"> <div class="import-panel__checkbox import-panel__checkbox--last">
<input type="checkbox" name="classic_homepage" id="classic_homepage" {%if request.user.preference.classic_homepage %}checked{% endif %}> <input type="checkbox" name="classic_homepage" id="classic_homepage" {%if request.user.preference.classic_homepage %}checked{% endif %} style="margin-bottom:1.5em">
<label for="classic_homepage">{% trans '默认登录后显示好友动态,如果希望登录后显示原版风格个人主页可选中此处' %}</label> <label for="classic_homepage">{% trans '默认登录后显示好友动态,如果希望登录后显示原版风格个人主页可选中此处' %}</label>
</div> </div>
<br>
<span>{% trans '显示最近编辑者:' %}</span>
<div class="import-panel__checkbox import-panel__checkbox--last">
<input type="checkbox" name="show_last_edit" id="show_last_edit" {%if request.user.preference.show_last_edit %}checked{% endif %}>
<label for="show_last_edit">{% trans '默认不显示最近编辑条目的用户,除非该用户选中此选项。' %}</label>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,5 +1,6 @@
from django.urls import path from django.urls import path
from .views import * from .views import *
from .feeds import ReviewFeed
app_name = 'users' app_name = 'users'
urlpatterns = [ urlpatterns = [
@ -37,6 +38,7 @@ urlpatterns = [
path('<str:id>/movie/<str:status>/', movie_list, name='movie_list'), path('<str:id>/movie/<str:status>/', movie_list, name='movie_list'),
path('<str:id>/music/<str:status>/', music_list, name='music_list'), path('<str:id>/music/<str:status>/', music_list, name='music_list'),
path('<str:id>/game/<str:status>/', game_list, name='game_list'), path('<str:id>/game/<str:status>/', game_list, name='game_list'),
path('<str:id>/feed/reviews/', ReviewFeed(), name='review_feed'),
path('report/', report, name='report'), path('report/', report, name='report'),
path('manage_report/', manage_report, name='manage_report'), path('manage_report/', manage_report, name='manage_report'),
] ]