new data model: pages for collection
This commit is contained in:
parent
0454e8830c
commit
8aa6324334
20 changed files with 1475 additions and 820 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.venv
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
|
@ -12,7 +12,23 @@ from functools import partialmethod
|
||||||
from django.db.models import JSONField
|
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):
|
class JSONFieldDescriptor(object):
|
||||||
|
@ -26,12 +42,12 @@ class JSONFieldDescriptor(object):
|
||||||
if isinstance(json_value, dict):
|
if isinstance(json_value, dict):
|
||||||
if self.field.attname in json_value or not self.field.has_default():
|
if self.field.attname in json_value or not self.field.has_default():
|
||||||
value = json_value.get(self.field.attname, None)
|
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)
|
value = self.field.from_json(value)
|
||||||
return value
|
return value
|
||||||
else:
|
else:
|
||||||
default = self.field.get_default()
|
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)
|
json_value[self.field.attname] = self.field.to_json(default)
|
||||||
else:
|
else:
|
||||||
json_value[self.field.attname] = default
|
json_value[self.field.attname] = default
|
||||||
|
@ -45,7 +61,7 @@ class JSONFieldDescriptor(object):
|
||||||
else:
|
else:
|
||||||
json_value = {}
|
json_value = {}
|
||||||
|
|
||||||
if hasattr(self.field, 'to_json'):
|
if hasattr(self.field, "to_json"):
|
||||||
value = self.field.to_json(value)
|
value = self.field.to_json(value)
|
||||||
|
|
||||||
if not value and self.field.blank and not self.field.null:
|
if not value and self.field.blank and not self.field.null:
|
||||||
|
@ -66,7 +82,7 @@ class JSONFieldMixin(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
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)
|
super(JSONFieldMixin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name, private_only=False):
|
def contribute_to_class(self, cls, name, private_only=False):
|
||||||
|
@ -81,8 +97,11 @@ class JSONFieldMixin(object):
|
||||||
setattr(cls, self.attname, descriptor)
|
setattr(cls, self.attname, descriptor)
|
||||||
|
|
||||||
if self.choices is not None:
|
if self.choices is not None:
|
||||||
setattr(cls, 'get_%s_display' % self.name,
|
setattr(
|
||||||
partialmethod(cls._get_FIELD_display, field=self))
|
cls,
|
||||||
|
"get_%s_display" % self.name,
|
||||||
|
partialmethod(cls._get_FIELD_display, field=self),
|
||||||
|
)
|
||||||
|
|
||||||
def get_lookup(self, lookup_name):
|
def get_lookup(self, lookup_name):
|
||||||
# Always return None, to make get_transform been called
|
# Always return None, to make get_transform been called
|
||||||
|
@ -101,15 +120,17 @@ class JSONFieldMixin(object):
|
||||||
lhs.output_field = self.json_field
|
lhs.output_field = self.json_field
|
||||||
transform = self.transform(lhs, **kwargs)
|
transform = self.transform(lhs, **kwargs)
|
||||||
transform._original_get_lookup = transform.get_lookup
|
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
|
return transform
|
||||||
|
|
||||||
json_field = self.model._meta.get_field(self.json_field_name)
|
json_field = self.model._meta.get_field(self.json_field_name)
|
||||||
transform = json_field.get_transform(self.name)
|
transform = json_field.get_transform(self.name)
|
||||||
if transform is None:
|
if transform is None:
|
||||||
raise FieldError(
|
raise FieldError(
|
||||||
"JSONField '%s' has no support for key '%s' %s lookup" %
|
"JSONField '%s' has no support for key '%s' %s lookup"
|
||||||
(self.json_field_name, self.name, name)
|
% (self.json_field_name, self.name, name)
|
||||||
)
|
)
|
||||||
|
|
||||||
return TransformFactoryWrapper(json_field, transform, name)
|
return TransformFactoryWrapper(json_field, transform, name)
|
||||||
|
@ -118,13 +139,16 @@ class JSONFieldMixin(object):
|
||||||
class BooleanField(JSONFieldMixin, fields.BooleanField):
|
class BooleanField(JSONFieldMixin, fields.BooleanField):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(BooleanField, self).__init__(*args, **kwargs)
|
super(BooleanField, self).__init__(*args, **kwargs)
|
||||||
if django.VERSION < (2, ):
|
if django.VERSION < (2,):
|
||||||
self.blank = False
|
self.blank = False
|
||||||
|
|
||||||
|
|
||||||
class CharField(JSONFieldMixin, fields.CharField):
|
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
|
return value if isinstance(value, str) else None
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -133,7 +157,7 @@ class DateField(JSONFieldMixin, fields.DateField):
|
||||||
if value:
|
if value:
|
||||||
if not isinstance(value, (datetime, date)):
|
if not isinstance(value, (datetime, date)):
|
||||||
value = dateparse.parse_date(value)
|
value = dateparse.parse_date(value)
|
||||||
return value.strftime('%Y-%m-%d')
|
return value.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
def from_json(self, value):
|
def from_json(self, value):
|
||||||
if value is not None:
|
if value is not None:
|
||||||
|
|
|
@ -15,94 +15,95 @@ from users.models import User
|
||||||
|
|
||||||
|
|
||||||
class SiteName(models.TextChoices):
|
class SiteName(models.TextChoices):
|
||||||
Douban = 'douban', _('豆瓣')
|
Douban = "douban", _("豆瓣")
|
||||||
Goodreads = 'goodreads', _('Goodreads')
|
Goodreads = "goodreads", _("Goodreads")
|
||||||
GoogleBooks = 'googlebooks', _('谷歌图书')
|
GoogleBooks = "googlebooks", _("谷歌图书")
|
||||||
IMDB = 'imdb', _('IMDB')
|
IMDB = "imdb", _("IMDB")
|
||||||
TMDB = 'tmdb', _('The Movie Database')
|
TMDB = "tmdb", _("The Movie Database")
|
||||||
Bandcamp = 'bandcamp', _('Bandcamp')
|
Bandcamp = "bandcamp", _("Bandcamp")
|
||||||
Spotify = 'spotify', _('Spotify')
|
Spotify = "spotify", _("Spotify")
|
||||||
IGDB = 'igdb', _('IGDB')
|
IGDB = "igdb", _("IGDB")
|
||||||
Steam = 'steam', _('Steam')
|
Steam = "steam", _("Steam")
|
||||||
Bangumi = 'bangumi', _('Bangumi')
|
Bangumi = "bangumi", _("Bangumi")
|
||||||
ApplePodcast = 'apple_podcast', _('苹果播客')
|
ApplePodcast = "apple_podcast", _("苹果播客")
|
||||||
|
|
||||||
|
|
||||||
class IdType(models.TextChoices):
|
class IdType(models.TextChoices):
|
||||||
WikiData = 'wikidata', _('维基数据')
|
WikiData = "wikidata", _("维基数据")
|
||||||
ISBN10 = 'isbn10', _('ISBN10')
|
ISBN10 = "isbn10", _("ISBN10")
|
||||||
ISBN = 'isbn', _('ISBN') # ISBN 13
|
ISBN = "isbn", _("ISBN") # ISBN 13
|
||||||
ASIN = 'asin', _('ASIN')
|
ASIN = "asin", _("ASIN")
|
||||||
ISSN = 'issn', _('ISSN')
|
ISSN = "issn", _("ISSN")
|
||||||
CUBN = 'cubn', _('统一书号')
|
CUBN = "cubn", _("统一书号")
|
||||||
ISRC = 'isrc', _('ISRC') # only for songs
|
ISRC = "isrc", _("ISRC") # only for songs
|
||||||
GTIN = 'gtin', _('GTIN UPC EAN码') # ISBN is separate
|
GTIN = "gtin", _("GTIN UPC EAN码") # ISBN is separate
|
||||||
Feed = 'feed', _('Feed URL')
|
Feed = "feed", _("Feed URL")
|
||||||
IMDB = 'imdb', _('IMDb')
|
IMDB = "imdb", _("IMDb")
|
||||||
TMDB_TV = 'tmdb_tv', _('TMDB剧集')
|
TMDB_TV = "tmdb_tv", _("TMDB剧集")
|
||||||
TMDB_TVSeason = 'tmdb_tvseason', _('TMDB剧集')
|
TMDB_TVSeason = "tmdb_tvseason", _("TMDB剧集")
|
||||||
TMDB_TVEpisode = 'tmdb_tvepisode', _('TMDB剧集')
|
TMDB_TVEpisode = "tmdb_tvepisode", _("TMDB剧集")
|
||||||
TMDB_Movie = 'tmdb_movie', _('TMDB电影')
|
TMDB_Movie = "tmdb_movie", _("TMDB电影")
|
||||||
Goodreads = 'goodreads', _('Goodreads')
|
Goodreads = "goodreads", _("Goodreads")
|
||||||
Goodreads_Work = 'goodreads_work', _('Goodreads著作')
|
Goodreads_Work = "goodreads_work", _("Goodreads著作")
|
||||||
GoogleBooks = 'googlebooks', _('谷歌图书')
|
GoogleBooks = "googlebooks", _("谷歌图书")
|
||||||
DoubanBook = 'doubanbook', _('豆瓣读书')
|
DoubanBook = "doubanbook", _("豆瓣读书")
|
||||||
DoubanBook_Work = 'doubanbook_work', _('豆瓣读书著作')
|
DoubanBook_Work = "doubanbook_work", _("豆瓣读书著作")
|
||||||
DoubanMovie = 'doubanmovie', _('豆瓣电影')
|
DoubanMovie = "doubanmovie", _("豆瓣电影")
|
||||||
DoubanMusic = 'doubanmusic', _('豆瓣音乐')
|
DoubanMusic = "doubanmusic", _("豆瓣音乐")
|
||||||
DoubanGame = 'doubangame', _('豆瓣游戏')
|
DoubanGame = "doubangame", _("豆瓣游戏")
|
||||||
DoubanDrama = 'doubandrama', _('豆瓣舞台剧')
|
DoubanDrama = "doubandrama", _("豆瓣舞台剧")
|
||||||
Bandcamp = 'bandcamp', _('Bandcamp')
|
Bandcamp = "bandcamp", _("Bandcamp")
|
||||||
Spotify_Album = 'spotify_album', _('Spotify专辑')
|
Spotify_Album = "spotify_album", _("Spotify专辑")
|
||||||
Spotify_Show = 'spotify_show', _('Spotify播客')
|
Spotify_Show = "spotify_show", _("Spotify播客")
|
||||||
Discogs_Release = 'discogs_release', ('Discogs Release')
|
Discogs_Release = "discogs_release", ("Discogs Release")
|
||||||
Discogs_Master = 'discogs_master', ('Discogs Master')
|
Discogs_Master = "discogs_master", ("Discogs Master")
|
||||||
MusicBrainz = 'musicbrainz', ('MusicBrainz ID')
|
MusicBrainz = "musicbrainz", ("MusicBrainz ID")
|
||||||
DoubanBook_Author = 'doubanbook_author', _('豆瓣读书作者')
|
DoubanBook_Author = "doubanbook_author", _("豆瓣读书作者")
|
||||||
DoubanCelebrity = 'doubanmovie_celebrity', _('豆瓣电影影人')
|
DoubanCelebrity = "doubanmovie_celebrity", _("豆瓣电影影人")
|
||||||
Goodreads_Author = 'goodreads_author', _('Goodreads作者')
|
Goodreads_Author = "goodreads_author", _("Goodreads作者")
|
||||||
Spotify_Artist = 'spotify_artist', _('Spotify艺术家')
|
Spotify_Artist = "spotify_artist", _("Spotify艺术家")
|
||||||
TMDB_Person = 'tmdb_person', _('TMDB影人')
|
TMDB_Person = "tmdb_person", _("TMDB影人")
|
||||||
IGDB = 'igdb', _('IGDB游戏')
|
IGDB = "igdb", _("IGDB游戏")
|
||||||
Steam = 'steam', _('Steam游戏')
|
Steam = "steam", _("Steam游戏")
|
||||||
Bangumi = 'bangumi', _('Bangumi')
|
Bangumi = "bangumi", _("Bangumi")
|
||||||
ApplePodcast = 'apple_podcast', _('苹果播客')
|
ApplePodcast = "apple_podcast", _("苹果播客")
|
||||||
|
|
||||||
|
|
||||||
class ItemType(models.TextChoices):
|
class ItemType(models.TextChoices):
|
||||||
Book = 'book', _('书')
|
Book = "book", _("书")
|
||||||
TV = 'tv', _('剧集')
|
TV = "tv", _("剧集")
|
||||||
TVSeason = 'tvseason', _('剧集分季')
|
TVSeason = "tvseason", _("剧集分季")
|
||||||
TVEpisode = 'tvepisode', _('剧集分集')
|
TVEpisode = "tvepisode", _("剧集分集")
|
||||||
Movie = 'movie', _('电影')
|
Movie = "movie", _("电影")
|
||||||
Music = 'music', _('音乐')
|
Music = "music", _("音乐")
|
||||||
Game = 'game', _('游戏')
|
Game = "game", _("游戏")
|
||||||
Boardgame = 'boardgame', _('桌游')
|
Boardgame = "boardgame", _("桌游")
|
||||||
Podcast = 'podcast', _('播客')
|
Podcast = "podcast", _("播客")
|
||||||
FanFic = 'fanfic', _('网文')
|
FanFic = "fanfic", _("网文")
|
||||||
Performance = 'performance', _('演出')
|
Performance = "performance", _("演出")
|
||||||
Exhibition = 'exhibition', _('展览')
|
Exhibition = "exhibition", _("展览")
|
||||||
Collection = 'collection', _('收藏单')
|
Collection = "collection", _("收藏单")
|
||||||
|
|
||||||
|
|
||||||
class ItemCategory(models.TextChoices):
|
class ItemCategory(models.TextChoices):
|
||||||
Book = 'book', _('书')
|
Book = "book", _("书")
|
||||||
Movie = 'movie', _('电影')
|
Movie = "movie", _("电影")
|
||||||
TV = 'tv', _('剧集')
|
TV = "tv", _("剧集")
|
||||||
Music = 'music', _('音乐')
|
Music = "music", _("音乐")
|
||||||
Game = 'game', _('游戏')
|
Game = "game", _("游戏")
|
||||||
Boardgame = 'boardgame', _('桌游')
|
Boardgame = "boardgame", _("桌游")
|
||||||
Podcast = 'podcast', _('播客')
|
Podcast = "podcast", _("播客")
|
||||||
FanFic = 'fanfic', _('网文')
|
FanFic = "fanfic", _("网文")
|
||||||
Performance = 'performance', _('演出')
|
Performance = "performance", _("演出")
|
||||||
Exhibition = 'exhibition', _('展览')
|
Exhibition = "exhibition", _("展览")
|
||||||
Collection = 'collection', _('收藏单')
|
Collection = "collection", _("收藏单")
|
||||||
|
|
||||||
|
|
||||||
class SubItemType(models.TextChoices):
|
class SubItemType(models.TextChoices):
|
||||||
Season = 'season', _('剧集分季')
|
Season = "season", _("剧集分季")
|
||||||
Episode = 'episode', _('剧集分集')
|
Episode = "episode", _("剧集分集")
|
||||||
Version = 'version', _('版本')
|
Version = "version", _("版本")
|
||||||
|
|
||||||
|
|
||||||
# class CreditType(models.TextChoices):
|
# class CreditType(models.TextChoices):
|
||||||
# Author = 'author', _('作者')
|
# Author = 'author', _('作者')
|
||||||
|
@ -176,37 +177,64 @@ class Item(SoftDeleteMixin, PolymorphicModel):
|
||||||
category = None # subclass must specify this
|
category = None # subclass must specify this
|
||||||
demonstrative = None # subclass must specify this
|
demonstrative = None # subclass must specify this
|
||||||
uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
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="")
|
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_type = models.CharField(
|
||||||
primary_lookup_id_value = models.CharField(_("1234/tt789"), blank=False, null=True, max_length=1000)
|
_("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)
|
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)
|
created_time = models.DateTimeField(auto_now_add=True)
|
||||||
edited_time = models.DateTimeField(auto_now=True)
|
edited_time = models.DateTimeField(auto_now=True)
|
||||||
is_deleted = models.BooleanField(default=False, db_index=True)
|
is_deleted = models.BooleanField(default=False, db_index=True)
|
||||||
history = HistoricalRecords()
|
history = HistoricalRecords()
|
||||||
merged_to_item = models.ForeignKey('Item', null=True, on_delete=models.SET_NULL, default=None, related_name="merged_from_items")
|
merged_to_item = models.ForeignKey(
|
||||||
last_editor = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='+', null=True, blank=False)
|
"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:
|
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):
|
def clear(self):
|
||||||
self.primary_lookup_id_value = None
|
self.primary_lookup_id_value = None
|
||||||
self.primary_lookup_id_type = None
|
self.primary_lookup_id_type = None
|
||||||
|
|
||||||
def __str__(self):
|
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
|
@classmethod
|
||||||
def get_best_lookup_id(cls, lookup_ids):
|
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 = [
|
best_id_types = [
|
||||||
IdType.ISBN, IdType.CUBN, IdType.ASIN,
|
IdType.ISBN,
|
||||||
IdType.GTIN, IdType.ISRC, IdType.MusicBrainz,
|
IdType.CUBN,
|
||||||
|
IdType.ASIN,
|
||||||
|
IdType.GTIN,
|
||||||
|
IdType.ISRC,
|
||||||
|
IdType.MusicBrainz,
|
||||||
IdType.Feed,
|
IdType.Feed,
|
||||||
IdType.IMDB, IdType.TMDB_TVSeason
|
IdType.IMDB,
|
||||||
|
IdType.TMDB_TVSeason,
|
||||||
]
|
]
|
||||||
for t in best_id_types:
|
for t in best_id_types:
|
||||||
if lookup_ids.get(t):
|
if lookup_ids.get(t):
|
||||||
|
@ -215,11 +243,11 @@ class Item(SoftDeleteMixin, PolymorphicModel):
|
||||||
|
|
||||||
def merge(self, to_item):
|
def merge(self, to_item):
|
||||||
if to_item is None:
|
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:
|
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__:
|
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:
|
else:
|
||||||
self.merged_to_item = to_item
|
self.merged_to_item = to_item
|
||||||
|
|
||||||
|
@ -229,15 +257,15 @@ class Item(SoftDeleteMixin, PolymorphicModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
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
|
@property
|
||||||
def absolute_url(self):
|
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
|
@property
|
||||||
def api_url(self):
|
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
|
@property
|
||||||
def class_name(self):
|
def class_name(self):
|
||||||
|
@ -245,7 +273,7 @@ class Item(SoftDeleteMixin, PolymorphicModel):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_url(cls, url_or_b62):
|
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)))
|
return cls.objects.get(uid=uuid.UUID(int=base62.decode(b62)))
|
||||||
|
|
||||||
# def get_lookup_id(self, id_type: str) -> str:
|
# 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))
|
# ll = list(filter(lambda a, b: b, ll))
|
||||||
pass
|
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
|
@classmethod
|
||||||
def copy_metadata(cls, metadata):
|
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):
|
def has_cover(self):
|
||||||
return self.cover and self.cover != DEFAULT_ITEM_COVER
|
return self.cover and self.cover != DEFAULT_ITEM_COVER
|
||||||
|
@ -286,21 +321,38 @@ class Item(SoftDeleteMixin, PolymorphicModel):
|
||||||
|
|
||||||
|
|
||||||
class ItemLookupId(models.Model):
|
class ItemLookupId(models.Model):
|
||||||
item = models.ForeignKey(Item, null=True, on_delete=models.SET_NULL, related_name='lookup_ids')
|
item = models.ForeignKey(
|
||||||
id_type = models.CharField(_("源网站"), blank=True, choices=IdType.choices, max_length=50)
|
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)
|
id_value = models.CharField(_("源网站ID"), blank=True, max_length=1000)
|
||||||
raw_url = models.CharField(_("源网站ID"), blank=True, max_length=1000, unique=True)
|
raw_url = models.CharField(_("源网站ID"), blank=True, max_length=1000, unique=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [['id_type', 'id_value']]
|
unique_together = [["id_type", "id_value"]]
|
||||||
|
|
||||||
|
|
||||||
class ExternalResource(models.Model):
|
class ExternalResource(models.Model):
|
||||||
item = models.ForeignKey(Item, null=True, on_delete=models.SET_NULL, related_name='external_resources')
|
item = models.ForeignKey(
|
||||||
id_type = models.CharField(_("IdType of the source site"), blank=False, choices=IdType.choices, max_length=50)
|
Item, null=True, on_delete=models.SET_NULL, related_name="external_resources"
|
||||||
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)
|
id_type = models.CharField(
|
||||||
cover = models.ImageField(upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True)
|
_("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)
|
other_lookup_ids = models.JSONField(default=dict)
|
||||||
metadata = models.JSONField(default=dict)
|
metadata = models.JSONField(default=dict)
|
||||||
scraped_time = models.DateTimeField(null=True)
|
scraped_time = models.DateTimeField(null=True)
|
||||||
|
@ -310,10 +362,10 @@ class ExternalResource(models.Model):
|
||||||
related_resources = jsondata.ArrayField(null=False, blank=False, default=list)
|
related_resources = jsondata.ArrayField(null=False, blank=False, default=list)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [['id_type', 'id_value']]
|
unique_together = [["id_type", "id_value"]]
|
||||||
|
|
||||||
def __str__(self):
|
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
|
@property
|
||||||
def site_name(self):
|
def site_name(self):
|
||||||
|
@ -323,9 +375,12 @@ class ExternalResource(models.Model):
|
||||||
self.other_lookup_ids = resource_content.lookup_ids
|
self.other_lookup_ids = resource_content.lookup_ids
|
||||||
self.metadata = resource_content.metadata
|
self.metadata = resource_content.metadata
|
||||||
if resource_content.cover_image and resource_content.cover_image_extention:
|
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:
|
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.scraped_time = timezone.now()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
@ -340,11 +395,13 @@ class ExternalResource(models.Model):
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def get_preferred_model(self):
|
def get_preferred_model(self):
|
||||||
model = self.metadata.get('preferred_model')
|
model = self.metadata.get("preferred_model")
|
||||||
if 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:
|
if m:
|
||||||
return m.model_class()
|
return m.model_class()
|
||||||
else:
|
else:
|
||||||
raise ValueError(f'preferred model {model} does not exist')
|
raise ValueError(f"preferred model {model} does not exist")
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -244,7 +244,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="review-panel__actions">
|
<span class="review-panel__actions">
|
||||||
<a href="{% url 'journal:review_edit' item.uuid review.uuid %}">{% trans '编辑' %}</a>
|
<a href="{% url 'journal:review_edit' item.uuid review.uuid %}">{% trans '编辑' %}</a>
|
||||||
<a href="{% url 'journal:review_delete' review.uuid %}">{% trans '删除' %}</a>
|
<a href="{% url 'journal:review_delete' review.uuid %}?return_url={{ item.url }}">{% trans '删除' %}</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="review-panel__time">{{ review.edited_time }}</div>
|
<div class="review-panel__time">{{ review.edited_time }}</div>
|
||||||
|
|
|
@ -3,16 +3,17 @@ from easy_thumbnails.templatetags.thumbnail import thumbnail_url
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def thumb(source, alias):
|
def thumb(source, alias):
|
||||||
"""
|
"""
|
||||||
This filter modifies that from `easy_thumbnails` so that
|
This filter modifies that from `easy_thumbnails` so that
|
||||||
it can neglect .svg file.
|
it can neglect .svg file.
|
||||||
"""
|
"""
|
||||||
if source.url.endswith('.svg'):
|
if source.url.endswith(".svg"):
|
||||||
return source.url
|
return source.url
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return thumbnail_url(source, alias)
|
return thumbnail_url(source, alias)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return ''
|
return ""
|
||||||
|
|
|
@ -4,7 +4,6 @@ from users.models import User
|
||||||
from catalog.common.models import Item, ItemCategory
|
from catalog.common.models import Item, ItemCategory
|
||||||
from .mixins import UserOwnedObjectMixin
|
from .mixins import UserOwnedObjectMixin
|
||||||
from catalog.collection.models import Collection as CatalogCollection
|
from catalog.collection.models import Collection as CatalogCollection
|
||||||
from decimal import *
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from markdownx.models import MarkdownxField
|
from markdownx.models import MarkdownxField
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -24,12 +23,13 @@ from catalog.models import *
|
||||||
import mistune
|
import mistune
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
|
from catalog.common import jsondata
|
||||||
|
|
||||||
|
|
||||||
class VisibilityType(models.IntegerChoices):
|
class VisibilityType(models.IntegerChoices):
|
||||||
Public = 0, _('公开')
|
Public = 0, _("公开")
|
||||||
Follower_Only = 1, _('仅关注者')
|
Follower_Only = 1, _("仅关注者")
|
||||||
Private = 2, _('仅自己')
|
Private = 2, _("仅自己")
|
||||||
|
|
||||||
|
|
||||||
def q_visible_to(viewer, owner):
|
def q_visible_to(viewer, owner):
|
||||||
|
@ -44,7 +44,11 @@ def q_visible_to(viewer, owner):
|
||||||
|
|
||||||
|
|
||||||
def query_visible(user):
|
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):
|
def query_following(user):
|
||||||
|
@ -63,14 +67,26 @@ def query_item_category(item_category):
|
||||||
|
|
||||||
|
|
||||||
class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
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)
|
uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
||||||
owner = models.ForeignKey(User, on_delete=models.PROTECT)
|
owner = models.ForeignKey(User, on_delete=models.PROTECT)
|
||||||
visibility = models.PositiveSmallIntegerField(default=0) # 0: Public / 1: Follower only / 2: Self only
|
visibility = models.PositiveSmallIntegerField(
|
||||||
created_time = models.DateTimeField(default=timezone.now) # auto_now_add=True FIXME revert this after migration
|
default=0
|
||||||
edited_time = models.DateTimeField(default=timezone.now) # auto_now=True FIXME revert this after migration
|
) # 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)
|
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
|
@property
|
||||||
def uuid(self):
|
def uuid(self):
|
||||||
|
@ -78,7 +94,7 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
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
|
@property
|
||||||
def absolute_url(self):
|
def absolute_url(self):
|
||||||
|
@ -86,7 +102,7 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def api_url(self):
|
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):
|
class Content(Piece):
|
||||||
|
@ -106,7 +122,7 @@ class Content(Piece):
|
||||||
|
|
||||||
|
|
||||||
class Like(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
|
@staticmethod
|
||||||
def user_like_piece(user, piece):
|
def user_like_piece(user, piece):
|
||||||
|
@ -117,8 +133,14 @@ class Like(Piece):
|
||||||
like = Like.objects.create(owner=user, target=piece)
|
like = Like.objects.create(owner=user, target=piece)
|
||||||
return like
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -133,7 +155,9 @@ class Comment(Content):
|
||||||
comment.delete()
|
comment.delete()
|
||||||
comment = None
|
comment = None
|
||||||
elif comment is 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:
|
elif comment.text != text or comment.visibility != visibility:
|
||||||
comment.text = text
|
comment.text = text
|
||||||
comment.visibility = visibility
|
comment.visibility = visibility
|
||||||
|
@ -142,7 +166,7 @@ class Comment(Content):
|
||||||
|
|
||||||
|
|
||||||
class Review(Content):
|
class Review(Content):
|
||||||
url_path = 'review'
|
url_path = "review"
|
||||||
title = models.CharField(max_length=500, blank=False, null=False)
|
title = models.CharField(max_length=500, blank=False, null=False)
|
||||||
body = MarkdownxField()
|
body = MarkdownxField()
|
||||||
|
|
||||||
|
@ -154,10 +178,17 @@ class Review(Content):
|
||||||
def rating_grade(self):
|
def rating_grade(self):
|
||||||
return Rating.get_item_rating_by_user(self.item, self.owner)
|
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):
|
def review_item_by_user(item, user, title, body, metadata={}, visibility=0):
|
||||||
# allow multiple reviews per item per user.
|
# 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()
|
review = Review.objects.filter(owner=user, item=item).first()
|
||||||
if title is None:
|
if title is None:
|
||||||
|
@ -176,36 +207,44 @@ class Review(Content):
|
||||||
|
|
||||||
|
|
||||||
class Rating(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):
|
def get_rating_for_item(item):
|
||||||
stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(average=Avg('grade'), count=Count('item'))
|
stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(
|
||||||
return stat['average'] if stat['count'] >= 5 else None
|
average=Avg("grade"), count=Count("item")
|
||||||
|
)
|
||||||
|
return stat["average"] if stat["count"] >= 5 else None
|
||||||
|
|
||||||
@ staticmethod
|
@staticmethod
|
||||||
def get_rating_count_for_item(item):
|
def get_rating_count_for_item(item):
|
||||||
stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(count=Count('item'))
|
stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(
|
||||||
return stat['count']
|
count=Count("item")
|
||||||
|
)
|
||||||
|
return stat["count"]
|
||||||
|
|
||||||
@ staticmethod
|
@staticmethod
|
||||||
def rate_item_by_user(item, user, rating_grade, visibility=0):
|
def rate_item_by_user(item, user, rating_grade, visibility=0):
|
||||||
if rating_grade and (rating_grade < 1 or rating_grade > 10):
|
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()
|
rating = Rating.objects.filter(owner=user, item=item).first()
|
||||||
if not rating_grade:
|
if not rating_grade:
|
||||||
if rating:
|
if rating:
|
||||||
rating.delete()
|
rating.delete()
|
||||||
rating = None
|
rating = None
|
||||||
elif rating is 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:
|
elif rating.grade != rating_grade or rating.visibility != visibility:
|
||||||
rating.visibility = visibility
|
rating.visibility = visibility
|
||||||
rating.grade = rating_grade
|
rating.grade = rating_grade
|
||||||
rating.save()
|
rating.save()
|
||||||
return rating
|
return rating
|
||||||
|
|
||||||
@ staticmethod
|
@staticmethod
|
||||||
def get_item_rating_by_user(item, user):
|
def get_item_rating_by_user(item, user):
|
||||||
rating = Rating.objects.filter(owner=user, item=item).first()
|
rating = Rating.objects.filter(owner=user, item=item).first()
|
||||||
return rating.grade if rating else None
|
return rating.grade if rating else None
|
||||||
|
@ -216,7 +255,9 @@ Item.rating_count = property(Rating.get_rating_count_for_item)
|
||||||
|
|
||||||
|
|
||||||
class Reply(Piece):
|
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)
|
title = models.CharField(max_length=500, null=True)
|
||||||
body = MarkdownxField()
|
body = MarkdownxField()
|
||||||
pass
|
pass
|
||||||
|
@ -234,7 +275,9 @@ class List(Piece):
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
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):
|
def save(self, *args, **kwargs):
|
||||||
self._owner = self.owner
|
self._owner = self.owner
|
||||||
|
@ -246,43 +289,56 @@ class List(Piece):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ordered_members(self):
|
def ordered_members(self):
|
||||||
return self.members.all().order_by('position', 'item_id')
|
return self.members.all().order_by("position")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ordered_items(self):
|
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
|
@property
|
||||||
def recent_items(self):
|
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
|
@property
|
||||||
def recent_members(self):
|
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):
|
def get_member_for_item(self, item):
|
||||||
return self.members.filter(item=item).count() > 0
|
return self.members.filter(item=item).first()
|
||||||
|
|
||||||
def append_item(self, item, **params):
|
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
|
return None
|
||||||
else:
|
else:
|
||||||
ml = self.ordered_members
|
ml = self.ordered_members
|
||||||
p = {'parent': self}
|
p = {"parent": self}
|
||||||
p.update(params)
|
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)
|
member = self.MEMBER_CLASS.objects.create(
|
||||||
list_add.send(sender=self.__class__, instance=self, item=item, member=member)
|
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
|
return member
|
||||||
|
|
||||||
def remove_item(self, item):
|
def remove_item(self, item):
|
||||||
member = self.members.all().filter(item=item).first()
|
member = self.get_member_for_item(item)
|
||||||
if member:
|
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()
|
member.delete()
|
||||||
|
|
||||||
def move_up_item(self, item):
|
def move_up_item(self, item):
|
||||||
members = self.ordered_members
|
members = self.ordered_members
|
||||||
member = members.filter(item=item).first()
|
member = self.get_member_for_item(item)
|
||||||
if member:
|
if member:
|
||||||
other = members.filter(position__lt=member.position).last()
|
other = members.filter(position__lt=member.position).last()
|
||||||
if other:
|
if other:
|
||||||
|
@ -294,7 +350,7 @@ class List(Piece):
|
||||||
|
|
||||||
def move_down_item(self, item):
|
def move_down_item(self, item):
|
||||||
members = self.ordered_members
|
members = self.ordered_members
|
||||||
member = members.filter(item=item).first()
|
member = self.get_member_for_item(item)
|
||||||
if member:
|
if member:
|
||||||
other = members.filter(position__gt=member.position).first()
|
other = members.filter(position__gt=member.position).first()
|
||||||
if other:
|
if other:
|
||||||
|
@ -304,6 +360,12 @@ class List(Piece):
|
||||||
other.save()
|
other.save()
|
||||||
member.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):
|
class ListMember(Piece):
|
||||||
"""
|
"""
|
||||||
|
@ -312,6 +374,7 @@ class ListMember(Piece):
|
||||||
|
|
||||||
parent = models.ForeignKey('List', related_name='members', on_delete=models.CASCADE)
|
parent = models.ForeignKey('List', related_name='members', on_delete=models.CASCADE)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
item = models.ForeignKey(Item, on_delete=models.PROTECT)
|
item = models.ForeignKey(Item, on_delete=models.PROTECT)
|
||||||
position = models.PositiveIntegerField()
|
position = models.PositiveIntegerField()
|
||||||
|
|
||||||
|
@ -325,7 +388,7 @@ class ListMember(Piece):
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
class ShelfType(models.TextChoices):
|
||||||
WISHLIST = ('wishlist', '未开始')
|
WISHLIST = ("wishlist", "未开始")
|
||||||
PROGRESS = ('progress', '进行中')
|
PROGRESS = ("progress", "进行中")
|
||||||
COMPLETE = ('complete', '完成')
|
COMPLETE = ("complete", "完成")
|
||||||
# DISCARDED = ('discarded', '放弃')
|
# DISCARDED = ('discarded', '放弃')
|
||||||
|
|
||||||
|
|
||||||
ShelfTypeNames = [
|
ShelfTypeNames = [
|
||||||
[ItemCategory.Book, ShelfType.WISHLIST, _('想读')],
|
[ItemCategory.Book, ShelfType.WISHLIST, _("想读")],
|
||||||
[ItemCategory.Book, ShelfType.PROGRESS, _('在读')],
|
[ItemCategory.Book, ShelfType.PROGRESS, _("在读")],
|
||||||
[ItemCategory.Book, ShelfType.COMPLETE, _('读过')],
|
[ItemCategory.Book, ShelfType.COMPLETE, _("读过")],
|
||||||
[ItemCategory.Movie, ShelfType.WISHLIST, _('想看')],
|
[ItemCategory.Movie, ShelfType.WISHLIST, _("想看")],
|
||||||
[ItemCategory.Movie, ShelfType.PROGRESS, _('在看')],
|
[ItemCategory.Movie, ShelfType.PROGRESS, _("在看")],
|
||||||
[ItemCategory.Movie, ShelfType.COMPLETE, _('看过')],
|
[ItemCategory.Movie, ShelfType.COMPLETE, _("看过")],
|
||||||
[ItemCategory.TV, ShelfType.WISHLIST, _('想看')],
|
[ItemCategory.TV, ShelfType.WISHLIST, _("想看")],
|
||||||
[ItemCategory.TV, ShelfType.PROGRESS, _('在看')],
|
[ItemCategory.TV, ShelfType.PROGRESS, _("在看")],
|
||||||
[ItemCategory.TV, ShelfType.COMPLETE, _('看过')],
|
[ItemCategory.TV, ShelfType.COMPLETE, _("看过")],
|
||||||
[ItemCategory.Music, ShelfType.WISHLIST, _('想听')],
|
[ItemCategory.Music, ShelfType.WISHLIST, _("想听")],
|
||||||
[ItemCategory.Music, ShelfType.PROGRESS, _('在听')],
|
[ItemCategory.Music, ShelfType.PROGRESS, _("在听")],
|
||||||
[ItemCategory.Music, ShelfType.COMPLETE, _('听过')],
|
[ItemCategory.Music, ShelfType.COMPLETE, _("听过")],
|
||||||
[ItemCategory.Game, ShelfType.WISHLIST, _('想玩')],
|
[ItemCategory.Game, ShelfType.WISHLIST, _("想玩")],
|
||||||
[ItemCategory.Game, ShelfType.PROGRESS, _('在玩')],
|
[ItemCategory.Game, ShelfType.PROGRESS, _("在玩")],
|
||||||
[ItemCategory.Game, ShelfType.COMPLETE, _('玩过')],
|
[ItemCategory.Game, ShelfType.COMPLETE, _("玩过")],
|
||||||
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ShelfMember(ListMember):
|
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 Shelf(List):
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [['_owner', 'item_category', 'shelf_type']]
|
unique_together = [["_owner", "item_category", "shelf_type"]]
|
||||||
|
|
||||||
MEMBER_CLASS = ShelfMember
|
MEMBER_CLASS = ShelfMember
|
||||||
items = models.ManyToManyField(Item, through='ShelfMember', related_name="+")
|
items = models.ManyToManyField(Item, through="ShelfMember", related_name="+")
|
||||||
item_category = models.CharField(choices=ItemCategory.choices, max_length=100, null=False, blank=False)
|
item_category = models.CharField(
|
||||||
shelf_type = models.CharField(choices=ShelfType.choices, max_length=100, null=False, blank=False)
|
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):
|
def __str__(self):
|
||||||
return f'{self.id} {self.title}'
|
return f"{self.id} {self.title}"
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def item_category_label(self):
|
def item_category_label(self):
|
||||||
|
@ -383,26 +450,41 @@ class Shelf(List):
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def shelf_label(self):
|
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
|
@cached_property
|
||||||
def title(self):
|
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 q
|
||||||
# return _("{user}'s {shelf_name}").format(user=self.owner.mastodon_username, shelf_name=q)
|
# return _("{user}'s {shelf_name}").format(user=self.owner.mastodon_username, shelf_name=q)
|
||||||
|
|
||||||
|
|
||||||
class ShelfLogEntry(models.Model):
|
class ShelfLogEntry(models.Model):
|
||||||
owner = models.ForeignKey(User, on_delete=models.PROTECT)
|
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)
|
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)
|
metadata = models.JSONField(default=dict)
|
||||||
created_time = models.DateTimeField(auto_now_add=True)
|
created_time = models.DateTimeField(auto_now_add=True)
|
||||||
edited_time = models.DateTimeField(auto_now=True)
|
edited_time = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
def __str__(self):
|
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:
|
class ShelfManager:
|
||||||
|
@ -422,12 +504,16 @@ class ShelfManager:
|
||||||
Shelf.objects.create(owner=self.owner, item_category=ic, shelf_type=qt)
|
Shelf.objects.create(owner=self.owner, item_category=ic, shelf_type=qt)
|
||||||
|
|
||||||
def _shelf_member_for_item(self, item):
|
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):
|
def _shelf_for_item_and_type(item, shelf_type):
|
||||||
if not item or not shelf_type:
|
if not item or not shelf_type:
|
||||||
return None
|
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):
|
def locate_item(self, item):
|
||||||
member = ShelfMember.objects.filter(owner=self.owner, item=item).first()
|
member = ShelfMember.objects.filter(owner=self.owner, item=item).first()
|
||||||
|
@ -437,7 +523,7 @@ class ShelfManager:
|
||||||
# shelf_type=None means remove from current shelf
|
# shelf_type=None means remove from current shelf
|
||||||
# metadata=None means no change
|
# metadata=None means no change
|
||||||
if not item:
|
if not item:
|
||||||
raise ValueError('empty item')
|
raise ValueError("empty item")
|
||||||
new_shelfmember = None
|
new_shelfmember = None
|
||||||
last_shelfmember = self._shelf_member_for_item(item)
|
last_shelfmember = self._shelf_member_for_item(item)
|
||||||
last_shelf = last_shelfmember.parent if last_shelfmember else None
|
last_shelf = last_shelfmember.parent if last_shelfmember else None
|
||||||
|
@ -450,9 +536,11 @@ class ShelfManager:
|
||||||
if last_shelf:
|
if last_shelf:
|
||||||
last_shelf.remove_item(item)
|
last_shelf.remove_item(item)
|
||||||
if shelf:
|
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:
|
elif last_shelf is None:
|
||||||
raise ValueError('empty shelf')
|
raise ValueError("empty shelf")
|
||||||
else:
|
else:
|
||||||
new_shelfmember = last_shelfmember
|
new_shelfmember = last_shelfmember
|
||||||
if metadata is not None and metadata != last_metadata: # change metadata
|
if metadata is not None and metadata != last_metadata: # change metadata
|
||||||
|
@ -466,29 +554,41 @@ class ShelfManager:
|
||||||
if changed:
|
if changed:
|
||||||
if metadata is None:
|
if metadata is None:
|
||||||
metadata = last_metadata or {}
|
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
|
return new_shelfmember
|
||||||
|
|
||||||
def get_log(self):
|
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):
|
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):
|
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):
|
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
|
return shelf.members.all().order_by
|
||||||
|
|
||||||
@ staticmethod
|
@staticmethod
|
||||||
def get_manager_for_user(user):
|
def get_manager_for_user(user):
|
||||||
return ShelfManager(user)
|
return ShelfManager(user)
|
||||||
|
|
||||||
|
|
||||||
User.shelf_manager = cached_property(ShelfManager.get_manager_for_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):
|
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
|
note = jsondata.CharField(_("备注"), null=True, blank=True)
|
||||||
def note(self):
|
|
||||||
return self.metadata.get('comment')
|
|
||||||
|
|
||||||
|
|
||||||
class Collection(List):
|
class Collection(List):
|
||||||
url_path = 'collection'
|
url_path = "collection"
|
||||||
MEMBER_CLASS = CollectionMember
|
MEMBER_CLASS = CollectionMember
|
||||||
catalog_item = models.OneToOneField(CatalogCollection, on_delete=models.PROTECT)
|
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="")
|
brief = models.TextField(_("简介"), blank=True, default="")
|
||||||
cover = models.ImageField(upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True)
|
cover = models.ImageField(
|
||||||
items = models.ManyToManyField(Item, through='CollectionMember', related_name="collections")
|
upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True
|
||||||
collaborative = models.PositiveSmallIntegerField(default=0) # 0: Editable by owner only / 1: Editable by bi-direction followers
|
)
|
||||||
|
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
|
@property
|
||||||
def html(self):
|
def html(self):
|
||||||
|
@ -522,12 +630,15 @@ class Collection(List):
|
||||||
@property
|
@property
|
||||||
def plain_description(self):
|
def plain_description(self):
|
||||||
html = markdown(self.brief)
|
html = markdown(self.brief)
|
||||||
return RE_HTML_TAG.sub(' ', html)
|
return RE_HTML_TAG.sub(" ", html)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
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()
|
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.title = self.title
|
||||||
self.catalog_item.brief = self.brief
|
self.catalog_item.brief = self.brief
|
||||||
self.catalog_item.cover = self.cover
|
self.catalog_item.cover = self.cover
|
||||||
|
@ -541,72 +652,93 @@ Tag
|
||||||
|
|
||||||
|
|
||||||
class TagMember(ListMember):
|
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):
|
class Tag(List):
|
||||||
MEMBER_CLASS = TagMember
|
MEMBER_CLASS = TagMember
|
||||||
items = models.ManyToManyField(Item, through='TagMember')
|
items = models.ManyToManyField(Item, through="TagMember")
|
||||||
title = models.CharField(max_length=100, null=False, blank=False, validators=TagValidators)
|
title = models.CharField(
|
||||||
|
max_length=100, null=False, blank=False, validators=TagValidators
|
||||||
|
)
|
||||||
# TODO case convert and space removal on save
|
# TODO case convert and space removal on save
|
||||||
# TODO check on save
|
# TODO check on save
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [['_owner', 'title']]
|
unique_together = [["_owner", "title"]]
|
||||||
|
|
||||||
@ staticmethod
|
@staticmethod
|
||||||
def cleanup_title(title):
|
def cleanup_title(title):
|
||||||
return title.strip().lower()
|
return title.strip().lower()
|
||||||
|
|
||||||
|
|
||||||
class TagManager:
|
class TagManager:
|
||||||
@ staticmethod
|
@staticmethod
|
||||||
def public_tags_for_item(item):
|
def public_tags_for_item(item):
|
||||||
tags = item.tag_set.all().filter(visibility=0).values('title').annotate(frequency=Count('owner')).order_by('-frequency')[: 20]
|
tags = (
|
||||||
return sorted(list(map(lambda t: t['title'], 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):
|
def all_tags_for_user(user):
|
||||||
tags = user.tag_set.all().values('title').annotate(frequency=Count('members__id')).order_by('-frequency')
|
tags = (
|
||||||
return list(map(lambda t: t['title'], 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):
|
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])
|
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:
|
for title in titles - current_titles:
|
||||||
tag = Tag.objects.filter(owner=user, title=title).first()
|
tag = Tag.objects.filter(owner=user, title=title).first()
|
||||||
if not tag:
|
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)
|
tag.append_item(item)
|
||||||
for title in current_titles - titles:
|
for title in current_titles - titles:
|
||||||
tag = Tag.objects.filter(owner=user, title=title).first()
|
tag = Tag.objects.filter(owner=user, title=title).first()
|
||||||
tag.remove_item(item)
|
tag.remove_item(item)
|
||||||
|
|
||||||
@ staticmethod
|
@staticmethod
|
||||||
def get_item_tags_by_user(item, user):
|
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
|
return current_titles
|
||||||
|
|
||||||
@ staticmethod
|
@staticmethod
|
||||||
def add_tag_by_user(item, tag_title, user, default_visibility=0):
|
def add_tag_by_user(item, tag_title, user, default_visibility=0):
|
||||||
title = Tag.cleanup_title(tag_title)
|
title = Tag.cleanup_title(tag_title)
|
||||||
tag = Tag.objects.filter(owner=user, title=title).first()
|
tag = Tag.objects.filter(owner=user, title=title).first()
|
||||||
if not tag:
|
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)
|
tag.append_item(item)
|
||||||
|
|
||||||
@ staticmethod
|
@staticmethod
|
||||||
def get_manager_for_user(user):
|
def get_manager_for_user(user):
|
||||||
return TagManager(user)
|
return TagManager(user)
|
||||||
|
|
||||||
def __init__(self, user):
|
def __init__(self, user):
|
||||||
self.owner = user
|
self.owner = user
|
||||||
|
|
||||||
@ property
|
@property
|
||||||
def all_tags(self):
|
def all_tags(self):
|
||||||
return TagManager.all_tags_for_user(self.owner)
|
return TagManager.all_tags_for_user(self.owner)
|
||||||
|
|
||||||
|
@ -615,83 +747,111 @@ class TagManager:
|
||||||
TagManager.add_tag_by_user(item, tag, self.owner, visibility)
|
TagManager.add_tag_by_user(item, tag, self.owner, visibility)
|
||||||
|
|
||||||
def get_item_tags(self, item):
|
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)
|
Item.tags = property(TagManager.public_tags_for_item)
|
||||||
User.tags = property(TagManager.all_tags_for_user)
|
User.tags = property(TagManager.all_tags_for_user)
|
||||||
User.tag_manager = cached_property(TagManager.get_manager_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:
|
class Mark:
|
||||||
""" this mimics previous mark behaviour """
|
"""this mimics previous mark behaviour"""
|
||||||
|
|
||||||
def __init__(self, user, item):
|
def __init__(self, user, item):
|
||||||
self.owner = user
|
self.owner = user
|
||||||
self.item = item
|
self.item = item
|
||||||
|
|
||||||
@ cached_property
|
@cached_property
|
||||||
def shelfmember(self):
|
def shelfmember(self):
|
||||||
return self.owner.shelf_manager.locate_item(self.item)
|
return self.owner.shelf_manager.locate_item(self.item)
|
||||||
|
|
||||||
@ property
|
@property
|
||||||
def id(self):
|
def id(self):
|
||||||
return self.shelfmember.id if self.shelfmember else None
|
return self.shelfmember.id if self.shelfmember else None
|
||||||
|
|
||||||
@ property
|
@property
|
||||||
def shelf(self):
|
def shelf(self):
|
||||||
return self.shelfmember.parent if self.shelfmember else None
|
return self.shelfmember.parent if self.shelfmember else None
|
||||||
|
|
||||||
@ property
|
@property
|
||||||
def shelf_type(self):
|
def shelf_type(self):
|
||||||
return self.shelfmember.parent.shelf_type if self.shelfmember else None
|
return self.shelfmember.parent.shelf_type if self.shelfmember else None
|
||||||
|
|
||||||
@ property
|
@property
|
||||||
def shelf_label(self):
|
def shelf_label(self):
|
||||||
return self.shelfmember.parent.shelf_label if self.shelfmember else None
|
return self.shelfmember.parent.shelf_label if self.shelfmember else None
|
||||||
|
|
||||||
@ property
|
@property
|
||||||
def created_time(self):
|
def created_time(self):
|
||||||
return self.shelfmember.created_time if self.shelfmember else None
|
return self.shelfmember.created_time if self.shelfmember else None
|
||||||
|
|
||||||
@ property
|
@property
|
||||||
def metadata(self):
|
def metadata(self):
|
||||||
return self.shelfmember.metadata if self.shelfmember else None
|
return self.shelfmember.metadata if self.shelfmember else None
|
||||||
|
|
||||||
@ property
|
@property
|
||||||
def visibility(self):
|
def visibility(self):
|
||||||
return self.shelfmember.visibility if self.shelfmember else None
|
return self.shelfmember.visibility if self.shelfmember else None
|
||||||
|
|
||||||
@ cached_property
|
@cached_property
|
||||||
def tags(self):
|
def tags(self):
|
||||||
return self.owner.tag_manager.get_item_tags(self.item)
|
return self.owner.tag_manager.get_item_tags(self.item)
|
||||||
|
|
||||||
@ cached_property
|
@cached_property
|
||||||
def rating(self):
|
def rating(self):
|
||||||
return Rating.get_item_rating_by_user(self.item, self.owner)
|
return Rating.get_item_rating_by_user(self.item, self.owner)
|
||||||
|
|
||||||
@ cached_property
|
@cached_property
|
||||||
def comment(self):
|
def comment(self):
|
||||||
return Comment.objects.filter(owner=self.owner, item=self.item).first()
|
return Comment.objects.filter(owner=self.owner, item=self.item).first()
|
||||||
|
|
||||||
@ property
|
@property
|
||||||
def text(self):
|
def text(self):
|
||||||
return self.comment.text if self.comment else None
|
return self.comment.text if self.comment else None
|
||||||
|
|
||||||
@ cached_property
|
@cached_property
|
||||||
def review(self):
|
def review(self):
|
||||||
return Review.objects.filter(owner=self.owner, item=self.item).first()
|
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):
|
def update(
|
||||||
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)
|
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:
|
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:
|
if self.shelfmember and created_time:
|
||||||
self.shelfmember.created_time = created_time
|
self.shelfmember.created_time = created_time
|
||||||
self.shelfmember.save()
|
self.shelfmember.save()
|
||||||
if comment_text != self.text or visibility != self.visibility:
|
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:
|
if rating_grade != self.rating or visibility != self.visibility:
|
||||||
Rating.rate_item_by_user(self.item, self.owner, rating_grade, visibility)
|
Rating.rate_item_by_user(self.item, self.owner, rating_grade, visibility)
|
||||||
self.rating = rating_grade
|
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,
|
# this is a bit hacky but let's keep it until move to implement ActivityPub,
|
||||||
# by then, we'll just change this to boost
|
# by then, we'll just change this to boost
|
||||||
from mastodon.api import share_mark
|
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.translated_status = self.shelf_label
|
||||||
self.save = lambda **args: None
|
self.save = lambda **args: None
|
||||||
if not share_mark(self):
|
if not share_mark(self):
|
||||||
raise ValueError("sharing failed")
|
raise ValueError("sharing failed")
|
||||||
if not self.shelfmember.metadata:
|
if not self.shelfmember.metadata:
|
||||||
self.shelfmember.metadata = {}
|
self.shelfmember.metadata = {}
|
||||||
if self.shelfmember.metadata.get('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.metadata["shared_link"] = self.shared_link
|
||||||
self.shelfmember.save()
|
self.shelfmember.save()
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
|
|
|
@ -33,13 +33,17 @@
|
||||||
<div class="dividing-line"></div>
|
<div class="dividing-line"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="single-section-wrapper">
|
<div class="single-section-wrapper">
|
||||||
<div class="entity-list" hx-get="{% url 'journal:collection_retrieve_items' collection.uuid %}?edit=1" hx-trigger="load"></div>
|
<div id="collection_items" class="entity-list" hx-get="{% url 'journal:collection_retrieve_items' collection.uuid %}?edit=1" hx-trigger="load"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{% include "partial/_footer.html" %}
|
{% include "partial/_footer.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
document.body.addEventListener('htmx:configRequest', (event) => {
|
||||||
|
event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
|
||||||
|
})
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load l10n %}
|
{% load l10n %}
|
||||||
<ul class="entity-list__entities">
|
<ul class="entity-list__entities">
|
||||||
{% for member in collection.members.all %}
|
{% for member in collection.ordered_members %}
|
||||||
{% with "list_item_"|add:member.item.class_name|add:".html" as template %}
|
{% with "list_item_"|add:member.item.class_name|add:".html" as template %}
|
||||||
{% include template with item=member.item mark=None collection_member=member %}
|
{% include template with item=member.item mark=None collection_member=member %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
5
journal/templates/collection_update_item_note.html
Normal file
5
journal/templates/collection_update_item_note.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<form hx-post="{% url 'journal:collection_update_item_note' collection.uuid item.uuid %}" hx-target="#collection_items">
|
||||||
|
<input name="note" value="{{ note }}">
|
||||||
|
<input type="submit" style="width:unset;" value="修改">
|
||||||
|
<button style="width:unset;" hx-get="{% url 'journal:collection_retrieve_items' collection.uuid %}?edit=1" hx-target="#collection_items">取消</button>
|
||||||
|
</form>
|
|
@ -19,12 +19,12 @@
|
||||||
{% if collection_edit %}
|
{% if collection_edit %}
|
||||||
<div class="collection-item-position-edit">
|
<div class="collection-item-position-edit">
|
||||||
{% if not forloop.first %}
|
{% if not forloop.first %}
|
||||||
<a hx-target=".entity-list" hx-post="{% url 'journal:collection_move_up_item' form.instance.uuid collection_member.id %}">▲</a>
|
<a hx-target=".entity-list" hx-post="{% url 'journal:collection_move_item' form.instance.uuid 'up' item.uuid %}">▲</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not forloop.last %}
|
{% if not forloop.last %}
|
||||||
<a hx-target=".entity-list" hx-post="{% url 'journal:collection_move_down_item' form.instance.uuid collection_member.id %}">▼</a>
|
<a hx-target=".entity-list" hx-post="{% url 'journal:collection_move_item' form.instance.uuid 'down' item.uuid %}">▼</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a hx-target=".entity-list" hx-post="{% url 'journal:collection_delete_item' form.instance.uuid collection_member.id %}">✖</a>
|
<a hx-target=".entity-list" hx-post="{% url 'journal:collection_remove_item' form.instance.uuid item.uuid %}">✖</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@
|
||||||
|
|
||||||
{{ collection_member.note }}
|
{{ collection_member.note }}
|
||||||
{% if collection_edit %}
|
{% if collection_edit %}
|
||||||
<a class="action-icon" hx-get="{% url 'journal:collection_update_item_note' collection.uuid collection_member.uuid %}"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M19,20H5a1,1,0,0,0,0,2H19a1,1,0,0,0,0-2Z"/><path d="M5,18h.09l4.17-.38a2,2,0,0,0,1.21-.57l9-9a1.92,1.92,0,0,0-.07-2.71h0L16.66,2.6A2,2,0,0,0,14,2.53l-9,9a2,2,0,0,0-.57,1.21L4,16.91a1,1,0,0,0,.29.8A1,1,0,0,0,5,18ZM15.27,4,18,6.73,16,8.68,13.32,6Zm-8.9,8.91L12,7.32l2.7,2.7-5.6,5.6-3,.28Z"/></g></svg></a>
|
<a class="action-icon" hx-get="{% url 'journal:collection_update_item_note' collection.uuid item.uuid %}"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M19,20H5a1,1,0,0,0,0,2H19a1,1,0,0,0,0-2Z"/><path d="M5,18h.09l4.17-.38a2,2,0,0,0,1.21-.57l9-9a1.92,1.92,0,0,0-.07-2.71h0L16.66,2.6A2,2,0,0,0,14,2.53l-9,9a2,2,0,0,0-.57,1.21L4,16.91a1,1,0,0,0,.29.8A1,1,0,0,0,5,18ZM15.27,4,18,6.73,16,8.68,13.32,6Zm-8.9,8.91L12,7.32l2.7,2.7-5.6,5.6-3,.28Z"/></g></svg></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
59
journal/templates/piece_delete.html
Normal file
59
journal/templates/piece_delete.html
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load admin_url %}
|
||||||
|
{% load mastodon %}
|
||||||
|
{% load oauth_token %}
|
||||||
|
{% load truncate %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ site_name }} - {% trans '确认删除' %}</title>
|
||||||
|
{% include "common_libs.html" with jquery=0 %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="page-wrapper">
|
||||||
|
<div id="content-wrapper">
|
||||||
|
{% include "partial/_navbar.html" %}
|
||||||
|
<section id="content">
|
||||||
|
<div class="grid">
|
||||||
|
<div class="single-section-wrapper" id="main">
|
||||||
|
<h5>{% trans '确认删除吗?' %}</h5>
|
||||||
|
<div class="dividing-line"></div>
|
||||||
|
<div class="review-head">
|
||||||
|
<h5 class="review-head__title">
|
||||||
|
{{ piece.title }}
|
||||||
|
</h5>
|
||||||
|
{% if piece.visibility > 0 %}
|
||||||
|
<span class="icon-lock">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20">
|
||||||
|
<path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
<div class="review-head__body">
|
||||||
|
<div class="review-head__info">
|
||||||
|
<a href="{% url 'users:home' piece.owner.mastodon_username %}" class="review-head__owner-link">{{ piece.owner.username }}</a>
|
||||||
|
<span class="review-head__time">{{ piece.edited_time }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dividing-line"></div>
|
||||||
|
<div class="clearfix">
|
||||||
|
<form action="?return_url={{ return_url }}" method="post" class="float-right">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input class="button" type="submit" value="{% trans '确认' %}">
|
||||||
|
</form>
|
||||||
|
<button onclick="history.back()" class="button button-clear float-right">{% trans '返回' %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% include "partial/_footer.html" %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -52,12 +52,9 @@
|
||||||
<ul class="entity-sort__entity-list">
|
<ul class="entity-sort__entity-list">
|
||||||
{% for member in shelf.members %}
|
{% for member in shelf.members %}
|
||||||
<li class="entity-sort__entity">
|
<li class="entity-sort__entity">
|
||||||
|
|
||||||
<a href="{{ member.item.url }}">
|
<a href="{{ member.item.url }}">
|
||||||
<img src="{{ member.item.cover|thumb:'normal' }}"
|
<img src="{{ member.item.cover.url }}" alt="{{ member.item.title }}" class="entity-sort__entity-img">
|
||||||
alt="{{ member.item.title }}" class="entity-sort__entity-img">
|
<div class="entity-sort__entity-name" title="{{ member.item.title }}"> {{ member.item.title }}</div>
|
||||||
<div class="entity-sort__entity-name" title="{{ member.item.title }}">
|
|
||||||
{{ member.item.title }}</div>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load admin_url %}
|
|
||||||
{% load mastodon %}
|
|
||||||
{% load oauth_token %}
|
|
||||||
{% load truncate %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{{ site_name }} - {% trans '删除评论' %}</title>
|
|
||||||
<script src="https://cdn.staticfile.org/jquery/3.6.1/jquery.min.js"></script>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
|
||||||
<link rel="stylesheet" href="{% static 'css/boofilsic.min.css' %}">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="page-wrapper">
|
|
||||||
<div id="content-wrapper">
|
|
||||||
{% include "partial/_navbar.html" %}
|
|
||||||
|
|
||||||
<section id="content">
|
|
||||||
<div class="grid">
|
|
||||||
<div class="single-section-wrapper" id="main">
|
|
||||||
<h5>{% trans '确认删除这篇评论吗?' %}</h5>
|
|
||||||
|
|
||||||
<div class="dividing-line"></div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="review-head">
|
|
||||||
|
|
||||||
<h5 class="review-head__title">
|
|
||||||
{{ review.title }}
|
|
||||||
</h5>
|
|
||||||
{% if review.visibility > 0 %}
|
|
||||||
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"><svg xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20">
|
|
||||||
<path
|
|
||||||
d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z" />
|
|
||||||
</svg></span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="review-head__body">
|
|
||||||
<div class="review-head__info">
|
|
||||||
|
|
||||||
<a href="{% url 'users:home' review.owner.mastodon_username %}"
|
|
||||||
class="review-head__owner-link">{{ review.owner.username }}</a>
|
|
||||||
|
|
||||||
<span class="review-head__time">{{ review.edited_time }}</span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div id="rawContent" class="delete-preview">
|
|
||||||
{{ form.body }}
|
|
||||||
</div>
|
|
||||||
{{ form.media }}
|
|
||||||
|
|
||||||
<div class="dividing-line"></div>
|
|
||||||
|
|
||||||
<div class="clearfix">
|
|
||||||
<form action="{% url 'journal:review_delete' review.uuid %}" method="post" class="float-right">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input class="button" type="submit" value="{% trans '确认' %}">
|
|
||||||
</form>
|
|
||||||
<button onclick="history.back()"
|
|
||||||
class="button button-clear float-right">{% trans '返回' %}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% include "partial/_footer.html" %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
$(".markdownx textarea").hide();
|
|
||||||
$(".markdownx .markdownx-preview").show();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
|
||||||
</html>
|
|
90
journal/templates/user_collection_list.html
Normal file
90
journal/templates/user_collection_list.html
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load l10n %}
|
||||||
|
{% load humanize %}
|
||||||
|
{% load admin_url %}
|
||||||
|
{% load mastodon %}
|
||||||
|
{% load oauth_token %}
|
||||||
|
{% load truncate %}
|
||||||
|
{% load highlight %}
|
||||||
|
{% load thumb %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ site_name }} - {{ user.mastodon_username }} - 收藏单</title>
|
||||||
|
{% include "common_libs.html" with jquery=0 %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="page-wrapper">
|
||||||
|
<div id="content-wrapper">
|
||||||
|
{% include "partial/_navbar.html" %}
|
||||||
|
|
||||||
|
<section id="content">
|
||||||
|
<div class="grid">
|
||||||
|
<div class="grid__main" id="main">
|
||||||
|
<div class="main-section-wrapper">
|
||||||
|
<div class="entity-reviews">
|
||||||
|
<h5 class="entity-reviews__title entity-reviews__title--stand-alone">
|
||||||
|
{{ title }}
|
||||||
|
</h5>
|
||||||
|
<ul class="entity-reviews__review-list">
|
||||||
|
|
||||||
|
{% for collection in collections %}
|
||||||
|
|
||||||
|
<li class="entity-reviews__review entity-reviews__review--wider">
|
||||||
|
<img src="{{ collection.cover.url }}" style="width:40px; float:right"class="entity-card__img">
|
||||||
|
<span class="entity-reviews__review-title"><a href="{{ collection.url }}">{{ collection.title }}</a></span>
|
||||||
|
<span class="entity-reviews__review-time">{{ collection.edited_time }}</span>
|
||||||
|
{% if collection.visibility > 0 %}
|
||||||
|
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<div>{% trans '无结果' %}</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="pagination">
|
||||||
|
|
||||||
|
{% if collections.pagination.has_prev %}
|
||||||
|
<a href="?page=1" class="pagination__nav-link pagination__nav-link">«</a>
|
||||||
|
<a href="?page={{ collections.previous_page_number }}"
|
||||||
|
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for page in collections.pagination.page_range %}
|
||||||
|
|
||||||
|
{% if page == collections.pagination.current_page %}
|
||||||
|
<a href="?page={{ page }}" class="pagination__page-link pagination__page-link--current">{{ page }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if collections.pagination.has_next %}
|
||||||
|
<a href="?page={{ collections.next_page_number }}"
|
||||||
|
class="pagination__nav-link pagination__nav-link--left-margin">›</a>
|
||||||
|
<a href="?page={{ collections.pagination.last_page }}" class="pagination__nav-link">»</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
{% include "partial/_footer.html" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
|
||||||
|
</html>
|
|
@ -15,13 +15,17 @@ class CollectionTest(TestCase):
|
||||||
collection = Collection.objects.create(title="test", owner=self.user)
|
collection = Collection.objects.create(title="test", owner=self.user)
|
||||||
collection = Collection.objects.filter(title="test", owner=self.user).first()
|
collection = Collection.objects.filter(title="test", owner=self.user).first()
|
||||||
self.assertEqual(collection.catalog_item.title, "test")
|
self.assertEqual(collection.catalog_item.title, "test")
|
||||||
collection.append_item(self.book1)
|
member1 = collection.append_item(self.book1)
|
||||||
|
member1.note = "my notes"
|
||||||
|
member1.save()
|
||||||
collection.append_item(self.book2)
|
collection.append_item(self.book2)
|
||||||
self.assertEqual(list(collection.ordered_items), [self.book1, self.book2])
|
self.assertEqual(list(collection.ordered_items), [self.book1, self.book2])
|
||||||
collection.move_up_item(self.book1)
|
collection.move_up_item(self.book1)
|
||||||
self.assertEqual(list(collection.ordered_items), [self.book1, self.book2])
|
self.assertEqual(list(collection.ordered_items), [self.book1, self.book2])
|
||||||
collection.move_up_item(self.book2)
|
collection.move_up_item(self.book2)
|
||||||
self.assertEqual(list(collection.ordered_items), [self.book2, self.book1])
|
self.assertEqual(list(collection.ordered_items), [self.book2, self.book1])
|
||||||
|
member1 = collection.get_member_for_item(self.book1)
|
||||||
|
self.assertEqual(member1.note, "my notes")
|
||||||
|
|
||||||
|
|
||||||
class ShelfTest(TestCase):
|
class ShelfTest(TestCase):
|
||||||
|
@ -47,26 +51,28 @@ class ShelfTest(TestCase):
|
||||||
shelf_manager.move_item(book1, ShelfType.PROGRESS)
|
shelf_manager.move_item(book1, ShelfType.PROGRESS)
|
||||||
self.assertEqual(q1.members.all().count(), 1)
|
self.assertEqual(q1.members.all().count(), 1)
|
||||||
self.assertEqual(q2.members.all().count(), 1)
|
self.assertEqual(q2.members.all().count(), 1)
|
||||||
shelf_manager.move_item(book1, ShelfType.PROGRESS, metadata={'progress': 1})
|
shelf_manager.move_item(book1, ShelfType.PROGRESS, metadata={"progress": 1})
|
||||||
self.assertEqual(q1.members.all().count(), 1)
|
self.assertEqual(q1.members.all().count(), 1)
|
||||||
self.assertEqual(q2.members.all().count(), 1)
|
self.assertEqual(q2.members.all().count(), 1)
|
||||||
log = shelf_manager.get_log_for_item(book1)
|
log = shelf_manager.get_log_for_item(book1)
|
||||||
self.assertEqual(log.count(), 3)
|
self.assertEqual(log.count(), 3)
|
||||||
shelf_manager.move_item(book1, ShelfType.PROGRESS, metadata={'progress': 1})
|
shelf_manager.move_item(book1, ShelfType.PROGRESS, metadata={"progress": 1})
|
||||||
log = shelf_manager.get_log_for_item(book1)
|
log = shelf_manager.get_log_for_item(book1)
|
||||||
self.assertEqual(log.count(), 3)
|
self.assertEqual(log.count(), 3)
|
||||||
shelf_manager.move_item(book1, ShelfType.PROGRESS, metadata={'progress': 10})
|
shelf_manager.move_item(book1, ShelfType.PROGRESS, metadata={"progress": 10})
|
||||||
log = shelf_manager.get_log_for_item(book1)
|
log = shelf_manager.get_log_for_item(book1)
|
||||||
self.assertEqual(log.count(), 4)
|
self.assertEqual(log.count(), 4)
|
||||||
shelf_manager.move_item(book1, ShelfType.PROGRESS)
|
shelf_manager.move_item(book1, ShelfType.PROGRESS)
|
||||||
log = shelf_manager.get_log_for_item(book1)
|
log = shelf_manager.get_log_for_item(book1)
|
||||||
self.assertEqual(log.count(), 4)
|
self.assertEqual(log.count(), 4)
|
||||||
self.assertEqual(log.last().metadata, {'progress': 10})
|
self.assertEqual(log.last().metadata, {"progress": 10})
|
||||||
shelf_manager.move_item(book1, ShelfType.PROGRESS, metadata={'progress': 90})
|
shelf_manager.move_item(book1, ShelfType.PROGRESS, metadata={"progress": 90})
|
||||||
log = shelf_manager.get_log_for_item(book1)
|
log = shelf_manager.get_log_for_item(book1)
|
||||||
self.assertEqual(log.count(), 5)
|
self.assertEqual(log.count(), 5)
|
||||||
self.assertEqual(Mark(user, book1).visibility, 0)
|
self.assertEqual(Mark(user, book1).visibility, 0)
|
||||||
shelf_manager.move_item(book1, ShelfType.PROGRESS, metadata={'progress': 90}, visibility=1)
|
shelf_manager.move_item(
|
||||||
|
book1, ShelfType.PROGRESS, metadata={"progress": 90}, visibility=1
|
||||||
|
)
|
||||||
self.assertEqual(Mark(user, book1).visibility, 1)
|
self.assertEqual(Mark(user, book1).visibility, 1)
|
||||||
self.assertEqual(shelf_manager.get_log_for_item(book1).count(), 5)
|
self.assertEqual(shelf_manager.get_log_for_item(book1).count(), 5)
|
||||||
|
|
||||||
|
@ -82,18 +88,18 @@ class TagTest(TestCase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_user_tag(self):
|
def test_user_tag(self):
|
||||||
t1 = 'tag-1'
|
t1 = "tag-1"
|
||||||
t2 = 'tag-2'
|
t2 = "tag-2"
|
||||||
t3 = 'tag-3'
|
t3 = "tag-3"
|
||||||
TagManager.tag_item_by_user(self.book1, self.user2, [t1, t3])
|
TagManager.tag_item_by_user(self.book1, self.user2, [t1, t3])
|
||||||
self.assertEqual(self.book1.tags, [t1, t3])
|
self.assertEqual(self.book1.tags, [t1, t3])
|
||||||
TagManager.tag_item_by_user(self.book1, self.user2, [t2, t3])
|
TagManager.tag_item_by_user(self.book1, self.user2, [t2, t3])
|
||||||
self.assertEqual(self.book1.tags, [t2, t3])
|
self.assertEqual(self.book1.tags, [t2, t3])
|
||||||
|
|
||||||
def test_tag(self):
|
def test_tag(self):
|
||||||
t1 = 'tag-1'
|
t1 = "tag-1"
|
||||||
t2 = 'tag-2'
|
t2 = "tag-2"
|
||||||
t3 = 'tag-3'
|
t3 = "tag-3"
|
||||||
TagManager.add_tag_by_user(self.book1, t3, self.user2)
|
TagManager.add_tag_by_user(self.book1, t3, self.user2)
|
||||||
TagManager.add_tag_by_user(self.book1, t1, self.user1)
|
TagManager.add_tag_by_user(self.book1, t1, self.user1)
|
||||||
TagManager.add_tag_by_user(self.book1, t1, self.user2)
|
TagManager.add_tag_by_user(self.book1, t1, self.user2)
|
||||||
|
@ -129,21 +135,21 @@ class MarkTest(TestCase):
|
||||||
self.assertEqual(mark.visibility, None)
|
self.assertEqual(mark.visibility, None)
|
||||||
self.assertEqual(mark.review, None)
|
self.assertEqual(mark.review, None)
|
||||||
self.assertEqual(mark.tags, [])
|
self.assertEqual(mark.tags, [])
|
||||||
mark.update(ShelfType.WISHLIST, 'a gentle comment', 9, 1)
|
mark.update(ShelfType.WISHLIST, "a gentle comment", 9, 1)
|
||||||
|
|
||||||
mark = Mark(self.user1, self.book1)
|
mark = Mark(self.user1, self.book1)
|
||||||
self.assertEqual(mark.shelf_type, ShelfType.WISHLIST)
|
self.assertEqual(mark.shelf_type, ShelfType.WISHLIST)
|
||||||
self.assertEqual(mark.shelf_label, '想读')
|
self.assertEqual(mark.shelf_label, "想读")
|
||||||
self.assertEqual(mark.text, 'a gentle comment')
|
self.assertEqual(mark.text, "a gentle comment")
|
||||||
self.assertEqual(mark.rating, 9)
|
self.assertEqual(mark.rating, 9)
|
||||||
self.assertEqual(mark.visibility, 1)
|
self.assertEqual(mark.visibility, 1)
|
||||||
self.assertEqual(mark.review, None)
|
self.assertEqual(mark.review, None)
|
||||||
self.assertEqual(mark.tags, [])
|
self.assertEqual(mark.tags, [])
|
||||||
|
|
||||||
review = Review.review_item_by_user(self.book1, self.user1, 'Critic', 'Review')
|
review = Review.review_item_by_user(self.book1, self.user1, "Critic", "Review")
|
||||||
mark = Mark(self.user1, self.book1)
|
mark = Mark(self.user1, self.book1)
|
||||||
self.assertEqual(mark.review, review)
|
self.assertEqual(mark.review, review)
|
||||||
|
|
||||||
TagManager.tag_item_by_user(self.book1, self.user1, [' Sci-Fi ', ' fic '])
|
TagManager.tag_item_by_user(self.book1, self.user1, [" Sci-Fi ", " fic "])
|
||||||
mark = Mark(self.user1, self.book1)
|
mark = Mark(self.user1, self.book1)
|
||||||
self.assertEqual(mark.tags, ['fic', 'sci-fi'])
|
self.assertEqual(mark.tags, ["fic", "sci-fi"])
|
||||||
|
|
116
journal/urls.py
116
journal/urls.py
|
@ -3,7 +3,7 @@ from .views import *
|
||||||
from catalog.models import *
|
from catalog.models import *
|
||||||
|
|
||||||
|
|
||||||
app_name = 'journal'
|
app_name = "journal"
|
||||||
|
|
||||||
|
|
||||||
def _get_all_categories():
|
def _get_all_categories():
|
||||||
|
@ -16,34 +16,88 @@ def _get_all_shelf_types():
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('wish/<str:item_uuid>', wish, name='wish'),
|
path("wish/<str:item_uuid>", wish, name="wish"),
|
||||||
path('like/<str:piece_uuid>', like, name='like'),
|
path("like/<str:piece_uuid>", like, name="like"),
|
||||||
path('mark/<str:item_uuid>', mark, name='mark'),
|
path("mark/<str:item_uuid>", mark, name="mark"),
|
||||||
path('add_to_collection/<str:item_uuid>', add_to_collection, name='add_to_collection'),
|
path(
|
||||||
|
"add_to_collection/<str:item_uuid>", add_to_collection, name="add_to_collection"
|
||||||
path('review/<str:review_uuid>', review_retrieve, name='review_retrieve'),
|
),
|
||||||
path('review/create/<str:item_uuid>/', review_edit, name='review_create'),
|
path("review/<str:review_uuid>", review_retrieve, name="review_retrieve"),
|
||||||
path('review/edit/<str:item_uuid>/<str:review_uuid>', review_edit, name='review_edit'),
|
path("review/create/<str:item_uuid>/", review_edit, name="review_create"),
|
||||||
path('review/delete/<str:review_uuid>', review_delete, name='review_delete'),
|
path(
|
||||||
|
"review/edit/<str:item_uuid>/<str:review_uuid>", review_edit, name="review_edit"
|
||||||
path('collection/<str:collection_uuid>', collection_retrieve, name='collection_retrieve'),
|
),
|
||||||
path('collection/create/', collection_edit, name='collection_create'),
|
path("review/delete/<str:piece_uuid>", piece_delete, name="review_delete"),
|
||||||
path('collection/edit/<str:collection_uuid>', collection_edit, name='collection_edit'),
|
path(
|
||||||
path('collection/delete/<str:collection_uuid>', collection_delete, name='collection_delete'),
|
"collection/<str:collection_uuid>",
|
||||||
path('collection/<str:collection_uuid>/items', collection_retrieve_items, name='collection_retrieve_items'),
|
collection_retrieve,
|
||||||
path('collection/<str:collection_uuid>/append_item', collection_append_item, name='collection_append_item'),
|
name="collection_retrieve",
|
||||||
path('collection/<str:collection_uuid>/delete_item/<str:collection_member_uuid>', collection_delete_item, name='collection_delete_item'),
|
),
|
||||||
path('collection/<str:collection_uuid>/move_up_item/<str:collection_member_uuid>', collection_move_up_item, name='collection_move_up_item'),
|
path("collection/create/", collection_edit, name="collection_create"),
|
||||||
path('collection/<str:collection_uuid>/move_down_item/<str:collection_member_uuid>', collection_move_down_item, name='collection_move_down_item'),
|
path(
|
||||||
path('collection/<str:collection_uuid>/update_item_note/<str:collection_member_uuid>', collection_update_item_note, name='collection_update_item_note'),
|
"collection/edit/<str:collection_uuid>", collection_edit, name="collection_edit"
|
||||||
|
),
|
||||||
re_path(r'^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/(?P<shelf_type>' + _get_all_shelf_types() + ')/(?P<item_category>' + _get_all_categories() + ')/$', user_mark_list, name='user_mark_list'),
|
path("collection/delete/<str:piece_uuid>", piece_delete, name="collection_delete"),
|
||||||
re_path(r'^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/reviews/(?P<item_category>' + _get_all_categories() + ')/$', user_review_list, name='user_review_list'),
|
path(
|
||||||
re_path(r'^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/tags/(?P<tag_title>[^/]+)/$', user_tag_member_list, name='user_tag_member_list'),
|
"collection/<str:collection_uuid>/items",
|
||||||
re_path(r'^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/collections/$', user_collection_list, name='user_collection_list'),
|
collection_retrieve_items,
|
||||||
re_path(r'^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/like/collections/$', user_liked_collection_list, name='user_liked_collection_list'),
|
name="collection_retrieve_items",
|
||||||
re_path(r'^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/tags/$', user_tag_list, name='user_tag_list'),
|
),
|
||||||
|
path(
|
||||||
re_path(r'^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/$', home, name='user_profile'),
|
"collection/<str:collection_uuid>/append_item",
|
||||||
|
collection_append_item,
|
||||||
|
name="collection_append_item",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"collection/<str:collection_uuid>/remove_item/<str:item_uuid>",
|
||||||
|
collection_remove_item,
|
||||||
|
name="collection_remove_item",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"collection/<str:collection_uuid>/move_item/<str:direction>/<str:item_uuid>",
|
||||||
|
collection_move_item,
|
||||||
|
name="collection_move_item",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"collection/<str:collection_uuid>/update_item_note/<str:item_uuid>",
|
||||||
|
collection_update_item_note,
|
||||||
|
name="collection_update_item_note",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/(?P<shelf_type>"
|
||||||
|
+ _get_all_shelf_types()
|
||||||
|
+ ")/(?P<item_category>"
|
||||||
|
+ _get_all_categories()
|
||||||
|
+ ")/$",
|
||||||
|
user_mark_list,
|
||||||
|
name="user_mark_list",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/reviews/(?P<item_category>"
|
||||||
|
+ _get_all_categories()
|
||||||
|
+ ")/$",
|
||||||
|
user_review_list,
|
||||||
|
name="user_review_list",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/tags/(?P<tag_title>[^/]+)/$",
|
||||||
|
user_tag_member_list,
|
||||||
|
name="user_tag_member_list",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/collections/$",
|
||||||
|
user_collection_list,
|
||||||
|
name="user_collection_list",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/like/collections/$",
|
||||||
|
user_liked_collection_list,
|
||||||
|
name="user_liked_collection_list",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/tags/$",
|
||||||
|
user_tag_list,
|
||||||
|
name="user_tag_list",
|
||||||
|
),
|
||||||
|
re_path(r"^user/(?P<user_name>[A-Za-z0-9_\-.@]+)/$", home, name="user_profile"),
|
||||||
]
|
]
|
||||||
|
|
495
journal/views.py
495
journal/views.py
|
@ -2,9 +2,13 @@ import logging
|
||||||
from django.shortcuts import render, get_object_or_404, redirect, reverse
|
from django.shortcuts import render, get_object_or_404, redirect, reverse
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseServerError, HttpResponseNotFound
|
from django.http import (
|
||||||
|
HttpResponse,
|
||||||
|
HttpResponseBadRequest,
|
||||||
|
HttpResponseServerError,
|
||||||
|
HttpResponseNotFound,
|
||||||
|
)
|
||||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||||
from django.db import IntegrityError, transaction
|
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
|
@ -24,187 +28,257 @@ from users.models import User, Report, Preference
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
PAGE_SIZE = 10
|
PAGE_SIZE = 10
|
||||||
|
|
||||||
|
_checkmark = "✔️".encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def wish(request, item_uuid):
|
def wish(request, item_uuid):
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
||||||
if not item:
|
if not item:
|
||||||
return HttpResponseNotFound("item not found")
|
return HttpResponseNotFound(b"item not found")
|
||||||
request.user.shelf_manager.move_item(item, ShelfType.WISHLIST)
|
request.user.shelf_manager.move_item(item, ShelfType.WISHLIST)
|
||||||
return HttpResponse("✔️")
|
if request.GET.get("back"):
|
||||||
|
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
|
||||||
|
return HttpResponse(_checkmark)
|
||||||
else:
|
else:
|
||||||
return HttpResponseBadRequest("invalid request")
|
return HttpResponseBadRequest(b"invalid request")
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def like(request, piece_uuid):
|
def like(request, piece_uuid):
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
piece = get_object_or_404(Collection, uid=base62.decode(piece_uuid))
|
piece = get_object_or_404(Collection, uid=base62.decode(piece_uuid))
|
||||||
if not piece:
|
if not piece:
|
||||||
return HttpResponseNotFound("piece not found")
|
return HttpResponseNotFound(b"piece not found")
|
||||||
Like.user_like_piece(request.user, piece)
|
Like.user_like_piece(request.user, piece)
|
||||||
return HttpResponse("✔️")
|
return HttpResponse(_checkmark)
|
||||||
else:
|
else:
|
||||||
return HttpResponseBadRequest("invalid request")
|
return HttpResponseBadRequest(b"invalid request")
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def unlike(request, piece_uuid):
|
||||||
|
if request.method == "POST":
|
||||||
|
piece = get_object_or_404(Collection, uid=base62.decode(piece_uuid))
|
||||||
|
if not piece:
|
||||||
|
return HttpResponseNotFound(b"piece not found")
|
||||||
|
Like.user_unlike_piece(request.user, piece)
|
||||||
|
if request.GET.get("back"):
|
||||||
|
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
|
||||||
|
return HttpResponse(_checkmark)
|
||||||
|
else:
|
||||||
|
return HttpResponseBadRequest(b"invalid request")
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def add_to_collection(request, item_uuid):
|
def add_to_collection(request, item_uuid):
|
||||||
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
collections = Collection.objects.filter(owner=request.user)
|
collections = Collection.objects.filter(owner=request.user)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
'add_to_collection.html',
|
"add_to_collection.html",
|
||||||
{
|
{
|
||||||
'item': item,
|
"item": item,
|
||||||
'collections': collections,
|
"collections": collections,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
cid = int(request.POST.get('collection_id', default=0))
|
cid = int(request.POST.get("collection_id", default=0))
|
||||||
if not cid:
|
if not cid:
|
||||||
cid = Collection.objects.create(owner=request.user, title=f'{request.user.username}的收藏单').id
|
cid = Collection.objects.create(
|
||||||
|
owner=request.user, title=f"{request.user.username}的收藏单"
|
||||||
|
).id
|
||||||
collection = Collection.objects.get(owner=request.user, id=cid)
|
collection = Collection.objects.get(owner=request.user, id=cid)
|
||||||
collection.append_item(item, metadata={'comment': request.POST.get('comment')})
|
collection.append_item(item, metadata={"comment": request.POST.get("comment")})
|
||||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
|
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
|
||||||
|
|
||||||
|
|
||||||
def render_relogin(request):
|
def render_relogin(request):
|
||||||
return render(request, 'common/error.html', {
|
return render(
|
||||||
'url': reverse("users:connect") + '?domain=' + request.user.mastodon_site,
|
request,
|
||||||
'msg': _("信息已保存,但是未能分享到联邦网络"),
|
"common/error.html",
|
||||||
'secondary_msg': _("可能是你在联邦网络(Mastodon/Pleroma/...)的登录状态过期了,正在跳转到联邦网络重新登录😼")})
|
{
|
||||||
|
"url": reverse("users:connect") + "?domain=" + request.user.mastodon_site,
|
||||||
|
"msg": _("信息已保存,但是未能分享到联邦网络"),
|
||||||
|
"secondary_msg": _(
|
||||||
|
"可能是你在联邦网络(Mastodon/Pleroma/...)的登录状态过期了,正在跳转到联邦网络重新登录😼"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def mark(request, item_uuid):
|
def mark(request, item_uuid):
|
||||||
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
||||||
mark = Mark(request.user, item)
|
mark = Mark(request.user, item)
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
tags = TagManager.get_item_tags_by_user(item, request.user)
|
tags = TagManager.get_item_tags_by_user(item, request.user)
|
||||||
shelf_types = [(n[1], n[2]) for n in iter(ShelfTypeNames) if n[0] == item.category]
|
shelf_types = [
|
||||||
shelf_type = request.GET.get('shelf_type', mark.shelf_type)
|
(n[1], n[2]) for n in iter(ShelfTypeNames) if n[0] == item.category
|
||||||
return render(request, 'mark.html', {
|
]
|
||||||
'item': item,
|
shelf_type = request.GET.get("shelf_type", mark.shelf_type)
|
||||||
'mark': mark,
|
return render(
|
||||||
'shelf_type': shelf_type,
|
request,
|
||||||
'tags': ','.join(tags),
|
"mark.html",
|
||||||
'shelf_types': shelf_types,
|
{
|
||||||
})
|
"item": item,
|
||||||
elif request.method == 'POST':
|
"mark": mark,
|
||||||
if request.POST.get('delete', default=False):
|
"shelf_type": shelf_type,
|
||||||
|
"tags": ",".join(tags),
|
||||||
|
"shelf_types": shelf_types,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
elif request.method == "POST":
|
||||||
|
if request.POST.get("delete", default=False):
|
||||||
mark.delete()
|
mark.delete()
|
||||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
|
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
|
||||||
else:
|
else:
|
||||||
visibility = int(request.POST.get('visibility', default=0))
|
visibility = int(request.POST.get("visibility", default=0))
|
||||||
rating = request.POST.get('rating', default=0)
|
rating = request.POST.get("rating", default=0)
|
||||||
rating = int(rating) if rating else None
|
rating = int(rating) if rating else None
|
||||||
status = ShelfType(request.POST.get('status'))
|
status = ShelfType(request.POST.get("status"))
|
||||||
text = request.POST.get('text')
|
text = request.POST.get("text")
|
||||||
tags = request.POST.get('tags')
|
tags = request.POST.get("tags")
|
||||||
tags = tags.split(',') if tags else []
|
tags = tags.split(",") if tags else []
|
||||||
share_to_mastodon = bool(request.POST.get('share_to_mastodon', default=False))
|
share_to_mastodon = bool(
|
||||||
|
request.POST.get("share_to_mastodon", default=False)
|
||||||
|
)
|
||||||
TagManager.tag_item_by_user(item, request.user, tags, visibility)
|
TagManager.tag_item_by_user(item, request.user, tags, visibility)
|
||||||
try:
|
try:
|
||||||
mark.update(status, text, rating, visibility, share_to_mastodon=share_to_mastodon)
|
mark.update(
|
||||||
|
status,
|
||||||
|
text,
|
||||||
|
rating,
|
||||||
|
visibility,
|
||||||
|
share_to_mastodon=share_to_mastodon,
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return render_relogin(request)
|
return render_relogin(request)
|
||||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
|
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
|
||||||
|
|
||||||
|
|
||||||
def collection_retrieve(request, collection_uuid):
|
def collection_retrieve(request, collection_uuid):
|
||||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||||
if not collection.is_visible_to(request.user):
|
if not collection.is_visible_to(request.user):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
return render(request, 'collection.html', {'collection': collection})
|
return render(request, "collection.html", {"collection": collection})
|
||||||
|
|
||||||
|
|
||||||
def collection_retrieve_items(request, collection_uuid):
|
def collection_retrieve_items(request, collection_uuid, edit=False):
|
||||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||||
if not collection.is_visible_to(request.user):
|
if not collection.is_visible_to(request.user):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
form = CollectionForm(instance=collection)
|
form = CollectionForm(instance=collection)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
'collection_items.html',
|
"collection_items.html",
|
||||||
{
|
{
|
||||||
'collection': collection,
|
"collection": collection,
|
||||||
'form': form,
|
"form": form,
|
||||||
'collection_edit': request.GET.get('edit'), # collection.is_editable_by(request.user),
|
"collection_edit": edit or request.GET.get("edit"),
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def collection_update_item_note(request, collection_uuid, collection_member_uuid):
|
|
||||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
|
||||||
if not collection.is_editable_by(request.user):
|
|
||||||
raise PermissionDenied()
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def collection_append_item(request, collection_uuid):
|
def collection_append_item(request, collection_uuid):
|
||||||
|
if request.method != "POST":
|
||||||
|
return HttpResponseBadRequest()
|
||||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||||
if not collection.is_editable_by(request.user):
|
if not collection.is_editable_by(request.user):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
url = request.POST.get("url")
|
||||||
|
note = request.POST.get("note")
|
||||||
|
item = Item.get_by_url(url)
|
||||||
|
collection.append_item(item, metadata={"note": note})
|
||||||
|
collection.save()
|
||||||
|
return collection_retrieve_items(request, collection_uuid, True)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def collection_delete_item(request, collection_uuid, collection_member_uuid):
|
def collection_remove_item(request, collection_uuid, item_uuid):
|
||||||
|
if request.method != "POST":
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||||
|
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
||||||
|
if not collection.is_editable_by(request.user):
|
||||||
|
raise PermissionDenied()
|
||||||
|
collection.remove_item(item)
|
||||||
|
return collection_retrieve_items(request, collection_uuid, True)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def collection_move_item(request, direction, collection_uuid, item_uuid):
|
||||||
|
if request.method != "POST":
|
||||||
|
return HttpResponseBadRequest()
|
||||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||||
if not collection.is_editable_by(request.user):
|
if not collection.is_editable_by(request.user):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
||||||
|
if direction == "up":
|
||||||
|
collection.move_up_item(item)
|
||||||
|
else:
|
||||||
|
collection.move_down_item(item)
|
||||||
|
return collection_retrieve_items(request, collection_uuid, True)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def collection_move_up_item(request, collection_uuid, collection_member_uuid):
|
def collection_update_item_note(request, collection_uuid, item_uuid):
|
||||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||||
if not collection.is_editable_by(request.user):
|
if not collection.is_editable_by(request.user):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
||||||
|
|
||||||
@login_required
|
|
||||||
def collection_move_down_item(request, collection_uuid, collection_member_uuid):
|
|
||||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
|
||||||
if not collection.is_editable_by(request.user):
|
if not collection.is_editable_by(request.user):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
if request.method == "POST":
|
||||||
|
collection.update_item_metadata(
|
||||||
@login_required
|
item, {"note": request.POST.get("note", default="")}
|
||||||
def collection_edit(request, collection_uuid=None):
|
)
|
||||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid)) if collection_uuid else None
|
return collection_retrieve_items(request, collection_uuid, True)
|
||||||
if collection and not collection.is_editable_by(request.user):
|
elif request.method == "GET":
|
||||||
raise PermissionDenied()
|
member = collection.get_member_for_item(item)
|
||||||
if request.method == 'GET':
|
return render(
|
||||||
form = CollectionForm(instance=collection) if collection else CollectionForm()
|
request,
|
||||||
return render(request, 'collection_edit.html', {'form': form, 'collection': collection})
|
"collection_update_item_note.html",
|
||||||
elif request.method == 'POST':
|
{"collection": collection, "item": item, "note": member.note},
|
||||||
form = CollectionForm(request.POST, instance=collection) if collection else CollectionForm(request.POST)
|
)
|
||||||
if form.is_valid():
|
|
||||||
if not collection:
|
|
||||||
form.instance.owner = request.user
|
|
||||||
form.instance.edited_time = timezone.now()
|
|
||||||
form.save()
|
|
||||||
return redirect(reverse("journal:collection_retrieve", args=[form.instance.uuid]))
|
|
||||||
else:
|
|
||||||
return HttpResponseBadRequest(form.errors)
|
|
||||||
else:
|
else:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def collection_delete(request, collection_uuid):
|
def collection_edit(request, collection_uuid=None):
|
||||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
collection = (
|
||||||
if not collection.is_editable_by(request.user):
|
get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||||
|
if collection_uuid
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if collection and not collection.is_editable_by(request.user):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
collection_form = CollectionForm(instance=collection)
|
form = CollectionForm(instance=collection) if collection else CollectionForm()
|
||||||
return render(request, 'collection_delete.html', {'form': collection_form, 'collection': collection})
|
return render(
|
||||||
elif request.method == 'POST':
|
request, "collection_edit.html", {"form": form, "collection": collection}
|
||||||
collection.delete()
|
)
|
||||||
return redirect(reverse("users:home"))
|
elif request.method == "POST":
|
||||||
|
form = (
|
||||||
|
CollectionForm(request.POST, instance=collection)
|
||||||
|
if collection
|
||||||
|
else CollectionForm(request.POST)
|
||||||
|
)
|
||||||
|
if form.is_valid():
|
||||||
|
if not collection:
|
||||||
|
form.instance.owner = request.user
|
||||||
|
form.instance.edited_time = timezone.now()
|
||||||
|
form.save()
|
||||||
|
return redirect(
|
||||||
|
reverse("journal:collection_retrieve", args=[form.instance.uuid])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return HttpResponseBadRequest(form.errors)
|
||||||
else:
|
else:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
@ -213,31 +287,45 @@ def review_retrieve(request, review_uuid):
|
||||||
piece = get_object_or_404(Review, uid=base62.decode(review_uuid))
|
piece = get_object_or_404(Review, uid=base62.decode(review_uuid))
|
||||||
if not piece.is_visible_to(request.user):
|
if not piece.is_visible_to(request.user):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
return render(request, 'review.html', {'review': piece})
|
return render(request, "review.html", {"review": piece})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def review_edit(request, item_uuid, review_uuid=None):
|
def review_edit(request, item_uuid, review_uuid=None):
|
||||||
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
||||||
review = get_object_or_404(Review, uid=base62.decode(review_uuid)) if review_uuid else None
|
review = (
|
||||||
|
get_object_or_404(Review, uid=base62.decode(review_uuid))
|
||||||
|
if review_uuid
|
||||||
|
else None
|
||||||
|
)
|
||||||
if review and not review.is_editable_by(request.user):
|
if review and not review.is_editable_by(request.user):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
form = ReviewForm(instance=review) if review else ReviewForm(initial={'item': item.id})
|
form = (
|
||||||
return render(request, 'review_edit.html', {'form': form, 'item': item})
|
ReviewForm(instance=review)
|
||||||
elif request.method == 'POST':
|
if review
|
||||||
form = ReviewForm(request.POST, instance=review) if review else ReviewForm(request.POST)
|
else ReviewForm(initial={"item": item.id})
|
||||||
|
)
|
||||||
|
return render(request, "review_edit.html", {"form": form, "item": item})
|
||||||
|
elif request.method == "POST":
|
||||||
|
form = (
|
||||||
|
ReviewForm(request.POST, instance=review)
|
||||||
|
if review
|
||||||
|
else ReviewForm(request.POST)
|
||||||
|
)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
if not review:
|
if not review:
|
||||||
form.instance.owner = request.user
|
form.instance.owner = request.user
|
||||||
form.instance.edited_time = timezone.now()
|
form.instance.edited_time = timezone.now()
|
||||||
form.save()
|
form.save()
|
||||||
if form.cleaned_data['share_to_mastodon']:
|
if form.cleaned_data["share_to_mastodon"]:
|
||||||
form.instance.save = lambda **args: None
|
form.instance.save = lambda **args: None
|
||||||
form.instance.shared_link = None
|
form.instance.shared_link = None
|
||||||
if not share_review(form.instance):
|
if not share_review(form.instance):
|
||||||
return render_relogin(request)
|
return render_relogin(request)
|
||||||
return redirect(reverse("journal:review_retrieve", args=[form.instance.uuid]))
|
return redirect(
|
||||||
|
reverse("journal:review_retrieve", args=[form.instance.uuid])
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return HttpResponseBadRequest(form.errors)
|
return HttpResponseBadRequest(form.errors)
|
||||||
else:
|
else:
|
||||||
|
@ -245,17 +333,18 @@ def review_edit(request, item_uuid, review_uuid=None):
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def review_delete(request, review_uuid):
|
def piece_delete(request, piece_uuid):
|
||||||
review = get_object_or_404(Review, uid=base62.decode(review_uuid))
|
piece = get_object_or_404(Piece, uid=base62.decode(piece_uuid))
|
||||||
if not review.is_editable_by(request.user):
|
return_url = request.GET.get("return_url", None) or "/"
|
||||||
|
if not piece.is_editable_by(request.user):
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
if request.method == 'GET':
|
if request.method == "GET":
|
||||||
review_form = ReviewForm(instance=review)
|
return render(
|
||||||
return render(request, 'review_delete.html', {'form': review_form, 'review': review})
|
request, "piece_delete.html", {"piece": piece, "return_url": return_url}
|
||||||
elif request.method == 'POST':
|
)
|
||||||
item = review.item
|
elif request.method == "POST":
|
||||||
review.delete()
|
piece.delete()
|
||||||
return redirect(item.url)
|
return redirect(return_url)
|
||||||
else:
|
else:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
@ -264,57 +353,67 @@ def render_list_not_fount(request):
|
||||||
msg = _("相关列表不存在")
|
msg = _("相关列表不存在")
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
'common/error.html',
|
"common/error.html",
|
||||||
{
|
{
|
||||||
'msg': msg,
|
"msg": msg,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _render_list(request, user_name, type, shelf_type=None, item_category=None, tag_title=None):
|
def _render_list(
|
||||||
|
request, user_name, type, shelf_type=None, item_category=None, tag_title=None
|
||||||
|
):
|
||||||
user = User.get(user_name)
|
user = User.get(user_name)
|
||||||
if user is None:
|
if user is None:
|
||||||
return render_user_not_found(request)
|
return render_user_not_found(request)
|
||||||
if user != request.user and (request.user.is_blocked_by(user) or request.user.is_blocking(user)):
|
if user != request.user and (
|
||||||
|
request.user.is_blocked_by(user) or request.user.is_blocking(user)
|
||||||
|
):
|
||||||
return render_user_blocked(request)
|
return render_user_blocked(request)
|
||||||
if type == 'mark':
|
if type == "mark":
|
||||||
shelf = user.shelf_manager.get_shelf(item_category, shelf_type)
|
shelf = user.shelf_manager.get_shelf(item_category, shelf_type)
|
||||||
queryset = ShelfMember.objects.filter(owner=user, parent=shelf)
|
queryset = ShelfMember.objects.filter(owner=user, parent=shelf)
|
||||||
elif type == 'tagmember':
|
elif type == "tagmember":
|
||||||
tag = Tag.objects.filter(owner=user, title=tag_title).first()
|
tag = Tag.objects.filter(owner=user, title=tag_title).first()
|
||||||
if not tag:
|
if not tag:
|
||||||
return render_list_not_fount(request)
|
return render_list_not_fount(request)
|
||||||
if tag.visibility != 0 and user != request.user:
|
if tag.visibility != 0 and user != request.user:
|
||||||
return render_list_not_fount(request)
|
return render_list_not_fount(request)
|
||||||
queryset = TagMember.objects.filter(parent=tag)
|
queryset = TagMember.objects.filter(parent=tag)
|
||||||
elif type == 'review':
|
elif type == "review":
|
||||||
queryset = Review.objects.filter(owner=user)
|
queryset = Review.objects.filter(owner=user)
|
||||||
queryset = queryset.filter(query_item_category(item_category))
|
queryset = queryset.filter(query_item_category(item_category))
|
||||||
else:
|
else:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
queryset = queryset.filter(q_visible_to(request.user, user))
|
queryset = queryset.filter(q_visible_to(request.user, user))
|
||||||
paginator = Paginator(queryset, PAGE_SIZE)
|
paginator = Paginator(queryset, PAGE_SIZE)
|
||||||
page_number = request.GET.get('page', default=1)
|
page_number = request.GET.get("page", default=1)
|
||||||
members = paginator.get_page(page_number)
|
members = paginator.get_page(page_number)
|
||||||
return render(request, f'user_{type}_list.html', {
|
return render(
|
||||||
'user': user,
|
request,
|
||||||
'members': members,
|
f"user_{type}_list.html",
|
||||||
})
|
{
|
||||||
|
"user": user,
|
||||||
|
"members": members,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def user_mark_list(request, user_name, shelf_type, item_category):
|
def user_mark_list(request, user_name, shelf_type, item_category):
|
||||||
return _render_list(request, user_name, 'mark', shelf_type=shelf_type, item_category=item_category)
|
return _render_list(
|
||||||
|
request, user_name, "mark", shelf_type=shelf_type, item_category=item_category
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def user_tag_member_list(request, user_name, tag_title):
|
def user_tag_member_list(request, user_name, tag_title):
|
||||||
return _render_list(request, user_name, 'tagmember', tag_title=tag_title)
|
return _render_list(request, user_name, "tagmember", tag_title=tag_title)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def user_review_list(request, user_name, item_category):
|
def user_review_list(request, user_name, item_category):
|
||||||
return _render_list(request, user_name, 'review', item_category=item_category)
|
return _render_list(request, user_name, "review", item_category=item_category)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -322,17 +421,23 @@ def user_tag_list(request, user_name):
|
||||||
user = User.get(user_name)
|
user = User.get(user_name)
|
||||||
if user is None:
|
if user is None:
|
||||||
return render_user_not_found(request)
|
return render_user_not_found(request)
|
||||||
if user != request.user and (request.user.is_blocked_by(user) or request.user.is_blocking(user)):
|
if user != request.user and (
|
||||||
|
request.user.is_blocked_by(user) or request.user.is_blocking(user)
|
||||||
|
):
|
||||||
return render_user_blocked(request)
|
return render_user_blocked(request)
|
||||||
tags = Tag.objects.filter(owner=user)
|
tags = Tag.objects.filter(owner=user)
|
||||||
tags = user.tag_set.all()
|
tags = user.tag_set.all()
|
||||||
if user != request.user:
|
if user != request.user:
|
||||||
tags = tags.filter(visibility=0)
|
tags = tags.filter(visibility=0)
|
||||||
tags = tags.values('title').annotate(total=Count('members')).order_by('-total')
|
tags = tags.values("title").annotate(total=Count("members")).order_by("-total")
|
||||||
return render(request, 'user_tag_list.html', {
|
return render(
|
||||||
'user': user,
|
request,
|
||||||
'tags': tags,
|
"user_tag_list.html",
|
||||||
})
|
{
|
||||||
|
"user": user,
|
||||||
|
"tags": tags,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -340,18 +445,24 @@ def user_collection_list(request, user_name):
|
||||||
user = User.get(user_name)
|
user = User.get(user_name)
|
||||||
if user is None:
|
if user is None:
|
||||||
return render_user_not_found(request)
|
return render_user_not_found(request)
|
||||||
if user != request.user and (request.user.is_blocked_by(user) or request.user.is_blocking(user)):
|
if user != request.user and (
|
||||||
|
request.user.is_blocked_by(user) or request.user.is_blocking(user)
|
||||||
|
):
|
||||||
return render_user_blocked(request)
|
return render_user_blocked(request)
|
||||||
collections = Tag.objects.filter(owner=user)
|
collections = Collection.objects.filter(owner=user)
|
||||||
if user != request.user:
|
if user != request.user:
|
||||||
if request.user.is_following(user):
|
if request.user.is_following(user):
|
||||||
collections = collections.filter(visibility__ne=2)
|
collections = collections.filter(visibility__ne=2)
|
||||||
else:
|
else:
|
||||||
collections = collections.filter(visibility=0)
|
collections = collections.filter(visibility=0)
|
||||||
return render(request, 'user_collection_list.html', {
|
return render(
|
||||||
'user': user,
|
request,
|
||||||
'collections': collections,
|
"user_collection_list.html",
|
||||||
})
|
{
|
||||||
|
"user": user,
|
||||||
|
"collections": collections,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -359,27 +470,37 @@ def user_liked_collection_list(request, user_name):
|
||||||
user = User.get(user_name)
|
user = User.get(user_name)
|
||||||
if user is None:
|
if user is None:
|
||||||
return render_user_not_found(request)
|
return render_user_not_found(request)
|
||||||
if user != request.user and (request.user.is_blocked_by(user) or request.user.is_blocking(user)):
|
if user != request.user and (
|
||||||
|
request.user.is_blocked_by(user) or request.user.is_blocking(user)
|
||||||
|
):
|
||||||
return render_user_blocked(request)
|
return render_user_blocked(request)
|
||||||
collections = Collection.objects.filter(likes__owner=user)
|
collections = Collection.objects.filter(likes__owner=user)
|
||||||
if user != request.user:
|
if user != request.user:
|
||||||
collections = collections.filter(query_visible(request.user))
|
collections = collections.filter(query_visible(request.user))
|
||||||
return render(request, 'user_collection_list.html', {
|
return render(
|
||||||
'user': user,
|
request,
|
||||||
'collections': collections,
|
"user_collection_list.html",
|
||||||
})
|
{
|
||||||
|
"user": user,
|
||||||
|
"collections": collections,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def home_anonymous(request, id):
|
def home_anonymous(request, id):
|
||||||
login_url = settings.LOGIN_URL + "?next=" + request.get_full_path()
|
login_url = settings.LOGIN_URL + "?next=" + request.get_full_path()
|
||||||
try:
|
try:
|
||||||
username = id.split('@')[0]
|
username = id.split("@")[0]
|
||||||
site = id.split('@')[1]
|
site = id.split("@")[1]
|
||||||
return render(request, 'users/home_anonymous.html', {
|
return render(
|
||||||
'login_url': login_url,
|
request,
|
||||||
'username': username,
|
"users/home_anonymous.html",
|
||||||
'site': site,
|
{
|
||||||
})
|
"login_url": login_url,
|
||||||
|
"username": username,
|
||||||
|
"site": site,
|
||||||
|
},
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return redirect(login_url)
|
return redirect(login_url)
|
||||||
|
|
||||||
|
@ -387,7 +508,7 @@ def home_anonymous(request, id):
|
||||||
def home(request, user_name):
|
def home(request, user_name):
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return home_anonymous(request, user_name)
|
return home_anonymous(request, user_name)
|
||||||
if request.method != 'GET':
|
if request.method != "GET":
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
user = User.get(user_name)
|
user = User.get(user_name)
|
||||||
if user is None:
|
if user is None:
|
||||||
|
@ -395,14 +516,13 @@ def home(request, user_name):
|
||||||
|
|
||||||
# access one's own home page
|
# access one's own home page
|
||||||
if user == request.user:
|
if user == request.user:
|
||||||
reports = Report.objects.order_by(
|
reports = Report.objects.order_by("-submitted_time").filter(is_read=False)
|
||||||
'-submitted_time').filter(is_read=False)
|
|
||||||
unread_announcements = Announcement.objects.filter(
|
unread_announcements = Announcement.objects.filter(
|
||||||
pk__gt=request.user.read_announcement_index).order_by('-pk')
|
pk__gt=request.user.read_announcement_index
|
||||||
|
).order_by("-pk")
|
||||||
try:
|
try:
|
||||||
request.user.read_announcement_index = Announcement.objects.latest(
|
request.user.read_announcement_index = Announcement.objects.latest("pk").pk
|
||||||
'pk').pk
|
request.user.save(update_fields=["read_announcement_index"])
|
||||||
request.user.save(update_fields=['read_announcement_index'])
|
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
# when there is no annoucenment
|
# when there is no annoucenment
|
||||||
pass
|
pass
|
||||||
|
@ -416,42 +536,51 @@ def home(request, user_name):
|
||||||
|
|
||||||
qv = q_visible_to(request.user, user)
|
qv = q_visible_to(request.user, user)
|
||||||
shelf_list = {}
|
shelf_list = {}
|
||||||
visbile_categories = [ItemCategory.Book, ItemCategory.Movie, ItemCategory.TV, ItemCategory.Music, ItemCategory.Game]
|
visbile_categories = [
|
||||||
|
ItemCategory.Book,
|
||||||
|
ItemCategory.Movie,
|
||||||
|
ItemCategory.TV,
|
||||||
|
ItemCategory.Music,
|
||||||
|
ItemCategory.Game,
|
||||||
|
]
|
||||||
for category in visbile_categories:
|
for category in visbile_categories:
|
||||||
shelf_list[category] = {}
|
shelf_list[category] = {}
|
||||||
for shelf_type in ShelfType:
|
for shelf_type in ShelfType:
|
||||||
shelf = user.shelf_manager.get_shelf(category, shelf_type)
|
shelf = user.shelf_manager.get_shelf(category, shelf_type)
|
||||||
members = shelf.recent_members.filter(qv)
|
members = shelf.recent_members.filter(qv)
|
||||||
shelf_list[category][shelf_type] = {
|
shelf_list[category][shelf_type] = {
|
||||||
'title': shelf.title,
|
"title": shelf.title,
|
||||||
'count': members.count(),
|
"count": members.count(),
|
||||||
'members': members[:5].prefetch_related('item'),
|
"members": members[:5].prefetch_related("item"),
|
||||||
}
|
}
|
||||||
reviews = Review.objects.filter(owner=user).filter(qv)
|
reviews = Review.objects.filter(owner=user).filter(qv)
|
||||||
shelf_list[category]['reviewed'] = {
|
shelf_list[category]["reviewed"] = {
|
||||||
'title': '评论过的' + category.label,
|
"title": "评论过的" + category.label,
|
||||||
'count': reviews.count(),
|
"count": reviews.count(),
|
||||||
'members': reviews[:5].prefetch_related('item'),
|
"members": reviews[:5].prefetch_related("item"),
|
||||||
}
|
}
|
||||||
collections = Collection.objects.filter(owner=user).filter(qv).order_by("-edited_time")
|
collections = (
|
||||||
liked_collections = Collection.objects.filter(likes__owner=user).order_by("-edited_time")
|
Collection.objects.filter(owner=user).filter(qv).order_by("-edited_time")
|
||||||
|
)
|
||||||
|
liked_collections = (
|
||||||
|
Collection.objects.none().filter(likes__owner=user).order_by("-edited_time")
|
||||||
|
)
|
||||||
if user != request.user:
|
if user != request.user:
|
||||||
liked_collections = liked_collections.filter(query_visible(request.user))
|
liked_collections = liked_collections.filter(query_visible(request.user))
|
||||||
|
|
||||||
layout = user.get_preference().get_serialized_home_layout()
|
layout = user.get_preference().get_serialized_home_layout()
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
'profile.html',
|
"profile.html",
|
||||||
{
|
{
|
||||||
'user': user,
|
"user": user,
|
||||||
'shelf_list': shelf_list,
|
"shelf_list": shelf_list,
|
||||||
'collections': collections[:5],
|
"collections": collections[:5],
|
||||||
'collections_count': collections.count(),
|
"collections_count": collections.count(),
|
||||||
'liked_collections': liked_collections.order_by("-edited_time")[:5],
|
"liked_collections": liked_collections.order_by("-edited_time")[:5],
|
||||||
'liked_collections_count': liked_collections.count(),
|
"liked_collections_count": liked_collections.count(),
|
||||||
'layout': layout,
|
"layout": layout,
|
||||||
'reports': reports,
|
"reports": reports,
|
||||||
'unread_announcements': unread_announcements,
|
"unread_announcements": unread_announcements,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,6 +14,7 @@ from catalog.common import *
|
||||||
from catalog.models import *
|
from catalog.models import *
|
||||||
from catalog.sites import *
|
from catalog.sites import *
|
||||||
from journal.models import *
|
from journal.models import *
|
||||||
|
|
||||||
# from social import models as social_models
|
# from social import models as social_models
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
|
@ -51,29 +52,53 @@ shelf_map = {
|
||||||
}
|
}
|
||||||
|
|
||||||
tag_map = {
|
tag_map = {
|
||||||
BookMark: 'bookmark_tags',
|
BookMark: "bookmark_tags",
|
||||||
MovieMark: 'moviemark_tags',
|
MovieMark: "moviemark_tags",
|
||||||
AlbumMark: 'albummark_tags',
|
AlbumMark: "albummark_tags",
|
||||||
GameMark: 'gamemark_tags',
|
GameMark: "gamemark_tags",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = 'Migrate legacy marks to user journal'
|
help = "Migrate legacy marks to user journal"
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument('--book', dest='types', action='append_const', const=BookMark)
|
parser.add_argument(
|
||||||
parser.add_argument('--movie', dest='types', action='append_const', const=MovieMark)
|
"--book", dest="types", action="append_const", const=BookMark
|
||||||
parser.add_argument('--album', dest='types', action='append_const', const=AlbumMark)
|
)
|
||||||
parser.add_argument('--game', dest='types', action='append_const', const=GameMark)
|
parser.add_argument(
|
||||||
parser.add_argument('--mark', help='migrate shelves/tags/ratings, then exit', action='store_true')
|
"--movie", dest="types", action="append_const", const=MovieMark
|
||||||
parser.add_argument('--review', help='migrate reviews, then exit', action='store_true')
|
)
|
||||||
parser.add_argument('--collection', help='migrate collections, then exit', action='store_true')
|
parser.add_argument(
|
||||||
parser.add_argument('--id', help='id to convert; or, if using with --max-id, the min id')
|
"--album", dest="types", action="append_const", const=AlbumMark
|
||||||
parser.add_argument('--maxid', help='max id to convert')
|
)
|
||||||
parser.add_argument('--failstop', help='stop on fail', action='store_true')
|
parser.add_argument(
|
||||||
parser.add_argument('--initshelf', help='initialize shelves for users, then exit', action='store_true')
|
"--game", dest="types", action="append_const", const=GameMark
|
||||||
parser.add_argument('--clear', help='clear all user pieces, then exit', action='store_true')
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--mark",
|
||||||
|
help="migrate shelves/tags/ratings, then exit",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--review", help="migrate reviews, then exit", action="store_true"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--collection", help="migrate collections, then exit", action="store_true"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--id", help="id to convert; or, if using with --max-id, the min id"
|
||||||
|
)
|
||||||
|
parser.add_argument("--maxid", help="max id to convert")
|
||||||
|
parser.add_argument("--failstop", help="stop on fail", action="store_true")
|
||||||
|
parser.add_argument(
|
||||||
|
"--initshelf",
|
||||||
|
help="initialize shelves for users, then exit",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--clear", help="clear all user pieces, then exit", action="store_true"
|
||||||
|
)
|
||||||
|
|
||||||
def initshelf(self):
|
def initshelf(self):
|
||||||
print("Initialize shelves")
|
print("Initialize shelves")
|
||||||
|
@ -91,7 +116,11 @@ class Command(BaseCommand):
|
||||||
def collection(self, options):
|
def collection(self, options):
|
||||||
collection_map = {}
|
collection_map = {}
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
qs = Legacy_Collection.objects.all().filter(owner__is_active=True).order_by('id')
|
qs = (
|
||||||
|
Legacy_Collection.objects.all()
|
||||||
|
.filter(owner__is_active=True)
|
||||||
|
.order_by("id")
|
||||||
|
)
|
||||||
for entity in tqdm(qs):
|
for entity in tqdm(qs):
|
||||||
c = Collection.objects.create(
|
c = Collection.objects.create(
|
||||||
owner_id=entity.owner_id,
|
owner_id=entity.owner_id,
|
||||||
|
@ -115,11 +144,15 @@ class Command(BaseCommand):
|
||||||
if old_id:
|
if old_id:
|
||||||
item_link = LinkModel.objects.get(old_id=old_id)
|
item_link = LinkModel.objects.get(old_id=old_id)
|
||||||
item = Item.objects.get(uid=item_link.new_uid)
|
item = Item.objects.get(uid=item_link.new_uid)
|
||||||
c.append_item(item, metadata={'comment': citem.comment})
|
c.append_item(item, metadata={"note": citem.comment})
|
||||||
else:
|
else:
|
||||||
# TODO convert song to album
|
# TODO convert song to album
|
||||||
print(f'{c.owner} {c.id} {c.title} {citem.item} were skipped')
|
print(f"{c.owner} {c.id} {c.title} {citem.item} were skipped")
|
||||||
qs = Legacy_CollectionMark.objects.all().filter(owner__is_active=True).order_by('id')
|
qs = (
|
||||||
|
Legacy_CollectionMark.objects.all()
|
||||||
|
.filter(owner__is_active=True)
|
||||||
|
.order_by("id")
|
||||||
|
)
|
||||||
for entity in tqdm(qs):
|
for entity in tqdm(qs):
|
||||||
Like.objects.create(
|
Like.objects.create(
|
||||||
owner_id=entity.owner_id,
|
owner_id=entity.owner_id,
|
||||||
|
@ -132,12 +165,14 @@ class Command(BaseCommand):
|
||||||
for typ in [GameReview, AlbumReview, BookReview, MovieReview]:
|
for typ in [GameReview, AlbumReview, BookReview, MovieReview]:
|
||||||
print(typ)
|
print(typ)
|
||||||
LinkModel = model_link[typ]
|
LinkModel = model_link[typ]
|
||||||
qs = typ.objects.all().filter(owner__is_active=True).order_by('id')
|
qs = typ.objects.all().filter(owner__is_active=True).order_by("id")
|
||||||
if options['id']:
|
if options["id"]:
|
||||||
if options['maxid']:
|
if options["maxid"]:
|
||||||
qs = qs.filter(id__gte=int(options['id']), id__lte=int(options['maxid']))
|
qs = qs.filter(
|
||||||
|
id__gte=int(options["id"]), id__lte=int(options["maxid"])
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
qs = qs.filter(id=int(options['id']))
|
qs = qs.filter(id=int(options["id"]))
|
||||||
pg = Paginator(qs, BATCH_SIZE)
|
pg = Paginator(qs, BATCH_SIZE)
|
||||||
for p in tqdm(pg.page_range):
|
for p in tqdm(pg.page_range):
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
|
@ -145,28 +180,42 @@ class Command(BaseCommand):
|
||||||
try:
|
try:
|
||||||
item_link = LinkModel.objects.get(old_id=entity.item.id)
|
item_link = LinkModel.objects.get(old_id=entity.item.id)
|
||||||
item = Item.objects.get(uid=item_link.new_uid)
|
item = Item.objects.get(uid=item_link.new_uid)
|
||||||
Review.objects.create(owner=entity.owner, item=item, title=entity.title, body=entity.content, metadata={'shared_link': entity.shared_link}, visibility=entity.visibility, created_time=entity.created_time, edited_time=entity.edited_time)
|
Review.objects.create(
|
||||||
|
owner=entity.owner,
|
||||||
|
item=item,
|
||||||
|
title=entity.title,
|
||||||
|
body=entity.content,
|
||||||
|
metadata={"shared_link": entity.shared_link},
|
||||||
|
visibility=entity.visibility,
|
||||||
|
created_time=entity.created_time,
|
||||||
|
edited_time=entity.edited_time,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'Convert failed for {typ} {entity.id}: {e}')
|
print(f"Convert failed for {typ} {entity.id}: {e}")
|
||||||
if options['failstop']:
|
if options["failstop"]:
|
||||||
raise(e)
|
raise (e)
|
||||||
|
|
||||||
def mark(self, options):
|
def mark(self, options):
|
||||||
types = options['types'] or [GameMark, AlbumMark, MovieMark, BookMark]
|
types = options["types"] or [GameMark, AlbumMark, MovieMark, BookMark]
|
||||||
print('Preparing cache')
|
print("Preparing cache")
|
||||||
tag_cache = {f'{t.owner_id}_{t.title}': t.id for t in Tag.objects.all()}
|
tag_cache = {f"{t.owner_id}_{t.title}": t.id for t in Tag.objects.all()}
|
||||||
shelf_cache = {f'{s.owner_id}_{s.item_category}_{shelf_map[s.shelf_type]}': s.id for s in Shelf.objects.all()}
|
shelf_cache = {
|
||||||
|
f"{s.owner_id}_{s.item_category}_{shelf_map[s.shelf_type]}": s.id
|
||||||
|
for s in Shelf.objects.all()
|
||||||
|
}
|
||||||
|
|
||||||
for typ in types:
|
for typ in types:
|
||||||
print(typ)
|
print(typ)
|
||||||
LinkModel = model_link[typ]
|
LinkModel = model_link[typ]
|
||||||
tag_field = tag_map[typ]
|
tag_field = tag_map[typ]
|
||||||
qs = typ.objects.all().filter(owner__is_active=True).order_by('id')
|
qs = typ.objects.all().filter(owner__is_active=True).order_by("id")
|
||||||
if options['id']:
|
if options["id"]:
|
||||||
if options['maxid']:
|
if options["maxid"]:
|
||||||
qs = qs.filter(id__gte=int(options['id']), id__lte=int(options['maxid']))
|
qs = qs.filter(
|
||||||
|
id__gte=int(options["id"]), id__lte=int(options["maxid"])
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
qs = qs.filter(id=int(options['id']))
|
qs = qs.filter(id=int(options["id"]))
|
||||||
|
|
||||||
pg = Paginator(qs, BATCH_SIZE)
|
pg = Paginator(qs, BATCH_SIZE)
|
||||||
for p in tqdm(pg.page_range):
|
for p in tqdm(pg.page_range):
|
||||||
|
@ -193,22 +242,42 @@ class Command(BaseCommand):
|
||||||
visibility = entity.visibility
|
visibility = entity.visibility
|
||||||
created_time = entity.created_time
|
created_time = entity.created_time
|
||||||
if entity.rating:
|
if entity.rating:
|
||||||
Rating.objects.create(owner_id=user_id, item_id=item_id, grade=entity.rating, visibility=visibility)
|
Rating.objects.create(
|
||||||
|
owner_id=user_id,
|
||||||
|
item_id=item_id,
|
||||||
|
grade=entity.rating,
|
||||||
|
visibility=visibility,
|
||||||
|
)
|
||||||
if entity.text:
|
if entity.text:
|
||||||
Comment.objects.create(owner_id=user_id, item_id=item_id, text=entity.text, visibility=visibility)
|
Comment.objects.create(
|
||||||
shelf = shelf_cache[f'{user_id}_{item.category}_{entity.status}']
|
owner_id=user_id,
|
||||||
|
item_id=item_id,
|
||||||
|
text=entity.text,
|
||||||
|
visibility=visibility,
|
||||||
|
)
|
||||||
|
shelf = shelf_cache[
|
||||||
|
f"{user_id}_{item.category}_{entity.status}"
|
||||||
|
]
|
||||||
ShelfMember.objects.create(
|
ShelfMember.objects.create(
|
||||||
parent_id=shelf,
|
parent_id=shelf,
|
||||||
owner_id=user_id,
|
owner_id=user_id,
|
||||||
position=0,
|
position=0,
|
||||||
item_id=item_id,
|
item_id=item_id,
|
||||||
metadata={'shared_link': entity.shared_link},
|
metadata={"shared_link": entity.shared_link},
|
||||||
created_time=created_time)
|
created_time=created_time,
|
||||||
ShelfLogEntry.objects.create(owner_id=user_id, shelf_id=shelf, item_id=item_id, timestamp=created_time)
|
)
|
||||||
|
ShelfLogEntry.objects.create(
|
||||||
|
owner_id=user_id,
|
||||||
|
shelf_id=shelf,
|
||||||
|
item_id=item_id,
|
||||||
|
timestamp=created_time,
|
||||||
|
)
|
||||||
for title in tags:
|
for title in tags:
|
||||||
tag_key = f'{user_id}_{title}'
|
tag_key = f"{user_id}_{title}"
|
||||||
if tag_key not in tag_cache:
|
if tag_key not in tag_cache:
|
||||||
tag = Tag.objects.create(owner_id=user_id, title=title, visibility=0).id
|
tag = Tag.objects.create(
|
||||||
|
owner_id=user_id, title=title, visibility=0
|
||||||
|
).id
|
||||||
tag_cache[tag_key] = tag
|
tag_cache[tag_key] = tag
|
||||||
else:
|
else:
|
||||||
tag = tag_cache[tag_key]
|
tag = tag_cache[tag_key]
|
||||||
|
@ -217,28 +286,31 @@ class Command(BaseCommand):
|
||||||
owner_id=user_id,
|
owner_id=user_id,
|
||||||
position=0,
|
position=0,
|
||||||
item_id=item_id,
|
item_id=item_id,
|
||||||
created_time=created_time)
|
created_time=created_time,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'Convert failed for {typ} {entity.id}: {e}')
|
print(f"Convert failed for {typ} {entity.id}: {e}")
|
||||||
if options['failstop']:
|
if options["failstop"]:
|
||||||
raise(e)
|
raise (e)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
if options['initshelf']:
|
if options["initshelf"]:
|
||||||
self.initshelf()
|
self.initshelf()
|
||||||
elif options['collection']:
|
elif options["collection"]:
|
||||||
if options['clear']:
|
if options["clear"]:
|
||||||
self.clear([Collection, Like])
|
self.clear([Collection, Like])
|
||||||
else:
|
else:
|
||||||
self.collection(options)
|
self.collection(options)
|
||||||
elif options['review']:
|
elif options["review"]:
|
||||||
if options['clear']:
|
if options["clear"]:
|
||||||
self.clear([Review])
|
self.clear([Review])
|
||||||
else:
|
else:
|
||||||
self.review(options)
|
self.review(options)
|
||||||
elif options['mark']:
|
elif options["mark"]:
|
||||||
if options['clear']:
|
if options["clear"]:
|
||||||
self.clear([Comment, Rating, TagMember, Tag, ShelfLogEntry, ShelfMember])
|
self.clear(
|
||||||
|
[Comment, Rating, TagMember, Tag, ShelfLogEntry, ShelfMember]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.mark(options)
|
self.mark(options)
|
||||||
self.stdout.write(self.style.SUCCESS(f'Done.'))
|
self.stdout.write(self.style.SUCCESS(f"Done."))
|
||||||
|
|
378
mastodon/api.py
378
mastodon/api.py
|
@ -19,47 +19,47 @@ logger = logging.getLogger(__name__)
|
||||||
# returns user info
|
# returns user info
|
||||||
# retruns the same info as verify account credentials
|
# retruns the same info as verify account credentials
|
||||||
# GET
|
# GET
|
||||||
API_GET_ACCOUNT = '/api/v1/accounts/:id'
|
API_GET_ACCOUNT = "/api/v1/accounts/:id"
|
||||||
|
|
||||||
# returns user info if valid, 401 if invalid
|
# returns user info if valid, 401 if invalid
|
||||||
# GET
|
# GET
|
||||||
API_VERIFY_ACCOUNT = '/api/v1/accounts/verify_credentials'
|
API_VERIFY_ACCOUNT = "/api/v1/accounts/verify_credentials"
|
||||||
|
|
||||||
# obtain token
|
# obtain token
|
||||||
# GET
|
# GET
|
||||||
API_OBTAIN_TOKEN = '/oauth/token'
|
API_OBTAIN_TOKEN = "/oauth/token"
|
||||||
|
|
||||||
# obatin auth code
|
# obatin auth code
|
||||||
# GET
|
# GET
|
||||||
API_OAUTH_AUTHORIZE = '/oauth/authorize'
|
API_OAUTH_AUTHORIZE = "/oauth/authorize"
|
||||||
|
|
||||||
# revoke token
|
# revoke token
|
||||||
# POST
|
# POST
|
||||||
API_REVOKE_TOKEN = '/oauth/revoke'
|
API_REVOKE_TOKEN = "/oauth/revoke"
|
||||||
|
|
||||||
# relationships
|
# relationships
|
||||||
# GET
|
# GET
|
||||||
API_GET_RELATIONSHIPS = '/api/v1/accounts/relationships'
|
API_GET_RELATIONSHIPS = "/api/v1/accounts/relationships"
|
||||||
|
|
||||||
# toot
|
# toot
|
||||||
# POST
|
# POST
|
||||||
API_PUBLISH_TOOT = '/api/v1/statuses'
|
API_PUBLISH_TOOT = "/api/v1/statuses"
|
||||||
|
|
||||||
# create new app
|
# create new app
|
||||||
# POST
|
# POST
|
||||||
API_CREATE_APP = '/api/v1/apps'
|
API_CREATE_APP = "/api/v1/apps"
|
||||||
|
|
||||||
# search
|
# search
|
||||||
# GET
|
# GET
|
||||||
API_SEARCH = '/api/v2/search'
|
API_SEARCH = "/api/v2/search"
|
||||||
|
|
||||||
TWITTER_DOMAIN = 'twitter.com'
|
TWITTER_DOMAIN = "twitter.com"
|
||||||
|
|
||||||
TWITTER_API_ME = 'https://api.twitter.com/2/users/me'
|
TWITTER_API_ME = "https://api.twitter.com/2/users/me"
|
||||||
|
|
||||||
TWITTER_API_POST = 'https://api.twitter.com/2/tweets'
|
TWITTER_API_POST = "https://api.twitter.com/2/tweets"
|
||||||
|
|
||||||
TWITTER_API_TOKEN = 'https://api.twitter.com/2/oauth2/token'
|
TWITTER_API_TOKEN = "https://api.twitter.com/2/oauth2/token"
|
||||||
|
|
||||||
USER_AGENT = f"{settings.CLIENT_NAME}/1.0"
|
USER_AGENT = f"{settings.CLIENT_NAME}/1.0"
|
||||||
|
|
||||||
|
@ -70,43 +70,38 @@ post = functools.partial(requests.post, timeout=settings.MASTODON_TIMEOUT)
|
||||||
|
|
||||||
# low level api below
|
# low level api below
|
||||||
def get_relationships(site, id_list, token): # no longer in use
|
def get_relationships(site, id_list, token): # no longer in use
|
||||||
url = 'https://' + site + API_GET_RELATIONSHIPS
|
url = "https://" + site + API_GET_RELATIONSHIPS
|
||||||
payload = {'id[]': id_list}
|
payload = {"id[]": id_list}
|
||||||
headers = {
|
headers = {"User-Agent": USER_AGENT, "Authorization": f"Bearer {token}"}
|
||||||
'User-Agent': USER_AGENT,
|
|
||||||
'Authorization': f'Bearer {token}'
|
|
||||||
}
|
|
||||||
response = get(url, headers=headers, params=payload)
|
response = get(url, headers=headers, params=payload)
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
def post_toot(site, content, visibility, token, local_only=False, update_id=None):
|
def post_toot(site, content, visibility, token, local_only=False, update_id=None):
|
||||||
headers = {
|
headers = {
|
||||||
'User-Agent': USER_AGENT,
|
"User-Agent": USER_AGENT,
|
||||||
'Authorization': f'Bearer {token}',
|
"Authorization": f"Bearer {token}",
|
||||||
'Idempotency-Key': random_string_generator(16)
|
"Idempotency-Key": random_string_generator(16),
|
||||||
}
|
}
|
||||||
if site == TWITTER_DOMAIN:
|
if site == TWITTER_DOMAIN:
|
||||||
url = TWITTER_API_POST
|
url = TWITTER_API_POST
|
||||||
payload = {
|
payload = {"text": content if len(content) <= 150 else content[0:150] + "..."}
|
||||||
'text': content if len(content) <= 150 else content[0:150] + '...'
|
|
||||||
}
|
|
||||||
response = post(url, headers=headers, json=payload)
|
response = post(url, headers=headers, json=payload)
|
||||||
if response.status_code == 201:
|
if response.status_code == 201:
|
||||||
response.status_code = 200
|
response.status_code = 200
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logger.error(f"Error {url} {response.status_code}")
|
logger.error(f"Error {url} {response.status_code}")
|
||||||
else:
|
else:
|
||||||
url = 'https://' + site + API_PUBLISH_TOOT
|
url = "https://" + site + API_PUBLISH_TOOT
|
||||||
payload = {
|
payload = {
|
||||||
'status': content,
|
"status": content,
|
||||||
'visibility': visibility,
|
"visibility": visibility,
|
||||||
}
|
}
|
||||||
if local_only:
|
if local_only:
|
||||||
payload['local_only'] = True
|
payload["local_only"] = True
|
||||||
try:
|
try:
|
||||||
if update_id:
|
if update_id:
|
||||||
response = put(url + '/' + update_id, headers=headers, data=payload)
|
response = put(url + "/" + update_id, headers=headers, data=payload)
|
||||||
if update_id is None or response.status_code != 200:
|
if update_id is None or response.status_code != 200:
|
||||||
response = post(url, headers=headers, data=payload)
|
response = post(url, headers=headers, data=payload)
|
||||||
if response.status_code == 201:
|
if response.status_code == 201:
|
||||||
|
@ -120,78 +115,81 @@ def post_toot(site, content, visibility, token, local_only=False, update_id=None
|
||||||
|
|
||||||
def get_instance_info(domain_name):
|
def get_instance_info(domain_name):
|
||||||
if domain_name.lower().strip() == TWITTER_DOMAIN:
|
if domain_name.lower().strip() == TWITTER_DOMAIN:
|
||||||
return TWITTER_DOMAIN, ''
|
return TWITTER_DOMAIN, ""
|
||||||
try:
|
try:
|
||||||
url = f'https://{domain_name}/api/v1/instance'
|
url = f"https://{domain_name}/api/v1/instance"
|
||||||
response = get(url, headers={'User-Agent': USER_AGENT})
|
response = get(url, headers={"User-Agent": USER_AGENT})
|
||||||
j = response.json()
|
j = response.json()
|
||||||
return j['uri'].lower().split('//')[-1].split('/')[0], j['version']
|
return j["uri"].lower().split("//")[-1].split("/")[0], j["version"]
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error(f"Error {url}")
|
logger.error(f"Error {url}")
|
||||||
return domain_name, ''
|
return domain_name, ""
|
||||||
|
|
||||||
|
|
||||||
def create_app(domain_name):
|
def create_app(domain_name):
|
||||||
# naive protocal strip
|
# naive protocal strip
|
||||||
is_http = False
|
is_http = False
|
||||||
if domain_name.startswith("https://"):
|
if domain_name.startswith("https://"):
|
||||||
domain_name = domain_name.replace("https://", '')
|
domain_name = domain_name.replace("https://", "")
|
||||||
elif domain_name.startswith("http://"):
|
elif domain_name.startswith("http://"):
|
||||||
is_http = True
|
is_http = True
|
||||||
domain_name = domain_name.replace("http://", '')
|
domain_name = domain_name.replace("http://", "")
|
||||||
if domain_name.endswith('/'):
|
if domain_name.endswith("/"):
|
||||||
domain_name = domain_name[0:-1]
|
domain_name = domain_name[0:-1]
|
||||||
|
|
||||||
if not is_http:
|
if not is_http:
|
||||||
url = 'https://' + domain_name + API_CREATE_APP
|
url = "https://" + domain_name + API_CREATE_APP
|
||||||
else:
|
else:
|
||||||
url = 'http://' + domain_name + API_CREATE_APP
|
url = "http://" + domain_name + API_CREATE_APP
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
'client_name': settings.CLIENT_NAME,
|
"client_name": settings.CLIENT_NAME,
|
||||||
'scopes': settings.MASTODON_CLIENT_SCOPE,
|
"scopes": settings.MASTODON_CLIENT_SCOPE,
|
||||||
'redirect_uris': settings.REDIRECT_URIS,
|
"redirect_uris": settings.REDIRECT_URIS,
|
||||||
'website': settings.APP_WEBSITE
|
"website": settings.APP_WEBSITE,
|
||||||
}
|
}
|
||||||
|
|
||||||
response = post(url, data=payload, headers={'User-Agent': USER_AGENT})
|
response = post(url, data=payload, headers={"User-Agent": USER_AGENT})
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def get_site_id(username, user_site, target_site, token):
|
def get_site_id(username, user_site, target_site, token):
|
||||||
url = 'https://' + target_site + API_SEARCH
|
url = "https://" + target_site + API_SEARCH
|
||||||
payload = {
|
payload = {
|
||||||
'limit': 1,
|
"limit": 1,
|
||||||
'type': 'accounts',
|
"type": "accounts",
|
||||||
'resolve': True,
|
"resolve": True,
|
||||||
'q': f"{username}@{user_site}"
|
"q": f"{username}@{user_site}",
|
||||||
}
|
|
||||||
headers = {
|
|
||||||
'User-Agent': USER_AGENT,
|
|
||||||
'Authorization': f'Bearer {token}'
|
|
||||||
}
|
}
|
||||||
|
headers = {"User-Agent": USER_AGENT, "Authorization": f"Bearer {token}"}
|
||||||
response = get(url, params=payload, headers=headers)
|
response = get(url, params=payload, headers=headers)
|
||||||
try:
|
try:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error(f"Error parsing JSON from {url}")
|
logger.error(f"Error parsing JSON from {url}")
|
||||||
return None
|
return None
|
||||||
if 'accounts' not in data:
|
if "accounts" not in data:
|
||||||
return None
|
return None
|
||||||
elif len(data['accounts']) == 0: # target site may return empty if no cache of this user
|
elif (
|
||||||
|
len(data["accounts"]) == 0
|
||||||
|
): # target site may return empty if no cache of this user
|
||||||
return None
|
return None
|
||||||
elif data['accounts'][0]['acct'] != f"{username}@{user_site}": # or return another user with a similar id which needs to be skipped
|
elif (
|
||||||
|
data["accounts"][0]["acct"] != f"{username}@{user_site}"
|
||||||
|
): # or return another user with a similar id which needs to be skipped
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return data['accounts'][0]['id']
|
return data["accounts"][0]["id"]
|
||||||
|
|
||||||
|
|
||||||
# high level api below
|
# high level api below
|
||||||
def get_relationship(request_user, target_user, useless_token=None):
|
def get_relationship(request_user, target_user, useless_token=None):
|
||||||
return [{
|
return [
|
||||||
'blocked_by': target_user.is_blocking(request_user),
|
{
|
||||||
'following': request_user.is_following(target_user),
|
"blocked_by": target_user.is_blocking(request_user),
|
||||||
}]
|
"following": request_user.is_following(target_user),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_cross_site_id(target_user, target_site, token):
|
def get_cross_site_id(target_user, target_site, token):
|
||||||
|
@ -209,19 +207,22 @@ def get_cross_site_id(target_user, target_site, token):
|
||||||
try:
|
try:
|
||||||
cross_site_info = CrossSiteUserInfo.objects.get(
|
cross_site_info = CrossSiteUserInfo.objects.get(
|
||||||
uid=f"{target_user.username}@{target_user.mastodon_site}",
|
uid=f"{target_user.username}@{target_user.mastodon_site}",
|
||||||
target_site=target_site
|
target_site=target_site,
|
||||||
)
|
)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
cross_site_id = get_site_id(
|
cross_site_id = get_site_id(
|
||||||
target_user.username, target_user.mastodon_site, target_site, token)
|
target_user.username, target_user.mastodon_site, target_site, token
|
||||||
|
)
|
||||||
if not cross_site_id:
|
if not cross_site_id:
|
||||||
logger.error(f'unable to find cross_site_id for {target_user} on {target_site}')
|
logger.error(
|
||||||
|
f"unable to find cross_site_id for {target_user} on {target_site}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
cross_site_info = CrossSiteUserInfo.objects.create(
|
cross_site_info = CrossSiteUserInfo.objects.create(
|
||||||
uid=f"{target_user.username}@{target_user.mastodon_site}",
|
uid=f"{target_user.username}@{target_user.mastodon_site}",
|
||||||
target_site=target_site,
|
target_site=target_site,
|
||||||
site_id=cross_site_id,
|
site_id=cross_site_id,
|
||||||
local_id=target_user.id
|
local_id=target_user.id,
|
||||||
)
|
)
|
||||||
return cross_site_info.site_id
|
return cross_site_info.site_id
|
||||||
|
|
||||||
|
@ -229,31 +230,41 @@ def get_cross_site_id(target_user, target_site, token):
|
||||||
# utils below
|
# utils below
|
||||||
def random_string_generator(n):
|
def random_string_generator(n):
|
||||||
s = string.ascii_letters + string.punctuation + string.digits
|
s = string.ascii_letters + string.punctuation + string.digits
|
||||||
return ''.join(random.choice(s) for i in range(n))
|
return "".join(random.choice(s) for i in range(n))
|
||||||
|
|
||||||
|
|
||||||
def verify_account(site, token):
|
def verify_account(site, token):
|
||||||
if site == TWITTER_DOMAIN:
|
if site == TWITTER_DOMAIN:
|
||||||
url = TWITTER_API_ME + '?user.fields=id,username,name,description,profile_image_url,created_at,protected'
|
url = (
|
||||||
|
TWITTER_API_ME
|
||||||
|
+ "?user.fields=id,username,name,description,profile_image_url,created_at,protected"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
response = get(url, headers={'User-Agent': USER_AGENT, 'Authorization': f'Bearer {token}'})
|
response = get(
|
||||||
|
url,
|
||||||
|
headers={"User-Agent": USER_AGENT, "Authorization": f"Bearer {token}"},
|
||||||
|
)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logger.error(f"Error {url} {response.status_code}")
|
logger.error(f"Error {url} {response.status_code}")
|
||||||
return response.status_code, None
|
return response.status_code, None
|
||||||
r = response.json()['data']
|
r = response.json()["data"]
|
||||||
r['display_name'] = r['name']
|
r["display_name"] = r["name"]
|
||||||
r['note'] = r['description']
|
r["note"] = r["description"]
|
||||||
r['avatar'] = r['profile_image_url']
|
r["avatar"] = r["profile_image_url"]
|
||||||
r['avatar_static'] = r['profile_image_url']
|
r["avatar_static"] = r["profile_image_url"]
|
||||||
r['locked'] = r['protected']
|
r["locked"] = r["protected"]
|
||||||
r['url'] = f'https://{TWITTER_DOMAIN}/{r["username"]}'
|
r["url"] = f'https://{TWITTER_DOMAIN}/{r["username"]}'
|
||||||
return 200, r
|
return 200, r
|
||||||
except Exception:
|
except Exception:
|
||||||
return -1, None
|
return -1, None
|
||||||
url = 'https://' + site + API_VERIFY_ACCOUNT
|
url = "https://" + site + API_VERIFY_ACCOUNT
|
||||||
try:
|
try:
|
||||||
response = get(url, headers={'User-Agent': USER_AGENT, 'Authorization': f'Bearer {token}'})
|
response = get(
|
||||||
return response.status_code, (response.json() if response.status_code == 200 else None)
|
url, headers={"User-Agent": USER_AGENT, "Authorization": f"Bearer {token}"}
|
||||||
|
)
|
||||||
|
return response.status_code, (
|
||||||
|
response.json() if response.status_code == 200 else None
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return -1, None
|
return -1, None
|
||||||
|
|
||||||
|
@ -261,94 +272,130 @@ def verify_account(site, token):
|
||||||
def get_related_acct_list(site, token, api):
|
def get_related_acct_list(site, token, api):
|
||||||
if site == TWITTER_DOMAIN:
|
if site == TWITTER_DOMAIN:
|
||||||
return []
|
return []
|
||||||
url = 'https://' + site + api
|
url = "https://" + site + api
|
||||||
results = []
|
results = []
|
||||||
while url:
|
while url:
|
||||||
response = get(url, headers={'User-Agent': USER_AGENT, 'Authorization': f'Bearer {token}'})
|
response = get(
|
||||||
|
url, headers={"User-Agent": USER_AGENT, "Authorization": f"Bearer {token}"}
|
||||||
|
)
|
||||||
url = None
|
url = None
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
results.extend(map(lambda u: (u['acct'] if u['acct'].find('@') != -1 else u['acct'] + '@' + site) if 'acct' in u else u, response.json()))
|
results.extend(
|
||||||
if 'Link' in response.headers:
|
map(
|
||||||
for ls in response.headers['Link'].split(','):
|
lambda u: (
|
||||||
li = ls.strip().split(';')
|
u["acct"]
|
||||||
|
if u["acct"].find("@") != -1
|
||||||
|
else u["acct"] + "@" + site
|
||||||
|
)
|
||||||
|
if "acct" in u
|
||||||
|
else u,
|
||||||
|
response.json(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if "Link" in response.headers:
|
||||||
|
for ls in response.headers["Link"].split(","):
|
||||||
|
li = ls.strip().split(";")
|
||||||
if li[1].strip() == 'rel="next"':
|
if li[1].strip() == 'rel="next"':
|
||||||
url = li[0].strip().replace('>', '').replace('<', '')
|
url = li[0].strip().replace(">", "").replace("<", "")
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
class TootVisibilityEnum:
|
class TootVisibilityEnum:
|
||||||
PUBLIC = 'public'
|
PUBLIC = "public"
|
||||||
PRIVATE = 'private'
|
PRIVATE = "private"
|
||||||
DIRECT = 'direct'
|
DIRECT = "direct"
|
||||||
UNLISTED = 'unlisted'
|
UNLISTED = "unlisted"
|
||||||
|
|
||||||
|
|
||||||
def get_mastodon_application(domain):
|
def get_mastodon_application(domain):
|
||||||
app = MastodonApplication.objects.filter(domain_name=domain).first()
|
app = MastodonApplication.objects.filter(domain_name=domain).first()
|
||||||
if app is not None:
|
if app is not None:
|
||||||
return app, ''
|
return app, ""
|
||||||
if domain == TWITTER_DOMAIN:
|
if domain == TWITTER_DOMAIN:
|
||||||
return None, 'Twitter未配置'
|
return None, "Twitter未配置"
|
||||||
error_msg = None
|
error_msg = None
|
||||||
try:
|
try:
|
||||||
response = create_app(domain)
|
response = create_app(domain)
|
||||||
except (requests.exceptions.Timeout, ConnectionError):
|
except (requests.exceptions.Timeout, ConnectionError):
|
||||||
error_msg = "联邦网络请求超时。"
|
error_msg = "联邦网络请求超时。"
|
||||||
logger.error(f'Error creating app for {domain}: Timeout')
|
logger.error(f"Error creating app for {domain}: Timeout")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = "联邦网络请求失败 " + str(e)
|
error_msg = "联邦网络请求失败 " + str(e)
|
||||||
logger.error(f'Error creating app for {domain}: {e}')
|
logger.error(f"Error creating app for {domain}: {e}")
|
||||||
else:
|
else:
|
||||||
# fill the form with returned data
|
# fill the form with returned data
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
error_msg = "实例连接错误,代码: " + str(response.status_code)
|
error_msg = "实例连接错误,代码: " + str(response.status_code)
|
||||||
logger.error(f'Error creating app for {domain}: {response.status_code}')
|
logger.error(f"Error creating app for {domain}: {response.status_code}")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
except Exception:
|
except Exception:
|
||||||
error_msg = "实例返回内容无法识别"
|
error_msg = "实例返回内容无法识别"
|
||||||
logger.error(f'Error creating app for {domain}: unable to parse response')
|
logger.error(
|
||||||
|
f"Error creating app for {domain}: unable to parse response"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if settings.MASTODON_ALLOW_ANY_SITE:
|
if settings.MASTODON_ALLOW_ANY_SITE:
|
||||||
app = MastodonApplication.objects.create(domain_name=domain, app_id=data['id'], client_id=data['client_id'],
|
app = MastodonApplication.objects.create(
|
||||||
client_secret=data['client_secret'], vapid_key=data['vapid_key'] if 'vapid_key' in data else '')
|
domain_name=domain,
|
||||||
|
app_id=data["id"],
|
||||||
|
client_id=data["client_id"],
|
||||||
|
client_secret=data["client_secret"],
|
||||||
|
vapid_key=data["vapid_key"] if "vapid_key" in data else "",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
error_msg = "不支持其它实例登录"
|
error_msg = "不支持其它实例登录"
|
||||||
logger.error(f'Disallowed to create app for {domain}')
|
logger.error(f"Disallowed to create app for {domain}")
|
||||||
return app, error_msg
|
return app, error_msg
|
||||||
|
|
||||||
|
|
||||||
def get_mastodon_login_url(app, login_domain, version, request):
|
def get_mastodon_login_url(app, login_domain, version, request):
|
||||||
url = request.scheme + "://" + request.get_host() + reverse('users:OAuth2_login')
|
url = request.scheme + "://" + request.get_host() + reverse("users:OAuth2_login")
|
||||||
if login_domain == TWITTER_DOMAIN:
|
if login_domain == TWITTER_DOMAIN:
|
||||||
return f"https://twitter.com/i/oauth2/authorize?response_type=code&client_id={app.client_id}&redirect_uri={quote(url)}&scope={quote(settings.TWITTER_CLIENT_SCOPE)}&state=state&code_challenge=challenge&code_challenge_method=plain"
|
return f"https://twitter.com/i/oauth2/authorize?response_type=code&client_id={app.client_id}&redirect_uri={quote(url)}&scope={quote(settings.TWITTER_CLIENT_SCOPE)}&state=state&code_challenge=challenge&code_challenge_method=plain"
|
||||||
scope = settings.MASTODON_LEGACY_CLIENT_SCOPE if 'Pixelfed' in version else settings.MASTODON_CLIENT_SCOPE
|
scope = (
|
||||||
return "https://" + login_domain + "/oauth/authorize?client_id=" + app.client_id + "&scope=" + quote(scope) + "&redirect_uri=" + url + "&response_type=code"
|
settings.MASTODON_LEGACY_CLIENT_SCOPE
|
||||||
|
if "Pixelfed" in version
|
||||||
|
else settings.MASTODON_CLIENT_SCOPE
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
"https://"
|
||||||
|
+ login_domain
|
||||||
|
+ "/oauth/authorize?client_id="
|
||||||
|
+ app.client_id
|
||||||
|
+ "&scope="
|
||||||
|
+ quote(scope)
|
||||||
|
+ "&redirect_uri="
|
||||||
|
+ url
|
||||||
|
+ "&response_type=code"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def obtain_token(site, request, code):
|
def obtain_token(site, request, code):
|
||||||
""" Returns token if success else None. """
|
"""Returns token if success else None."""
|
||||||
mast_app = MastodonApplication.objects.get(domain_name=site)
|
mast_app = MastodonApplication.objects.get(domain_name=site)
|
||||||
redirect_uri = request.scheme + "://" + request.get_host() + reverse('users:OAuth2_login')
|
redirect_uri = (
|
||||||
|
request.scheme + "://" + request.get_host() + reverse("users:OAuth2_login")
|
||||||
|
)
|
||||||
payload = {
|
payload = {
|
||||||
'client_id': mast_app.client_id,
|
"client_id": mast_app.client_id,
|
||||||
'client_secret': mast_app.client_secret,
|
"client_secret": mast_app.client_secret,
|
||||||
'redirect_uri': redirect_uri,
|
"redirect_uri": redirect_uri,
|
||||||
'grant_type': 'authorization_code',
|
"grant_type": "authorization_code",
|
||||||
'code': code
|
"code": code,
|
||||||
}
|
}
|
||||||
headers = {'User-Agent': USER_AGENT}
|
headers = {"User-Agent": USER_AGENT}
|
||||||
auth = None
|
auth = None
|
||||||
if mast_app.is_proxy:
|
if mast_app.is_proxy:
|
||||||
url = 'https://' + mast_app.proxy_to + API_OBTAIN_TOKEN
|
url = "https://" + mast_app.proxy_to + API_OBTAIN_TOKEN
|
||||||
elif site == TWITTER_DOMAIN:
|
elif site == TWITTER_DOMAIN:
|
||||||
url = TWITTER_API_TOKEN
|
url = TWITTER_API_TOKEN
|
||||||
auth = (mast_app.client_id, mast_app.client_secret)
|
auth = (mast_app.client_id, mast_app.client_secret)
|
||||||
del payload['client_secret']
|
del payload["client_secret"]
|
||||||
payload['code_verifier'] = 'challenge'
|
payload["code_verifier"] = "challenge"
|
||||||
else:
|
else:
|
||||||
url = 'https://' + mast_app.domain_name + API_OBTAIN_TOKEN
|
url = "https://" + mast_app.domain_name + API_OBTAIN_TOKEN
|
||||||
try:
|
try:
|
||||||
response = post(url, data=payload, headers=headers, auth=auth)
|
response = post(url, data=payload, headers=headers, auth=auth)
|
||||||
# {"token_type":"bearer","expires_in":7200,"access_token":"VGpkOEZGR3FQRDJ5NkZ0dmYyYWIwS0dqeHpvTnk4eXp0NV9nWDJ2TEpmM1ZTOjE2NDg3ODMxNTU4Mzc6MToxOmF0OjE","scope":"block.read follows.read offline.access tweet.write users.read mute.read","refresh_token":"b1pXbGEzeUF1WE5yZHJOWmxTeWpvMTBrQmZPd0czLU0tQndZQTUyU3FwRDVIOjE2NDg3ODMxNTU4Mzg6MToxOnJ0OjE"}
|
# {"token_type":"bearer","expires_in":7200,"access_token":"VGpkOEZGR3FQRDJ5NkZ0dmYyYWIwS0dqeHpvTnk4eXp0NV9nWDJ2TEpmM1ZTOjE2NDg3ODMxNTU4Mzc6MToxOmF0OjE","scope":"block.read follows.read offline.access tweet.write users.read mute.read","refresh_token":"b1pXbGEzeUF1WE5yZHJOWmxTeWpvMTBrQmZPd0czLU0tQndZQTUyU3FwRDVIOjE2NDg3ODMxNTU4Mzg6MToxOnJ0OjE"}
|
||||||
|
@ -359,7 +406,7 @@ def obtain_token(site, request, code):
|
||||||
logger.error(f"Error {url} {e}")
|
logger.error(f"Error {url} {e}")
|
||||||
return None, None
|
return None, None
|
||||||
data = response.json()
|
data = response.json()
|
||||||
return data.get('access_token'), data.get('refresh_token', '')
|
return data.get("access_token"), data.get("refresh_token", "")
|
||||||
|
|
||||||
|
|
||||||
def refresh_access_token(site, refresh_token):
|
def refresh_access_token(site, refresh_token):
|
||||||
|
@ -368,34 +415,34 @@ def refresh_access_token(site, refresh_token):
|
||||||
mast_app = MastodonApplication.objects.get(domain_name=site)
|
mast_app = MastodonApplication.objects.get(domain_name=site)
|
||||||
url = TWITTER_API_TOKEN
|
url = TWITTER_API_TOKEN
|
||||||
payload = {
|
payload = {
|
||||||
'client_id': mast_app.client_id,
|
"client_id": mast_app.client_id,
|
||||||
'refresh_token': refresh_token,
|
"refresh_token": refresh_token,
|
||||||
'grant_type': 'refresh_token',
|
"grant_type": "refresh_token",
|
||||||
}
|
}
|
||||||
headers = {'User-Agent': USER_AGENT}
|
headers = {"User-Agent": USER_AGENT}
|
||||||
auth = (mast_app.client_id, mast_app.client_secret)
|
auth = (mast_app.client_id, mast_app.client_secret)
|
||||||
response = post(url, data=payload, headers=headers, auth=auth)
|
response = post(url, data=payload, headers=headers, auth=auth)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logger.error(f"Error {url} {response.status_code}")
|
logger.error(f"Error {url} {response.status_code}")
|
||||||
return None
|
return None
|
||||||
data = response.json()
|
data = response.json()
|
||||||
return data.get('access_token')
|
return data.get("access_token")
|
||||||
|
|
||||||
|
|
||||||
def revoke_token(site, token):
|
def revoke_token(site, token):
|
||||||
mast_app = MastodonApplication.objects.get(domain_name=site)
|
mast_app = MastodonApplication.objects.get(domain_name=site)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
'client_id': mast_app.client_id,
|
"client_id": mast_app.client_id,
|
||||||
'client_secret': mast_app.client_secret,
|
"client_secret": mast_app.client_secret,
|
||||||
'token': token
|
"token": token,
|
||||||
}
|
}
|
||||||
|
|
||||||
if mast_app.is_proxy:
|
if mast_app.is_proxy:
|
||||||
url = 'https://' + mast_app.proxy_to + API_REVOKE_TOKEN
|
url = "https://" + mast_app.proxy_to + API_REVOKE_TOKEN
|
||||||
else:
|
else:
|
||||||
url = 'https://' + site + API_REVOKE_TOKEN
|
url = "https://" + site + API_REVOKE_TOKEN
|
||||||
post(url, data=payload, headers={'User-Agent': USER_AGENT})
|
post(url, data=payload, headers={"User-Agent": USER_AGENT})
|
||||||
|
|
||||||
|
|
||||||
def share_mark(mark):
|
def share_mark(mark):
|
||||||
|
@ -408,22 +455,38 @@ def share_mark(mark):
|
||||||
visibility = TootVisibilityEnum.PUBLIC
|
visibility = TootVisibilityEnum.PUBLIC
|
||||||
else:
|
else:
|
||||||
visibility = TootVisibilityEnum.UNLISTED
|
visibility = TootVisibilityEnum.UNLISTED
|
||||||
tags = '\n' + user.get_preference().mastodon_append_tag.replace('[category]', str(mark.item.verbose_category_name)) if user.get_preference().mastodon_append_tag else ''
|
tags = (
|
||||||
stars = rating_to_emoji(mark.rating, MastodonApplication.objects.get(domain_name=user.mastodon_site).star_mode)
|
"\n"
|
||||||
|
+ user.get_preference().mastodon_append_tag.replace(
|
||||||
|
"[category]", str(mark.item.verbose_category_name)
|
||||||
|
)
|
||||||
|
if user.get_preference().mastodon_append_tag
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
stars = rating_to_emoji(
|
||||||
|
mark.rating,
|
||||||
|
MastodonApplication.objects.get(domain_name=user.mastodon_site).star_mode,
|
||||||
|
)
|
||||||
content = f"{mark.translated_status}《{mark.item.title}》{stars}\n{mark.item.absolute_url}\n{mark.text}{tags}"
|
content = f"{mark.translated_status}《{mark.item.title}》{stars}\n{mark.item.absolute_url}\n{mark.text}{tags}"
|
||||||
update_id = None
|
update_id = None
|
||||||
if mark.shared_link: # "https://mastodon.social/@username/1234567890"
|
if mark.shared_link: # "https://mastodon.social/@username/1234567890"
|
||||||
r = re.match(r'.+/(\w+)$', mark.shared_link) # might be re.match(r'.+/([^/]+)$', u) if Pleroma supports edit
|
r = re.match(
|
||||||
|
r".+/(\w+)$", mark.shared_link
|
||||||
|
) # might be re.match(r'.+/([^/]+)$', u) if Pleroma supports edit
|
||||||
update_id = r[1] if r else None
|
update_id = r[1] if r else None
|
||||||
response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token, False, update_id)
|
response = post_toot(
|
||||||
|
user.mastodon_site, content, visibility, user.mastodon_token, False, update_id
|
||||||
|
)
|
||||||
if response and response.status_code in [200, 201]:
|
if response and response.status_code in [200, 201]:
|
||||||
j = response.json()
|
j = response.json()
|
||||||
if 'url' in j:
|
if "url" in j:
|
||||||
mark.shared_link = j['url']
|
mark.shared_link = j["url"]
|
||||||
elif 'data' in j:
|
elif "data" in j:
|
||||||
mark.shared_link = f"https://twitter.com/{user.username}/status/{j['data']['id']}"
|
mark.shared_link = (
|
||||||
|
f"https://twitter.com/{user.username}/status/{j['data']['id']}"
|
||||||
|
)
|
||||||
if mark.shared_link:
|
if mark.shared_link:
|
||||||
mark.save(update_fields=['shared_link'])
|
mark.save(update_fields=["shared_link"])
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
@ -439,21 +502,34 @@ def share_review(review):
|
||||||
visibility = TootVisibilityEnum.PUBLIC
|
visibility = TootVisibilityEnum.PUBLIC
|
||||||
else:
|
else:
|
||||||
visibility = TootVisibilityEnum.UNLISTED
|
visibility = TootVisibilityEnum.UNLISTED
|
||||||
tags = '\n' + user.get_preference().mastodon_append_tag.replace('[category]', str(review.item.verbose_category_name)) if user.get_preference().mastodon_append_tag else ''
|
tags = (
|
||||||
|
"\n"
|
||||||
|
+ user.get_preference().mastodon_append_tag.replace(
|
||||||
|
"[category]", str(review.item.verbose_category_name)
|
||||||
|
)
|
||||||
|
if user.get_preference().mastodon_append_tag
|
||||||
|
else ""
|
||||||
|
)
|
||||||
content = f"发布了关于《{review.item.title}》的评论\n{review.url}\n{review.title}{tags}"
|
content = f"发布了关于《{review.item.title}》的评论\n{review.url}\n{review.title}{tags}"
|
||||||
update_id = None
|
update_id = None
|
||||||
if review.shared_link: # "https://mastodon.social/@username/1234567890"
|
if review.shared_link: # "https://mastodon.social/@username/1234567890"
|
||||||
r = re.match(r'.+/(\w+)$', review.shared_link) # might be re.match(r'.+/([^/]+)$', u) if Pleroma supports edit
|
r = re.match(
|
||||||
|
r".+/(\w+)$", review.shared_link
|
||||||
|
) # might be re.match(r'.+/([^/]+)$', u) if Pleroma supports edit
|
||||||
update_id = r[1] if r else None
|
update_id = r[1] if r else None
|
||||||
response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token, False, update_id)
|
response = post_toot(
|
||||||
|
user.mastodon_site, content, visibility, user.mastodon_token, False, update_id
|
||||||
|
)
|
||||||
if response and response.status_code in [200, 201]:
|
if response and response.status_code in [200, 201]:
|
||||||
j = response.json()
|
j = response.json()
|
||||||
if 'url' in j:
|
if "url" in j:
|
||||||
review.shared_link = j['url']
|
review.shared_link = j["url"]
|
||||||
elif 'data' in j:
|
elif "data" in j:
|
||||||
review.shared_link = f"https://twitter.com/{user.username}/status/{j['data']['id']}"
|
review.shared_link = (
|
||||||
|
f"https://twitter.com/{user.username}/status/{j['data']['id']}"
|
||||||
|
)
|
||||||
if review.shared_link:
|
if review.shared_link:
|
||||||
review.save(update_fields=['shared_link'])
|
review.save(update_fields=["shared_link"])
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
@ -468,15 +544,21 @@ def share_collection(collection, comment, user, visibility_no):
|
||||||
visibility = TootVisibilityEnum.PUBLIC
|
visibility = TootVisibilityEnum.PUBLIC
|
||||||
else:
|
else:
|
||||||
visibility = TootVisibilityEnum.UNLISTED
|
visibility = TootVisibilityEnum.UNLISTED
|
||||||
tags = '\n' + user.get_preference().mastodon_append_tag.replace('[category]', '收藏单') if user.get_preference().mastodon_append_tag else ''
|
tags = (
|
||||||
|
"\n" + user.get_preference().mastodon_append_tag.replace("[category]", "收藏单")
|
||||||
|
if user.get_preference().mastodon_append_tag
|
||||||
|
else ""
|
||||||
|
)
|
||||||
content = f"分享收藏单《{collection.title}》\n{collection.absolute_url}\n{comment}{tags}"
|
content = f"分享收藏单《{collection.title}》\n{collection.absolute_url}\n{comment}{tags}"
|
||||||
response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token)
|
response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token)
|
||||||
if response and response.status_code in [200, 201]:
|
if response and response.status_code in [200, 201]:
|
||||||
j = response.json()
|
j = response.json()
|
||||||
if 'url' in j:
|
if "url" in j:
|
||||||
shared_link = j['url']
|
shared_link = j["url"]
|
||||||
elif 'data' in j:
|
elif "data" in j:
|
||||||
shared_link = f"https://twitter.com/{user.username}/status/{j['data']['id']}"
|
shared_link = (
|
||||||
|
f"https://twitter.com/{user.username}/status/{j['data']['id']}"
|
||||||
|
)
|
||||||
if shared_link:
|
if shared_link:
|
||||||
pass
|
pass
|
||||||
return True
|
return True
|
||||||
|
|
2
pyproject.toml
Normal file
2
pyproject.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[tool.pyright]
|
||||||
|
exclude = [ "media", ".venv", ".git" ]
|
Loading…
Add table
Reference in a new issue