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" %}
-
+