reformat with black

This commit is contained in:
Your Name 2023-01-11 19:11:31 -05:00 committed by Henri Dickson
parent 43f0b36bbc
commit 160b8b1c46
39 changed files with 244 additions and 1036 deletions

View file

@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'boofilsic.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "boofilsic.settings")
application = get_asgi_application()

View file

@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'boofilsic.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "boofilsic.settings")
application = get_wsgi_application()

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class CommonConfig(AppConfig):
name = 'common'
name = "common"

View file

@ -11,31 +11,35 @@ class KeyValueInput(forms.Widget):
"""
Input widget for Json field
"""
template_name = 'widgets/hstore.html'
template_name = "widgets/hstore.html"
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
data = None
if context['widget']['value'] is not None:
data = json.loads(context['widget']['value'])
context['widget']['value'] = [{p[0]: p[1]} for p in data.items()] if data else []
if context["widget"]["value"] is not None:
data = json.loads(context["widget"]["value"])
context["widget"]["value"] = (
[{p[0]: p[1]} for p in data.items()] if data else []
)
return context
class Media:
js = ('js/key_value_input.js',)
js = ("js/key_value_input.js",)
class HstoreInput(forms.Widget):
"""
Input widget for Hstore field
"""
template_name = 'widgets/hstore.html'
template_name = "widgets/hstore.html"
def format_value(self, value):
"""
Return a value as it should appear when rendered in a template.
"""
if value == '' or value is None:
if value == "" or value is None:
return None
if self.is_localized:
return formats.localize_input(value)
@ -43,11 +47,12 @@ class HstoreInput(forms.Widget):
return value
class Media:
js = ('js/key_value_input.js',)
js = ("js/key_value_input.js",)
class JSONField(forms.fields.JSONField):
widget = KeyValueInput
def to_python(self, value):
if not value:
return None
@ -55,7 +60,7 @@ class JSONField(forms.fields.JSONField):
if isinstance(value, dict):
j = value
else:
pairs = json.loads('[' + value + ']')
pairs = json.loads("[" + value + "]")
if isinstance(pairs, dict):
j = pairs
else:
@ -74,7 +79,7 @@ class RadioBooleanField(forms.ChoiceField):
# will submit for False. Also check for '0', since this is what
# RadioSelect will provide. Because bool("True") == bool('1') == True,
# we don't need to handle that explicitly.
if isinstance(value, str) and value.lower() in ('false', '0'):
if isinstance(value, str) and value.lower() in ("false", "0"):
value = False
else:
value = bool(value)
@ -82,22 +87,24 @@ class RadioBooleanField(forms.ChoiceField):
class RatingValidator:
""" empty value is not validated """
"""empty value is not validated"""
def __call__(self, value):
if not isinstance(value, int):
raise ValidationError(
_('%(value)s is not an integer'),
params={'value': value},
_("%(value)s is not an integer"),
params={"value": value},
)
if not str(value) in [str(i) for i in range(0, 11)]:
raise ValidationError(
_('%(value)s is not an integer in range 1-10'),
params={'value': value},
_("%(value)s is not an integer in range 1-10"),
params={"value": value},
)
class PreviewImageInput(forms.FileInput):
template_name = 'widgets/image.html'
template_name = "widgets/image.html"
def format_value(self, value):
"""
Return the file object if it has a defined url attribute.
@ -112,63 +119,70 @@ class PreviewImageInput(forms.FileInput):
"""
Return whether value is considered to be initial value.
"""
return bool(value and getattr(value, 'url', False))
return bool(value and getattr(value, "url", False))
class TagInput(forms.TextInput):
"""
Dump tag queryset into tag list
"""
template_name = 'widgets/tag.html'
template_name = "widgets/tag.html"
def format_value(self, value):
if value == '' or value is None or len(value) == 0:
return ''
if value == "" or value is None or len(value) == 0:
return ""
tag_list = []
try:
tag_list = [t['content'] for t in value]
tag_list = [t["content"] for t in value]
except TypeError:
tag_list = [t.content for t in value]
# return ','.join(tag_list)
return tag_list
class Media:
css = {
'all': ('lib/css/tag-input.css',)
}
js = ('lib/js/tag-input.js',)
css = {"all": ("lib/css/tag-input.css",)}
js = ("lib/js/tag-input.js",)
class TagField(forms.CharField):
"""
Split comma connected string into tag list
"""
widget = TagInput
def to_python(self, value):
value = super().to_python(value)
if not value:
return
return [t.strip() for t in value.split(',')]
return [t.strip() for t in value.split(",")]
class MultiSelect(forms.SelectMultiple):
template_name = 'widgets/multi_select.html'
template_name = "widgets/multi_select.html"
class Media:
css = {
'all': ('https://cdn.jsdelivr.net/npm/multiple-select@1.5.2/dist/multiple-select.min.css',)
"all": (
"https://cdn.jsdelivr.net/npm/multiple-select@1.5.2/dist/multiple-select.min.css",
)
}
js = ('https://cdn.jsdelivr.net/npm/multiple-select@1.5.2/dist/multiple-select.min.js',)
js = (
"https://cdn.jsdelivr.net/npm/multiple-select@1.5.2/dist/multiple-select.min.js",
)
class HstoreField(forms.CharField):
widget = HstoreInput
def to_python(self, value):
if not value:
return None
# already in python types
if isinstance(value, list):
return value
pairs = json.loads('[' + value + ']')
pairs = json.loads("[" + value + "]")
return pairs
@ -176,12 +190,13 @@ class DurationInput(forms.TextInput):
"""
HH:mm:ss input widget
"""
input_type = "time"
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
# context['widget']['type'] = self.input_type
context['widget']['attrs']['step'] = "1"
context["widget"]["attrs"]["step"] = "1"
return context
def format_value(self, value):
@ -206,10 +221,11 @@ class DurationInput(forms.TextInput):
class DurationField(forms.TimeField):
widget = DurationInput
def to_python(self, value):
# empty value
if value is None or value == '':
if value is None or value == "":
return
# if value is integer in ms
@ -217,7 +233,7 @@ class DurationField(forms.TimeField):
return value
# if value is string in time format
h, m, s = value.split(':')
h, m, s = value.split(":")
return (int(h) * 3600 + int(m) * 60 + int(s)) * 1000
@ -234,30 +250,31 @@ VISIBILITY_CHOICES = [
class MarkForm(forms.ModelForm):
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
share_to_mastodon = forms.BooleanField(
label=_("分享到联邦网络"), initial=True, required=False)
label=_("分享到联邦网络"), initial=True, required=False
)
rating = forms.IntegerField(
label=_("评分"), validators=[RatingValidator()], widget=forms.HiddenInput(), required=False)
label=_("评分"),
validators=[RatingValidator()],
widget=forms.HiddenInput(),
required=False,
)
visibility = forms.TypedChoiceField(
label=_("可见性"),
initial=0,
coerce=int,
choices=VISIBILITY_CHOICES,
widget=forms.RadioSelect
widget=forms.RadioSelect,
)
tags = TagField(
required=False,
widget=TagInput(attrs={'placeholder': _("回车增加标签")}),
label=_("标签")
widget=TagInput(attrs={"placeholder": _("回车增加标签")}),
label=_("标签"),
)
text = forms.CharField(
required=False,
widget=forms.Textarea(
attrs={
"placeholder": _("最多只能写360字哦~"),
"maxlength": 360
}
attrs={"placeholder": _("最多只能写360字哦~"), "maxlength": 360}
),
label=_("短评"),
)
@ -266,12 +283,13 @@ class ReviewForm(forms.ModelForm):
title = forms.CharField(label=_("标题"))
content = MarkdownxFormField(label=_("正文 (Markdown)"))
share_to_mastodon = forms.BooleanField(
label=_("分享到联邦网络"), initial=True, required=False)
label=_("分享到联邦网络"), initial=True, required=False
)
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
visibility = forms.TypedChoiceField(
label=_("可见性"),
initial=0,
coerce=int,
choices=VISIBILITY_CHOICES,
widget=forms.RadioSelect
widget=forms.RadioSelect,
)

View file

@ -1,12 +0,0 @@
from django.conf import settings
if settings.SEARCH_BACKEND == 'MEILISEARCH':
from .search.meilisearch import Indexer
elif settings.SEARCH_BACKEND == 'TYPESENSE':
from .search.typesense import Indexer
else:
class Indexer:
@classmethod
def update_model_indexable(self, cls):
pass

View file

@ -6,14 +6,14 @@ from rq import Queue
class Command(BaseCommand):
help = 'Delete a job'
help = "Delete a job"
def add_arguments(self, parser):
parser.add_argument('job_id', type=str, help='Job ID')
parser.add_argument("job_id", type=str, help="Job ID")
def handle(self, *args, **options):
redis = Redis()
job_id = str(options['job_id'])
job_id = str(options["job_id"])
job = Job.fetch(job_id, connection=redis)
job.delete()
self.stdout.write(self.style.SUCCESS(f'Deleted {job}'))
self.stdout.write(self.style.SUCCESS(f"Deleted {job}"))

View file

@ -6,19 +6,25 @@ from rq import Queue
class Command(BaseCommand):
help = 'Show jobs in queue'
help = "Show jobs in queue"
def add_arguments(self, parser):
parser.add_argument('queue', type=str, help='Queue')
parser.add_argument("queue", type=str, help="Queue")
def handle(self, *args, **options):
redis = Redis()
queue = Queue(str(options['queue']), connection=redis)
for registry in [queue.started_job_registry, queue.deferred_job_registry, queue.finished_job_registry, queue.failed_job_registry, queue.scheduled_job_registry]:
self.stdout.write(self.style.SUCCESS(f'Registry {registry}'))
queue = Queue(str(options["queue"]), connection=redis)
for registry in [
queue.started_job_registry,
queue.deferred_job_registry,
queue.finished_job_registry,
queue.failed_job_registry,
queue.scheduled_job_registry,
]:
self.stdout.write(self.style.SUCCESS(f"Registry {registry}"))
for job_id in registry.get_job_ids():
try:
job = Job.fetch(job_id, connection=redis)
pprint.pp(job)
except Exception as e:
print(f'Error fetching {job_id}')
print(f"Error fetching {job_id}")

View file

@ -1,28 +0,0 @@
from django.core.management.base import BaseCommand
from redis import Redis
from rq.job import Job
from sync.models import SyncTask
from sync.jobs import import_doufen_task
from django.utils import timezone
import django_rq
class Command(BaseCommand):
help = 'Restart a sync task'
def add_arguments(self, parser):
parser.add_argument('synctask_id', type=int, help='Sync Task ID')
def handle(self, *args, **options):
task = SyncTask.objects.get(id=options['synctask_id'])
task.finished_items = 0
task.failed_urls = []
task.success_items = 0
task.total_items = 0
task.is_finished = False
task.is_failed = False
task.break_point = ''
task.started_time = timezone.now()
task.save()
django_rq.get_queue('doufen').enqueue(import_doufen_task, task, job_id=f'SyncTask_{task.id}')
self.stdout.write(self.style.SUCCESS(f'Queued {task}'))

View file

@ -1,25 +0,0 @@
from django.core.management.base import BaseCommand
from common.scraper import get_scraper_by_url, get_normalized_url
import pprint
class Command(BaseCommand):
help = 'Scrape an item from URL (but not save it)'
def add_arguments(self, parser):
parser.add_argument('url', type=str, help='URL to scrape')
def handle(self, *args, **options):
url = str(options['url'])
url = get_normalized_url(url)
scraper = get_scraper_by_url(url)
if scraper is None:
self.stdout.write(self.style.ERROR(f'Unable to match a scraper for {url}'))
return
effective_url = scraper.get_effective_url(url)
self.stdout.write(f'Fetching {effective_url} via {scraper.__name__}')
data, img = scraper.scrape(effective_url)
self.stdout.write(self.style.SUCCESS(f'Done.'))
pprint.pp(data)

View file

@ -1,367 +0,0 @@
import re
from decimal import *
from markdown import markdown
from django.utils.translation import gettext_lazy as _
from django.db import models, IntegrityError
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import Q, Count, Sum
from markdownx.models import MarkdownxField
from users.models import User
from django.utils import timezone
from django.conf import settings
RE_HTML_TAG = re.compile(r"<[^>]*>")
MAX_TOP_TAGS = 5
# abstract base classes
###################################
class SourceSiteEnum(models.TextChoices):
IN_SITE = "in-site", settings.CLIENT_NAME
DOUBAN = "douban", _("豆瓣")
SPOTIFY = "spotify", _("Spotify")
IMDB = "imdb", _("IMDb")
STEAM = "steam", _("STEAM")
BANGUMI = 'bangumi', _("bangumi")
GOODREADS = "goodreads", _("goodreads")
TMDB = "tmdb", _("The Movie Database")
GOOGLEBOOKS = "googlebooks", _("Google Books")
BANDCAMP = "bandcamp", _("BandCamp")
IGDB = "igdb", _("IGDB")
class Entity(models.Model):
rating_total_score = models.PositiveIntegerField(null=True, blank=True)
rating_number = models.PositiveIntegerField(null=True, blank=True)
rating = models.DecimalField(
null=True, blank=True, max_digits=3, decimal_places=1)
created_time = models.DateTimeField(auto_now_add=True)
edited_time = models.DateTimeField(auto_now=True)
last_editor = models.ForeignKey(
User, on_delete=models.SET_NULL, related_name='%(class)s_last_editor', null=True, blank=False)
brief = models.TextField(_("简介"), blank=True, default="")
other_info = models.JSONField(_("其他信息"),
blank=True, null=True, encoder=DjangoJSONEncoder, default=dict)
# source_url should include shceme, which is normally https://
source_url = models.URLField(_("URL"), max_length=500, unique=True)
source_site = models.CharField(_("源网站"), choices=SourceSiteEnum.choices, max_length=50)
class Meta:
abstract = True
constraints = [
models.CheckConstraint(check=models.Q(
rating__gte=0), name='%(class)s_rating_lowerbound'),
models.CheckConstraint(check=models.Q(
rating__lte=10), name='%(class)s_rating_upperbound'),
]
def get_absolute_url(self):
raise NotImplementedError("Subclass should implement this method")
@property
def url(self):
return self.get_absolute_url()
@property
def absolute_url(self):
"""URL with host and protocol"""
return settings.APP_WEBSITE + self.url
def get_json(self):
return {
'title': self.title,
'brief': self.brief,
'rating': self.rating,
'url': self.url,
'cover_url': self.cover.url,
'top_tags': self.tags[:5],
'category_name': self.verbose_category_name,
'other_info': self.other_info,
}
def save(self, *args, **kwargs):
""" update rating and strip source url scheme & querystring before save to db """
if self.rating_number and self.rating_total_score:
self.rating = Decimal(
str(round(self.rating_total_score / self.rating_number, 1)))
elif self.rating_number is None and self.rating_total_score is None:
self.rating = None
else:
raise IntegrityError()
super().save(*args, **kwargs)
def calculate_rating(self, old_rating, new_rating):
if (not (self.rating and self.rating_total_score and self.rating_number)
and (self.rating or self.rating_total_score or self.rating_number))\
or (not (self.rating or self.rating_number or self.rating_total_score) and old_rating is not None):
raise IntegrityError("Rating integiry error.")
if old_rating:
if new_rating:
# old -> new
self.rating_total_score += (new_rating - old_rating)
else:
# old -> none
if self.rating_number >= 2:
self.rating_total_score -= old_rating
self.rating_number -= 1
else:
# only one rating record
self.rating_number = None
self.rating_total_score = None
pass
else:
if new_rating:
# none -> new
if self.rating_number and self.rating_number >= 1:
self.rating_total_score += new_rating
self.rating_number += 1
else:
# no rating record before
self.rating_number = 1
self.rating_total_score = new_rating
else:
# none -> none
pass
def update_rating(self, old_rating, new_rating):
"""
@param old_rating: the old mark rating
@param new_rating: the new mark rating
"""
self.calculate_rating(old_rating, new_rating)
self.save()
def refresh_rating(self): # TODO: replace update_rating()
a = self.marks.filter(rating__gt=0).aggregate(Sum('rating'), Count('rating'))
if self.rating_total_score != a['rating__sum'] or self.rating_number != a['rating__count']:
self.rating_total_score = a['rating__sum']
self.rating_number = a['rating__count']
self.rating = a['rating__sum'] / a['rating__count'] if a['rating__count'] > 0 else None
self.save()
return self.rating
def get_tags_manager(self):
"""
Since relation between tag and entity is foreign key, and related name has to be unique,
this method works like interface.
"""
raise NotImplementedError("Subclass should implement this method.")
@property
def top_tags(self):
return self.get_tags_manager().values('content').annotate(tag_frequency=Count('content')).order_by('-tag_frequency')[:MAX_TOP_TAGS]
def get_marks_manager(self):
"""
Normally this won't be used.
There is no ocassion where visitor can simply view all the marks.
"""
raise NotImplementedError("Subclass should implement this method.")
def get_reviews_manager(self):
"""
Normally this won't be used.
There is no ocassion where visitor can simply view all the reviews.
"""
raise NotImplementedError("Subclass should implement this method.")
@property
def all_tag_list(self):
return self.get_tags_manager().values('content').annotate(frequency=Count('content')).order_by('-frequency')
@property
def tags(self):
return list(map(lambda t: t['content'], self.all_tag_list))
@property
def marks(self):
params = {self.__class__.__name__.lower() + '_id': self.id}
return self.mark_class.objects.filter(**params)
@classmethod
def get_category_mapping_dict(cls):
category_mapping_dict = {}
for subclass in cls.__subclasses__():
category_mapping_dict[subclass.__name__.lower()] = subclass
return category_mapping_dict
@property
def category_name(self):
return self.__class__.__name__
@property
def verbose_category_name(self):
raise NotImplementedError("Subclass should implement this.")
@property
def mark_class(self):
raise NotImplementedError("Subclass should implement this.")
@property
def tag_class(self):
raise NotImplementedError("Subclass should implement this.")
class UserOwnedEntity(models.Model):
is_private = models.BooleanField(default=False, null=True) # first set allow null, then migration, finally (in a few days) remove for good
visibility = models.PositiveSmallIntegerField(default=0) # 0: Public / 1: Follower only / 2: Self only
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_%(class)ss')
created_time = models.DateTimeField(default=timezone.now)
edited_time = models.DateTimeField(default=timezone.now)
class Meta:
abstract = True
def is_visible_to(self, viewer):
if not viewer.is_authenticated:
return self.visibility == 0
owner = self.owner
if owner == viewer:
return True
if not owner.is_active:
return False
if self.visibility == 2:
return False
if viewer.is_blocking(owner) or owner.is_blocking(viewer) or viewer.is_muting(owner):
return False
if self.visibility == 1:
return viewer.is_following(owner)
else:
return True
def is_editable_by(self, viewer):
return True if viewer.is_staff or viewer.is_superuser or viewer == self.owner else False
@classmethod
def get_available(cls, entity, request_user, following_only=False):
# e.g. SongMark.get_available(song, request.user)
query_kwargs = {entity.__class__.__name__.lower(): entity}
all_entities = cls.objects.filter(**query_kwargs).order_by("-created_time") # get all marks for song
visible_entities = list(filter(lambda _entity: _entity.is_visible_to(request_user) and (_entity.owner.mastodon_username in request_user.mastodon_following if following_only else True), all_entities))
return visible_entities
@classmethod
def get_available_for_identicals(cls, entity, request_user, following_only=False):
# e.g. SongMark.get_available(song, request.user)
query_kwargs = {entity.__class__.__name__.lower() + '__in': entity.get_identicals()}
all_entities = cls.objects.filter(**query_kwargs).order_by("-created_time") # get all marks for song
visible_entities = list(filter(lambda _entity: _entity.is_visible_to(request_user) and (_entity.owner.mastodon_username in request_user.mastodon_following if following_only else True), all_entities))
return visible_entities
@classmethod
def get_available_by_user(cls, owner, is_following): # FIXME
"""
Returns all avaliable owner's entities.
Mute/Block relation is not handled in this method.
:param owner: visited user
:param is_following: if the current user is following the owner
"""
user_owned_entities = cls.objects.filter(owner=owner)
if is_following:
user_owned_entities = user_owned_entities.exclude(visibility=2)
else:
user_owned_entities = user_owned_entities.filter(visibility=0)
return user_owned_entities
@property
def item(self):
attr = re.findall(r'[A-Z](?:[a-z]+|[A-Z]*(?=[A-Z]|$))', self.__class__.__name__)[0].lower()
return getattr(self, attr)
# commonly used entity classes
###################################
class MarkStatusEnum(models.TextChoices):
WISH = 'wish', _('Wish')
DO = 'do', _('Do')
COLLECT = 'collect', _('Collect')
class Mark(UserOwnedEntity):
status = models.CharField(choices=MarkStatusEnum.choices, max_length=20)
rating = models.PositiveSmallIntegerField(blank=True, null=True)
text = models.CharField(max_length=5000, blank=True, default='')
shared_link = models.CharField(max_length=5000, blank=True, default='')
def __str__(self):
return f"Mark({self.id} {self.owner} {self.status.upper()})"
@property
def translated_status(self):
raise NotImplementedError("Subclass should implement this.")
@property
def tags(self):
tags = self.item.tag_class.objects.filter(mark_id=self.id)
return tags
class Meta:
abstract = True
constraints = [
models.CheckConstraint(check=models.Q(
rating__gte=0), name='mark_rating_lowerbound'),
models.CheckConstraint(check=models.Q(
rating__lte=10), name='mark_rating_upperbound'),
]
# TODO update entity rating when save
# TODO update tags
class Review(UserOwnedEntity):
title = models.CharField(max_length=120)
content = MarkdownxField()
shared_link = models.CharField(max_length=5000, blank=True, default='')
def __str__(self):
return self.title
def get_plain_content(self):
"""
Get plain text format content
"""
html = markdown(self.content)
return RE_HTML_TAG.sub(' ', html)
class Meta:
abstract = True
@property
def translated_status(self):
return '评论了'
class Tag(models.Model):
content = models.CharField(max_length=50)
def __str__(self):
return self.content
@property
def edited_time(self):
return self.mark.edited_time
@property
def created_time(self):
return self.mark.created_time
@property
def text(self):
return self.mark.text
@classmethod
def find_by_user(cls, tag, owner, viewer):
qs = cls.objects.filter(content=tag, mark__owner=owner)
if owner != viewer:
qs = qs.filter(mark__visibility__lte=owner.get_max_visibility(viewer))
return qs
@classmethod
def all_by_user(cls, owner):
return cls.objects.filter(mark__owner=owner).values('content').annotate(total=Count('content')).order_by('-total')
class Meta:
abstract = True

View file

@ -1,183 +0,0 @@
import logging
import meilisearch
from django.conf import settings
from django.db.models.signals import post_save, post_delete
import types
INDEX_NAME = 'items'
SEARCHABLE_ATTRIBUTES = ['title', 'orig_title', 'other_title', 'subtitle', 'artist', 'author', 'translator', 'developer', 'director', 'actor', 'playwright', 'pub_house', 'company', 'publisher', 'isbn', 'imdb_code']
INDEXABLE_DIRECT_TYPES = ['BigAutoField', 'BooleanField', 'CharField', 'PositiveIntegerField', 'PositiveSmallIntegerField', 'TextField', 'ArrayField']
INDEXABLE_TIME_TYPES = ['DateTimeField']
INDEXABLE_DICT_TYPES = ['JSONField']
INDEXABLE_FLOAT_TYPES = ['DecimalField']
# NONINDEXABLE_TYPES = ['ForeignKey', 'FileField',]
SEARCH_PAGE_SIZE = 20
logger = logging.getLogger(__name__)
def item_post_save_handler(sender, instance, created, **kwargs):
if not created and settings.SEARCH_INDEX_NEW_ONLY:
return
Indexer.replace_item(instance)
def item_post_delete_handler(sender, instance, **kwargs):
Indexer.delete_item(instance)
def tag_post_save_handler(sender, instance, **kwargs):
pass
def tag_post_delete_handler(sender, instance, **kwargs):
pass
class Indexer:
class_map = {}
_instance = None
@classmethod
def instance(self):
if self._instance is None:
self._instance = meilisearch.Client(settings.MEILISEARCH_SERVER, settings.MEILISEARCH_KEY).index(INDEX_NAME)
return self._instance
@classmethod
def init(self):
meilisearch.Client(settings.MEILISEARCH_SERVER, settings.MEILISEARCH_KEY).create_index(INDEX_NAME, {'primaryKey': '_id'})
self.update_settings()
@classmethod
def update_settings(self):
self.instance().update_searchable_attributes(SEARCHABLE_ATTRIBUTES)
self.instance().update_filterable_attributes(['_class', 'tags', 'source_site'])
self.instance().update_settings({'displayedAttributes': ['_id', '_class', 'id', 'title', 'tags']})
@classmethod
def get_stats(self):
return self.instance().get_stats()
@classmethod
def busy(self):
return self.instance().get_stats()['isIndexing']
@classmethod
def update_model_indexable(self, model):
if settings.SEARCH_BACKEND is None:
return
self.class_map[model.__name__] = model
model.indexable_fields = ['tags']
model.indexable_fields_time = []
model.indexable_fields_dict = []
model.indexable_fields_float = []
for field in model._meta.get_fields():
type = field.get_internal_type()
if type in INDEXABLE_DIRECT_TYPES:
model.indexable_fields.append(field.name)
elif type in INDEXABLE_TIME_TYPES:
model.indexable_fields_time.append(field.name)
elif type in INDEXABLE_DICT_TYPES:
model.indexable_fields_dict.append(field.name)
elif type in INDEXABLE_FLOAT_TYPES:
model.indexable_fields_float.append(field.name)
post_save.connect(item_post_save_handler, sender=model)
post_delete.connect(item_post_delete_handler, sender=model)
@classmethod
def obj_to_dict(self, obj):
pk = f'{obj.__class__.__name__}-{obj.id}'
item = {
'_id': pk,
'_class': obj.__class__.__name__,
# 'id': obj.id
}
for field in obj.__class__.indexable_fields:
item[field] = getattr(obj, field)
for field in obj.__class__.indexable_fields_time:
item[field] = getattr(obj, field).timestamp()
for field in obj.__class__.indexable_fields_float:
item[field] = float(getattr(obj, field)) if getattr(obj, field) else None
for field in obj.__class__.indexable_fields_dict:
d = getattr(obj, field)
if d.__class__ is dict:
item.update(d)
item = {k: v for k, v in item.items() if v}
return item
@classmethod
def replace_item(self, obj):
try:
self.instance().add_documents([self.obj_to_dict(obj)])
except Exception as e:
logger.error(f"replace item error: \n{e}")
@classmethod
def replace_batch(self, objects):
try:
self.instance().update_documents(documents=objects)
except Exception as e:
logger.error(f"replace batch error: \n{e}")
@classmethod
def delete_item(self, obj):
pk = f'{obj.__class__.__name__}-{obj.id}'
try:
self.instance().delete_document(pk)
except Exception as e:
logger.error(f"delete item error: \n{e}")
@classmethod
def patch_item(self, obj, fields):
pk = f'{obj.__class__.__name__}-{obj.id}'
data = {}
for f in fields:
data[f] = getattr(obj, f)
try:
self.instance().update_documents(documents=[data], primary_key=[pk])
except Exception as e:
logger.error(f"patch item error: \n{e}")
@classmethod
def search(self, q, page=1, category=None, tag=None, sort=None):
if category or tag:
f = []
if category == 'music':
f.append("(_class = 'Album' OR _class = 'Song')")
elif category:
f.append(f"_class = '{category}'")
if tag:
t = tag.replace("'", "\'")
f.append(f"tags = '{t}'")
filter = ' AND '.join(f)
else:
filter = None
options = {
'offset': (page - 1) * SEARCH_PAGE_SIZE,
'limit': SEARCH_PAGE_SIZE,
'filter': filter,
'facetsDistribution': ['_class'],
'sort': None
}
try:
r = self.instance().search(q, options)
except Exception as e:
logger.error(f"MeiliSearch error: \n{e}")
r = {'nbHits': 0, 'hits': []}
# print(r)
results = types.SimpleNamespace()
results.items = list([x for x in map(lambda i: self.item_to_obj(i), r['hits']) if x is not None])
results.num_pages = (r['nbHits'] + SEARCH_PAGE_SIZE - 1) // SEARCH_PAGE_SIZE
# print(results)
return results
@classmethod
def item_to_obj(self, item):
try:
return self.class_map[item['_class']].objects.get(id=item['id'])
except Exception as e:
logger.error(f"unable to load search result item from db:\n{item}")
return None

View file

@ -1,217 +0,0 @@
import types
import logging
import typesense
from typesense.exceptions import ObjectNotFound
from django.conf import settings
from django.db.models.signals import post_save, post_delete
INDEX_NAME = 'items'
SEARCHABLE_ATTRIBUTES = ['title', 'orig_title', 'other_title', 'subtitle', 'artist', 'author', 'translator',
'developer', 'director', 'actor', 'playwright', 'pub_house', 'company', 'publisher', 'isbn', 'imdb_code']
FILTERABLE_ATTRIBUTES = ['_class', 'tags', 'source_site']
INDEXABLE_DIRECT_TYPES = ['BigAutoField', 'BooleanField', 'CharField',
'PositiveIntegerField', 'PositiveSmallIntegerField', 'TextField', 'ArrayField']
INDEXABLE_TIME_TYPES = ['DateTimeField']
INDEXABLE_DICT_TYPES = ['JSONField']
INDEXABLE_FLOAT_TYPES = ['DecimalField']
SORTING_ATTRIBUTE = None
# NONINDEXABLE_TYPES = ['ForeignKey', 'FileField',]
SEARCH_PAGE_SIZE = 20
logger = logging.getLogger(__name__)
def item_post_save_handler(sender, instance, created, **kwargs):
if not created and settings.SEARCH_INDEX_NEW_ONLY:
return
Indexer.replace_item(instance)
def item_post_delete_handler(sender, instance, **kwargs):
Indexer.delete_item(instance)
def tag_post_save_handler(sender, instance, **kwargs):
pass
def tag_post_delete_handler(sender, instance, **kwargs):
pass
class Indexer:
class_map = {}
_instance = None
@classmethod
def instance(self):
if self._instance is None:
self._instance = typesense.Client(settings.TYPESENSE_CONNECTION)
return self._instance
@classmethod
def init(self):
# self.instance().collections[INDEX_NAME].delete()
# fields = [
# {"name": "_class", "type": "string", "facet": True},
# {"name": "source_site", "type": "string", "facet": True},
# {"name": ".*", "type": "auto", "locale": "zh"},
# ]
# use dumb schema below before typesense fix a bug
fields = [
{'name': 'id', 'type': 'string'},
{'name': '_id', 'type': 'int64'},
{'name': '_class', 'type': 'string', "facet": True},
{'name': 'source_site', 'type': 'string', "facet": True},
{'name': 'isbn', 'optional': True, 'type': 'string'},
{'name': 'imdb_code', 'optional': True, 'type': 'string'},
{'name': 'author', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
{'name': 'orig_title', 'optional': True, 'locale': 'zh', 'type': 'string'},
{'name': 'pub_house', 'optional': True, 'locale': 'zh', 'type': 'string'},
{'name': 'title', 'optional': True, 'locale': 'zh', 'type': 'string'},
{'name': 'translator', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
{'name': 'subtitle', 'optional': True, 'locale': 'zh', 'type': 'string'},
{'name': 'artist', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
{'name': 'company', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
{'name': 'developer', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
{'name': 'other_title', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
{'name': 'publisher', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
{'name': 'actor', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
{'name': 'director', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
{'name': 'playwright', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
{'name': 'tags', 'optional': True, 'locale': 'zh', 'type': 'string[]'},
{'name': '.*', 'optional': True, 'locale': 'zh', 'type': 'auto'},
]
self.instance().collections.create({
"name": INDEX_NAME,
"fields": fields
})
@classmethod
def update_settings(self):
# https://github.com/typesense/typesense/issues/96
print('not supported by typesense yet')
pass
@classmethod
def get_stats(self):
return self.instance().collections[INDEX_NAME].retrieve()
@classmethod
def busy(self):
return False
@classmethod
def update_model_indexable(self, model):
if settings.SEARCH_BACKEND is None:
return
self.class_map[model.__name__] = model
model.indexable_fields = ['tags']
model.indexable_fields_time = []
model.indexable_fields_dict = []
model.indexable_fields_float = []
for field in model._meta.get_fields():
type = field.get_internal_type()
if type in INDEXABLE_DIRECT_TYPES:
model.indexable_fields.append(field.name)
elif type in INDEXABLE_TIME_TYPES:
model.indexable_fields_time.append(field.name)
elif type in INDEXABLE_DICT_TYPES:
model.indexable_fields_dict.append(field.name)
elif type in INDEXABLE_FLOAT_TYPES:
model.indexable_fields_float.append(field.name)
post_save.connect(item_post_save_handler, sender=model)
post_delete.connect(item_post_delete_handler, sender=model)
@classmethod
def obj_to_dict(self, obj):
pk = f'{obj.__class__.__name__}-{obj.id}'
item = {
'_class': obj.__class__.__name__,
}
for field in obj.__class__.indexable_fields:
item[field] = getattr(obj, field)
for field in obj.__class__.indexable_fields_time:
item[field] = getattr(obj, field).timestamp()
for field in obj.__class__.indexable_fields_float:
item[field] = float(getattr(obj, field)) if getattr(
obj, field) else None
for field in obj.__class__.indexable_fields_dict:
d = getattr(obj, field)
if d.__class__ is dict:
item.update(d)
item = {k: v for k, v in item.items() if v and (
k in SEARCHABLE_ATTRIBUTES or k in FILTERABLE_ATTRIBUTES or k == 'id')}
item['_id'] = obj.id
# typesense requires primary key to be named 'id', type string
item['id'] = pk
return item
@classmethod
def replace_item(self, obj):
try:
self.instance().collections[INDEX_NAME].documents.upsert(self.obj_to_dict(obj), {
'dirty_values': 'coerce_or_drop'
})
except Exception as e:
logger.error(f"replace item error: \n{e}")
@classmethod
def replace_batch(self, objects):
try:
self.instance().collections[INDEX_NAME].documents.import_(
objects, {'action': 'upsert'})
except Exception as e:
logger.error(f"replace batch error: \n{e}")
@classmethod
def delete_item(self, obj):
pk = f'{obj.__class__.__name__}-{obj.id}'
try:
self.instance().collections[INDEX_NAME].documents[pk].delete()
except Exception as e:
logger.error(f"delete item error: \n{e}")
@classmethod
def search(self, q, page=1, category=None, tag=None, sort=None):
f = []
if category == 'music':
f.append('_class:= [Album, Song]')
elif category:
f.append('_class:= ' + category)
else:
f.append('')
if tag:
f.append(f"tags:= '{tag}'")
filter = ' && '.join(f)
options = {
'q': q,
'page': page,
'per_page': SEARCH_PAGE_SIZE,
'query_by': ','.join(SEARCHABLE_ATTRIBUTES),
'filter_by': filter,
# 'facetsDistribution': ['_class'],
# 'sort_by': None,
}
results = types.SimpleNamespace()
try:
r = self.instance().collections[INDEX_NAME].documents.search(options)
results.items = list([x for x in map(lambda i: self.item_to_obj(i['document']), r['hits']) if x is not None])
results.num_pages = (r['found'] + SEARCH_PAGE_SIZE - 1) // SEARCH_PAGE_SIZE
except ObjectNotFound:
results.items = []
results.num_pages = 1
return results
@classmethod
def item_to_obj(self, item):
try:
return self.class_map[item['_class']].objects.get(id=item['_id'])
except Exception as e:
logger.error(f"unable to load search result item from db:\n{item}")
return None

View file

@ -9,8 +9,8 @@ register = template.Library()
@register.simple_tag
def admin_url():
url = settings.ADMIN_URL
if not url.startswith('/'):
url = '/' + url
if not url.endswith('/'):
url += '/'
if not url.startswith("/"):
url = "/" + url
if not url.endswith("/"):
url += "/"
return format_html(url)

View file

@ -4,14 +4,14 @@ from django.template.defaultfilters import stringfilter
from opencc import OpenCC
cc = OpenCC('t2s')
cc = OpenCC("t2s")
register = template.Library()
@register.filter
@stringfilter
def highlight(text, search):
for s in cc.convert(search.strip().lower()).split(' '):
for s in cc.convert(search.strip().lower()).split(" "):
if s:
p = cc.convert(text.lower()).find(s)
if p != -1:

View file

@ -8,19 +8,19 @@ register = template.Library()
@register.simple_tag
def mastodon(domain):
url = 'https://' + domain
url = "https://" + domain
return url
@register.simple_tag(takes_context=True)
def current_user_relationship(context, user):
current_user = context['request'].user
current_user = context["request"].user
if current_user and current_user.is_authenticated:
if current_user.is_following(user):
if current_user.is_followed_by(user):
return '互相关注'
return "互相关注"
else:
return '已关注'
return "已关注"
elif current_user.is_followed_by(user):
return '被ta关注'
return "被ta关注"
return None

View file

@ -4,9 +4,10 @@ from django.utils.html import format_html
register = template.Library()
class OAuthTokenNode(template.Node):
def render(self, context):
request = context.get('request')
request = context.get("request")
oauth_token = request.user.mastodon_token
return format_html(oauth_token)

View file

@ -11,12 +11,12 @@ def prettydate(d):
diff = timezone.now() - d
s = diff.seconds
if diff.days > 14 or diff.days < 0:
return d.strftime('%Y年%m月%d')
return d.strftime("%Y年%m月%d")
elif diff.days >= 1:
return '{} 天前'.format(diff.days)
return "{} 天前".format(diff.days)
elif s < 120:
return '刚刚'
return "刚刚"
elif s < 3600:
return '{} 分钟前'.format(s // 60)
return "{} 分钟前".format(s // 60)
else:
return '{} 小时前'.format(s // 3600)
return "{} 小时前".format(s // 3600)

View file

@ -7,12 +7,12 @@ register = template.Library()
@register.filter(is_safe=True)
@stringfilter
def strip_scheme(value):
""" Strip the `https://.../` part of urls"""
"""Strip the `https://.../` part of urls"""
if value.startswith("https://"):
value = value.lstrip("https://")
elif value.startswith("http://"):
value = value.lstrip("http://")
if value.endswith('/'):
if value.endswith("/"):
value = value[0:-1]
return value

View file

@ -1,12 +1,14 @@
import uuid
from django.utils import timezone
class PageLinksGenerator:
# TODO inherit django paginator
"""
Calculate the pages for multiple links pagination.
length -- the number of page links in pagination
"""
def __init__(self, length, current_page, total_pages):
current_page = int(current_page)
self.current_page = current_page
@ -23,8 +25,7 @@ class PageLinksGenerator:
# decision is based on the start page and the end page
# both sides overflow
if (start_page < 1 and end_page > total_pages)\
or length >= total_pages:
if (start_page < 1 and end_page > total_pages) or length >= total_pages:
self.start_page = 1
self.end_page = total_pages
self.has_prev = False
@ -62,16 +63,12 @@ class PageLinksGenerator:
# assert self.has_prev is not None and self.has_next is not None
def ChoicesDictGenerator(choices_enum):
choices_dict = dict(choices_enum.choices)
return choices_dict
def GenerateDateUUIDMediaFilePath(instance, filename, path_root):
ext = filename.split('.')[-1]
ext = filename.split(".")[-1]
filename = "%s.%s" % (uuid.uuid4(), ext)
root = ''
if path_root.endswith('/'):
root = ""
if path_root.endswith("/"):
root = path_root
else:
root = path_root + '/'
return root + timezone.now().strftime('%Y/%m/%d') + f'{filename}'
root = path_root + "/"
return root + timezone.now().strftime("%Y/%m/%d") + f"{filename}"

View file

@ -1,10 +1,6 @@
import logging
from django.shortcuts import render, redirect
from django.shortcuts import redirect
from django.urls import reverse
from django.contrib.auth.decorators import login_required
from django.utils.translation import gettext_lazy as _
_logger = logging.getLogger(__name__)
@login_required

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class ManagementConfig(AppConfig):
name = 'management'
name = "management"

View file

@ -14,25 +14,27 @@ class Announcement(models.Model):
title = models.CharField(max_length=200)
content = MarkdownxField()
slug = models.SlugField(max_length=300, allow_unicode=True, unique=True, null=True, blank=True)
slug = models.SlugField(
max_length=300, allow_unicode=True, unique=True, null=True, blank=True
)
created_time = models.DateTimeField(auto_now_add=True)
edited_time = models.DateTimeField(auto_now_add=True)
class Meta:
"""Meta definition for Announcement."""
verbose_name = 'Announcement'
verbose_name_plural = 'Announcements'
verbose_name = "Announcement"
verbose_name_plural = "Announcements"
def get_absolute_url(self):
return reverse('management:retrieve', kwargs={'pk': self.pk})
return reverse("management:retrieve", kwargs={"pk": self.pk})
def get_plain_content(self):
"""
Get plain text format content
"""
html = markdown(self.content)
return RE_HTML_TAG.sub(' ', html)
return RE_HTML_TAG.sub(" ", html)
def __str__(self):
"""Unicode representation of Announcement."""

View file

@ -2,12 +2,12 @@ from django.urls import path
from .views import *
app_name = 'management'
app_name = "management"
urlpatterns = [
path('', AnnouncementListView.as_view(), name='list'),
path('<int:pk>/', AnnouncementDetailView.as_view(), name='retrieve'),
path('create/', AnnouncementCreateView.as_view(), name='create'),
path('<str:slug>/', AnnouncementDetailView.as_view(), name='retrieve_slug'),
path('<int:pk>/update/', AnnouncementUpdateView.as_view(), name='update'),
path('<int:pk>/delete/', AnnouncementDeleteView.as_view(), name='delete'),
path("", AnnouncementListView.as_view(), name="list"),
path("<int:pk>/", AnnouncementDetailView.as_view(), name="retrieve"),
path("create/", AnnouncementCreateView.as_view(), name="create"),
path("<str:slug>/", AnnouncementDetailView.as_view(), name="retrieve_slug"),
path("<int:pk>/update/", AnnouncementUpdateView.as_view(), name="update"),
path("<int:pk>/delete/", AnnouncementDeleteView.as_view(), name="delete"),
]

View file

@ -16,7 +16,7 @@ decorators = [login_required, user_passes_test(lambda u: u.is_superuser)]
class AnnouncementDetailView(DetailView, ModelFormMixin):
model = Announcement
fields = ['content']
fields = ["content"]
template_name = "management/detail.html"
@ -26,31 +26,29 @@ class AnnouncementListView(ListView):
template_name = "management/list.html"
def get_queryset(self):
return Announcement.objects.all().order_by('-pk')
return Announcement.objects.all().order_by("-pk")
@method_decorator(decorators, name='dispatch')
@method_decorator(decorators, name="dispatch")
class AnnouncementDeleteView(DeleteView):
model = Announcement
success_url = reverse_lazy("management:list")
template_name = "management/delete.html"
@method_decorator(decorators, name='dispatch')
@method_decorator(decorators, name="dispatch")
class AnnouncementCreateView(CreateView):
model = Announcement
fields = '__all__'
fields = "__all__"
template_name = "management/create_update.html"
@method_decorator(decorators, name='dispatch')
@method_decorator(decorators, name="dispatch")
class AnnouncementUpdateView(UpdateView):
model = Announcement
fields = '__all__'
fields = "__all__"
template_name = "management/create_update.html"
def form_valid(self, form):
form.instance.edited_time = timezone.now()
return super().form_valid(form)

View file

@ -8,50 +8,63 @@ from django.core.exceptions import ObjectDoesNotExist
# Register your models here.
@admin.register(MastodonApplication)
class MastodonApplicationModelAdmin(admin.ModelAdmin):
def add_view(self, request, form_url='', extra_context=None):
def add_view(self, request, form_url="", extra_context=None):
"""
Dirty code here, use POST['domain_name'] to pass error message to user.
"""
if request.method == 'POST':
if not request.POST.get('client_id') and not request.POST.get('client_secret'):
if request.method == "POST":
if not request.POST.get("client_id") and not request.POST.get(
"client_secret"
):
# make the post data mutable
request.POST = request.POST.copy()
# (is_proxy xor proxy_to) or (proxy_to!=null and is_proxy=false)
if (bool(request.POST.get('is_proxy')) or bool(request.POST.get('proxy_to'))) and\
not (bool(request.POST.get('is_proxy')) and bool(request.POST.get('proxy_to'))) or\
(not bool(request.POST.get('is_proxy')) and bool(request.POST.get('proxy_to'))):
request.POST['domain_name'] = _("请同时填写is_proxy和proxy_to。")
if (
(
bool(request.POST.get("is_proxy"))
or bool(request.POST.get("proxy_to"))
)
and not (
bool(request.POST.get("is_proxy"))
and bool(request.POST.get("proxy_to"))
)
or (
not bool(request.POST.get("is_proxy"))
and bool(request.POST.get("proxy_to"))
)
):
request.POST["domain_name"] = _("请同时填写is_proxy和proxy_to。")
else:
if request.POST.get("is_proxy"):
try:
origin = MastodonApplication.objects.get(domain_name=request.POST['proxy_to'])
origin = MastodonApplication.objects.get(
domain_name=request.POST["proxy_to"]
)
# set proxy credentials to those of its original site
request.POST['app_id'] = origin.app_id
request.POST['client_id'] = origin.client_id
request.POST['client_secret'] = origin.client_secret
request.POST['vapid_key'] = origin.vapid_key
request.POST["app_id"] = origin.app_id
request.POST["client_id"] = origin.client_id
request.POST["client_secret"] = origin.client_secret
request.POST["vapid_key"] = origin.vapid_key
except ObjectDoesNotExist:
request.POST['domain_name'] = _("proxy_to所指域名不存在请先添加原站点。")
request.POST["domain_name"] = _("proxy_to所指域名不存在请先添加原站点。")
else:
# create mastodon app
try:
response = create_app(request.POST.get('domain_name'))
response = create_app(request.POST.get("domain_name"))
except (Timeout, ConnectionError):
request.POST['domain_name'] = _("联邦网络请求超时。")
request.POST["domain_name"] = _("联邦网络请求超时。")
except Exception as e:
request.POST['domain_name'] = str(e)
request.POST["domain_name"] = str(e)
else:
# fill the form with returned data
data = response.json()
if response.status_code != 200:
request.POST['domain_name'] = str(data)
request.POST["domain_name"] = str(data)
else:
request.POST['app_id'] = data['id']
request.POST['client_id'] = data['client_id']
request.POST['client_secret'] = data['client_secret']
request.POST['vapid_key'] = data['vapid_key']
request.POST["app_id"] = data["id"]
request.POST["client_id"] = data["client_id"]
request.POST["client_secret"] = data["client_secret"]
request.POST["vapid_key"] = data["vapid_key"]
return super().add_view(request, form_url=form_url, extra_context=extra_context)

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class MastodonConfig(AppConfig):
name = 'mastodon'
name = "mastodon"

View file

@ -3,27 +3,30 @@ from .api import verify_account
class OAuth2Backend(ModelBackend):
""" Used to glue OAuth2 and Django User model """
"""Used to glue OAuth2 and Django User model"""
# "authenticate() should check the credentials it gets and returns
# a user object that matches those credentials."
# arg request is an interface specification, not used in this implementation
def authenticate(self, request, token=None, username=None, site=None, **kwargs):
""" when username is provided, assume that token is newly obtained and valid """
"""when username is provided, assume that token is newly obtained and valid"""
if token is None or site is None:
return
if username is None:
code, user_data = verify_account(site, token)
if code == 200:
userid = user_data['id']
userid = user_data["id"]
else:
# aquiring user data fail means token is invalid thus auth fail
return None
# when username is provided, assume that token is newly obtained and valid
try:
user = UserModel._default_manager.get(mastodon_id=userid, mastodon_site=site)
user = UserModel._default_manager.get(
mastodon_id=userid, mastodon_site=site
)
except UserModel.DoesNotExist:
return None
else:

View file

@ -6,19 +6,17 @@ from requests.exceptions import Timeout
def mastodon_request_included(func):
""" Handles timeout exception of requests to mastodon, returns http 500 """
"""Handles timeout exception of requests to mastodon, returns http 500"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except (Timeout, ConnectionError):
return render(
args[0],
'common/error.html',
{
'msg': _("联邦网络请求超时叻_(´ཀ`」 ∠)__ ")
}
args[0], "common/error.html", {"msg": _("联邦网络请求超时叻_(´ཀ`」 ∠)__ ")}
)
return wrapper

