From 8aa6324334e8d2ba2f64fab3d3e47c84db81ab12 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 29 Dec 2022 14:30:31 -0500 Subject: [PATCH] new data model: pages for collection --- .gitignore | 1 + catalog/common/jsondata.py | 50 +- catalog/common/models.py | 281 ++++++---- catalog/templates/item_base.html | 2 +- common/templatetags/thumb.py | 7 +- journal/models.py | 455 +++++++++++----- journal/templates/collection_edit.html | 8 +- journal/templates/collection_items.html | 2 +- .../collection_update_item_note.html | 5 + journal/templates/list_item_base.html | 8 +- journal/templates/piece_delete.html | 59 +++ journal/templates/profile.html | 7 +- journal/templates/review_delete.html | 93 ---- journal/templates/user_collection_list.html | 90 ++++ journal/tests.py | 44 +- journal/urls.py | 116 ++-- journal/views.py | 495 +++++++++++------- legacy/management/commands/migrate_journal.py | 192 ++++--- mastodon/api.py | 378 +++++++------ pyproject.toml | 2 + 20 files changed, 1475 insertions(+), 820 deletions(-) create mode 100644 journal/templates/collection_update_item_note.html create mode 100644 journal/templates/piece_delete.html delete mode 100644 journal/templates/review_delete.html create mode 100644 journal/templates/user_collection_list.html create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore index 3f1b5d7f..754760cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +.venv # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/catalog/common/jsondata.py b/catalog/common/jsondata.py index f20188e3..ccb6c9b0 100644 --- a/catalog/common/jsondata.py +++ b/catalog/common/jsondata.py @@ -12,7 +12,23 @@ from functools import partialmethod from django.db.models import JSONField -__all__ = ('BooleanField', 'CharField', 'DateField', 'DateTimeField', 'DecimalField', 'EmailField', 'FloatField', 'IntegerField', 'IPAddressField', 'GenericIPAddressField', 'NullBooleanField', 'TextField', 'TimeField', 'URLField', 'ArrayField') +__all__ = ( + "BooleanField", + "CharField", + "DateField", + "DateTimeField", + "DecimalField", + "EmailField", + "FloatField", + "IntegerField", + "IPAddressField", + "GenericIPAddressField", + "NullBooleanField", + "TextField", + "TimeField", + "URLField", + "ArrayField", +) class JSONFieldDescriptor(object): @@ -26,12 +42,12 @@ class JSONFieldDescriptor(object): if isinstance(json_value, dict): if self.field.attname in json_value or not self.field.has_default(): value = json_value.get(self.field.attname, None) - if hasattr(self.field, 'from_json'): + if hasattr(self.field, "from_json"): value = self.field.from_json(value) return value else: default = self.field.get_default() - if hasattr(self.field, 'to_json'): + if hasattr(self.field, "to_json"): json_value[self.field.attname] = self.field.to_json(default) else: json_value[self.field.attname] = default @@ -45,7 +61,7 @@ class JSONFieldDescriptor(object): else: json_value = {} - if hasattr(self.field, 'to_json'): + if hasattr(self.field, "to_json"): value = self.field.to_json(value) if not value and self.field.blank and not self.field.null: @@ -66,7 +82,7 @@ class JSONFieldMixin(object): """ def __init__(self, *args, **kwargs): - self.json_field_name = kwargs.pop('json_field_name', 'metadata') + 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): @@ -81,8 +97,11 @@ class JSONFieldMixin(object): setattr(cls, self.attname, descriptor) if self.choices is not None: - setattr(cls, 'get_%s_display' % self.name, - partialmethod(cls._get_FIELD_display, field=self)) + setattr( + cls, + "get_%s_display" % self.name, + partialmethod(cls._get_FIELD_display, field=self), + ) def get_lookup(self, lookup_name): # Always return None, to make get_transform been called @@ -101,15 +120,17 @@ class JSONFieldMixin(object): lhs.output_field = self.json_field transform = self.transform(lhs, **kwargs) transform._original_get_lookup = transform.get_lookup - transform.get_lookup = lambda name: transform._original_get_lookup(self.original_lookup) + transform.get_lookup = lambda name: transform._original_get_lookup( + self.original_lookup + ) return transform json_field = self.model._meta.get_field(self.json_field_name) transform = json_field.get_transform(self.name) if transform is None: raise FieldError( - "JSONField '%s' has no support for key '%s' %s lookup" % - (self.json_field_name, self.name, name) + "JSONField '%s' has no support for key '%s' %s lookup" + % (self.json_field_name, self.name, name) ) return TransformFactoryWrapper(json_field, transform, name) @@ -118,13 +139,16 @@ class JSONFieldMixin(object): class BooleanField(JSONFieldMixin, fields.BooleanField): def __init__(self, *args, **kwargs): super(BooleanField, self).__init__(*args, **kwargs) - if django.VERSION < (2, ): + if django.VERSION < (2,): self.blank = False class CharField(JSONFieldMixin, fields.CharField): - def from_json(self, value): # TODO workaound some bad data in migration, should be removed after clean up + def from_json( + self, value + ): # TODO workaound some bad data in migration, should be removed after clean up return value if isinstance(value, str) else None + pass @@ -133,7 +157,7 @@ class DateField(JSONFieldMixin, fields.DateField): if value: if not isinstance(value, (datetime, date)): value = dateparse.parse_date(value) - return value.strftime('%Y-%m-%d') + return value.strftime("%Y-%m-%d") def from_json(self, value): if value is not None: diff --git a/catalog/common/models.py b/catalog/common/models.py index de5c5e89..0cdec28d 100644 --- a/catalog/common/models.py +++ b/catalog/common/models.py @@ -15,94 +15,95 @@ from users.models import User class SiteName(models.TextChoices): - Douban = 'douban', _('豆瓣') - Goodreads = 'goodreads', _('Goodreads') - GoogleBooks = 'googlebooks', _('谷歌图书') - IMDB = 'imdb', _('IMDB') - TMDB = 'tmdb', _('The Movie Database') - Bandcamp = 'bandcamp', _('Bandcamp') - Spotify = 'spotify', _('Spotify') - IGDB = 'igdb', _('IGDB') - Steam = 'steam', _('Steam') - Bangumi = 'bangumi', _('Bangumi') - ApplePodcast = 'apple_podcast', _('苹果播客') + Douban = "douban", _("豆瓣") + Goodreads = "goodreads", _("Goodreads") + GoogleBooks = "googlebooks", _("谷歌图书") + IMDB = "imdb", _("IMDB") + TMDB = "tmdb", _("The Movie Database") + Bandcamp = "bandcamp", _("Bandcamp") + Spotify = "spotify", _("Spotify") + IGDB = "igdb", _("IGDB") + Steam = "steam", _("Steam") + Bangumi = "bangumi", _("Bangumi") + ApplePodcast = "apple_podcast", _("苹果播客") class IdType(models.TextChoices): - WikiData = 'wikidata', _('维基数据') - ISBN10 = 'isbn10', _('ISBN10') - ISBN = 'isbn', _('ISBN') # ISBN 13 - ASIN = 'asin', _('ASIN') - ISSN = 'issn', _('ISSN') - CUBN = 'cubn', _('统一书号') - ISRC = 'isrc', _('ISRC') # only for songs - GTIN = 'gtin', _('GTIN UPC EAN码') # ISBN is separate - Feed = 'feed', _('Feed URL') - IMDB = 'imdb', _('IMDb') - TMDB_TV = 'tmdb_tv', _('TMDB剧集') - TMDB_TVSeason = 'tmdb_tvseason', _('TMDB剧集') - TMDB_TVEpisode = 'tmdb_tvepisode', _('TMDB剧集') - TMDB_Movie = 'tmdb_movie', _('TMDB电影') - Goodreads = 'goodreads', _('Goodreads') - Goodreads_Work = 'goodreads_work', _('Goodreads著作') - GoogleBooks = 'googlebooks', _('谷歌图书') - DoubanBook = 'doubanbook', _('豆瓣读书') - DoubanBook_Work = 'doubanbook_work', _('豆瓣读书著作') - DoubanMovie = 'doubanmovie', _('豆瓣电影') - DoubanMusic = 'doubanmusic', _('豆瓣音乐') - DoubanGame = 'doubangame', _('豆瓣游戏') - DoubanDrama = 'doubandrama', _('豆瓣舞台剧') - Bandcamp = 'bandcamp', _('Bandcamp') - Spotify_Album = 'spotify_album', _('Spotify专辑') - Spotify_Show = 'spotify_show', _('Spotify播客') - Discogs_Release = 'discogs_release', ('Discogs Release') - Discogs_Master = 'discogs_master', ('Discogs Master') - MusicBrainz = 'musicbrainz', ('MusicBrainz ID') - DoubanBook_Author = 'doubanbook_author', _('豆瓣读书作者') - DoubanCelebrity = 'doubanmovie_celebrity', _('豆瓣电影影人') - Goodreads_Author = 'goodreads_author', _('Goodreads作者') - Spotify_Artist = 'spotify_artist', _('Spotify艺术家') - TMDB_Person = 'tmdb_person', _('TMDB影人') - IGDB = 'igdb', _('IGDB游戏') - Steam = 'steam', _('Steam游戏') - Bangumi = 'bangumi', _('Bangumi') - ApplePodcast = 'apple_podcast', _('苹果播客') + WikiData = "wikidata", _("维基数据") + ISBN10 = "isbn10", _("ISBN10") + ISBN = "isbn", _("ISBN") # ISBN 13 + ASIN = "asin", _("ASIN") + ISSN = "issn", _("ISSN") + CUBN = "cubn", _("统一书号") + ISRC = "isrc", _("ISRC") # only for songs + GTIN = "gtin", _("GTIN UPC EAN码") # ISBN is separate + Feed = "feed", _("Feed URL") + IMDB = "imdb", _("IMDb") + TMDB_TV = "tmdb_tv", _("TMDB剧集") + TMDB_TVSeason = "tmdb_tvseason", _("TMDB剧集") + TMDB_TVEpisode = "tmdb_tvepisode", _("TMDB剧集") + TMDB_Movie = "tmdb_movie", _("TMDB电影") + Goodreads = "goodreads", _("Goodreads") + Goodreads_Work = "goodreads_work", _("Goodreads著作") + GoogleBooks = "googlebooks", _("谷歌图书") + DoubanBook = "doubanbook", _("豆瓣读书") + DoubanBook_Work = "doubanbook_work", _("豆瓣读书著作") + DoubanMovie = "doubanmovie", _("豆瓣电影") + DoubanMusic = "doubanmusic", _("豆瓣音乐") + DoubanGame = "doubangame", _("豆瓣游戏") + DoubanDrama = "doubandrama", _("豆瓣舞台剧") + Bandcamp = "bandcamp", _("Bandcamp") + Spotify_Album = "spotify_album", _("Spotify专辑") + Spotify_Show = "spotify_show", _("Spotify播客") + Discogs_Release = "discogs_release", ("Discogs Release") + Discogs_Master = "discogs_master", ("Discogs Master") + MusicBrainz = "musicbrainz", ("MusicBrainz ID") + DoubanBook_Author = "doubanbook_author", _("豆瓣读书作者") + DoubanCelebrity = "doubanmovie_celebrity", _("豆瓣电影影人") + Goodreads_Author = "goodreads_author", _("Goodreads作者") + Spotify_Artist = "spotify_artist", _("Spotify艺术家") + TMDB_Person = "tmdb_person", _("TMDB影人") + IGDB = "igdb", _("IGDB游戏") + Steam = "steam", _("Steam游戏") + Bangumi = "bangumi", _("Bangumi") + ApplePodcast = "apple_podcast", _("苹果播客") class ItemType(models.TextChoices): - Book = 'book', _('书') - TV = 'tv', _('剧集') - TVSeason = 'tvseason', _('剧集分季') - TVEpisode = 'tvepisode', _('剧集分集') - Movie = 'movie', _('电影') - Music = 'music', _('音乐') - Game = 'game', _('游戏') - Boardgame = 'boardgame', _('桌游') - Podcast = 'podcast', _('播客') - FanFic = 'fanfic', _('网文') - Performance = 'performance', _('演出') - Exhibition = 'exhibition', _('展览') - Collection = 'collection', _('收藏单') + Book = "book", _("书") + TV = "tv", _("剧集") + TVSeason = "tvseason", _("剧集分季") + TVEpisode = "tvepisode", _("剧集分集") + Movie = "movie", _("电影") + Music = "music", _("音乐") + Game = "game", _("游戏") + Boardgame = "boardgame", _("桌游") + Podcast = "podcast", _("播客") + FanFic = "fanfic", _("网文") + Performance = "performance", _("演出") + Exhibition = "exhibition", _("展览") + Collection = "collection", _("收藏单") class ItemCategory(models.TextChoices): - Book = 'book', _('书') - Movie = 'movie', _('电影') - TV = 'tv', _('剧集') - Music = 'music', _('音乐') - Game = 'game', _('游戏') - Boardgame = 'boardgame', _('桌游') - Podcast = 'podcast', _('播客') - FanFic = 'fanfic', _('网文') - Performance = 'performance', _('演出') - Exhibition = 'exhibition', _('展览') - Collection = 'collection', _('收藏单') + Book = "book", _("书") + Movie = "movie", _("电影") + TV = "tv", _("剧集") + Music = "music", _("音乐") + Game = "game", _("游戏") + Boardgame = "boardgame", _("桌游") + Podcast = "podcast", _("播客") + FanFic = "fanfic", _("网文") + Performance = "performance", _("演出") + Exhibition = "exhibition", _("展览") + Collection = "collection", _("收藏单") class SubItemType(models.TextChoices): - Season = 'season', _('剧集分季') - Episode = 'episode', _('剧集分集') - Version = 'version', _('版本') + Season = "season", _("剧集分季") + Episode = "episode", _("剧集分集") + Version = "version", _("版本") + # class CreditType(models.TextChoices): # Author = 'author', _('作者') @@ -176,37 +177,64 @@ class Item(SoftDeleteMixin, PolymorphicModel): category = None # subclass must specify this demonstrative = None # subclass must specify this uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True) - title = models.CharField(_("title in primary language"), max_length=1000, default="") + title = models.CharField( + _("title in primary language"), max_length=1000, default="" + ) brief = models.TextField(_("简介"), blank=True, default="") - primary_lookup_id_type = models.CharField(_("isbn/cubn/imdb"), blank=False, null=True, max_length=50) - primary_lookup_id_value = models.CharField(_("1234/tt789"), blank=False, null=True, max_length=1000) + primary_lookup_id_type = models.CharField( + _("isbn/cubn/imdb"), blank=False, null=True, max_length=50 + ) + primary_lookup_id_value = models.CharField( + _("1234/tt789"), blank=False, null=True, max_length=1000 + ) metadata = models.JSONField(_("其他信息"), blank=True, null=True, default=dict) - cover = models.ImageField(upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True) + cover = models.ImageField( + upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True + ) created_time = models.DateTimeField(auto_now_add=True) edited_time = models.DateTimeField(auto_now=True) is_deleted = models.BooleanField(default=False, db_index=True) history = HistoricalRecords() - merged_to_item = models.ForeignKey('Item', null=True, on_delete=models.SET_NULL, default=None, related_name="merged_from_items") - last_editor = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='+', null=True, blank=False) + merged_to_item = models.ForeignKey( + "Item", + null=True, + on_delete=models.SET_NULL, + default=None, + related_name="merged_from_items", + ) + last_editor = models.ForeignKey( + User, on_delete=models.SET_NULL, related_name="+", null=True, blank=False + ) class Meta: - unique_together = [['polymorphic_ctype_id', 'primary_lookup_id_type', 'primary_lookup_id_value']] + unique_together = [ + [ + "polymorphic_ctype_id", + "primary_lookup_id_type", + "primary_lookup_id_value", + ] + ] def clear(self): self.primary_lookup_id_value = None self.primary_lookup_id_type = None def __str__(self): - return f"{self.id}|{self.uuid}{' ' + self.primary_lookup_id_type + ':' + self.primary_lookup_id_value if self.primary_lookup_id_value else ''} ({self.title})" + return f"{self.id}|{self.uuid} {self.primary_lookup_id_type}:{self.primary_lookup_id_value if self.primary_lookup_id_value else ''} ({self.title})" @classmethod def get_best_lookup_id(cls, lookup_ids): - """ get best available lookup id, ideally commonly used """ + """get best available lookup id, ideally commonly used""" best_id_types = [ - IdType.ISBN, IdType.CUBN, IdType.ASIN, - IdType.GTIN, IdType.ISRC, IdType.MusicBrainz, + IdType.ISBN, + IdType.CUBN, + IdType.ASIN, + IdType.GTIN, + IdType.ISRC, + IdType.MusicBrainz, IdType.Feed, - IdType.IMDB, IdType.TMDB_TVSeason + IdType.IMDB, + IdType.TMDB_TVSeason, ] for t in best_id_types: if lookup_ids.get(t): @@ -215,11 +243,11 @@ class Item(SoftDeleteMixin, PolymorphicModel): def merge(self, to_item): if to_item is None: - raise(ValueError('cannot merge to an empty item')) + raise (ValueError("cannot merge to an empty item")) elif to_item.merged_to_item is not None: - raise(ValueError('cannot merge with an item aleady merged')) + raise (ValueError("cannot merge with an item aleady merged")) elif to_item.__class__ != self.__class__: - raise(ValueError('cannot merge with an item in different class')) + raise (ValueError("cannot merge with an item in different class")) else: self.merged_to_item = to_item @@ -229,15 +257,15 @@ class Item(SoftDeleteMixin, PolymorphicModel): @property def url(self): - return f'/{self.url_path}/{self.uuid}' if self.url_path else None + return f"/{self.url_path}/{self.uuid}" if self.url_path else None @property def absolute_url(self): - return (settings.APP_WEBSITE + self.url) if self.url_path else None + return f"{settings.APP_WEBSITE}{self.url}" if self.url_path else None @property def api_url(self): - return ('/api/' + self.url) if self.url_path else None + return f"/api/{self.url}" if self.url_path else None @property def class_name(self): @@ -245,7 +273,7 @@ class Item(SoftDeleteMixin, PolymorphicModel): @classmethod def get_by_url(cls, url_or_b62): - b62 = url_or_b62.strip().split('/')[-1] + b62 = url_or_b62.strip().split("/")[-1] return cls.objects.get(uid=uuid.UUID(int=base62.decode(b62))) # def get_lookup_id(self, id_type: str) -> str: @@ -258,11 +286,18 @@ class Item(SoftDeleteMixin, PolymorphicModel): # ll = list(filter(lambda a, b: b, ll)) pass - METADATA_COPY_LIST = ['title', 'brief'] # list of metadata keys to copy from resource to item + METADATA_COPY_LIST = [ + "title", + "brief", + ] # list of metadata keys to copy from resource to item @classmethod def copy_metadata(cls, metadata): - return dict((k, v) for k, v in metadata.items() if k in cls.METADATA_COPY_LIST and v is not None) + return dict( + (k, v) + for k, v in metadata.items() + if k in cls.METADATA_COPY_LIST and v is not None + ) def has_cover(self): return self.cover and self.cover != DEFAULT_ITEM_COVER @@ -286,21 +321,38 @@ class Item(SoftDeleteMixin, PolymorphicModel): class ItemLookupId(models.Model): - item = models.ForeignKey(Item, null=True, on_delete=models.SET_NULL, related_name='lookup_ids') - id_type = models.CharField(_("源网站"), blank=True, choices=IdType.choices, max_length=50) + item = models.ForeignKey( + Item, null=True, on_delete=models.SET_NULL, related_name="lookup_ids" + ) + id_type = models.CharField( + _("源网站"), blank=True, choices=IdType.choices, max_length=50 + ) id_value = models.CharField(_("源网站ID"), blank=True, max_length=1000) raw_url = models.CharField(_("源网站ID"), blank=True, max_length=1000, unique=True) class Meta: - unique_together = [['id_type', 'id_value']] + unique_together = [["id_type", "id_value"]] class ExternalResource(models.Model): - item = models.ForeignKey(Item, null=True, on_delete=models.SET_NULL, related_name='external_resources') - id_type = models.CharField(_("IdType of the source site"), blank=False, choices=IdType.choices, max_length=50) - id_value = models.CharField(_("Primary Id on the source site"), blank=False, max_length=1000) - url = models.CharField(_("url to the resource"), blank=False, max_length=1000, unique=True) - cover = models.ImageField(upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True) + item = models.ForeignKey( + Item, null=True, on_delete=models.SET_NULL, related_name="external_resources" + ) + id_type = models.CharField( + _("IdType of the source site"), + blank=False, + choices=IdType.choices, + max_length=50, + ) + id_value = models.CharField( + _("Primary Id on the source site"), blank=False, max_length=1000 + ) + url = models.CharField( + _("url to the resource"), blank=False, max_length=1000, unique=True + ) + cover = models.ImageField( + upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True + ) other_lookup_ids = models.JSONField(default=dict) metadata = models.JSONField(default=dict) scraped_time = models.DateTimeField(null=True) @@ -310,10 +362,10 @@ class ExternalResource(models.Model): related_resources = jsondata.ArrayField(null=False, blank=False, default=list) class Meta: - unique_together = [['id_type', 'id_value']] + unique_together = [["id_type", "id_value"]] def __str__(self): - return f"{self.id}{':' + self.id_type + ':' + self.id_value if self.id_value else ''} ({self.url})" + return f"{self.id}:{self.id_type}:{self.id_value if self.id_value else ''} ({self.url})" @property def site_name(self): @@ -323,9 +375,12 @@ class ExternalResource(models.Model): self.other_lookup_ids = resource_content.lookup_ids self.metadata = resource_content.metadata if resource_content.cover_image and resource_content.cover_image_extention: - self.cover = SimpleUploadedFile('temp.' + resource_content.cover_image_extention, resource_content.cover_image) + self.cover = SimpleUploadedFile( + "temp." + resource_content.cover_image_extention, + resource_content.cover_image, + ) else: - self.cover = resource_content.metadata.get('cover_image_path') + self.cover = resource_content.metadata.get("cover_image_path") self.scraped_time = timezone.now() self.save() @@ -340,11 +395,13 @@ class ExternalResource(models.Model): return d def get_preferred_model(self): - model = self.metadata.get('preferred_model') + model = self.metadata.get("preferred_model") if model: - m = ContentType.objects.filter(app_label='catalog', model=model.lower()).first() + m = ContentType.objects.filter( + app_label="catalog", model=model.lower() + ).first() if m: return m.model_class() else: - raise ValueError(f'preferred model {model} does not exist') + raise ValueError(f"preferred model {model} does not exist") return None diff --git a/catalog/templates/item_base.html b/catalog/templates/item_base.html index e82aeb9f..853225d5 100644 --- a/catalog/templates/item_base.html +++ b/catalog/templates/item_base.html @@ -244,7 +244,7 @@ {% endif %} {% trans '编辑' %} - {% trans '删除' %} + {% trans '删除' %}
{{ review.edited_time }}
diff --git a/common/templatetags/thumb.py b/common/templatetags/thumb.py index aa698abb..f7adbb37 100644 --- a/common/templatetags/thumb.py +++ b/common/templatetags/thumb.py @@ -3,16 +3,17 @@ from easy_thumbnails.templatetags.thumbnail import thumbnail_url register = template.Library() + @register.filter def thumb(source, alias): """ This filter modifies that from `easy_thumbnails` so that it can neglect .svg file. """ - if source.url.endswith('.svg'): + if source.url.endswith(".svg"): return source.url else: try: return thumbnail_url(source, alias) - except Exception as e: - return '' + except Exception: + return "" diff --git a/journal/models.py b/journal/models.py index 554a0db0..a48883d6 100644 --- a/journal/models.py +++ b/journal/models.py @@ -4,7 +4,6 @@ from users.models import User from catalog.common.models import Item, ItemCategory from .mixins import UserOwnedObjectMixin from catalog.collection.models import Collection as CatalogCollection -from decimal import * from enum import Enum from markdownx.models import MarkdownxField from django.utils import timezone @@ -24,12 +23,13 @@ from catalog.models import * import mistune from django.contrib.contenttypes.models import ContentType from markdown import markdown +from catalog.common import jsondata class VisibilityType(models.IntegerChoices): - Public = 0, _('公开') - Follower_Only = 1, _('仅关注者') - Private = 2, _('仅自己') + Public = 0, _("公开") + Follower_Only = 1, _("仅关注者") + Private = 2, _("仅自己") def q_visible_to(viewer, owner): @@ -44,7 +44,11 @@ def q_visible_to(viewer, owner): def query_visible(user): - return Q(visibility=0) | Q(owner_id__in=user.following, visibility=1) | Q(owner_id=user.id) + return ( + Q(visibility=0) + | Q(owner_id__in=user.following, visibility=1) + | Q(owner_id=user.id) + ) def query_following(user): @@ -63,14 +67,26 @@ def query_item_category(item_category): class Piece(PolymorphicModel, UserOwnedObjectMixin): - url_path = 'piece' # subclass must specify this + url_path = "piece" # subclass must specify this uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True) owner = models.ForeignKey(User, on_delete=models.PROTECT) - visibility = models.PositiveSmallIntegerField(default=0) # 0: Public / 1: Follower only / 2: Self only - created_time = models.DateTimeField(default=timezone.now) # auto_now_add=True FIXME revert this after migration - edited_time = models.DateTimeField(default=timezone.now) # auto_now=True FIXME revert this after migration + visibility = models.PositiveSmallIntegerField( + default=0 + ) # 0: Public / 1: Follower only / 2: Self only + created_time = models.DateTimeField( + default=timezone.now + ) # auto_now_add=True FIXME revert this after migration + edited_time = models.DateTimeField( + default=timezone.now + ) # auto_now=True FIXME revert this after migration metadata = models.JSONField(default=dict) - attached_to = models.ForeignKey(User, null=True, default=None, on_delete=models.SET_NULL, related_name="attached_with") + attached_to = models.ForeignKey( + User, + null=True, + default=None, + on_delete=models.SET_NULL, + related_name="attached_with", + ) @property def uuid(self): @@ -78,7 +94,7 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin): @property def url(self): - return f'/{self.url_path}/{self.uuid}' if self.url_path else None + return f"/{self.url_path}/{self.uuid}" if self.url_path else None @property def absolute_url(self): @@ -86,7 +102,7 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin): @property def api_url(self): - return ('/api/' + self.url) if self.url_path else None + return ("/api/" + self.url) if self.url_path else None class Content(Piece): @@ -106,7 +122,7 @@ class Content(Piece): class Like(Piece): - target = models.ForeignKey(Piece, on_delete=models.CASCADE, related_name='likes') + target = models.ForeignKey(Piece, on_delete=models.CASCADE, related_name="likes") @staticmethod def user_like_piece(user, piece): @@ -117,8 +133,14 @@ class Like(Piece): like = Like.objects.create(owner=user, target=piece) return like + @staticmethod + def user_unlike_piece(user, piece): + if not piece: + return + Like.objects.filter(owner=user, target=piece).delete() -class Note(Content): + +class Memo(Content): pass @@ -133,7 +155,9 @@ class Comment(Content): comment.delete() comment = None elif comment is None: - comment = Comment.objects.create(owner=user, item=item, text=text, visibility=visibility) + comment = Comment.objects.create( + owner=user, item=item, text=text, visibility=visibility + ) elif comment.text != text or comment.visibility != visibility: comment.text = text comment.visibility = visibility @@ -142,7 +166,7 @@ class Comment(Content): class Review(Content): - url_path = 'review' + url_path = "review" title = models.CharField(max_length=500, blank=False, null=False) body = MarkdownxField() @@ -154,10 +178,17 @@ class Review(Content): def rating_grade(self): return Rating.get_item_rating_by_user(self.item, self.owner) - @ staticmethod + @staticmethod def review_item_by_user(item, user, title, body, metadata={}, visibility=0): # allow multiple reviews per item per user. - review = Review.objects.create(owner=user, item=item, title=title, body=body, metadata=metadata, visibility=visibility) + review = Review.objects.create( + owner=user, + item=item, + title=title, + body=body, + metadata=metadata, + visibility=visibility, + ) """ review = Review.objects.filter(owner=user, item=item).first() if title is None: @@ -176,36 +207,44 @@ class Review(Content): class Rating(Content): - grade = models.PositiveSmallIntegerField(default=0, validators=[MaxValueValidator(10), MinValueValidator(1)], null=True) + grade = models.PositiveSmallIntegerField( + default=0, validators=[MaxValueValidator(10), MinValueValidator(1)], null=True + ) - @ staticmethod + @staticmethod def get_rating_for_item(item): - stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(average=Avg('grade'), count=Count('item')) - return stat['average'] if stat['count'] >= 5 else None + stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate( + average=Avg("grade"), count=Count("item") + ) + return stat["average"] if stat["count"] >= 5 else None - @ staticmethod + @staticmethod def get_rating_count_for_item(item): - stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(count=Count('item')) - return stat['count'] + stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate( + count=Count("item") + ) + return stat["count"] - @ staticmethod + @staticmethod def rate_item_by_user(item, user, rating_grade, visibility=0): if rating_grade and (rating_grade < 1 or rating_grade > 10): - raise ValueError(f'Invalid rating grade: {rating_grade}') + raise ValueError(f"Invalid rating grade: {rating_grade}") rating = Rating.objects.filter(owner=user, item=item).first() if not rating_grade: if rating: rating.delete() rating = None elif rating is None: - rating = Rating.objects.create(owner=user, item=item, grade=rating_grade, visibility=visibility) + rating = Rating.objects.create( + owner=user, item=item, grade=rating_grade, visibility=visibility + ) elif rating.grade != rating_grade or rating.visibility != visibility: rating.visibility = visibility rating.grade = rating_grade rating.save() return rating - @ staticmethod + @staticmethod def get_item_rating_by_user(item, user): rating = Rating.objects.filter(owner=user, item=item).first() return rating.grade if rating else None @@ -216,7 +255,9 @@ Item.rating_count = property(Rating.get_rating_count_for_item) class Reply(Piece): - reply_to_content = models.ForeignKey(Piece, on_delete=models.SET_NULL, related_name='replies', null=True) + reply_to_content = models.ForeignKey( + Piece, on_delete=models.SET_NULL, related_name="replies", null=True + ) title = models.CharField(max_length=500, null=True) body = MarkdownxField() pass @@ -234,7 +275,9 @@ class List(Piece): class Meta: abstract = True - _owner = models.ForeignKey(User, on_delete=models.PROTECT) # duplicated owner field to make unique key possible for subclasses + _owner = models.ForeignKey( + User, on_delete=models.PROTECT + ) # duplicated owner field to make unique key possible for subclasses def save(self, *args, **kwargs): self._owner = self.owner @@ -246,43 +289,56 @@ class List(Piece): @property def ordered_members(self): - return self.members.all().order_by('position', 'item_id') + return self.members.all().order_by("position") @property def ordered_items(self): - return self.items.all().order_by(self.MEMBER_CLASS.__name__.lower() + '__position') + return self.items.all().order_by( + self.MEMBER_CLASS.__name__.lower() + "__position" + ) @property def recent_items(self): - return self.items.all().order_by('-' + self.MEMBER_CLASS.__name__.lower() + '__created_time') + return self.items.all().order_by( + "-" + self.MEMBER_CLASS.__name__.lower() + "__created_time" + ) @property def recent_members(self): - return self.members.all().order_by('-created_time') + return self.members.all().order_by("-created_time") - def has_item(self, item): - return self.members.filter(item=item).count() > 0 + def get_member_for_item(self, item): + return self.members.filter(item=item).first() def append_item(self, item, **params): - if item is None or self.has_item(item): + if item is None or self.get_member_for_item(item): return None else: ml = self.ordered_members - p = {'parent': self} + p = {"parent": self} p.update(params) - member = self.MEMBER_CLASS.objects.create(owner=self.owner, position=ml.last().position + 1 if ml.count() else 1, item=item, **p) - list_add.send(sender=self.__class__, instance=self, item=item, member=member) + member = self.MEMBER_CLASS.objects.create( + owner=self.owner, + position=ml.last().position + 1 if ml.count() else 1, + item=item, + **p, + ) + list_add.send( + sender=self.__class__, instance=self, item=item, member=member + ) return member def remove_item(self, item): - member = self.members.all().filter(item=item).first() + member = self.get_member_for_item(item) if member: - list_remove.send(sender=self.__class__, instance=self, item=item, member=member) + list_remove.send( + sender=self.__class__, instance=self, item=item, member=member + ) member.delete() def move_up_item(self, item): members = self.ordered_members - member = members.filter(item=item).first() + member = self.get_member_for_item(item) if member: other = members.filter(position__lt=member.position).last() if other: @@ -294,7 +350,7 @@ class List(Piece): def move_down_item(self, item): members = self.ordered_members - member = members.filter(item=item).first() + member = self.get_member_for_item(item) if member: other = members.filter(position__gt=member.position).first() if other: @@ -304,6 +360,12 @@ class List(Piece): other.save() member.save() + def update_item_metadata(self, item, metadata): + member = self.get_member_for_item(item) + if member: + member.metadata = metadata + member.save() + class ListMember(Piece): """ @@ -312,6 +374,7 @@ class ListMember(Piece): parent = models.ForeignKey('List', related_name='members', on_delete=models.CASCADE) """ + item = models.ForeignKey(Item, on_delete=models.PROTECT) position = models.PositiveIntegerField() @@ -325,7 +388,7 @@ class ListMember(Piece): abstract = True def __str__(self): - return f'{self.id}:{self.position} ({self.item})' + return f"{self.id}:{self.position} ({self.item})" """ @@ -334,48 +397,52 @@ Shelf class ShelfType(models.TextChoices): - WISHLIST = ('wishlist', '未开始') - PROGRESS = ('progress', '进行中') - COMPLETE = ('complete', '完成') + WISHLIST = ("wishlist", "未开始") + PROGRESS = ("progress", "进行中") + COMPLETE = ("complete", "完成") # DISCARDED = ('discarded', '放弃') ShelfTypeNames = [ - [ItemCategory.Book, ShelfType.WISHLIST, _('想读')], - [ItemCategory.Book, ShelfType.PROGRESS, _('在读')], - [ItemCategory.Book, ShelfType.COMPLETE, _('读过')], - [ItemCategory.Movie, ShelfType.WISHLIST, _('想看')], - [ItemCategory.Movie, ShelfType.PROGRESS, _('在看')], - [ItemCategory.Movie, ShelfType.COMPLETE, _('看过')], - [ItemCategory.TV, ShelfType.WISHLIST, _('想看')], - [ItemCategory.TV, ShelfType.PROGRESS, _('在看')], - [ItemCategory.TV, ShelfType.COMPLETE, _('看过')], - [ItemCategory.Music, ShelfType.WISHLIST, _('想听')], - [ItemCategory.Music, ShelfType.PROGRESS, _('在听')], - [ItemCategory.Music, ShelfType.COMPLETE, _('听过')], - [ItemCategory.Game, ShelfType.WISHLIST, _('想玩')], - [ItemCategory.Game, ShelfType.PROGRESS, _('在玩')], - [ItemCategory.Game, ShelfType.COMPLETE, _('玩过')], - - + [ItemCategory.Book, ShelfType.WISHLIST, _("想读")], + [ItemCategory.Book, ShelfType.PROGRESS, _("在读")], + [ItemCategory.Book, ShelfType.COMPLETE, _("读过")], + [ItemCategory.Movie, ShelfType.WISHLIST, _("想看")], + [ItemCategory.Movie, ShelfType.PROGRESS, _("在看")], + [ItemCategory.Movie, ShelfType.COMPLETE, _("看过")], + [ItemCategory.TV, ShelfType.WISHLIST, _("想看")], + [ItemCategory.TV, ShelfType.PROGRESS, _("在看")], + [ItemCategory.TV, ShelfType.COMPLETE, _("看过")], + [ItemCategory.Music, ShelfType.WISHLIST, _("想听")], + [ItemCategory.Music, ShelfType.PROGRESS, _("在听")], + [ItemCategory.Music, ShelfType.COMPLETE, _("听过")], + [ItemCategory.Game, ShelfType.WISHLIST, _("想玩")], + [ItemCategory.Game, ShelfType.PROGRESS, _("在玩")], + [ItemCategory.Game, ShelfType.COMPLETE, _("玩过")], ] class ShelfMember(ListMember): - parent = models.ForeignKey('Shelf', related_name='members', on_delete=models.CASCADE) + parent = models.ForeignKey( + "Shelf", related_name="members", on_delete=models.CASCADE + ) class Shelf(List): class Meta: - unique_together = [['_owner', 'item_category', 'shelf_type']] + unique_together = [["_owner", "item_category", "shelf_type"]] MEMBER_CLASS = ShelfMember - items = models.ManyToManyField(Item, through='ShelfMember', related_name="+") - item_category = models.CharField(choices=ItemCategory.choices, max_length=100, null=False, blank=False) - shelf_type = models.CharField(choices=ShelfType.choices, max_length=100, null=False, blank=False) + items = models.ManyToManyField(Item, through="ShelfMember", related_name="+") + item_category = models.CharField( + choices=ItemCategory.choices, max_length=100, null=False, blank=False + ) + shelf_type = models.CharField( + choices=ShelfType.choices, max_length=100, null=False, blank=False + ) def __str__(self): - return f'{self.id} {self.title}' + return f"{self.id} {self.title}" @cached_property def item_category_label(self): @@ -383,26 +450,41 @@ class Shelf(List): @cached_property def shelf_label(self): - return next(iter([n[2] for n in iter(ShelfTypeNames) if n[0] == self.item_category and n[1] == self.shelf_type]), self.shelf_type) + return next( + iter( + [ + n[2] + for n in iter(ShelfTypeNames) + if n[0] == self.item_category and n[1] == self.shelf_type + ] + ), + self.shelf_type, + ) @cached_property def title(self): - q = _("{shelf_label}的{item_category}").format(shelf_label=self.shelf_label, item_category=self.item_category_label) + q = _("{shelf_label}的{item_category}").format( + shelf_label=self.shelf_label, item_category=self.item_category_label + ) return q # return _("{user}'s {shelf_name}").format(user=self.owner.mastodon_username, shelf_name=q) class ShelfLogEntry(models.Model): owner = models.ForeignKey(User, on_delete=models.PROTECT) - shelf = models.ForeignKey(Shelf, on_delete=models.CASCADE, related_name='entries', null=True) # None means removed from any shelf + shelf = models.ForeignKey( + Shelf, on_delete=models.CASCADE, related_name="entries", null=True + ) # None means removed from any shelf item = models.ForeignKey(Item, on_delete=models.PROTECT) - timestamp = models.DateTimeField(default=timezone.now) # this may later be changed by user + timestamp = models.DateTimeField( + default=timezone.now + ) # this may later be changed by user metadata = models.JSONField(default=dict) created_time = models.DateTimeField(auto_now_add=True) edited_time = models.DateTimeField(auto_now=True) def __str__(self): - return f'{self.owner}:{self.shelf}:{self.item}:{self.metadata}' + return f"{self.owner}:{self.shelf}:{self.item}:{self.metadata}" class ShelfManager: @@ -422,12 +504,16 @@ class ShelfManager: Shelf.objects.create(owner=self.owner, item_category=ic, shelf_type=qt) def _shelf_member_for_item(self, item): - return ShelfMember.objects.filter(item=item, parent__in=self.owner.shelf_set.all()).first() + return ShelfMember.objects.filter( + item=item, parent__in=self.owner.shelf_set.all() + ).first() def _shelf_for_item_and_type(item, shelf_type): if not item or not shelf_type: return None - return self.owner.shelf_set.all().filter(item_category=item.category, shelf_type=shelf_type) + return self.owner.shelf_set.all().filter( + item_category=item.category, shelf_type=shelf_type + ) def locate_item(self, item): member = ShelfMember.objects.filter(owner=self.owner, item=item).first() @@ -437,7 +523,7 @@ class ShelfManager: # shelf_type=None means remove from current shelf # metadata=None means no change if not item: - raise ValueError('empty item') + raise ValueError("empty item") new_shelfmember = None last_shelfmember = self._shelf_member_for_item(item) last_shelf = last_shelfmember.parent if last_shelfmember else None @@ -450,9 +536,11 @@ class ShelfManager: if last_shelf: last_shelf.remove_item(item) if shelf: - new_shelfmember = shelf.append_item(item, visibility=visibility, metadata=metadata or {}) + new_shelfmember = shelf.append_item( + item, visibility=visibility, metadata=metadata or {} + ) elif last_shelf is None: - raise ValueError('empty shelf') + raise ValueError("empty shelf") else: new_shelfmember = last_shelfmember if metadata is not None and metadata != last_metadata: # change metadata @@ -466,29 +554,41 @@ class ShelfManager: if changed: if metadata is None: metadata = last_metadata or {} - ShelfLogEntry.objects.create(owner=self.owner, shelf=shelf, item=item, metadata=metadata) + ShelfLogEntry.objects.create( + owner=self.owner, shelf=shelf, item=item, metadata=metadata + ) return new_shelfmember def get_log(self): - return ShelfLogEntry.objects.filter(owner=self.owner).order_by('timestamp') + return ShelfLogEntry.objects.filter(owner=self.owner).order_by("timestamp") def get_log_for_item(self, item): - return ShelfLogEntry.objects.filter(owner=self.owner, item=item).order_by('timestamp') + return ShelfLogEntry.objects.filter(owner=self.owner, item=item).order_by( + "timestamp" + ) def get_shelf(self, item_category, shelf_type): - return self.owner.shelf_set.all().filter(item_category=item_category, shelf_type=shelf_type).first() + return ( + self.owner.shelf_set.all() + .filter(item_category=item_category, shelf_type=shelf_type) + .first() + ) def get_items_on_shelf(self, item_category, shelf_type): - shelf = self.owner.shelf_set.all().filter(item_category=item_category, shelf_type=shelf_type).first() + shelf = ( + self.owner.shelf_set.all() + .filter(item_category=item_category, shelf_type=shelf_type) + .first() + ) return shelf.members.all().order_by - @ staticmethod + @staticmethod def get_manager_for_user(user): return ShelfManager(user) User.shelf_manager = cached_property(ShelfManager.get_manager_for_user) -User.shelf_manager.__set_name__(User, 'shelf_manager') +User.shelf_manager.__set_name__(User, "shelf_manager") """ @@ -497,22 +597,30 @@ Collection class CollectionMember(ListMember): - parent = models.ForeignKey('Collection', related_name='members', on_delete=models.CASCADE) + parent = models.ForeignKey( + "Collection", related_name="members", on_delete=models.CASCADE + ) - @property - def note(self): - return self.metadata.get('comment') + note = jsondata.CharField(_("备注"), null=True, blank=True) class Collection(List): - url_path = 'collection' + url_path = "collection" MEMBER_CLASS = CollectionMember catalog_item = models.OneToOneField(CatalogCollection, on_delete=models.PROTECT) - title = models.CharField(_("title in primary language"), max_length=1000, default="") + title = models.CharField( + _("title in primary language"), max_length=1000, default="" + ) brief = models.TextField(_("简介"), blank=True, default="") - cover = models.ImageField(upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True) - items = models.ManyToManyField(Item, through='CollectionMember', related_name="collections") - collaborative = models.PositiveSmallIntegerField(default=0) # 0: Editable by owner only / 1: Editable by bi-direction followers + cover = models.ImageField( + upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True + ) + items = models.ManyToManyField( + Item, through="CollectionMember", related_name="collections" + ) + collaborative = models.PositiveSmallIntegerField( + default=0 + ) # 0: Editable by owner only / 1: Editable by bi-direction followers @property def html(self): @@ -522,12 +630,15 @@ class Collection(List): @property def plain_description(self): html = markdown(self.brief) - return RE_HTML_TAG.sub(' ', html) + return RE_HTML_TAG.sub(" ", html) def save(self, *args, **kwargs): - if getattr(self, 'catalog_item', None) is None: + if getattr(self, "catalog_item", None) is None: self.catalog_item = CatalogCollection() - if self.catalog_item.title != self.title or self.catalog_item.brief != self.brief: + if ( + self.catalog_item.title != self.title + or self.catalog_item.brief != self.brief + ): self.catalog_item.title = self.title self.catalog_item.brief = self.brief self.catalog_item.cover = self.cover @@ -541,72 +652,93 @@ Tag class TagMember(ListMember): - parent = models.ForeignKey('Tag', related_name='members', on_delete=models.CASCADE) + parent = models.ForeignKey("Tag", related_name="members", on_delete=models.CASCADE) -TagValidators = [RegexValidator(regex=r'\s+', inverse_match=True)] +TagValidators = [RegexValidator(regex=r"\s+", inverse_match=True)] class Tag(List): MEMBER_CLASS = TagMember - items = models.ManyToManyField(Item, through='TagMember') - title = models.CharField(max_length=100, null=False, blank=False, validators=TagValidators) + items = models.ManyToManyField(Item, through="TagMember") + title = models.CharField( + max_length=100, null=False, blank=False, validators=TagValidators + ) # TODO case convert and space removal on save # TODO check on save class Meta: - unique_together = [['_owner', 'title']] + unique_together = [["_owner", "title"]] - @ staticmethod + @staticmethod def cleanup_title(title): return title.strip().lower() class TagManager: - @ staticmethod + @staticmethod def public_tags_for_item(item): - tags = item.tag_set.all().filter(visibility=0).values('title').annotate(frequency=Count('owner')).order_by('-frequency')[: 20] - return sorted(list(map(lambda t: t['title'], tags))) + tags = ( + item.tag_set.all() + .filter(visibility=0) + .values("title") + .annotate(frequency=Count("owner")) + .order_by("-frequency")[:20] + ) + return sorted(list(map(lambda t: t["title"], tags))) - @ staticmethod + @staticmethod def all_tags_for_user(user): - tags = user.tag_set.all().values('title').annotate(frequency=Count('members__id')).order_by('-frequency') - return list(map(lambda t: t['title'], tags)) + tags = ( + user.tag_set.all() + .values("title") + .annotate(frequency=Count("members__id")) + .order_by("-frequency") + ) + return list(map(lambda t: t["title"], tags)) - @ staticmethod + @staticmethod def tag_item_by_user(item, user, tag_titles, default_visibility=0): titles = set([Tag.cleanup_title(tag_title) for tag_title in tag_titles]) - current_titles = set([m.parent.title for m in TagMember.objects.filter(owner=user, item=item)]) + current_titles = set( + [m.parent.title for m in TagMember.objects.filter(owner=user, item=item)] + ) for title in titles - current_titles: tag = Tag.objects.filter(owner=user, title=title).first() if not tag: - tag = Tag.objects.create(owner=user, title=title, visibility=default_visibility) + tag = Tag.objects.create( + owner=user, title=title, visibility=default_visibility + ) tag.append_item(item) for title in current_titles - titles: tag = Tag.objects.filter(owner=user, title=title).first() tag.remove_item(item) - @ staticmethod + @staticmethod def get_item_tags_by_user(item, user): - current_titles = [m.parent.title for m in TagMember.objects.filter(owner=user, item=item)] + current_titles = [ + m.parent.title for m in TagMember.objects.filter(owner=user, item=item) + ] return current_titles - @ staticmethod + @staticmethod def add_tag_by_user(item, tag_title, user, default_visibility=0): title = Tag.cleanup_title(tag_title) tag = Tag.objects.filter(owner=user, title=title).first() if not tag: - tag = Tag.objects.create(owner=user, title=title, visibility=default_visibility) + tag = Tag.objects.create( + owner=user, title=title, visibility=default_visibility + ) tag.append_item(item) - @ staticmethod + @staticmethod def get_manager_for_user(user): return TagManager(user) def __init__(self, user): self.owner = user - @ property + @property def all_tags(self): return TagManager.all_tags_for_user(self.owner) @@ -615,83 +747,111 @@ class TagManager: TagManager.add_tag_by_user(item, tag, self.owner, visibility) def get_item_tags(self, item): - return sorted([m['parent__title'] for m in TagMember.objects.filter(parent__owner=self.owner, item=item).values('parent__title')]) + return sorted( + [ + m["parent__title"] + for m in TagMember.objects.filter( + parent__owner=self.owner, item=item + ).values("parent__title") + ] + ) Item.tags = property(TagManager.public_tags_for_item) User.tags = property(TagManager.all_tags_for_user) User.tag_manager = cached_property(TagManager.get_manager_for_user) -User.tag_manager.__set_name__(User, 'tag_manager') +User.tag_manager.__set_name__(User, "tag_manager") class Mark: - """ this mimics previous mark behaviour """ + """this mimics previous mark behaviour""" def __init__(self, user, item): self.owner = user self.item = item - @ cached_property + @cached_property def shelfmember(self): return self.owner.shelf_manager.locate_item(self.item) - @ property + @property def id(self): return self.shelfmember.id if self.shelfmember else None - @ property + @property def shelf(self): return self.shelfmember.parent if self.shelfmember else None - @ property + @property def shelf_type(self): return self.shelfmember.parent.shelf_type if self.shelfmember else None - @ property + @property def shelf_label(self): return self.shelfmember.parent.shelf_label if self.shelfmember else None - @ property + @property def created_time(self): return self.shelfmember.created_time if self.shelfmember else None - @ property + @property def metadata(self): return self.shelfmember.metadata if self.shelfmember else None - @ property + @property def visibility(self): return self.shelfmember.visibility if self.shelfmember else None - @ cached_property + @cached_property def tags(self): return self.owner.tag_manager.get_item_tags(self.item) - @ cached_property + @cached_property def rating(self): return Rating.get_item_rating_by_user(self.item, self.owner) - @ cached_property + @cached_property def comment(self): return Comment.objects.filter(owner=self.owner, item=self.item).first() - @ property + @property def text(self): return self.comment.text if self.comment else None - @ cached_property + @cached_property def review(self): return Review.objects.filter(owner=self.owner, item=self.item).first() - def update(self, shelf_type, comment_text, rating_grade, visibility, metadata=None, created_time=None, share_to_mastodon=False): - share = share_to_mastodon and shelf_type is not None and (shelf_type != self.shelf_type or comment_text != self.text or rating_grade != self.rating) + def update( + self, + shelf_type, + comment_text, + rating_grade, + visibility, + metadata=None, + created_time=None, + share_to_mastodon=False, + ): + share = ( + share_to_mastodon + and shelf_type is not None + and ( + shelf_type != self.shelf_type + or comment_text != self.text + or rating_grade != self.rating + ) + ) if shelf_type != self.shelf_type or visibility != self.visibility: - self.shelfmember = self.owner.shelf_manager.move_item(self.item, shelf_type, visibility=visibility, metadata=metadata) + self.shelfmember = self.owner.shelf_manager.move_item( + self.item, shelf_type, visibility=visibility, metadata=metadata + ) if self.shelfmember and created_time: self.shelfmember.created_time = created_time self.shelfmember.save() if comment_text != self.text or visibility != self.visibility: - self.comment = Comment.comment_item_by_user(self.item, self.owner, comment_text, visibility) + self.comment = Comment.comment_item_by_user( + self.item, self.owner, comment_text, visibility + ) if rating_grade != self.rating or visibility != self.visibility: Rating.rate_item_by_user(self.item, self.owner, rating_grade, visibility) self.rating = rating_grade @@ -699,15 +859,20 @@ class Mark: # this is a bit hacky but let's keep it until move to implement ActivityPub, # by then, we'll just change this to boost from mastodon.api import share_mark - self.shared_link = self.shelfmember.metadata.get('shared_link') if self.shelfmember.metadata else None + + self.shared_link = ( + self.shelfmember.metadata.get("shared_link") + if self.shelfmember.metadata + else None + ) self.translated_status = self.shelf_label self.save = lambda **args: None if not share_mark(self): raise ValueError("sharing failed") if not self.shelfmember.metadata: self.shelfmember.metadata = {} - if self.shelfmember.metadata.get('shared_link') != self.shared_link: - self.shelfmember.metadata['shared_link'] = self.shared_link + if self.shelfmember.metadata.get("shared_link") != self.shared_link: + self.shelfmember.metadata["shared_link"] = self.shared_link self.shelfmember.save() def delete(self): diff --git a/journal/templates/collection_edit.html b/journal/templates/collection_edit.html index a5cd0db7..11672e35 100644 --- a/journal/templates/collection_edit.html +++ b/journal/templates/collection_edit.html @@ -33,13 +33,17 @@
-
+
{% include "partial/_footer.html" %} - + diff --git a/journal/templates/collection_items.html b/journal/templates/collection_items.html index e50b0c54..e41991d5 100644 --- a/journal/templates/collection_items.html +++ b/journal/templates/collection_items.html @@ -2,7 +2,7 @@ {% load i18n %} {% load l10n %}