diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 0c6d1a42..1e7a948e 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -32,12 +32,12 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + cache: pip - name: Install Dependencies run: | - python -m pip install --upgrade pip pip install -r requirements.txt - name: Run Tests run: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 14483c1e..19f6bbc8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,9 +1,26 @@ -name: Lint +name: check on: [push, pull_request] jobs: lint: + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: ['3.11'] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: pip + - name: Run pre-commit + run: | + python -m pip install pre_commit + SKIP=pyright python -m pre_commit run -a --show-diff-on-failure + type-checker: runs-on: ubuntu-latest strategy: max-parallel: 4 @@ -19,6 +36,7 @@ jobs: - name: Install dependencies run: | python -m pip install -r requirements-dev.txt - - name: Run pre-commit + python -m pip install -r requirements.txt + - name: Run pyright run: | - python -m pre_commit run -a --show-diff-on-failure + python -m pyright diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa88b952..2f55a153 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,3 +37,11 @@ repos: hooks: - id: djlint-reformat-django - id: djlint-django + + - repo: local + hooks: + - id: pyright + name: pyright + entry: python -m pyright + language: system + pass_filenames: false diff --git a/boofilsic/settings.py b/boofilsic/settings.py index 08ddd03a..2904b9b0 100644 --- a/boofilsic/settings.py +++ b/boofilsic/settings.py @@ -155,7 +155,7 @@ AUTHENTICATION_BACKENDS = [ ] -MARKDOWNX_MARKDOWNIFY_FUNCTION = "journal.renderers.render_md" +MARKDOWNX_MARKDOWNIFY_FUNCTION = "journal.models.render_md" # Internationalization diff --git a/catalog/book/models.py b/catalog/book/models.py index 5a03771d..5be14f2d 100644 --- a/catalog/book/models.py +++ b/catalog/book/models.py @@ -21,7 +21,18 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.utils.translation import gettext_lazy as _ -from catalog.common.models import * +from catalog.common import ( + BaseSchema, + ExternalResource, + IdType, + Item, + ItemCategory, + ItemInSchema, + ItemSchema, + ItemType, + PrimaryLookupIdDescriptor, + jsondata, +) from .utils import * diff --git a/catalog/book/utils.py b/catalog/book/utils.py index 15a02a56..5436e6f6 100644 --- a/catalog/book/utils.py +++ b/catalog/book/utils.py @@ -1,6 +1,6 @@ import re -from .models import IdType +from ..common.models import IdType def check_digit_10(isbn): diff --git a/catalog/common/downloaders.py b/catalog/common/downloaders.py index c571667b..a579a1e8 100644 --- a/catalog/common/downloaders.py +++ b/catalog/common/downloaders.py @@ -4,6 +4,7 @@ import re import time from io import BytesIO, StringIO from pathlib import Path +from typing import Tuple from urllib.parse import quote import filetype @@ -11,6 +12,7 @@ import requests from django.conf import settings from lxml import html from PIL import Image +from requests import Response from requests.exceptions import RequestException _logger = logging.getLogger(__name__) @@ -52,6 +54,53 @@ def get_mock_file(url): return fn +_local_response_path = ( + str(Path(__file__).parent.parent.parent.absolute()) + "/test_data/" +) + + +class MockResponse: + def __init__(self, url): + self.url = url + fn = _local_response_path + get_mock_file(url) + try: + self.content = Path(fn).read_bytes() + self.status_code = 200 + _logger.debug(f"use local response for {url} from {fn}") + except Exception: + self.content = b"Error: response file not found" + self.status_code = 404 + _logger.debug(f"local response not found for {url} at {fn}") + + @property + def text(self): + return self.content.decode("utf-8") + + def json(self): + return json.load(StringIO(self.text)) + + def html(self): + return html.fromstring( # may throw exception unexpectedly due to OS bug, see https://github.com/neodb-social/neodb/issues/5 + self.content.decode("utf-8") + ) + + @property + def headers(self): + return { + "Content-Type": "image/jpeg" if self.url.endswith("jpg") else "text/html" + } + + +requests.Response.html = MockResponse.html # type:ignore + + +class DownloaderResponse(Response): + def html(self): + return html.fromstring( # may throw exception unexpectedly due to OS bug, see https://github.com/neodb-social/neodb/issues/5 + self.content.decode("utf-8") + ) + + class DownloadError(Exception): def __init__(self, downloader, msg=None): self.url = downloader.url @@ -101,7 +150,7 @@ class BasicDownloader: else: return RESPONSE_INVALID_CONTENT - def _download(self, url): + def _download(self, url) -> Tuple[DownloaderResponse | MockResponse, int]: try: if not _mock_mode: # TODO cache = get/set from redis @@ -125,12 +174,12 @@ class BasicDownloader: {"response_type": response_type, "url": url, "exception": None} ) - return resp, response_type + return resp, response_type # type: ignore except RequestException as e: self.logs.append( {"response_type": RESPONSE_NETWORK_ERROR, "url": url, "exception": e} ) - return None, RESPONSE_NETWORK_ERROR + return None, RESPONSE_NETWORK_ERROR # type: ignore def download(self): resp, self.response_type = self._download(self.url) @@ -209,9 +258,10 @@ class RetryDownloader(BasicDownloader): class ImageDownloaderMixin: def __init__(self, url, referer=None): + self.extention = None if referer is not None: - self.headers["Referer"] = referer - super().__init__(url) + self.headers["Referer"] = referer # type: ignore + super().__init__(url) # type: ignore def validate_response(self, response): if response and response.status_code == 200: @@ -236,12 +286,12 @@ class ImageDownloaderMixin: @classmethod def download_image(cls, image_url, page_url, headers=None): - imgdl = cls(image_url, page_url) + imgdl: BasicDownloader = cls(image_url, page_url) # type:ignore if headers is not None: imgdl.headers = headers try: image = imgdl.download().content - image_extention = imgdl.extention + image_extention = imgdl.extention # type:ignore return image, image_extention except Exception: return None, None @@ -253,43 +303,3 @@ class BasicImageDownloader(ImageDownloaderMixin, BasicDownloader): class ProxiedImageDownloader(ImageDownloaderMixin, ProxiedDownloader): pass - - -_local_response_path = ( - str(Path(__file__).parent.parent.parent.absolute()) + "/test_data/" -) - - -class MockResponse: - def __init__(self, url): - self.url = url - fn = _local_response_path + get_mock_file(url) - try: - self.content = Path(fn).read_bytes() - self.status_code = 200 - _logger.debug(f"use local response for {url} from {fn}") - except Exception: - self.content = b"Error: response file not found" - self.status_code = 404 - _logger.debug(f"local response not found for {url} at {fn}") - - @property - def text(self): - return self.content.decode("utf-8") - - def json(self): - return json.load(StringIO(self.text)) - - def html(self): - return html.fromstring( # may throw exception unexpectedly due to OS bug, see https://github.com/neodb-social/neodb/issues/5 - self.content.decode("utf-8") - ) - - @property - def headers(self): - return { - "Content-Type": "image/jpeg" if self.url.endswith("jpg") else "text/html" - } - - -requests.Response.html = MockResponse.html diff --git a/catalog/common/jsondata.py b/catalog/common/jsondata.py index 1fd8c703..f3a188c9 100644 --- a/catalog/common/jsondata.py +++ b/catalog/common/jsondata.py @@ -80,6 +80,9 @@ class JSONFieldDescriptor(object): setattr(instance, self.field.json_field_name, json_value) +fields.CharField + + class JSONFieldMixin(object): """ Override django.db.model.fields.Field.contribute_to_class @@ -90,11 +93,11 @@ class JSONFieldMixin(object): self.json_field_name = kwargs.pop("json_field_name", "metadata") super(JSONFieldMixin, self).__init__(*args, **kwargs) - def contribute_to_class(self, cls, name, private_only=False): + def contribute_to_class(self: "fields.Field", cls, name, private_only=False): # type: ignore self.set_attributes_from_name(name) self.model = cls self.concrete = False - self.column = self.json_field_name + self.column = self.json_field_name # type: ignore cls._meta.add_field(self, private=True) if not getattr(cls, self.attname, None): @@ -131,11 +134,11 @@ class JSONFieldMixin(object): return transform json_field = self.model._meta.get_field(self.json_field_name) - transform = json_field.get_transform(self.name) + transform = json_field.get_transform(self.name) # type: ignore if transform is None: raise FieldError( "JSONField '%s' has no support for key '%s' %s lookup" - % (self.json_field_name, self.name, name) + % (self.json_field_name, self.name, name) # type: ignore ) return TransformFactoryWrapper(json_field, transform, name) @@ -170,10 +173,17 @@ class DateField(JSONFieldMixin, fields.DateField): class DateTimeField(JSONFieldMixin, fields.DateTimeField): - def to_json(self, value): + def to_json(self, value: datetime | date | str): if value: if not isinstance(value, (datetime, date)): - value = dateparse.parse_date(value) + v = dateparse.parse_date(value) + if v is None: + raise ValueError( + "DateTimeField: '{value}' has invalid datatime format" + ) + value = v + if isinstance(value, date): + value = datetime.combine(value, datetime.time.min()) if not timezone.is_aware(value): value = timezone.make_aware(value) return value.isoformat() diff --git a/catalog/common/mixins.py b/catalog/common/mixins.py index 545ebba1..7b88220f 100644 --- a/catalog/common/mixins.py +++ b/catalog/common/mixins.py @@ -17,6 +17,6 @@ class SoftDeleteMixin: if soft: self.clear() self.is_deleted = True - self.save(using=using) + self.save(using=using) # type: ignore else: - return super().delete(using=using, *args, **kwargs) + return super().delete(using=using, *args, **kwargs) # type: ignore diff --git a/catalog/common/models.py b/catalog/common/models.py index 7186ec76..daa08ba4 100644 --- a/catalog/common/models.py +++ b/catalog/common/models.py @@ -2,7 +2,7 @@ import logging import re import uuid from functools import cached_property -from typing import cast +from typing import TYPE_CHECKING, cast from auditlog.context import disable_auditlog from auditlog.models import AuditlogHistoryField, LogEntry @@ -22,6 +22,9 @@ from users.models import User from .mixins import SoftDeleteMixin from .utils import DEFAULT_ITEM_COVER, item_cover_path, resource_cover_path +if TYPE_CHECKING: + from django.utils.functional import _StrOrPromise + _logger = logging.getLogger(__name__) @@ -247,7 +250,7 @@ class Item(SoftDeleteMixin, PolymorphicModel): type = None # subclass must specify this parent_class = None # subclass may specify this to allow create child item category: ItemCategory | None = None # subclass must specify this - demonstrative: str | None = None # subclass must specify this + demonstrative: "_StrOrPromise | None" = None # subclass must specify this uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True) title = models.CharField(_("标题"), max_length=1000, default="") brief = models.TextField(_("简介"), blank=True, default="") diff --git a/catalog/forms.py b/catalog/forms.py index a2bd3abe..8be61780 100644 --- a/catalog/forms.py +++ b/catalog/forms.py @@ -37,7 +37,7 @@ def _EditForm(item_model): } def clean(self): - data = super().clean() + data = super().clean() or {} t, v = self.Meta.model.lookup_id_cleanup( data.get("primary_lookup_id_type"), data.get("primary_lookup_id_value") ) diff --git a/catalog/game/models.py b/catalog/game/models.py index fc8bd5ed..8bfe50ff 100644 --- a/catalog/game/models.py +++ b/catalog/game/models.py @@ -3,7 +3,18 @@ from datetime import date from django.db import models from django.utils.translation import gettext_lazy as _ -from catalog.common.models import * +from catalog.common import ( + BaseSchema, + ExternalResource, + IdType, + Item, + ItemCategory, + ItemInSchema, + ItemSchema, + ItemType, + PrimaryLookupIdDescriptor, + jsondata, +) class GameInSchema(ItemInSchema): diff --git a/catalog/models.py b/catalog/models.py index 2a7b2b80..b37b3034 100644 --- a/catalog/models.py +++ b/catalog/models.py @@ -8,9 +8,13 @@ from .book.models import Edition, EditionInSchema, EditionSchema, Series, Work from .collection.models import Collection as CatalogCollection from .common.models import ( ExternalResource, + IdType, Item, ItemCategory, + ItemInSchema, ItemSchema, + ItemType, + SiteName, item_categories, item_content_types, ) diff --git a/catalog/movie/models.py b/catalog/movie/models.py index b347abb3..10d75ee4 100644 --- a/catalog/movie/models.py +++ b/catalog/movie/models.py @@ -1,7 +1,18 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from catalog.common.models import * +from catalog.common import ( + BaseSchema, + ExternalResource, + IdType, + Item, + ItemCategory, + ItemInSchema, + ItemSchema, + ItemType, + PrimaryLookupIdDescriptor, + jsondata, +) class MovieInSchema(ItemInSchema): diff --git a/catalog/music/models.py b/catalog/music/models.py index db076963..7ed03577 100644 --- a/catalog/music/models.py +++ b/catalog/music/models.py @@ -3,7 +3,18 @@ from datetime import date from django.db import models from django.utils.translation import gettext_lazy as _ -from catalog.common.models import * +from catalog.common import ( + BaseSchema, + ExternalResource, + IdType, + Item, + ItemCategory, + ItemInSchema, + ItemSchema, + ItemType, + PrimaryLookupIdDescriptor, + jsondata, +) class AlbumInSchema(ItemInSchema): diff --git a/catalog/podcast/models.py b/catalog/podcast/models.py index 44d59a10..e2bd6047 100644 --- a/catalog/podcast/models.py +++ b/catalog/podcast/models.py @@ -1,7 +1,18 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from catalog.common.models import * +from catalog.common import ( + BaseSchema, + ExternalResource, + IdType, + Item, + ItemCategory, + ItemInSchema, + ItemSchema, + ItemType, + PrimaryLookupIdDescriptor, + jsondata, +) class PodcastInSchema(ItemInSchema): diff --git a/catalog/search/external.py b/catalog/search/external.py index 9c524af9..51da0806 100644 --- a/catalog/search/external.py +++ b/catalog/search/external.py @@ -65,22 +65,26 @@ class Goodreads: r = requests.get(search_url) if r.url.startswith("https://www.goodreads.com/book/show/"): # Goodreads will 302 if only one result matches ISBN - res = SiteManager.get_site_by_url(r.url).get_resource_ready() - subtitle = f"{res.metadata['pub_year']} {', '.join(res.metadata['author'])} {', '.join(res.metadata['translator'] if res.metadata['translator'] else [])}" - results.append( - SearchResultItem( - ItemCategory.Book, - SiteName.Goodreads, - res.url, - res.metadata["title"], - subtitle, - res.metadata["brief"], - res.metadata["cover_image_url"], - ) - ) + site = SiteManager.get_site_by_url(r.url) + if site: + res = site.get_resource_ready() + if res: + subtitle = f"{res.metadata['pub_year']} {', '.join(res.metadata['author'])} {', '.join(res.metadata['translator'] if res.metadata['translator'] else [])}" + results.append( + SearchResultItem( + ItemCategory.Book, + SiteName.Goodreads, + res.url, + res.metadata["title"], + subtitle, + res.metadata["brief"], + res.metadata["cover_image_url"], + ) + ) else: h = html.fromstring(r.content.decode("utf-8")) - for c in h.xpath('//tr[@itemtype="http://schema.org/Book"]'): + books = h.xpath('//tr[@itemtype="http://schema.org/Book"]') + for c in books: # type:ignore el_cover = c.xpath('.//img[@class="bookCover"]/@src') cover = el_cover[0] if el_cover else None el_title = c.xpath('.//a[@class="bookTitle"]//text()') @@ -228,7 +232,8 @@ class Bandcamp: search_url = f"https://bandcamp.com/search?from=results&item_type=a&page={page}&q={quote_plus(q)}" r = requests.get(search_url) h = html.fromstring(r.content.decode("utf-8")) - for c in h.xpath('//li[@class="searchresult data-search"]'): + albums = h.xpath('//li[@class="searchresult data-search"]') + for c in albums: # type:ignore el_cover = c.xpath('.//div[@class="art"]/img/@src') cover = el_cover[0] if el_cover else None el_title = c.xpath('.//div[@class="heading"]//text()') diff --git a/catalog/tv/models.py b/catalog/tv/models.py index 70ddc844..467234d5 100644 --- a/catalog/tv/models.py +++ b/catalog/tv/models.py @@ -24,12 +24,24 @@ tv specials are are shown as movies For now, we follow Douban convention, but keep an eye on it in case it breaks its own rules... """ +import re from functools import cached_property from django.db import models from django.utils.translation import gettext_lazy as _ -from catalog.common.models import * +from catalog.common import ( + BaseSchema, + ExternalResource, + IdType, + Item, + ItemCategory, + ItemInSchema, + ItemSchema, + ItemType, + PrimaryLookupIdDescriptor, + jsondata, +) class TVShowInSchema(ItemInSchema): diff --git a/common/templatetags/oauth_token.py b/common/templatetags/oauth_token.py index ee8ef306..06a4b83d 100644 --- a/common/templatetags/oauth_token.py +++ b/common/templatetags/oauth_token.py @@ -8,7 +8,7 @@ register = template.Library() class OAuthTokenNode(template.Node): def render(self, context): request = context.get("request") - oauth_token = request.user.mastodon_token + oauth_token = request.user.mastodon_token if request else "" return format_html(oauth_token) diff --git a/journal/exporters/doufen.py b/journal/exporters/doufen.py index 64bbffb1..f37311a9 100644 --- a/journal/exporters/doufen.py +++ b/journal/exporters/doufen.py @@ -5,7 +5,7 @@ from django.conf import settings from django.utils.translation import gettext_lazy as _ from openpyxl import Workbook -from catalog.common.models import IdType +from catalog.models import * from common.utils import GenerateDateUUIDMediaFilePath from journal.models import * diff --git a/journal/importers/goodreads.py b/journal/importers/goodreads.py index 790167f6..b0b7276e 100644 --- a/journal/importers/goodreads.py +++ b/journal/importers/goodreads.py @@ -108,12 +108,15 @@ class GoodreadsImporter: @classmethod def get_book(cls, url, user): site = SiteManager.get_site_by_url(url) - book = site.get_item() - if not book: - book = site.get_resource_ready().item - book.last_editor = user - book.save() - return book + if site: + book = site.get_item() + if not book: + resource = site.get_resource_ready() + if resource and resource.item: + book = resource.item + book.last_editor = user + book.save() + return book @classmethod def parse_shelf(cls, url, user): @@ -129,12 +132,13 @@ class GoodreadsImporter: if not title_elem: print(f"Shelf parsing error {url_shelf}") break - title = title_elem[0].strip() + title = title_elem[0].strip() # type:ignore print("Shelf title: " + title) except Exception: print(f"Shelf loading/parsing error {url_shelf}") break - for cell in content.xpath("//tbody[@id='booksBody']/tr"): + cells = content.xpath("//tbody[@id='booksBody']/tr") + for cell in cells: # type:ignore url_book = ( "https://www.goodreads.com" + cell.xpath(".//td[@class='field title']//a/@href")[0].strip() @@ -167,10 +171,12 @@ class GoodreadsImporter: c2 = BasicDownloader(url_review).download().html() review_elem = c2.xpath("//div[@itemprop='reviewBody']/text()") review = ( - "\n".join(p.strip() for p in review_elem) if review_elem else "" + "\n".join(p.strip() for p in review_elem) # type:ignore + if review_elem + else "" ) date_elem = c2.xpath("//div[@class='readingTimeline__text']/text()") - for d in date_elem: + for d in date_elem: # type:ignore date_matched = re.search(r"(\w+)\s+(\d+),\s+(\d+)", d) if date_matched: last_updated = make_aware( @@ -201,7 +207,7 @@ class GoodreadsImporter: pass # likely just download error next_elem = content.xpath("//a[@class='next_page']/@href") url_shelf = ( - ("https://www.goodreads.com" + next_elem[0].strip()) + f"https://www.goodreads.com{next_elem[0].strip()}" if next_elem else None ) diff --git a/journal/models/__init__.py b/journal/models/__init__.py index b35724b1..d1609f0b 100644 --- a/journal/models/__init__.py +++ b/journal/models/__init__.py @@ -13,6 +13,7 @@ from .common import ( from .like import Like from .mark import Mark from .rating import Rating +from .renderers import render_md from .review import Review from .shelf import ( Shelf, diff --git a/journal/models/mixins.py b/journal/models/mixins.py index 538d76b7..69d597d2 100644 --- a/journal/models/mixins.py +++ b/journal/models/mixins.py @@ -1,3 +1,9 @@ +from typing import TYPE_CHECKING, Type + +if TYPE_CHECKING: + from .common import Piece + + class UserOwnedObjectMixin: """ UserOwnedObjectMixin @@ -7,7 +13,7 @@ class UserOwnedObjectMixin: visibility = models.PositiveSmallIntegerField(default=0) """ - def is_visible_to(self, viewer): + def is_visible_to(self: "Piece", viewer): # type: ignore owner = self.owner if owner == viewer: return True @@ -24,13 +30,13 @@ class UserOwnedObjectMixin: else: return True - def is_editable_by(self, viewer): + def is_editable_by(self: "Piece", viewer): # type: ignore return viewer.is_authenticated and ( viewer.is_staff or viewer.is_superuser or viewer == self.owner ) @classmethod - def get_available(cls, entity, request_user, following_only=False): + def get_available(cls: "Type[Piece]", entity, request_user, following_only=False): # type: ignore # e.g. SongMark.get_available(song, request.user) query_kwargs = {entity.__class__.__name__.lower(): entity} all_entities = cls.objects.filter(**query_kwargs).order_by( diff --git a/journal/models/renderers.py b/journal/models/renderers.py index 64e866fe..ef6c2f5a 100644 --- a/journal/models/renderers.py +++ b/journal/models/renderers.py @@ -4,8 +4,6 @@ from typing import cast import mistune from django.utils.html import escape -MARKDOWNX_MARKDOWNIFY_FUNCTION = "journal.renderers.render_md" - _mistune_plugins = [ "url", "strikethrough", diff --git a/management/views.py b/management/views.py index 6c753da1..2c1f0f7a 100644 --- a/management/views.py +++ b/management/views.py @@ -3,13 +3,19 @@ from django.shortcuts import get_object_or_404 from django.urls import reverse_lazy from django.utils import timezone from django.utils.decorators import method_decorator -from django.views.generic import * +from django.views.generic import ( + CreateView, + DeleteView, + DetailView, + ListView, + UpdateView, +) from django.views.generic.edit import ModelFormMixin from .models import Announcement # https://docs.djangoproject.com/en/3.1/topics/class-based-views/intro/ -decorators = [login_required, user_passes_test(lambda u: u.is_superuser)] +decorators = [login_required, user_passes_test(lambda u: u.is_superuser)] # type:ignore class AnnouncementDetailView(DetailView, ModelFormMixin): diff --git a/mastodon/admin.py b/mastodon/admin.py index 035446b1..694323fa 100644 --- a/mastodon/admin.py +++ b/mastodon/admin.py @@ -1,74 +1 @@ from django.contrib import admin -from django.core.exceptions import ObjectDoesNotExist -from django.utils.translation import gettext_lazy as _ -from requests.exceptions import Timeout - -from .api import create_app -from .models import * - - -# Register your models here. -@admin.register(MastodonApplication) -class MastodonApplicationModelAdmin(admin.ModelAdmin): - 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" - ): - # 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。") - else: - if request.POST.get("is_proxy"): - try: - 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 - except ObjectDoesNotExist: - request.POST["domain_name"] = _("proxy_to所指域名不存在,请先添加原站点。") - else: - # create mastodon app - try: - response = create_app(request.POST.get("domain_name")) - except (Timeout, ConnectionError): - request.POST["domain_name"] = _("联邦宇宙请求超时。") - except Exception as 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) - 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"] - - return super().add_view(request, form_url=form_url, extra_context=extra_context) - - -admin.site.register(CrossSiteUserInfo) diff --git a/pyproject.toml b/pyproject.toml index e440c80c..f07b2109 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.pyright] -exclude = [ "media", ".venv", ".git" ] +exclude = [ "media", ".venv", ".git", "playground", "**/tests.py", "neodb", "**/migrations", "**/commands", "**/importers", "**/sites" ] [tool.djlint] ignore="T002,T003,H006,H019,H020,H021,H023,H030,H031" @@ -17,3 +17,7 @@ plugins = ["mypy_django_plugin.main"] [tool.django-stubs] django_settings_module = "boofilsic.settings" + +[tool.ruff] +ignore = ['E501'] +exclude = [ "media", ".venv", ".git", "playground", "**/tests.py", "neodb", "**/migrations", "**/commands", "**/importers", "**/sites", "legacy" ] diff --git a/requirements-dev.txt b/requirements-dev.txt index 13ea78cc..381a219a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,8 @@ black~=22.12.0 coverage django-debug-toolbar +django-stubs djlint~=1.32.1 isort~=5.12.0 -pre-commit~=3.3.3 -types-dateparser -types-tqdm +pre-commit +pyright diff --git a/social/models.py b/social/models.py index 04acb68d..f0e4190e 100644 --- a/social/models.py +++ b/social/models.py @@ -81,10 +81,6 @@ class ActivityManager: return ActivityManager(user) -User.activity_manager = cached_property(ActivityManager.get_manager_for_user) # type: ignore -User.activity_manager.__set_name__(User, "activity_manager") # type: ignore - - class DataSignalManager: processors = {} diff --git a/users/management/commands/refresh_following.py b/users/management/commands/refresh_following.py index 82e2e9f6..c57329d2 100644 --- a/users/management/commands/refresh_following.py +++ b/users/management/commands/refresh_following.py @@ -13,7 +13,7 @@ class Command(BaseCommand): def handle(self, *args, **options): count = 0 for user in tqdm(User.objects.all()): - user.following = user.merge_following_ids() + user.following = user.merged_following_ids() if user.following: count += 1 user.save(update_fields=["following"]) diff --git a/users/models/user.py b/users/models/user.py index e4db652a..7f77db0c 100644 --- a/users/models/user.py +++ b/users/models/user.py @@ -4,6 +4,7 @@ from functools import cached_property from typing import TYPE_CHECKING from django.contrib.auth.models import AbstractUser +from django.contrib.auth.validators import UnicodeUsernameValidator from django.core import validators from django.core.exceptions import ValidationError from django.db import models @@ -33,7 +34,7 @@ _RESERVED_USERNAMES = [ @deconstructible -class UsernameValidator(validators.RegexValidator): +class UsernameValidator(UnicodeUsernameValidator): regex = r"^[a-zA-Z0-9_]{2,30}$" message = _( "Enter a valid username. This value may contain only unaccented lowercase a-z and uppercase A-Z letters, numbers, and _ characters." @@ -550,6 +551,12 @@ class User(AbstractUser): return ShelfManager.get_manager_for_user(self) + @cached_property + def activity_manager(self): + from social.models import ActivityManager + + return ActivityManager.get_manager_for_user(self) + class Follow(models.Model): owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="+")