View file

@ -6,16 +6,18 @@ from users.models import User
class Command(BaseCommand):
help = 'Find wrong sites'
help = "Find wrong sites"
def handle(self, *args, **options):
for site in MastodonApplication.objects.all():
d = site.domain_name
login_domain = d.strip().lower().split('//')[-1].split('/')[0].split('@')[-1]
login_domain = (
d.strip().lower().split("//")[-1].split("/")[0].split("@")[-1]
)
domain, version = get_instance_info(login_domain)
if d != domain:
print(f'{d} should be {domain}')
print(f"{d} should be {domain}")
for u in User.objects.filter(mastodon_site=d, is_active=True):
u.mastodon_site = domain
print(f'fixing {u}')
print(f"fixing {u}")
u.save()

View file

@ -1,17 +1,21 @@
from django.conf import settings
def rating_to_emoji(score, star_mode = 0):
""" convert score to mastodon star emoji code """
if score is None or score == '' or score == 0:
return ''
def rating_to_emoji(score, star_mode=0):
"""convert score to mastodon star emoji code"""
if score is None or score == "" or score == 0:
return ""
solid_stars = score // 2
half_star = int(bool(score % 2))
empty_stars = 5 - solid_stars if not half_star else 5 - solid_stars - 1
if star_mode == 1:
emoji_code = "🌕" * solid_stars + "🌗" * half_star + "🌑" * empty_stars
else:
emoji_code = settings.STAR_SOLID * solid_stars + settings.STAR_HALF * half_star + settings.STAR_EMPTY * empty_stars
emoji_code = (
settings.STAR_SOLID * solid_stars
+ settings.STAR_HALF * half_star
+ settings.STAR_EMPTY * empty_stars
)
emoji_code = emoji_code.replace("::", ": :")
emoji_code = ' ' + emoji_code + ' '
emoji_code = " " + emoji_code + " "
return emoji_code

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'
name = "users"

