From 160b8b1c46955a800d0031f256d678f94f2559f2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Jan 2023 19:11:31 -0500 Subject: [PATCH] reformat with black --- boofilsic/asgi.py | 2 +- boofilsic/wsgi.py | 2 +- common/apps.py | 2 +- common/forms.py | 114 +++--- common/index.py | 12 - common/management/commands/delete_job.py | 8 +- common/management/commands/list_jobs.py | 18 +- common/management/commands/restart_sync.py | 28 -- common/management/commands/scrape.py | 25 -- common/models.py | 367 ------------------ common/search/meilisearch.py | 183 --------- common/search/typesense.py | 217 ----------- common/templatetags/admin_url.py | 10 +- common/templatetags/highlight.py | 4 +- common/templatetags/mastodon.py | 14 +- common/templatetags/oauth_token.py | 5 +- common/templatetags/prettydate.py | 10 +- common/templatetags/strip_scheme.py | 8 +- common/templatetags/truncate.py | 2 +- common/utils.py | 23 +- common/views.py | 6 +- management/apps.py | 2 +- management/models.py | 12 +- management/urls.py | 14 +- management/views.py | 18 +- mastodon/__init__.py | 2 +- mastodon/admin.py | 59 +-- mastodon/apps.py | 2 +- mastodon/auth.py | 11 +- mastodon/decorators.py | 10 +- mastodon/management/commands/wrong_sites.py | 10 +- mastodon/utils.py | 18 +- users/admin.py | 2 +- users/apps.py | 2 +- users/forms.py | 19 +- .../management/commands/backfill_mastodon.py | 10 +- users/management/commands/disable_user.py | 10 +- .../management/commands/refresh_following.py | 6 +- users/management/commands/refresh_mastodon.py | 13 +- 39 files changed, 244 insertions(+), 1036 deletions(-) delete mode 100644 common/index.py delete mode 100644 common/management/commands/restart_sync.py delete mode 100644 common/management/commands/scrape.py delete mode 100644 common/search/meilisearch.py delete mode 100644 common/search/typesense.py diff --git a/boofilsic/asgi.py b/boofilsic/asgi.py index dbaccb32..1fde56b0 100644 --- a/boofilsic/asgi.py +++ b/boofilsic/asgi.py @@ -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() diff --git a/boofilsic/wsgi.py b/boofilsic/wsgi.py index f0f67843..71f5d002 100644 --- a/boofilsic/wsgi.py +++ b/boofilsic/wsgi.py @@ -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() diff --git a/common/apps.py b/common/apps.py index 5f2f0784..3ce38941 100644 --- a/common/apps.py +++ b/common/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class CommonConfig(AppConfig): - name = 'common' + name = "common" diff --git a/common/forms.py b/common/forms.py index 849b31c4..23a3517d 100644 --- a/common/forms.py +++ b/common/forms.py @@ -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 @@ -231,33 +247,34 @@ VISIBILITY_CHOICES = [ ] -class MarkForm(forms.ModelForm): +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, ) diff --git a/common/index.py b/common/index.py deleted file mode 100644 index 42227b60..00000000 --- a/common/index.py +++ /dev/null @@ -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 diff --git a/common/management/commands/delete_job.py b/common/management/commands/delete_job.py index 1c1d6c86..f74b91aa 100644 --- a/common/management/commands/delete_job.py +++ b/common/management/commands/delete_job.py @@ -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}")) diff --git a/common/management/commands/list_jobs.py b/common/management/commands/list_jobs.py index e843f527..085d6e00 100644 --- a/common/management/commands/list_jobs.py +++ b/common/management/commands/list_jobs.py @@ -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}") diff --git a/common/management/commands/restart_sync.py b/common/management/commands/restart_sync.py deleted file mode 100644 index 93903c57..00000000 --- a/common/management/commands/restart_sync.py +++ /dev/null @@ -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}')) diff --git a/common/management/commands/scrape.py b/common/management/commands/scrape.py deleted file mode 100644 index d48898e6..00000000 --- a/common/management/commands/scrape.py +++ /dev/null @@ -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) diff --git a/common/models.py b/common/models.py index d7589627..e69de29b 100644 --- a/common/models.py +++ b/common/models.py @@ -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 diff --git a/common/search/meilisearch.py b/common/search/meilisearch.py deleted file mode 100644 index d7a13a2f..00000000 --- a/common/search/meilisearch.py +++ /dev/null @@ -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 diff --git a/common/search/typesense.py b/common/search/typesense.py deleted file mode 100644 index 32dc815b..00000000 --- a/common/search/typesense.py +++ /dev/null @@ -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 diff --git a/common/templatetags/admin_url.py b/common/templatetags/admin_url.py index ce1bc765..3199716f 100644 --- a/common/templatetags/admin_url.py +++ b/common/templatetags/admin_url.py @@ -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 += '/' - return format_html(url) \ No newline at end of file + if not url.startswith("/"): + url = "/" + url + if not url.endswith("/"): + url += "/" + return format_html(url) diff --git a/common/templatetags/highlight.py b/common/templatetags/highlight.py index 02d79bbb..88e0c633 100644 --- a/common/templatetags/highlight.py +++ b/common/templatetags/highlight.py @@ -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: diff --git a/common/templatetags/mastodon.py b/common/templatetags/mastodon.py index 77e81315..48336db2 100644 --- a/common/templatetags/mastodon.py +++ b/common/templatetags/mastodon.py @@ -8,19 +8,19 @@ register = template.Library() @register.simple_tag def mastodon(domain): - url = 'https://' + domain - return url + 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 None \ No newline at end of file + return "被ta关注" + return None diff --git a/common/templatetags/oauth_token.py b/common/templatetags/oauth_token.py index b2f24677..ee8ef306 100644 --- a/common/templatetags/oauth_token.py +++ b/common/templatetags/oauth_token.py @@ -4,13 +4,14 @@ 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) @register.tag def oauth_token(parser, token): - return OAuthTokenNode() \ No newline at end of file + return OAuthTokenNode() diff --git a/common/templatetags/prettydate.py b/common/templatetags/prettydate.py index 4d37c538..d043d34a 100644 --- a/common/templatetags/prettydate.py +++ b/common/templatetags/prettydate.py @@ -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) diff --git a/common/templatetags/strip_scheme.py b/common/templatetags/strip_scheme.py index e73c7b0c..022158c8 100644 --- a/common/templatetags/strip_scheme.py +++ b/common/templatetags/strip_scheme.py @@ -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 \ No newline at end of file + return value diff --git a/common/templatetags/truncate.py b/common/templatetags/truncate.py index 558ec5c0..6e6764b5 100644 --- a/common/templatetags/truncate.py +++ b/common/templatetags/truncate.py @@ -14,4 +14,4 @@ def truncate(value, arg): length = int(arg) except ValueError: # Invalid literal for int(). return value # Fail silently. - return Truncator(value).chars(length, truncate="...") \ No newline at end of file + return Truncator(value).chars(length, truncate="...") diff --git a/common/utils.py b/common/utils.py index 36233ef5..313c0373 100644 --- a/common/utils.py +++ b/common/utils.py @@ -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,13 +25,12 @@ 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 self.has_next = False - + elif start_page < 1 and not end_page > total_pages: self.start_page = 1 # this won't overflow because the total pages are more than the length @@ -39,7 +40,7 @@ class PageLinksGenerator: self.has_next = False else: self.has_next = True - + elif not start_page < 1 and end_page > total_pages: self.end_page = total_pages self.start_page = start_page - (end_page - total_pages) @@ -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}" diff --git a/common/views.py b/common/views.py index 931e590b..41e92de3 100644 --- a/common/views.py +++ b/common/views.py @@ -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 diff --git a/management/apps.py b/management/apps.py index 51fee6c1..f4ad1a62 100644 --- a/management/apps.py +++ b/management/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class ManagementConfig(AppConfig): - name = 'management' + name = "management" diff --git a/management/models.py b/management/models.py index c5bf2f64..36e4bc15 100644 --- a/management/models.py +++ b/management/models.py @@ -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.""" diff --git a/management/urls.py b/management/urls.py index 175f9825..e47ee7c8 100644 --- a/management/urls.py +++ b/management/urls.py @@ -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('/', AnnouncementDetailView.as_view(), name='retrieve'), - path('create/', AnnouncementCreateView.as_view(), name='create'), - path('/', AnnouncementDetailView.as_view(), name='retrieve_slug'), - path('/update/', AnnouncementUpdateView.as_view(), name='update'), - path('/delete/', AnnouncementDeleteView.as_view(), name='delete'), + path("", AnnouncementListView.as_view(), name="list"), + path("/", AnnouncementDetailView.as_view(), name="retrieve"), + path("create/", AnnouncementCreateView.as_view(), name="create"), + path("/", AnnouncementDetailView.as_view(), name="retrieve_slug"), + path("/update/", AnnouncementUpdateView.as_view(), name="update"), + path("/delete/", AnnouncementDeleteView.as_view(), name="delete"), ] diff --git a/management/views.py b/management/views.py index 6b0ab887..4d4dd9c6 100644 --- a/management/views.py +++ b/management/views.py @@ -16,41 +16,39 @@ 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" - + class AnnouncementListView(ListView): model = Announcement # paginate_by = 1 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) - - diff --git a/mastodon/__init__.py b/mastodon/__init__.py index 42be4720..48bad832 100644 --- a/mastodon/__init__.py +++ b/mastodon/__init__.py @@ -1 +1 @@ -from .decorators import * \ No newline at end of file +from .decorators import * diff --git a/mastodon/admin.py b/mastodon/admin.py index e81bc74b..2594c59c 100644 --- a/mastodon/admin.py +++ b/mastodon/admin.py @@ -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) diff --git a/mastodon/apps.py b/mastodon/apps.py index 53ff1949..430aa6b7 100644 --- a/mastodon/apps.py +++ b/mastodon/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class MastodonConfig(AppConfig): - name = 'mastodon' + name = "mastodon" diff --git a/mastodon/auth.py b/mastodon/auth.py index 02d7703f..0ad9d3a0 100644 --- a/mastodon/auth.py +++ b/mastodon/auth.py @@ -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: diff --git a/mastodon/decorators.py b/mastodon/decorators.py index ef7a31d1..c0afe347 100644 --- a/mastodon/decorators.py +++ b/mastodon/decorators.py @@ -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 diff --git a/mastodon/management/commands/wrong_sites.py b/mastodon/management/commands/wrong_sites.py index f985a1e7..966553a5 100644 --- a/mastodon/management/commands/wrong_sites.py +++ b/mastodon/management/commands/wrong_sites.py @@ -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() diff --git a/mastodon/utils.py b/mastodon/utils.py index 8bada43a..2e5b833c 100644 --- a/mastodon/utils.py +++ b/mastodon/utils.py @@ -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 + ' ' - return emoji_code \ No newline at end of file + emoji_code = " " + emoji_code + " " + return emoji_code diff --git a/users/admin.py b/users/admin.py index 94e66175..4d85cf17 100644 --- a/users/admin.py +++ b/users/admin.py @@ -4,4 +4,4 @@ from .models import * admin.site.register(Report) admin.site.register(User) -admin.site.register(Preference) \ No newline at end of file +admin.site.register(Preference) diff --git a/users/apps.py b/users/apps.py index 4ce1fabc..3ef1284a 100644 --- a/users/apps.py +++ b/users/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class UsersConfig(AppConfig): - name = 'users' + name = "users" diff --git a/users/forms.py b/users/forms.py index 6de4bd53..3d33de32 100644 --- a/users/forms.py +++ b/users/forms.py @@ -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': _("详情") - } \ No newline at end of file + } + labels = {"reported_user": _("举报的用户"), "image": _("相关证据"), "message": _("详情")} diff --git a/users/management/commands/backfill_mastodon.py b/users/management/commands/backfill_mastodon.py index 55f4dea6..fe0e0ac1 100644 --- a/users/management/commands/backfill_mastodon.py +++ b/users/management/commands/backfill_mastodon.py @@ -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() diff --git a/users/management/commands/disable_user.py b/users/management/commands/disable_user.py index ac28b6a5..67b3577e 100644 --- a/users/management/commands/disable_user.py +++ b/users/management/commands/disable_user.py @@ -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") diff --git a/users/management/commands/refresh_following.py b/users/management/commands/refresh_following.py index b29f4f0c..9352db7d 100644 --- a/users/management/commands/refresh_following.py +++ b/users/management/commands/refresh_following.py @@ -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") diff --git a/users/management/commands/refresh_mastodon.py b/users/management/commands/refresh_mastodon.py index ff79d5fc..4cd62594 100644 --- a/users/management/commands/refresh_mastodon.py +++ b/users/management/commands/refresh_mastodon.py @@ -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")