View file

@ -3,21 +3,18 @@ from .models import Report
from django.utils.translation import gettext_lazy as _
from common.forms import PreviewImageInput
class ReportForm(forms.ModelForm):
class Meta:
model = Report
fields = [
'reported_user',
'image',
'message',
"reported_user",
"image",
"message",
]
widgets = {
'message': forms.Textarea(attrs={'placeholder': _("详情")}),
'image': PreviewImageInput()
"message": forms.Textarea(attrs={"placeholder": _("详情")}),
"image": PreviewImageInput()
# 'reported_user': forms.TextInput(),
}
labels = {
'reported_user': _("举报的用户"),
'image': _("相关证据"),
'message': _("详情")
}
labels = {"reported_user": _("举报的用户"), "image": _("相关证据"), "message": _("详情")}

View file

@ -4,16 +4,16 @@ from django.contrib.sessions.models import Session
class Command(BaseCommand):
help = 'Backfill Mastodon data if missing'
help = "Backfill Mastodon data if missing"
def handle(self, *args, **options):
for session in Session.objects.order_by('-expire_date'):
uid = session.get_decoded().get('_auth_user_id')
token = session.get_decoded().get('oauth_token')
for session in Session.objects.order_by("-expire_date"):
uid = session.get_decoded().get("_auth_user_id")
token = session.get_decoded().get("oauth_token")
if uid and token:
user = User.objects.get(pk=uid)
if user.mastodon_token:
print(f'skip {user}')
print(f"skip {user}")
continue
user.mastodon_token = token
user.refresh_mastodon_data()

View file

@ -5,15 +5,15 @@ from django.utils import timezone
class Command(BaseCommand):
help = 'disable user'
help = "disable user"
def add_arguments(self, parser):
parser.add_argument('id', type=int, help='user id')
parser.add_argument("id", type=int, help="user id")
def handle(self, *args, **options):
h = int(options['id'])
h = int(options["id"])
u = User.objects.get(id=h)
u.username = '(duplicated)'+u.username
u.username = "(duplicated)" + u.username
u.is_active = False
u.save()
print(f'{u} updated')
print(f"{u} updated")

View file

@ -6,7 +6,7 @@ from tqdm import tqdm
class Command(BaseCommand):
help = 'Refresh following data for all users'
help = "Refresh following data for all users"
def handle(self, *args, **options):
count = 0
@ -14,6 +14,6 @@ class Command(BaseCommand):
user.following = user.get_following_ids()
if user.following:
count += 1
user.save(update_fields=['following'])
user.save(update_fields=["following"])
print(f'{count} users updated')
print(f"{count} users updated")

View file

@ -6,11 +6,16 @@ from tqdm import tqdm
class Command(BaseCommand):
help = 'Refresh Mastodon data for all users if not updated in last 24h'
help = "Refresh Mastodon data for all users if not updated in last 24h"
def handle(self, *args, **options):
count = 0
for user in tqdm(User.objects.filter(mastodon_last_refresh__lt=timezone.now() - timedelta(hours=24), is_active=True)):
for user in tqdm(
User.objects.filter(
mastodon_last_refresh__lt=timezone.now() - timedelta(hours=24),
is_active=True,
)
):
if user.mastodon_token or user.mastodon_refresh_token:
tqdm.write(f"Refreshing {user}")
if user.refresh_mastodon_data():
@ -20,6 +25,6 @@ class Command(BaseCommand):
tqdm.write(f"Refresh failed for {user}")
user.save()
else:
tqdm.write(f'Missing token for {user}')
tqdm.write(f"Missing token for {user}")
print(f'{count} users updated')
print(f"{count} users updated")