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
|
||||
.venv
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
|
|
@ -12,7 +12,23 @@ from functools import partialmethod
|
|||
from django.db.models import JSONField
|
||||
|
||||
|
||||
__all__ = ('BooleanField', 'CharField', 'DateField', 'DateTimeField', 'DecimalField', 'EmailField', 'FloatField', 'IntegerField', 'IPAddressField', 'GenericIPAddressField', 'NullBooleanField', 'TextField', 'TimeField', 'URLField', 'ArrayField')
|
||||
__all__ = (
|
||||
"BooleanField",
|
||||
"CharField",
|
||||
"DateField",
|
||||
"DateTimeField",
|
||||
"DecimalField",
|
||||
"EmailField",
|
||||
"FloatField",
|
||||
"IntegerField",
|
||||
"IPAddressField",
|
||||
"GenericIPAddressField",
|
||||
"NullBooleanField",
|
||||
"TextField",
|
||||
"TimeField",
|
||||
"URLField",
|
||||
"ArrayField",
|
||||
)
|
||||
|
||||
|
||||
class JSONFieldDescriptor(object):
|
||||
|
@ -26,12 +42,12 @@ class JSONFieldDescriptor(object):
|
|||
if isinstance(json_value, dict):
|
||||
if self.field.attname in json_value or not self.field.has_default():
|
||||
value = json_value.get(self.field.attname, None)
|
||||
if hasattr(self.field, 'from_json'):
|
||||
if hasattr(self.field, "from_json"):
|
||||
value = self.field.from_json(value)
|
||||
return value
|
||||
else:
|
||||
default = self.field.get_default()
|
||||
if hasattr(self.field, 'to_json'):
|
||||
if hasattr(self.field, "to_json"):
|
||||
json_value[self.field.attname] = self.field.to_json(default)
|
||||
else:
|
||||
json_value[self.field.attname] = default
|
||||
|
@ -45,7 +61,7 @@ class JSONFieldDescriptor(object):
|
|||
else:
|
||||
json_value = {}
|
||||
|
||||
if hasattr(self.field, 'to_json'):
|
||||
if hasattr(self.field, "to_json"):
|
||||
value = self.field.to_json(value)
|
||||
|
||||
if not value and self.field.blank and not self.field.null:
|
||||
|
@ -66,7 +82,7 @@ class JSONFieldMixin(object):
|
|||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.json_field_name = kwargs.pop('json_field_name', 'metadata')
|
||||
self.json_field_name = kwargs.pop("json_field_name", "metadata")
|
||||
super(JSONFieldMixin, self).__init__(*args, **kwargs)
|
||||
|
||||
def contribute_to_class(self, cls, name, private_only=False):
|
||||
|
@ -81,8 +97,11 @@ class JSONFieldMixin(object):
|
|||
setattr(cls, self.attname, descriptor)
|
||||
|
||||
if self.choices is not None:
|
||||
setattr(cls, 'get_%s_display' % self.name,
|
||||
partialmethod(cls._get_FIELD_display, field=self))
|
||||
setattr(
|
||||
cls,
|
||||
"get_%s_display" % self.name,
|
||||
partialmethod(cls._get_FIELD_display, field=self),
|
||||
)
|
||||
|
||||
def get_lookup(self, lookup_name):
|
||||
# Always return None, to make get_transform been called
|
||||
|
@ -101,15 +120,17 @@ class JSONFieldMixin(object):
|
|||
lhs.output_field = self.json_field
|
||||
transform = self.transform(lhs, **kwargs)
|
||||
transform._original_get_lookup = transform.get_lookup
|
||||
transform.get_lookup = lambda name: transform._original_get_lookup(self.original_lookup)
|
||||
transform.get_lookup = lambda name: transform._original_get_lookup(
|
||||
self.original_lookup
|
||||
)
|
||||
return transform
|
||||
|
||||
json_field = self.model._meta.get_field(self.json_field_name)
|
||||
transform = json_field.get_transform(self.name)
|
||||
if transform is None:
|
||||
raise FieldError(
|
||||
"JSONField '%s' has no support for key '%s' %s lookup" %
|
||||
(self.json_field_name, self.name, name)
|
||||
"JSONField '%s' has no support for key '%s' %s lookup"
|
||||
% (self.json_field_name, self.name, name)
|
||||
)
|
||||
|
||||
return TransformFactoryWrapper(json_field, transform, name)
|
||||
|
@ -123,8 +144,11 @@ class BooleanField(JSONFieldMixin, fields.BooleanField):
|
|||
|
||||
|
||||
class CharField(JSONFieldMixin, fields.CharField):
|
||||
def from_json(self, value): # TODO workaound some bad data in migration, should be removed after clean up
|
||||
def from_json(
|
||||
self, value
|
||||
): # TODO workaound some bad data in migration, should be removed after clean up
|
||||
return value if isinstance(value, str) else None
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
@ -133,7 +157,7 @@ class DateField(JSONFieldMixin, fields.DateField):
|
|||
if value:
|
||||
if not isinstance(value, (datetime, date)):
|
||||
value = dateparse.parse_date(value)
|
||||
return value.strftime('%Y-%m-%d')
|
||||
return value.strftime("%Y-%m-%d")
|
||||
|
||||
def from_json(self, value):
|
||||
if value is not None:
|
||||
|
|
|
@ -15,94 +15,95 @@ from users.models import User
|
|||
|
||||
|
||||
class SiteName(models.TextChoices):
|
||||
Douban = 'douban', _('豆瓣')
|
||||
Goodreads = 'goodreads', _('Goodreads')
|
||||
GoogleBooks = 'googlebooks', _('谷歌图书')
|
||||
IMDB = 'imdb', _('IMDB')
|
||||
TMDB = 'tmdb', _('The Movie Database')
|
||||
Bandcamp = 'bandcamp', _('Bandcamp')
|
||||
Spotify = 'spotify', _('Spotify')
|
||||
IGDB = 'igdb', _('IGDB')
|
||||
Steam = 'steam', _('Steam')
|
||||
Bangumi = 'bangumi', _('Bangumi')
|
||||
ApplePodcast = 'apple_podcast', _('苹果播客')
|
||||
Douban = "douban", _("豆瓣")
|
||||
Goodreads = "goodreads", _("Goodreads")
|
||||
GoogleBooks = "googlebooks", _("谷歌图书")
|
||||
IMDB = "imdb", _("IMDB")
|
||||
TMDB = "tmdb", _("The Movie Database")
|
||||
Bandcamp = "bandcamp", _("Bandcamp")
|
||||
Spotify = "spotify", _("Spotify")
|
||||
IGDB = "igdb", _("IGDB")
|
||||
Steam = "steam", _("Steam")
|
||||
Bangumi = "bangumi", _("Bangumi")
|
||||
ApplePodcast = "apple_podcast", _("苹果播客")
|
||||
|
||||
|
||||
class IdType(models.TextChoices):
|
||||
WikiData = 'wikidata', _('维基数据')
|
||||
ISBN10 = 'isbn10', _('ISBN10')
|
||||
ISBN = 'isbn', _('ISBN') # ISBN 13
|
||||
ASIN = 'asin', _('ASIN')
|
||||
ISSN = 'issn', _('ISSN')
|
||||
CUBN = 'cubn', _('统一书号')
|
||||
ISRC = 'isrc', _('ISRC') # only for songs
|
||||
GTIN = 'gtin', _('GTIN UPC EAN码') # ISBN is separate
|
||||
Feed = 'feed', _('Feed URL')
|
||||
IMDB = 'imdb', _('IMDb')
|
||||
TMDB_TV = 'tmdb_tv', _('TMDB剧集')
|
||||
TMDB_TVSeason = 'tmdb_tvseason', _('TMDB剧集')
|
||||
TMDB_TVEpisode = 'tmdb_tvepisode', _('TMDB剧集')
|
||||
TMDB_Movie = 'tmdb_movie', _('TMDB电影')
|
||||
Goodreads = 'goodreads', _('Goodreads')
|
||||
Goodreads_Work = 'goodreads_work', _('Goodreads著作')
|
||||
GoogleBooks = 'googlebooks', _('谷歌图书')
|
||||
DoubanBook = 'doubanbook', _('豆瓣读书')
|
||||
DoubanBook_Work = 'doubanbook_work', _('豆瓣读书著作')
|
||||
DoubanMovie = 'doubanmovie', _('豆瓣电影')
|
||||
DoubanMusic = 'doubanmusic', _('豆瓣音乐')
|
||||
DoubanGame = 'doubangame', _('豆瓣游戏')
|
||||
DoubanDrama = 'doubandrama', _('豆瓣舞台剧')
|
||||
Bandcamp = 'bandcamp', _('Bandcamp')
|
||||
Spotify_Album = 'spotify_album', _('Spotify专辑')
|
||||
Spotify_Show = 'spotify_show', _('Spotify播客')
|
||||
Discogs_Release = 'discogs_release', ('Discogs Release')
|
||||
Discogs_Master = 'discogs_master', ('Discogs Master')
|
||||
MusicBrainz = 'musicbrainz', ('MusicBrainz ID')
|
||||
DoubanBook_Author = 'doubanbook_author', _('豆瓣读书作者')
|
||||
DoubanCelebrity = 'doubanmovie_celebrity', _('豆瓣电影影人')
|
||||
Goodreads_Author = 'goodreads_author', _('Goodreads作者')
|
||||
Spotify_Artist = 'spotify_artist', _('Spotify艺术家')
|
||||
TMDB_Person = 'tmdb_person', _('TMDB影人')
|
||||
IGDB = 'igdb', _('IGDB游戏')
|
||||
Steam = 'steam', _('Steam游戏')
|
||||
Bangumi = 'bangumi', _('Bangumi')
|
||||
ApplePodcast = 'apple_podcast', _('苹果播客')
|
||||
WikiData = "wikidata", _("维基数据")
|
||||
ISBN10 = "isbn10", _("ISBN10")
|
||||
ISBN = "isbn", _("ISBN") # ISBN 13
|
||||
ASIN = "asin", _("ASIN")
|
||||
ISSN = "issn", _("ISSN")
|
||||
CUBN = "cubn", _("统一书号")
|
||||
ISRC = "isrc", _("ISRC") # only for songs
|
||||
GTIN = "gtin", _("GTIN UPC EAN码") # ISBN is separate
|
||||
Feed = "feed", _("Feed URL")
|
||||
IMDB = "imdb", _("IMDb")
|
||||
TMDB_TV = "tmdb_tv", _("TMDB剧集")
|
||||
TMDB_TVSeason = "tmdb_tvseason", _("TMDB剧集")
|
||||
TMDB_TVEpisode = "tmdb_tvepisode", _("TMDB剧集")
|
||||
TMDB_Movie = "tmdb_movie", _("TMDB电影")
|
||||
Goodreads = "goodreads", _("Goodreads")
|
||||
Goodreads_Work = "goodreads_work", _("Goodreads著作")
|
||||
GoogleBooks = "googlebooks", _("谷歌图书")
|
||||
DoubanBook = "doubanbook", _("豆瓣读书")
|
||||
DoubanBook_Work = "doubanbook_work", _("豆瓣读书著作")
|
||||
DoubanMovie = "doubanmovie", _("豆瓣电影")
|
||||
DoubanMusic = "doubanmusic", _("豆瓣音乐")
|
||||
DoubanGame = "doubangame", _("豆瓣游戏")
|
||||
DoubanDrama = "doubandrama", _("豆瓣舞台剧")
|
||||
Bandcamp = "bandcamp", _("Bandcamp")
|
||||
Spotify_Album = "spotify_album", _("Spotify专辑")
|
||||
Spotify_Show = "spotify_show", _("Spotify播客")
|
||||
Discogs_Release = "discogs_release", ("Discogs Release")
|
||||
Discogs_Master = "discogs_master", ("Discogs Master")
|
||||
MusicBrainz = "musicbrainz", ("MusicBrainz ID")
|
||||
DoubanBook_Author = "doubanbook_author", _("豆瓣读书作者")
|
||||
DoubanCelebrity = "doubanmovie_celebrity", _("豆瓣电影影人")
|
||||
Goodreads_Author = "goodreads_author", _("Goodreads作者")
|
||||
Spotify_Artist = "spotify_artist", _("Spotify艺术家")
|
||||
TMDB_Person = "tmdb_person", _("TMDB影人")
|
||||
IGDB = "igdb", _("IGDB游戏")
|
||||
Steam = "steam", _("Steam游戏")
|
||||
Bangumi = "bangumi", _("Bangumi")
|
||||
ApplePodcast = "apple_podcast", _("苹果播客")
|
||||
|
||||
|
||||
class ItemType(models.TextChoices):
|
||||
Book = 'book', _('书')
|
||||
TV = 'tv', _('剧集')
|
||||
TVSeason = 'tvseason', _('剧集分季')
|
||||
TVEpisode = 'tvepisode', _('剧集分集')
|
||||
Movie = 'movie', _('电影')
|
||||
Music = 'music', _('音乐')
|
||||
Game = 'game', _('游戏')
|
||||
Boardgame = 'boardgame', _('桌游')
|
||||
Podcast = 'podcast', _('播客')
|
||||
FanFic = 'fanfic', _('网文')
|
||||
Performance = 'performance', _('演出')
|
||||
Exhibition = 'exhibition', _('展览')
|
||||
Collection = 'collection', _('收藏单')
|
||||
Book = "book", _("书")
|
||||
TV = "tv", _("剧集")
|
||||
TVSeason = "tvseason", _("剧集分季")
|
||||
TVEpisode = "tvepisode", _("剧集分集")
|
||||
Movie = "movie", _("电影")
|
||||
Music = "music", _("音乐")
|
||||
Game = "game", _("游戏")
|
||||
Boardgame = "boardgame", _("桌游")
|
||||
Podcast = "podcast", _("播客")
|
||||
FanFic = "fanfic", _("网文")
|
||||
Performance = "performance", _("演出")
|
||||
Exhibition = "exhibition", _("展览")
|
||||
Collection = "collection", _("收藏单")
|
||||
|
||||
|
||||
class ItemCategory(models.TextChoices):
|
||||
Book = 'book', _('书')
|
||||
Movie = 'movie', _('电影')
|
||||
TV = 'tv', _('剧集')
|
||||
Music = 'music', _('音乐')
|
||||
Game = 'game', _('游戏')
|
||||
Boardgame = 'boardgame', _('桌游')
|
||||
Podcast = 'podcast', _('播客')
|
||||
FanFic = 'fanfic', _('网文')
|
||||
Performance = 'performance', _('演出')
|
||||
Exhibition = 'exhibition', _('展览')
|
||||
Collection = 'collection', _('收藏单')
|
||||
Book = "book", _("书")
|
||||
Movie = "movie", _("电影")
|
||||
TV = "tv", _("剧集")
|
||||
Music = "music", _("音乐")
|
||||
Game = "game", _("游戏")
|
||||
Boardgame = "boardgame", _("桌游")
|
||||
Podcast = "podcast", _("播客")
|
||||
FanFic = "fanfic", _("网文")
|
||||
Performance = "performance", _("演出")
|
||||
Exhibition = "exhibition", _("展览")
|
||||
Collection = "collection", _("收藏单")
|
||||
|
||||
|
||||
class SubItemType(models.TextChoices):
|
||||
Season = 'season', _('剧集分季')
|
||||
Episode = 'episode', _('剧集分集')
|
||||
Version = 'version', _('版本')
|
||||
Season = "season", _("剧集分季")
|
||||
Episode = "episode", _("剧集分集")
|
||||
Version = "version", _("版本")
|
||||
|
||||
|
||||
# class CreditType(models.TextChoices):
|
||||
# Author = 'author', _('作者')
|
||||
|
@ -176,37 +177,64 @@ class Item(SoftDeleteMixin, PolymorphicModel):
|
|||
category = None # subclass must specify this
|
||||
demonstrative = None # subclass must specify this
|
||||
uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
||||
title = models.CharField(_("title in primary language"), max_length=1000, default="")
|
||||
title = models.CharField(
|
||||
_("title in primary language"), max_length=1000, default=""
|
||||
)
|
||||
brief = models.TextField(_("简介"), blank=True, default="")
|
||||
primary_lookup_id_type = models.CharField(_("isbn/cubn/imdb"), blank=False, null=True, max_length=50)
|
||||
primary_lookup_id_value = models.CharField(_("1234/tt789"), blank=False, null=True, max_length=1000)
|
||||
primary_lookup_id_type = models.CharField(
|
||||
_("isbn/cubn/imdb"), blank=False, null=True, max_length=50
|
||||
)
|
||||
primary_lookup_id_value = models.CharField(
|
||||
_("1234/tt789"), blank=False, null=True, max_length=1000
|
||||
)
|
||||
metadata = models.JSONField(_("其他信息"), blank=True, null=True, default=dict)
|
||||
cover = models.ImageField(upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True)
|
||||
cover = models.ImageField(
|
||||
upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True
|
||||
)
|
||||
created_time = models.DateTimeField(auto_now_add=True)
|
||||
edited_time = models.DateTimeField(auto_now=True)
|
||||
is_deleted = models.BooleanField(default=False, db_index=True)
|
||||
history = HistoricalRecords()
|
||||
merged_to_item = models.ForeignKey('Item', null=True, on_delete=models.SET_NULL, default=None, related_name="merged_from_items")
|
||||
last_editor = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='+', null=True, blank=False)
|
||||
merged_to_item = models.ForeignKey(
|
||||
"Item",
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
default=None,
|
||||
related_name="merged_from_items",
|
||||
)
|
||||
last_editor = models.ForeignKey(
|
||||
User, on_delete=models.SET_NULL, related_name="+", null=True, blank=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = [['polymorphic_ctype_id', 'primary_lookup_id_type', 'primary_lookup_id_value']]
|
||||
unique_together = [
|
||||
[
|
||||
"polymorphic_ctype_id",
|
||||
"primary_lookup_id_type",
|
||||
"primary_lookup_id_value",
|
||||
]
|
||||
]
|
||||
|
||||
def clear(self):
|
||||
self.primary_lookup_id_value = None
|
||||
self.primary_lookup_id_type = None
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.id}|{self.uuid}{' ' + self.primary_lookup_id_type + ':' + self.primary_lookup_id_value if self.primary_lookup_id_value else ''} ({self.title})"
|
||||
return f"{self.id}|{self.uuid} {self.primary_lookup_id_type}:{self.primary_lookup_id_value if self.primary_lookup_id_value else ''} ({self.title})"
|
||||
|
||||
@classmethod
|
||||
def get_best_lookup_id(cls, lookup_ids):
|
||||
"""get best available lookup id, ideally commonly used"""
|
||||
best_id_types = [
|
||||
IdType.ISBN, IdType.CUBN, IdType.ASIN,
|
||||
IdType.GTIN, IdType.ISRC, IdType.MusicBrainz,
|
||||
IdType.ISBN,
|
||||
IdType.CUBN,
|
||||
IdType.ASIN,
|
||||
IdType.GTIN,
|
||||
IdType.ISRC,
|
||||
IdType.MusicBrainz,
|
||||
IdType.Feed,
|
||||
IdType.IMDB, IdType.TMDB_TVSeason
|
||||
IdType.IMDB,
|
||||
IdType.TMDB_TVSeason,
|
||||
]
|
||||
for t in best_id_types:
|
||||
if lookup_ids.get(t):
|
||||
|
@ -215,11 +243,11 @@ class Item(SoftDeleteMixin, PolymorphicModel):
|
|||
|
||||
def merge(self, to_item):
|
||||
if to_item is None:
|
||||
raise(ValueError('cannot merge to an empty item'))
|
||||
raise (ValueError("cannot merge to an empty item"))
|
||||
elif to_item.merged_to_item is not None:
|
||||
raise(ValueError('cannot merge with an item aleady merged'))
|
||||
raise (ValueError("cannot merge with an item aleady merged"))
|
||||
elif to_item.__class__ != self.__class__:
|
||||
raise(ValueError('cannot merge with an item in different class'))
|
||||
raise (ValueError("cannot merge with an item in different class"))
|
||||
else:
|
||||
self.merged_to_item = to_item
|
||||
|
||||
|
@ -229,15 +257,15 @@ class Item(SoftDeleteMixin, PolymorphicModel):
|
|||
|
||||
@property
|
||||
def url(self):
|
||||
return f'/{self.url_path}/{self.uuid}' if self.url_path else None
|
||||
return f"/{self.url_path}/{self.uuid}" if self.url_path else None
|
||||
|
||||
@property
|
||||
def absolute_url(self):
|
||||
return (settings.APP_WEBSITE + self.url) if self.url_path else None
|
||||
return f"{settings.APP_WEBSITE}{self.url}" if self.url_path else None
|
||||
|
||||
@property
|
||||
def api_url(self):
|
||||
return ('/api/' + self.url) if self.url_path else None
|
||||
return f"/api/{self.url}" if self.url_path else None
|
||||
|
||||
@property
|
||||
def class_name(self):
|
||||
|
@ -245,7 +273,7 @@ class Item(SoftDeleteMixin, PolymorphicModel):
|
|||
|
||||
@classmethod
|
||||
def get_by_url(cls, url_or_b62):
|
||||
b62 = url_or_b62.strip().split('/')[-1]
|
||||
b62 = url_or_b62.strip().split("/")[-1]
|
||||
return cls.objects.get(uid=uuid.UUID(int=base62.decode(b62)))
|
||||
|
||||
# def get_lookup_id(self, id_type: str) -> str:
|
||||
|
@ -258,11 +286,18 @@ class Item(SoftDeleteMixin, PolymorphicModel):
|
|||
# ll = list(filter(lambda a, b: b, ll))
|
||||
pass
|
||||
|
||||
METADATA_COPY_LIST = ['title', 'brief'] # list of metadata keys to copy from resource to item
|
||||
METADATA_COPY_LIST = [
|
||||
"title",
|
||||
"brief",
|
||||
] # list of metadata keys to copy from resource to item
|
||||
|
||||
@classmethod
|
||||
def copy_metadata(cls, metadata):
|
||||
return dict((k, v) for k, v in metadata.items() if k in cls.METADATA_COPY_LIST and v is not None)
|
||||
return dict(
|
||||
(k, v)
|
||||
for k, v in metadata.items()
|
||||
if k in cls.METADATA_COPY_LIST and v is not None
|
||||
)
|
||||
|
||||
def has_cover(self):
|
||||
return self.cover and self.cover != DEFAULT_ITEM_COVER
|
||||
|
@ -286,21 +321,38 @@ class Item(SoftDeleteMixin, PolymorphicModel):
|
|||
|
||||
|
||||
class ItemLookupId(models.Model):
|
||||
item = models.ForeignKey(Item, null=True, on_delete=models.SET_NULL, related_name='lookup_ids')
|
||||
id_type = models.CharField(_("源网站"), blank=True, choices=IdType.choices, max_length=50)
|
||||
item = models.ForeignKey(
|
||||
Item, null=True, on_delete=models.SET_NULL, related_name="lookup_ids"
|
||||
)
|
||||
id_type = models.CharField(
|
||||
_("源网站"), blank=True, choices=IdType.choices, max_length=50
|
||||
)
|
||||
id_value = models.CharField(_("源网站ID"), blank=True, max_length=1000)
|
||||
raw_url = models.CharField(_("源网站ID"), blank=True, max_length=1000, unique=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = [['id_type', 'id_value']]
|
||||
unique_together = [["id_type", "id_value"]]
|
||||
|
||||
|
||||
class ExternalResource(models.Model):
|
||||
item = models.ForeignKey(Item, null=True, on_delete=models.SET_NULL, related_name='external_resources')
|
||||
id_type = models.CharField(_("IdType of the source site"), blank=False, choices=IdType.choices, max_length=50)
|
||||
id_value = models.CharField(_("Primary Id on the source site"), blank=False, max_length=1000)
|
||||
url = models.CharField(_("url to the resource"), blank=False, max_length=1000, unique=True)
|
||||
cover = models.ImageField(upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True)
|
||||
item = models.ForeignKey(
|
||||
Item, null=True, on_delete=models.SET_NULL, related_name="external_resources"
|
||||
)
|
||||
id_type = models.CharField(
|
||||
_("IdType of the source site"),
|
||||
blank=False,
|
||||
choices=IdType.choices,
|
||||
max_length=50,
|
||||
)
|
||||
id_value = models.CharField(
|
||||
_("Primary Id on the source site"), blank=False, max_length=1000
|
||||
)
|
||||
url = models.CharField(
|
||||
_("url to the resource"), blank=False, max_length=1000, unique=True
|
||||
)
|
||||
cover = models.ImageField(
|
||||
upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True
|
||||
)
|
||||
other_lookup_ids = models.JSONField(default=dict)
|
||||
metadata = models.JSONField(default=dict)
|
||||
scraped_time = models.DateTimeField(null=True)
|
||||
|
@ -310,10 +362,10 @@ class ExternalResource(models.Model):
|
|||
related_resources = jsondata.ArrayField(null=False, blank=False, default=list)
|
||||
|
||||
class Meta:
|
||||
unique_together = [['id_type', 'id_value']]
|
||||
unique_together = [["id_type", "id_value"]]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.id}{':' + self.id_type + ':' + self.id_value if self.id_value else ''} ({self.url})"
|
||||
return f"{self.id}:{self.id_type}:{self.id_value if self.id_value else ''} ({self.url})"
|
||||
|
||||
@property
|
||||
def site_name(self):
|
||||
|
@ -323,9 +375,12 @@ class ExternalResource(models.Model):
|
|||
self.other_lookup_ids = resource_content.lookup_ids
|
||||
self.metadata = resource_content.metadata
|
||||
if resource_content.cover_image and resource_content.cover_image_extention:
|
||||
self.cover = SimpleUploadedFile('temp.' + resource_content.cover_image_extention, resource_content.cover_image)
|
||||
self.cover = SimpleUploadedFile(
|
||||
"temp." + resource_content.cover_image_extention,
|
||||
resource_content.cover_image,
|
||||
)
|
||||
else:
|
||||
self.cover = resource_content.metadata.get('cover_image_path')
|
||||
self.cover = resource_content.metadata.get("cover_image_path")
|
||||
self.scraped_time = timezone.now()
|
||||
self.save()
|
||||
|
||||
|
@ -340,11 +395,13 @@ class ExternalResource(models.Model):
|
|||
return d
|
||||
|
||||
def get_preferred_model(self):
|
||||
model = self.metadata.get('preferred_model')
|
||||
model = self.metadata.get("preferred_model")
|
||||
if model:
|
||||
m = ContentType.objects.filter(app_label='catalog', model=model.lower()).first()
|
||||
m = ContentType.objects.filter(
|
||||
app_label="catalog", model=model.lower()
|
||||
).first()
|
||||
if m:
|
||||
return m.model_class()
|
||||
else:
|
||||
raise ValueError(f'preferred model {model} does not exist')
|
||||
raise ValueError(f"preferred model {model} does not exist")
|
||||
return None
|
||||
|
|
|
@ -244,7 +244,7 @@
|
|||
{% endif %}
|
||||
<span class="review-panel__actions">
|
||||
<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>
|
||||
|
||||
<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.filter
|
||||
def thumb(source, alias):
|
||||
"""
|
||||
This filter modifies that from `easy_thumbnails` so that
|
||||
it can neglect .svg file.
|
||||
"""
|
||||
if source.url.endswith('.svg'):
|
||||
if source.url.endswith(".svg"):
|
||||
return source.url
|
||||
else:
|
||||
try:
|
||||
return thumbnail_url(source, alias)
|
||||
except Exception as e:
|
||||
return ''
|
||||
except Exception:
|
||||
return ""
|
||||
|
|
|
@ -4,7 +4,6 @@ from users.models import User
|
|||
from catalog.common.models import Item, ItemCategory
|
||||
from .mixins import UserOwnedObjectMixin
|
||||
from catalog.collection.models import Collection as CatalogCollection
|
||||
from decimal import *
|
||||
from enum import Enum
|
||||
from markdownx.models import MarkdownxField
|
||||
from django.utils import timezone
|
||||
|
@ -24,12 +23,13 @@ from catalog.models import *
|
|||
import mistune
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from markdown import markdown
|
||||
from catalog.common import jsondata
|
||||
|
||||
|
||||
class VisibilityType(models.IntegerChoices):
|
||||
Public = 0, _('公开')
|
||||
Follower_Only = 1, _('仅关注者')
|
||||
Private = 2, _('仅自己')
|
||||
Public = 0, _("公开")
|
||||
Follower_Only = 1, _("仅关注者")
|
||||
Private = 2, _("仅自己")
|
||||
|
||||
|
||||
def q_visible_to(viewer, owner):
|
||||
|
@ -44,7 +44,11 @@ def q_visible_to(viewer, owner):
|
|||
|
||||
|
||||
def query_visible(user):
|
||||
return Q(visibility=0) | Q(owner_id__in=user.following, visibility=1) | Q(owner_id=user.id)
|
||||
return (
|
||||
Q(visibility=0)
|
||||
| Q(owner_id__in=user.following, visibility=1)
|
||||
| Q(owner_id=user.id)
|
||||
)
|
||||
|
||||
|
||||
def query_following(user):
|
||||
|
@ -63,14 +67,26 @@ def query_item_category(item_category):
|
|||
|
||||
|
||||
class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
||||
url_path = 'piece' # subclass must specify this
|
||||
url_path = "piece" # subclass must specify this
|
||||
uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
|
||||
owner = models.ForeignKey(User, on_delete=models.PROTECT)
|
||||
visibility = models.PositiveSmallIntegerField(default=0) # 0: Public / 1: Follower only / 2: Self only
|
||||
created_time = models.DateTimeField(default=timezone.now) # auto_now_add=True FIXME revert this after migration
|
||||
edited_time = models.DateTimeField(default=timezone.now) # auto_now=True FIXME revert this after migration
|
||||
visibility = models.PositiveSmallIntegerField(
|
||||
default=0
|
||||
) # 0: Public / 1: Follower only / 2: Self only
|
||||
created_time = models.DateTimeField(
|
||||
default=timezone.now
|
||||
) # auto_now_add=True FIXME revert this after migration
|
||||
edited_time = models.DateTimeField(
|
||||
default=timezone.now
|
||||
) # auto_now=True FIXME revert this after migration
|
||||
metadata = models.JSONField(default=dict)
|
||||
attached_to = models.ForeignKey(User, null=True, default=None, on_delete=models.SET_NULL, related_name="attached_with")
|
||||
attached_to = models.ForeignKey(
|
||||
User,
|
||||
null=True,
|
||||
default=None,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="attached_with",
|
||||
)
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
|
@ -78,7 +94,7 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
|||
|
||||
@property
|
||||
def url(self):
|
||||
return f'/{self.url_path}/{self.uuid}' if self.url_path else None
|
||||
return f"/{self.url_path}/{self.uuid}" if self.url_path else None
|
||||
|
||||
@property
|
||||
def absolute_url(self):
|
||||
|
@ -86,7 +102,7 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin):
|
|||
|
||||
@property
|
||||
def api_url(self):
|
||||
return ('/api/' + self.url) if self.url_path else None
|
||||
return ("/api/" + self.url) if self.url_path else None
|
||||
|
||||
|
||||
class Content(Piece):
|
||||
|
@ -106,7 +122,7 @@ class Content(Piece):
|
|||
|
||||
|
||||
class Like(Piece):
|
||||
target = models.ForeignKey(Piece, on_delete=models.CASCADE, related_name='likes')
|
||||
target = models.ForeignKey(Piece, on_delete=models.CASCADE, related_name="likes")
|
||||
|
||||
@staticmethod
|
||||
def user_like_piece(user, piece):
|
||||
|
@ -117,8 +133,14 @@ class Like(Piece):
|
|||
like = Like.objects.create(owner=user, target=piece)
|
||||
return like
|
||||
|
||||
@staticmethod
|
||||
def user_unlike_piece(user, piece):
|
||||
if not piece:
|
||||
return
|
||||
Like.objects.filter(owner=user, target=piece).delete()
|
||||
|
||||
class Note(Content):
|
||||
|
||||
class Memo(Content):
|
||||
pass
|
||||
|
||||
|
||||
|
@ -133,7 +155,9 @@ class Comment(Content):
|
|||
comment.delete()
|
||||
comment = None
|
||||
elif comment is None:
|
||||
comment = Comment.objects.create(owner=user, item=item, text=text, visibility=visibility)
|
||||
comment = Comment.objects.create(
|
||||
owner=user, item=item, text=text, visibility=visibility
|
||||
)
|
||||
elif comment.text != text or comment.visibility != visibility:
|
||||
comment.text = text
|
||||
comment.visibility = visibility
|
||||
|
@ -142,7 +166,7 @@ class Comment(Content):
|
|||
|
||||
|
||||
class Review(Content):
|
||||
url_path = 'review'
|
||||
url_path = "review"
|
||||
title = models.CharField(max_length=500, blank=False, null=False)
|
||||
body = MarkdownxField()
|
||||
|
||||
|
@ -157,7 +181,14 @@ class Review(Content):
|
|||
@staticmethod
|
||||
def review_item_by_user(item, user, title, body, metadata={}, visibility=0):
|
||||
# allow multiple reviews per item per user.
|
||||
review = Review.objects.create(owner=user, item=item, title=title, body=body, metadata=metadata, visibility=visibility)
|
||||
review = Review.objects.create(
|
||||
owner=user,
|
||||
item=item,
|
||||
title=title,
|
||||
body=body,
|
||||
metadata=metadata,
|
||||
visibility=visibility,
|
||||
)
|
||||
"""
|
||||
review = Review.objects.filter(owner=user, item=item).first()
|
||||
if title is None:
|
||||
|
@ -176,29 +207,37 @@ class Review(Content):
|
|||
|
||||
|
||||
class Rating(Content):
|
||||
grade = models.PositiveSmallIntegerField(default=0, validators=[MaxValueValidator(10), MinValueValidator(1)], null=True)
|
||||
grade = models.PositiveSmallIntegerField(
|
||||
default=0, validators=[MaxValueValidator(10), MinValueValidator(1)], null=True
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_rating_for_item(item):
|
||||
stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(average=Avg('grade'), count=Count('item'))
|
||||
return stat['average'] if stat['count'] >= 5 else None
|
||||
stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(
|
||||
average=Avg("grade"), count=Count("item")
|
||||
)
|
||||
return stat["average"] if stat["count"] >= 5 else None
|
||||
|
||||
@staticmethod
|
||||
def get_rating_count_for_item(item):
|
||||
stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(count=Count('item'))
|
||||
return stat['count']
|
||||
stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(
|
||||
count=Count("item")
|
||||
)
|
||||
return stat["count"]
|
||||
|
||||
@staticmethod
|
||||
def rate_item_by_user(item, user, rating_grade, visibility=0):
|
||||
if rating_grade and (rating_grade < 1 or rating_grade > 10):
|
||||
raise ValueError(f'Invalid rating grade: {rating_grade}')
|
||||
raise ValueError(f"Invalid rating grade: {rating_grade}")
|
||||
rating = Rating.objects.filter(owner=user, item=item).first()
|
||||
if not rating_grade:
|
||||
if rating:
|
||||
rating.delete()
|
||||
rating = None
|
||||
elif rating is None:
|
||||
rating = Rating.objects.create(owner=user, item=item, grade=rating_grade, visibility=visibility)
|
||||
rating = Rating.objects.create(
|
||||
owner=user, item=item, grade=rating_grade, visibility=visibility
|
||||
)
|
||||
elif rating.grade != rating_grade or rating.visibility != visibility:
|
||||
rating.visibility = visibility
|
||||
rating.grade = rating_grade
|
||||
|
@ -216,7 +255,9 @@ Item.rating_count = property(Rating.get_rating_count_for_item)
|
|||
|
||||
|
||||
class Reply(Piece):
|
||||
reply_to_content = models.ForeignKey(Piece, on_delete=models.SET_NULL, related_name='replies', null=True)
|
||||
reply_to_content = models.ForeignKey(
|
||||
Piece, on_delete=models.SET_NULL, related_name="replies", null=True
|
||||
)
|
||||
title = models.CharField(max_length=500, null=True)
|
||||
body = MarkdownxField()
|
||||
pass
|
||||
|
@ -234,7 +275,9 @@ class List(Piece):
|
|||
class Meta:
|
||||
abstract = True
|
||||
|
||||
_owner = models.ForeignKey(User, on_delete=models.PROTECT) # duplicated owner field to make unique key possible for subclasses
|
||||
_owner = models.ForeignKey(
|
||||
User, on_delete=models.PROTECT
|
||||
) # duplicated owner field to make unique key possible for subclasses
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self._owner = self.owner
|
||||
|
@ -246,43 +289,56 @@ class List(Piece):
|
|||
|
||||
@property
|
||||
def ordered_members(self):
|
||||
return self.members.all().order_by('position', 'item_id')
|
||||
return self.members.all().order_by("position")
|
||||
|
||||
@property
|
||||
def ordered_items(self):
|
||||
return self.items.all().order_by(self.MEMBER_CLASS.__name__.lower() + '__position')
|
||||
return self.items.all().order_by(
|
||||
self.MEMBER_CLASS.__name__.lower() + "__position"
|
||||
)
|
||||
|
||||
@property
|
||||
def recent_items(self):
|
||||
return self.items.all().order_by('-' + self.MEMBER_CLASS.__name__.lower() + '__created_time')
|
||||
return self.items.all().order_by(
|
||||
"-" + self.MEMBER_CLASS.__name__.lower() + "__created_time"
|
||||
)
|
||||
|
||||
@property
|
||||
def recent_members(self):
|
||||
return self.members.all().order_by('-created_time')
|
||||
return self.members.all().order_by("-created_time")
|
||||
|
||||
def has_item(self, item):
|
||||
return self.members.filter(item=item).count() > 0
|
||||
def get_member_for_item(self, item):
|
||||
return self.members.filter(item=item).first()
|
||||
|
||||
def append_item(self, item, **params):
|
||||
if item is None or self.has_item(item):
|
||||
if item is None or self.get_member_for_item(item):
|
||||
return None
|
||||
else:
|
||||
ml = self.ordered_members
|
||||
p = {'parent': self}
|
||||
p = {"parent": self}
|
||||
p.update(params)
|
||||
member = self.MEMBER_CLASS.objects.create(owner=self.owner, position=ml.last().position + 1 if ml.count() else 1, item=item, **p)
|
||||
list_add.send(sender=self.__class__, instance=self, item=item, member=member)
|
||||
member = self.MEMBER_CLASS.objects.create(
|
||||
owner=self.owner,
|
||||
position=ml.last().position + 1 if ml.count() else 1,
|
||||
item=item,
|
||||
**p,
|
||||
)
|
||||
list_add.send(
|
||||
sender=self.__class__, instance=self, item=item, member=member
|
||||
)
|
||||
return member
|
||||
|
||||
def remove_item(self, item):
|
||||
member = self.members.all().filter(item=item).first()
|
||||
member = self.get_member_for_item(item)
|
||||
if member:
|
||||
list_remove.send(sender=self.__class__, instance=self, item=item, member=member)
|
||||
list_remove.send(
|
||||
sender=self.__class__, instance=self, item=item, member=member
|
||||
)
|
||||
member.delete()
|
||||
|
||||
def move_up_item(self, item):
|
||||
members = self.ordered_members
|
||||
member = members.filter(item=item).first()
|
||||
member = self.get_member_for_item(item)
|
||||
if member:
|
||||
other = members.filter(position__lt=member.position).last()
|
||||
if other:
|
||||
|
@ -294,7 +350,7 @@ class List(Piece):
|
|||
|
||||
def move_down_item(self, item):
|
||||
members = self.ordered_members
|
||||
member = members.filter(item=item).first()
|
||||
member = self.get_member_for_item(item)
|
||||
if member:
|
||||
other = members.filter(position__gt=member.position).first()
|
||||
if other:
|
||||
|
@ -304,6 +360,12 @@ class List(Piece):
|
|||
other.save()
|
||||
member.save()
|
||||
|
||||
def update_item_metadata(self, item, metadata):
|
||||
member = self.get_member_for_item(item)
|
||||
if member:
|
||||
member.metadata = metadata
|
||||
member.save()
|
||||
|
||||
|
||||
class ListMember(Piece):
|
||||
"""
|
||||
|
@ -312,6 +374,7 @@ class ListMember(Piece):
|
|||
|
||||
parent = models.ForeignKey('List', related_name='members', on_delete=models.CASCADE)
|
||||
"""
|
||||
|
||||
item = models.ForeignKey(Item, on_delete=models.PROTECT)
|
||||
position = models.PositiveIntegerField()
|
||||
|
||||
|
@ -325,7 +388,7 @@ class ListMember(Piece):
|
|||
abstract = True
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.id}:{self.position} ({self.item})'
|
||||
return f"{self.id}:{self.position} ({self.item})"
|
||||
|
||||
|
||||
"""
|
||||
|
@ -334,48 +397,52 @@ Shelf
|
|||
|
||||
|
||||
class ShelfType(models.TextChoices):
|
||||
WISHLIST = ('wishlist', '未开始')
|
||||
PROGRESS = ('progress', '进行中')
|
||||
COMPLETE = ('complete', '完成')
|
||||
WISHLIST = ("wishlist", "未开始")
|
||||
PROGRESS = ("progress", "进行中")
|
||||
COMPLETE = ("complete", "完成")
|
||||
# DISCARDED = ('discarded', '放弃')
|
||||
|
||||
|
||||
ShelfTypeNames = [
|
||||
[ItemCategory.Book, ShelfType.WISHLIST, _('想读')],
|
||||
[ItemCategory.Book, ShelfType.PROGRESS, _('在读')],
|
||||
[ItemCategory.Book, ShelfType.COMPLETE, _('读过')],
|
||||
[ItemCategory.Movie, ShelfType.WISHLIST, _('想看')],
|
||||
[ItemCategory.Movie, ShelfType.PROGRESS, _('在看')],
|
||||
[ItemCategory.Movie, ShelfType.COMPLETE, _('看过')],
|
||||
[ItemCategory.TV, ShelfType.WISHLIST, _('想看')],
|
||||
[ItemCategory.TV, ShelfType.PROGRESS, _('在看')],
|
||||
[ItemCategory.TV, ShelfType.COMPLETE, _('看过')],
|
||||
[ItemCategory.Music, ShelfType.WISHLIST, _('想听')],
|
||||
[ItemCategory.Music, ShelfType.PROGRESS, _('在听')],
|
||||
[ItemCategory.Music, ShelfType.COMPLETE, _('听过')],
|
||||
[ItemCategory.Game, ShelfType.WISHLIST, _('想玩')],
|
||||
[ItemCategory.Game, ShelfType.PROGRESS, _('在玩')],
|
||||
[ItemCategory.Game, ShelfType.COMPLETE, _('玩过')],
|
||||
|
||||
|
||||
[ItemCategory.Book, ShelfType.WISHLIST, _("想读")],
|
||||
[ItemCategory.Book, ShelfType.PROGRESS, _("在读")],
|
||||
[ItemCategory.Book, ShelfType.COMPLETE, _("读过")],
|
||||
[ItemCategory.Movie, ShelfType.WISHLIST, _("想看")],
|
||||
[ItemCategory.Movie, ShelfType.PROGRESS, _("在看")],
|
||||
[ItemCategory.Movie, ShelfType.COMPLETE, _("看过")],
|
||||
[ItemCategory.TV, ShelfType.WISHLIST, _("想看")],
|
||||
[ItemCategory.TV, ShelfType.PROGRESS, _("在看")],
|
||||
[ItemCategory.TV, ShelfType.COMPLETE, _("看过")],
|
||||
[ItemCategory.Music, ShelfType.WISHLIST, _("想听")],
|
||||
[ItemCategory.Music, ShelfType.PROGRESS, _("在听")],
|
||||
[ItemCategory.Music, ShelfType.COMPLETE, _("听过")],
|
||||
[ItemCategory.Game, ShelfType.WISHLIST, _("想玩")],
|
||||
[ItemCategory.Game, ShelfType.PROGRESS, _("在玩")],
|
||||
[ItemCategory.Game, ShelfType.COMPLETE, _("玩过")],
|
||||
]
|
||||
|
||||
|
||||
class ShelfMember(ListMember):
|
||||
parent = models.ForeignKey('Shelf', related_name='members', on_delete=models.CASCADE)
|
||||
parent = models.ForeignKey(
|
||||
"Shelf", related_name="members", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
|
||||
class Shelf(List):
|
||||
class Meta:
|
||||
unique_together = [['_owner', 'item_category', 'shelf_type']]
|
||||
unique_together = [["_owner", "item_category", "shelf_type"]]
|
||||
|
||||
MEMBER_CLASS = ShelfMember
|
||||
items = models.ManyToManyField(Item, through='ShelfMember', related_name="+")
|
||||
item_category = models.CharField(choices=ItemCategory.choices, max_length=100, null=False, blank=False)
|
||||
shelf_type = models.CharField(choices=ShelfType.choices, max_length=100, null=False, blank=False)
|
||||
items = models.ManyToManyField(Item, through="ShelfMember", related_name="+")
|
||||
item_category = models.CharField(
|
||||
choices=ItemCategory.choices, max_length=100, null=False, blank=False
|
||||
)
|
||||
shelf_type = models.CharField(
|
||||
choices=ShelfType.choices, max_length=100, null=False, blank=False
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.id} {self.title}'
|
||||
return f"{self.id} {self.title}"
|
||||
|
||||
@cached_property
|
||||
def item_category_label(self):
|
||||
|
@ -383,26 +450,41 @@ class Shelf(List):
|
|||
|
||||
@cached_property
|
||||
def shelf_label(self):
|
||||
return next(iter([n[2] for n in iter(ShelfTypeNames) if n[0] == self.item_category and n[1] == self.shelf_type]), self.shelf_type)
|
||||
return next(
|
||||
iter(
|
||||
[
|
||||
n[2]
|
||||
for n in iter(ShelfTypeNames)
|
||||
if n[0] == self.item_category and n[1] == self.shelf_type
|
||||
]
|
||||
),
|
||||
self.shelf_type,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def title(self):
|
||||
q = _("{shelf_label}的{item_category}").format(shelf_label=self.shelf_label, item_category=self.item_category_label)
|
||||
q = _("{shelf_label}的{item_category}").format(
|
||||
shelf_label=self.shelf_label, item_category=self.item_category_label
|
||||
)
|
||||
return q
|
||||
# return _("{user}'s {shelf_name}").format(user=self.owner.mastodon_username, shelf_name=q)
|
||||
|
||||
|
||||
class ShelfLogEntry(models.Model):
|
||||
owner = models.ForeignKey(User, on_delete=models.PROTECT)
|
||||
shelf = models.ForeignKey(Shelf, on_delete=models.CASCADE, related_name='entries', null=True) # None means removed from any shelf
|
||||
shelf = models.ForeignKey(
|
||||
Shelf, on_delete=models.CASCADE, related_name="entries", null=True
|
||||
) # None means removed from any shelf
|
||||
item = models.ForeignKey(Item, on_delete=models.PROTECT)
|
||||
timestamp = models.DateTimeField(default=timezone.now) # this may later be changed by user
|
||||
timestamp = models.DateTimeField(
|
||||
default=timezone.now
|
||||
) # this may later be changed by user
|
||||
metadata = models.JSONField(default=dict)
|
||||
created_time = models.DateTimeField(auto_now_add=True)
|
||||
edited_time = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.owner}:{self.shelf}:{self.item}:{self.metadata}'
|
||||
return f"{self.owner}:{self.shelf}:{self.item}:{self.metadata}"
|
||||
|
||||
|
||||
class ShelfManager:
|
||||
|
@ -422,12 +504,16 @@ class ShelfManager:
|
|||
Shelf.objects.create(owner=self.owner, item_category=ic, shelf_type=qt)
|
||||
|
||||
def _shelf_member_for_item(self, item):
|
||||
return ShelfMember.objects.filter(item=item, parent__in=self.owner.shelf_set.all()).first()
|
||||
return ShelfMember.objects.filter(
|
||||
item=item, parent__in=self.owner.shelf_set.all()
|
||||
).first()
|
||||
|
||||
def _shelf_for_item_and_type(item, shelf_type):
|
||||
if not item or not shelf_type:
|
||||
return None
|
||||
return self.owner.shelf_set.all().filter(item_category=item.category, shelf_type=shelf_type)
|
||||
return self.owner.shelf_set.all().filter(
|
||||
item_category=item.category, shelf_type=shelf_type
|
||||
)
|
||||
|
||||
def locate_item(self, item):
|
||||
member = ShelfMember.objects.filter(owner=self.owner, item=item).first()
|
||||
|
@ -437,7 +523,7 @@ class ShelfManager:
|
|||
# shelf_type=None means remove from current shelf
|
||||
# metadata=None means no change
|
||||
if not item:
|
||||
raise ValueError('empty item')
|
||||
raise ValueError("empty item")
|
||||
new_shelfmember = None
|
||||
last_shelfmember = self._shelf_member_for_item(item)
|
||||
last_shelf = last_shelfmember.parent if last_shelfmember else None
|
||||
|
@ -450,9 +536,11 @@ class ShelfManager:
|
|||
if last_shelf:
|
||||
last_shelf.remove_item(item)
|
||||
if shelf:
|
||||
new_shelfmember = shelf.append_item(item, visibility=visibility, metadata=metadata or {})
|
||||
new_shelfmember = shelf.append_item(
|
||||
item, visibility=visibility, metadata=metadata or {}
|
||||
)
|
||||
elif last_shelf is None:
|
||||
raise ValueError('empty shelf')
|
||||
raise ValueError("empty shelf")
|
||||
else:
|
||||
new_shelfmember = last_shelfmember
|
||||
if metadata is not None and metadata != last_metadata: # change metadata
|
||||
|
@ -466,20 +554,32 @@ class ShelfManager:
|
|||
if changed:
|
||||
if metadata is None:
|
||||
metadata = last_metadata or {}
|
||||
ShelfLogEntry.objects.create(owner=self.owner, shelf=shelf, item=item, metadata=metadata)
|
||||
ShelfLogEntry.objects.create(
|
||||
owner=self.owner, shelf=shelf, item=item, metadata=metadata
|
||||
)
|
||||
return new_shelfmember
|
||||
|
||||
def get_log(self):
|
||||
return ShelfLogEntry.objects.filter(owner=self.owner).order_by('timestamp')
|
||||
return ShelfLogEntry.objects.filter(owner=self.owner).order_by("timestamp")
|
||||
|
||||
def get_log_for_item(self, item):
|
||||
return ShelfLogEntry.objects.filter(owner=self.owner, item=item).order_by('timestamp')
|
||||
return ShelfLogEntry.objects.filter(owner=self.owner, item=item).order_by(
|
||||
"timestamp"
|
||||
)
|
||||
|
||||
def get_shelf(self, item_category, shelf_type):
|
||||
return self.owner.shelf_set.all().filter(item_category=item_category, shelf_type=shelf_type).first()
|
||||
return (
|
||||
self.owner.shelf_set.all()
|
||||
.filter(item_category=item_category, shelf_type=shelf_type)
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_items_on_shelf(self, item_category, shelf_type):
|
||||
shelf = self.owner.shelf_set.all().filter(item_category=item_category, shelf_type=shelf_type).first()
|
||||
shelf = (
|
||||
self.owner.shelf_set.all()
|
||||
.filter(item_category=item_category, shelf_type=shelf_type)
|
||||
.first()
|
||||
)
|
||||
return shelf.members.all().order_by
|
||||
|
||||
@staticmethod
|
||||
|
@ -488,7 +588,7 @@ class ShelfManager:
|
|||
|
||||
|
||||
User.shelf_manager = cached_property(ShelfManager.get_manager_for_user)
|
||||
User.shelf_manager.__set_name__(User, 'shelf_manager')
|
||||
User.shelf_manager.__set_name__(User, "shelf_manager")
|
||||
|
||||
|
||||
"""
|
||||
|
@ -497,22 +597,30 @@ Collection
|
|||
|
||||
|
||||
class CollectionMember(ListMember):
|
||||
parent = models.ForeignKey('Collection', related_name='members', on_delete=models.CASCADE)
|
||||
parent = models.ForeignKey(
|
||||
"Collection", related_name="members", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
@property
|
||||
def note(self):
|
||||
return self.metadata.get('comment')
|
||||
note = jsondata.CharField(_("备注"), null=True, blank=True)
|
||||
|
||||
|
||||
class Collection(List):
|
||||
url_path = 'collection'
|
||||
url_path = "collection"
|
||||
MEMBER_CLASS = CollectionMember
|
||||
catalog_item = models.OneToOneField(CatalogCollection, on_delete=models.PROTECT)
|
||||
title = models.CharField(_("title in primary language"), max_length=1000, default="")
|
||||
title = models.CharField(
|
||||
_("title in primary language"), max_length=1000, default=""
|
||||
)
|
||||
brief = models.TextField(_("简介"), blank=True, default="")
|
||||
cover = models.ImageField(upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True)
|
||||
items = models.ManyToManyField(Item, through='CollectionMember', related_name="collections")
|
||||
collaborative = models.PositiveSmallIntegerField(default=0) # 0: Editable by owner only / 1: Editable by bi-direction followers
|
||||
cover = models.ImageField(
|
||||
upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True
|
||||
)
|
||||
items = models.ManyToManyField(
|
||||
Item, through="CollectionMember", related_name="collections"
|
||||
)
|
||||
collaborative = models.PositiveSmallIntegerField(
|
||||
default=0
|
||||
) # 0: Editable by owner only / 1: Editable by bi-direction followers
|
||||
|
||||
@property
|
||||
def html(self):
|
||||
|
@ -522,12 +630,15 @@ class Collection(List):
|
|||
@property
|
||||
def plain_description(self):
|
||||
html = markdown(self.brief)
|
||||
return RE_HTML_TAG.sub(' ', html)
|
||||
return RE_HTML_TAG.sub(" ", html)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if getattr(self, 'catalog_item', None) is None:
|
||||
if getattr(self, "catalog_item", None) is None:
|
||||
self.catalog_item = CatalogCollection()
|
||||
if self.catalog_item.title != self.title or self.catalog_item.brief != self.brief:
|
||||
if (
|
||||
self.catalog_item.title != self.title
|
||||
or self.catalog_item.brief != self.brief
|
||||
):
|
||||
self.catalog_item.title = self.title
|
||||
self.catalog_item.brief = self.brief
|
||||
self.catalog_item.cover = self.cover
|
||||
|
@ -541,21 +652,23 @@ Tag
|
|||
|
||||
|
||||
class TagMember(ListMember):
|
||||
parent = models.ForeignKey('Tag', related_name='members', on_delete=models.CASCADE)
|
||||
parent = models.ForeignKey("Tag", related_name="members", on_delete=models.CASCADE)
|
||||
|
||||
|
||||
TagValidators = [RegexValidator(regex=r'\s+', inverse_match=True)]
|
||||
TagValidators = [RegexValidator(regex=r"\s+", inverse_match=True)]
|
||||
|
||||
|
||||
class Tag(List):
|
||||
MEMBER_CLASS = TagMember
|
||||
items = models.ManyToManyField(Item, through='TagMember')
|
||||
title = models.CharField(max_length=100, null=False, blank=False, validators=TagValidators)
|
||||
items = models.ManyToManyField(Item, through="TagMember")
|
||||
title = models.CharField(
|
||||
max_length=100, null=False, blank=False, validators=TagValidators
|
||||
)
|
||||
# TODO case convert and space removal on save
|
||||
# TODO check on save
|
||||
|
||||
class Meta:
|
||||
unique_together = [['_owner', 'title']]
|
||||
unique_together = [["_owner", "title"]]
|
||||
|
||||
@staticmethod
|
||||
def cleanup_title(title):
|
||||
|
@ -565,22 +678,37 @@ class Tag(List):
|
|||
class TagManager:
|
||||
@staticmethod
|
||||
def public_tags_for_item(item):
|
||||
tags = item.tag_set.all().filter(visibility=0).values('title').annotate(frequency=Count('owner')).order_by('-frequency')[: 20]
|
||||
return sorted(list(map(lambda t: t['title'], tags)))
|
||||
tags = (
|
||||
item.tag_set.all()
|
||||
.filter(visibility=0)
|
||||
.values("title")
|
||||
.annotate(frequency=Count("owner"))
|
||||
.order_by("-frequency")[:20]
|
||||
)
|
||||
return sorted(list(map(lambda t: t["title"], tags)))
|
||||
|
||||
@staticmethod
|
||||
def all_tags_for_user(user):
|
||||
tags = user.tag_set.all().values('title').annotate(frequency=Count('members__id')).order_by('-frequency')
|
||||
return list(map(lambda t: t['title'], tags))
|
||||
tags = (
|
||||
user.tag_set.all()
|
||||
.values("title")
|
||||
.annotate(frequency=Count("members__id"))
|
||||
.order_by("-frequency")
|
||||
)
|
||||
return list(map(lambda t: t["title"], tags))
|
||||
|
||||
@staticmethod
|
||||
def tag_item_by_user(item, user, tag_titles, default_visibility=0):
|
||||
titles = set([Tag.cleanup_title(tag_title) for tag_title in tag_titles])
|
||||
current_titles = set([m.parent.title for m in TagMember.objects.filter(owner=user, item=item)])
|
||||
current_titles = set(
|
||||
[m.parent.title for m in TagMember.objects.filter(owner=user, item=item)]
|
||||
)
|
||||
for title in titles - current_titles:
|
||||
tag = Tag.objects.filter(owner=user, title=title).first()
|
||||
if not tag:
|
||||
tag = Tag.objects.create(owner=user, title=title, visibility=default_visibility)
|
||||
tag = Tag.objects.create(
|
||||
owner=user, title=title, visibility=default_visibility
|
||||
)
|
||||
tag.append_item(item)
|
||||
for title in current_titles - titles:
|
||||
tag = Tag.objects.filter(owner=user, title=title).first()
|
||||
|
@ -588,7 +716,9 @@ class TagManager:
|
|||
|
||||
@staticmethod
|
||||
def get_item_tags_by_user(item, user):
|
||||
current_titles = [m.parent.title for m in TagMember.objects.filter(owner=user, item=item)]
|
||||
current_titles = [
|
||||
m.parent.title for m in TagMember.objects.filter(owner=user, item=item)
|
||||
]
|
||||
return current_titles
|
||||
|
||||
@staticmethod
|
||||
|
@ -596,7 +726,9 @@ class TagManager:
|
|||
title = Tag.cleanup_title(tag_title)
|
||||
tag = Tag.objects.filter(owner=user, title=title).first()
|
||||
if not tag:
|
||||
tag = Tag.objects.create(owner=user, title=title, visibility=default_visibility)
|
||||
tag = Tag.objects.create(
|
||||
owner=user, title=title, visibility=default_visibility
|
||||
)
|
||||
tag.append_item(item)
|
||||
|
||||
@staticmethod
|
||||
|
@ -615,13 +747,20 @@ class TagManager:
|
|||
TagManager.add_tag_by_user(item, tag, self.owner, visibility)
|
||||
|
||||
def get_item_tags(self, item):
|
||||
return sorted([m['parent__title'] for m in TagMember.objects.filter(parent__owner=self.owner, item=item).values('parent__title')])
|
||||
return sorted(
|
||||
[
|
||||
m["parent__title"]
|
||||
for m in TagMember.objects.filter(
|
||||
parent__owner=self.owner, item=item
|
||||
).values("parent__title")
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
Item.tags = property(TagManager.public_tags_for_item)
|
||||
User.tags = property(TagManager.all_tags_for_user)
|
||||
User.tag_manager = cached_property(TagManager.get_manager_for_user)
|
||||
User.tag_manager.__set_name__(User, 'tag_manager')
|
||||
User.tag_manager.__set_name__(User, "tag_manager")
|
||||
|
||||
|
||||
class Mark:
|
||||
|
@ -683,15 +822,36 @@ class Mark:
|
|||
def review(self):
|
||||
return Review.objects.filter(owner=self.owner, item=self.item).first()
|
||||
|
||||
def update(self, shelf_type, comment_text, rating_grade, visibility, metadata=None, created_time=None, share_to_mastodon=False):
|
||||
share = share_to_mastodon and shelf_type is not None and (shelf_type != self.shelf_type or comment_text != self.text or rating_grade != self.rating)
|
||||
def update(
|
||||
self,
|
||||
shelf_type,
|
||||
comment_text,
|
||||
rating_grade,
|
||||
visibility,
|
||||
metadata=None,
|
||||
created_time=None,
|
||||
share_to_mastodon=False,
|
||||
):
|
||||
share = (
|
||||
share_to_mastodon
|
||||
and shelf_type is not None
|
||||
and (
|
||||
shelf_type != self.shelf_type
|
||||
or comment_text != self.text
|
||||
or rating_grade != self.rating
|
||||
)
|
||||
)
|
||||
if shelf_type != self.shelf_type or visibility != self.visibility:
|
||||
self.shelfmember = self.owner.shelf_manager.move_item(self.item, shelf_type, visibility=visibility, metadata=metadata)
|
||||
self.shelfmember = self.owner.shelf_manager.move_item(
|
||||
self.item, shelf_type, visibility=visibility, metadata=metadata
|
||||
)
|
||||
if self.shelfmember and created_time:
|
||||
self.shelfmember.created_time = created_time
|
||||
self.shelfmember.save()
|
||||
if comment_text != self.text or visibility != self.visibility:
|
||||
self.comment = Comment.comment_item_by_user(self.item, self.owner, comment_text, visibility)
|
||||
self.comment = Comment.comment_item_by_user(
|
||||
self.item, self.owner, comment_text, visibility
|
||||
)
|
||||
if rating_grade != self.rating or visibility != self.visibility:
|
||||
Rating.rate_item_by_user(self.item, self.owner, rating_grade, visibility)
|
||||
self.rating = rating_grade
|
||||
|
@ -699,15 +859,20 @@ class Mark:
|
|||
# this is a bit hacky but let's keep it until move to implement ActivityPub,
|
||||
# by then, we'll just change this to boost
|
||||
from mastodon.api import share_mark
|
||||
self.shared_link = self.shelfmember.metadata.get('shared_link') if self.shelfmember.metadata else None
|
||||
|
||||
self.shared_link = (
|
||||
self.shelfmember.metadata.get("shared_link")
|
||||
if self.shelfmember.metadata
|
||||
else None
|
||||
)
|
||||
self.translated_status = self.shelf_label
|
||||
self.save = lambda **args: None
|
||||
if not share_mark(self):
|
||||
raise ValueError("sharing failed")
|
||||
if not self.shelfmember.metadata:
|
||||
self.shelfmember.metadata = {}
|
||||
if self.shelfmember.metadata.get('shared_link') != self.shared_link:
|
||||
self.shelfmember.metadata['shared_link'] = self.shared_link
|
||||
if self.shelfmember.metadata.get("shared_link") != self.shared_link:
|
||||
self.shelfmember.metadata["shared_link"] = self.shared_link
|
||||
self.shelfmember.save()
|
||||
|
||||
def delete(self):
|
||||
|
|
|
@ -33,13 +33,17 @@
|
|||
<div class="dividing-line"></div>
|
||||
</div>
|
||||
<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>
|
||||
</section>
|
||||
</div>
|
||||
{% include "partial/_footer.html" %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.body.addEventListener('htmx:configRequest', (event) => {
|
||||
event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
<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 %}
|
||||
{% include template with item=member.item mark=None collection_member=member %}
|
||||
{% 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 %}
|
||||
<div class="collection-item-position-edit">
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
|
@ -129,7 +129,7 @@
|
|||
|
||||
{{ collection_member.note }}
|
||||
{% 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 %}
|
||||
|
||||
</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">
|
||||
{% for member in shelf.members %}
|
||||
<li class="entity-sort__entity">
|
||||
|
||||
<a href="{{ member.item.url }}">
|
||||
<img src="{{ member.item.cover|thumb:'normal' }}"
|
||||
alt="{{ member.item.title }}" class="entity-sort__entity-img">
|
||||
<div class="entity-sort__entity-name" title="{{ member.item.title }}">
|
||||
{{ member.item.title }}</div>
|
||||
<img src="{{ member.item.cover.url }}" alt="{{ member.item.title }}" class="entity-sort__entity-img">
|
||||
<div class="entity-sort__entity-name" title="{{ member.item.title }}"> {{ member.item.title }}</div>
|
||||
</a>
|
||||
</li>
|
||||
{% 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.filter(title="test", owner=self.user).first()
|
||||
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)
|
||||
self.assertEqual(list(collection.ordered_items), [self.book1, self.book2])
|
||||
collection.move_up_item(self.book1)
|
||||
self.assertEqual(list(collection.ordered_items), [self.book1, self.book2])
|
||||
collection.move_up_item(self.book2)
|
||||
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):
|
||||
|
@ -47,26 +51,28 @@ class ShelfTest(TestCase):
|
|||
shelf_manager.move_item(book1, ShelfType.PROGRESS)
|
||||
self.assertEqual(q1.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(q2.members.all().count(), 1)
|
||||
log = shelf_manager.get_log_for_item(book1)
|
||||
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)
|
||||
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)
|
||||
self.assertEqual(log.count(), 4)
|
||||
shelf_manager.move_item(book1, ShelfType.PROGRESS)
|
||||
log = shelf_manager.get_log_for_item(book1)
|
||||
self.assertEqual(log.count(), 4)
|
||||
self.assertEqual(log.last().metadata, {'progress': 10})
|
||||
shelf_manager.move_item(book1, ShelfType.PROGRESS, metadata={'progress': 90})
|
||||
self.assertEqual(log.last().metadata, {"progress": 10})
|
||||
shelf_manager.move_item(book1, ShelfType.PROGRESS, metadata={"progress": 90})
|
||||
log = shelf_manager.get_log_for_item(book1)
|
||||
self.assertEqual(log.count(), 5)
|
||||
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(shelf_manager.get_log_for_item(book1).count(), 5)
|
||||
|
||||
|
@ -82,18 +88,18 @@ class TagTest(TestCase):
|
|||
pass
|
||||
|
||||
def test_user_tag(self):
|
||||
t1 = 'tag-1'
|
||||
t2 = 'tag-2'
|
||||
t3 = 'tag-3'
|
||||
t1 = "tag-1"
|
||||
t2 = "tag-2"
|
||||
t3 = "tag-3"
|
||||
TagManager.tag_item_by_user(self.book1, self.user2, [t1, t3])
|
||||
self.assertEqual(self.book1.tags, [t1, t3])
|
||||
TagManager.tag_item_by_user(self.book1, self.user2, [t2, t3])
|
||||
self.assertEqual(self.book1.tags, [t2, t3])
|
||||
|
||||
def test_tag(self):
|
||||
t1 = 'tag-1'
|
||||
t2 = 'tag-2'
|
||||
t3 = 'tag-3'
|
||||
t1 = "tag-1"
|
||||
t2 = "tag-2"
|
||||
t3 = "tag-3"
|
||||
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.user2)
|
||||
|
@ -129,21 +135,21 @@ class MarkTest(TestCase):
|
|||
self.assertEqual(mark.visibility, None)
|
||||
self.assertEqual(mark.review, None)
|
||||
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)
|
||||
self.assertEqual(mark.shelf_type, ShelfType.WISHLIST)
|
||||
self.assertEqual(mark.shelf_label, '想读')
|
||||
self.assertEqual(mark.text, 'a gentle comment')
|
||||
self.assertEqual(mark.shelf_label, "想读")
|
||||
self.assertEqual(mark.text, "a gentle comment")
|
||||
self.assertEqual(mark.rating, 9)
|
||||
self.assertEqual(mark.visibility, 1)
|
||||
self.assertEqual(mark.review, None)
|
||||
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)
|
||||
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)
|
||||
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 *
|
||||
|
||||
|
||||
app_name = 'journal'
|
||||
app_name = "journal"
|
||||
|
||||
|
||||
def _get_all_categories():
|
||||
|
@ -16,34 +16,88 @@ def _get_all_shelf_types():
|
|||
|
||||
|
||||
urlpatterns = [
|
||||
path('wish/<str:item_uuid>', wish, name='wish'),
|
||||
path('like/<str:piece_uuid>', like, name='like'),
|
||||
path('mark/<str:item_uuid>', mark, name='mark'),
|
||||
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/edit/<str:item_uuid>/<str:review_uuid>', review_edit, name='review_edit'),
|
||||
path('review/delete/<str:review_uuid>', review_delete, name='review_delete'),
|
||||
|
||||
path('collection/<str:collection_uuid>', collection_retrieve, name='collection_retrieve'),
|
||||
path('collection/create/', collection_edit, name='collection_create'),
|
||||
path('collection/edit/<str:collection_uuid>', collection_edit, name='collection_edit'),
|
||||
path('collection/delete/<str:collection_uuid>', collection_delete, name='collection_delete'),
|
||||
path('collection/<str:collection_uuid>/items', collection_retrieve_items, name='collection_retrieve_items'),
|
||||
path('collection/<str:collection_uuid>/append_item', collection_append_item, name='collection_append_item'),
|
||||
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/<str:collection_uuid>/move_down_item/<str:collection_member_uuid>', collection_move_down_item, name='collection_move_down_item'),
|
||||
path('collection/<str:collection_uuid>/update_item_note/<str:collection_member_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'),
|
||||
|
||||
path("wish/<str:item_uuid>", wish, name="wish"),
|
||||
path("like/<str:piece_uuid>", like, name="like"),
|
||||
path("mark/<str:item_uuid>", mark, name="mark"),
|
||||
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/edit/<str:item_uuid>/<str:review_uuid>", review_edit, name="review_edit"
|
||||
),
|
||||
path("review/delete/<str:piece_uuid>", piece_delete, name="review_delete"),
|
||||
path(
|
||||
"collection/<str:collection_uuid>",
|
||||
collection_retrieve,
|
||||
name="collection_retrieve",
|
||||
),
|
||||
path("collection/create/", collection_edit, name="collection_create"),
|
||||
path(
|
||||
"collection/edit/<str:collection_uuid>", collection_edit, name="collection_edit"
|
||||
),
|
||||
path("collection/delete/<str:piece_uuid>", piece_delete, name="collection_delete"),
|
||||
path(
|
||||
"collection/<str:collection_uuid>/items",
|
||||
collection_retrieve_items,
|
||||
name="collection_retrieve_items",
|
||||
),
|
||||
path(
|
||||
"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"),
|
||||
]
|
||||
|
|
509
journal/views.py
509
journal/views.py
|
@ -2,9 +2,13 @@ import logging
|
|||
from django.shortcuts import render, get_object_or_404, redirect, reverse
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
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.db import IntegrityError, transaction
|
||||
from django.db.models import Count
|
||||
from django.utils import timezone
|
||||
from django.core.paginator import Paginator
|
||||
|
@ -24,187 +28,257 @@ from users.models import User, Report, Preference
|
|||
_logger = logging.getLogger(__name__)
|
||||
PAGE_SIZE = 10
|
||||
|
||||
_checkmark = "✔️".encode("utf-8")
|
||||
|
||||
|
||||
@login_required
|
||||
def wish(request, item_uuid):
|
||||
if request.method == 'POST':
|
||||
if request.method == "POST":
|
||||
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
||||
if not item:
|
||||
return HttpResponseNotFound("item not found")
|
||||
return HttpResponseNotFound(b"item not found")
|
||||
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:
|
||||
return HttpResponseBadRequest("invalid request")
|
||||
return HttpResponseBadRequest(b"invalid request")
|
||||
|
||||
|
||||
@login_required
|
||||
def like(request, piece_uuid):
|
||||
if request.method == 'POST':
|
||||
if request.method == "POST":
|
||||
piece = get_object_or_404(Collection, uid=base62.decode(piece_uuid))
|
||||
if not piece:
|
||||
return HttpResponseNotFound("piece not found")
|
||||
return HttpResponseNotFound(b"piece not found")
|
||||
Like.user_like_piece(request.user, piece)
|
||||
return HttpResponse("✔️")
|
||||
return HttpResponse(_checkmark)
|
||||
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
|
||||
def add_to_collection(request, 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)
|
||||
return render(
|
||||
request,
|
||||
'add_to_collection.html',
|
||||
"add_to_collection.html",
|
||||
{
|
||||
'item': item,
|
||||
'collections': collections,
|
||||
}
|
||||
"item": item,
|
||||
"collections": collections,
|
||||
},
|
||||
)
|
||||
else:
|
||||
cid = int(request.POST.get('collection_id', default=0))
|
||||
cid = int(request.POST.get("collection_id", default=0))
|
||||
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.append_item(item, metadata={'comment': request.POST.get('comment')})
|
||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
|
||||
collection.append_item(item, metadata={"comment": request.POST.get("comment")})
|
||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
|
||||
|
||||
|
||||
def render_relogin(request):
|
||||
return render(request, 'common/error.html', {
|
||||
'url': reverse("users:connect") + '?domain=' + request.user.mastodon_site,
|
||||
'msg': _("信息已保存,但是未能分享到联邦网络"),
|
||||
'secondary_msg': _("可能是你在联邦网络(Mastodon/Pleroma/...)的登录状态过期了,正在跳转到联邦网络重新登录😼")})
|
||||
return render(
|
||||
request,
|
||||
"common/error.html",
|
||||
{
|
||||
"url": reverse("users:connect") + "?domain=" + request.user.mastodon_site,
|
||||
"msg": _("信息已保存,但是未能分享到联邦网络"),
|
||||
"secondary_msg": _(
|
||||
"可能是你在联邦网络(Mastodon/Pleroma/...)的登录状态过期了,正在跳转到联邦网络重新登录😼"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def mark(request, item_uuid):
|
||||
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
||||
mark = Mark(request.user, item)
|
||||
if request.method == 'GET':
|
||||
if request.method == "GET":
|
||||
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_type = request.GET.get('shelf_type', mark.shelf_type)
|
||||
return render(request, 'mark.html', {
|
||||
'item': item,
|
||||
'mark': mark,
|
||||
'shelf_type': shelf_type,
|
||||
'tags': ','.join(tags),
|
||||
'shelf_types': shelf_types,
|
||||
})
|
||||
elif request.method == 'POST':
|
||||
if request.POST.get('delete', default=False):
|
||||
shelf_types = [
|
||||
(n[1], n[2]) for n in iter(ShelfTypeNames) if n[0] == item.category
|
||||
]
|
||||
shelf_type = request.GET.get("shelf_type", mark.shelf_type)
|
||||
return render(
|
||||
request,
|
||||
"mark.html",
|
||||
{
|
||||
"item": item,
|
||||
"mark": mark,
|
||||
"shelf_type": shelf_type,
|
||||
"tags": ",".join(tags),
|
||||
"shelf_types": shelf_types,
|
||||
},
|
||||
)
|
||||
elif request.method == "POST":
|
||||
if request.POST.get("delete", default=False):
|
||||
mark.delete()
|
||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
|
||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
|
||||
else:
|
||||
visibility = int(request.POST.get('visibility', default=0))
|
||||
rating = request.POST.get('rating', default=0)
|
||||
visibility = int(request.POST.get("visibility", default=0))
|
||||
rating = request.POST.get("rating", default=0)
|
||||
rating = int(rating) if rating else None
|
||||
status = ShelfType(request.POST.get('status'))
|
||||
text = request.POST.get('text')
|
||||
tags = request.POST.get('tags')
|
||||
tags = tags.split(',') if tags else []
|
||||
share_to_mastodon = bool(request.POST.get('share_to_mastodon', default=False))
|
||||
status = ShelfType(request.POST.get("status"))
|
||||
text = request.POST.get("text")
|
||||
tags = request.POST.get("tags")
|
||||
tags = tags.split(",") if tags else []
|
||||
share_to_mastodon = bool(
|
||||
request.POST.get("share_to_mastodon", default=False)
|
||||
)
|
||||
TagManager.tag_item_by_user(item, request.user, tags, visibility)
|
||||
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:
|
||||
return render_relogin(request)
|
||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
|
||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
|
||||
|
||||
|
||||
def collection_retrieve(request, collection_uuid):
|
||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||
if not collection.is_visible_to(request.user):
|
||||
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))
|
||||
if not collection.is_visible_to(request.user):
|
||||
raise PermissionDenied()
|
||||
form = CollectionForm(instance=collection)
|
||||
return render(
|
||||
request,
|
||||
'collection_items.html',
|
||||
"collection_items.html",
|
||||
{
|
||||
'collection': collection,
|
||||
'form': form,
|
||||
'collection_edit': request.GET.get('edit'), # collection.is_editable_by(request.user),
|
||||
}
|
||||
"collection": collection,
|
||||
"form": form,
|
||||
"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
|
||||
def collection_append_item(request, collection_uuid):
|
||||
if request.method != "POST":
|
||||
return HttpResponseBadRequest()
|
||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||
if not collection.is_editable_by(request.user):
|
||||
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
|
||||
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))
|
||||
if not collection.is_editable_by(request.user):
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
@login_required
|
||||
def collection_move_up_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):
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
@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):
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
@login_required
|
||||
def collection_edit(request, collection_uuid=None):
|
||||
collection = 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()
|
||||
if request.method == 'GET':
|
||||
form = CollectionForm(instance=collection) if collection else CollectionForm()
|
||||
return render(request, 'collection_edit.html', {'form': form, 'collection': collection})
|
||||
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]))
|
||||
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
||||
if direction == "up":
|
||||
collection.move_up_item(item)
|
||||
else:
|
||||
return HttpResponseBadRequest(form.errors)
|
||||
collection.move_down_item(item)
|
||||
return collection_retrieve_items(request, collection_uuid, True)
|
||||
|
||||
|
||||
@login_required
|
||||
def collection_update_item_note(request, collection_uuid, item_uuid):
|
||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||
if not collection.is_editable_by(request.user):
|
||||
raise PermissionDenied()
|
||||
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
||||
if not collection.is_editable_by(request.user):
|
||||
raise PermissionDenied()
|
||||
if request.method == "POST":
|
||||
collection.update_item_metadata(
|
||||
item, {"note": request.POST.get("note", default="")}
|
||||
)
|
||||
return collection_retrieve_items(request, collection_uuid, True)
|
||||
elif request.method == "GET":
|
||||
member = collection.get_member_for_item(item)
|
||||
return render(
|
||||
request,
|
||||
"collection_update_item_note.html",
|
||||
{"collection": collection, "item": item, "note": member.note},
|
||||
)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@login_required
|
||||
def collection_delete(request, collection_uuid):
|
||||
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
|
||||
if not collection.is_editable_by(request.user):
|
||||
def collection_edit(request, collection_uuid=None):
|
||||
collection = (
|
||||
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()
|
||||
if request.method == 'GET':
|
||||
collection_form = CollectionForm(instance=collection)
|
||||
return render(request, 'collection_delete.html', {'form': collection_form, 'collection': collection})
|
||||
elif request.method == 'POST':
|
||||
collection.delete()
|
||||
return redirect(reverse("users:home"))
|
||||
if request.method == "GET":
|
||||
form = CollectionForm(instance=collection) if collection else CollectionForm()
|
||||
return render(
|
||||
request, "collection_edit.html", {"form": form, "collection": collection}
|
||||
)
|
||||
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:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
@ -213,31 +287,45 @@ def review_retrieve(request, review_uuid):
|
|||
piece = get_object_or_404(Review, uid=base62.decode(review_uuid))
|
||||
if not piece.is_visible_to(request.user):
|
||||
raise PermissionDenied()
|
||||
return render(request, 'review.html', {'review': piece})
|
||||
return render(request, "review.html", {"review": piece})
|
||||
|
||||
|
||||
@login_required
|
||||
def review_edit(request, item_uuid, review_uuid=None):
|
||||
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):
|
||||
raise PermissionDenied()
|
||||
if request.method == 'GET':
|
||||
form = ReviewForm(instance=review) if review 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 request.method == "GET":
|
||||
form = (
|
||||
ReviewForm(instance=review)
|
||||
if review
|
||||
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 not review:
|
||||
form.instance.owner = request.user
|
||||
form.instance.edited_time = timezone.now()
|
||||
form.save()
|
||||
if form.cleaned_data['share_to_mastodon']:
|
||||
if form.cleaned_data["share_to_mastodon"]:
|
||||
form.instance.save = lambda **args: None
|
||||
form.instance.shared_link = None
|
||||
if not share_review(form.instance):
|
||||
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:
|
||||
return HttpResponseBadRequest(form.errors)
|
||||
else:
|
||||
|
@ -245,17 +333,18 @@ def review_edit(request, item_uuid, review_uuid=None):
|
|||
|
||||
|
||||
@login_required
|
||||
def review_delete(request, review_uuid):
|
||||
review = get_object_or_404(Review, uid=base62.decode(review_uuid))
|
||||
if not review.is_editable_by(request.user):
|
||||
def piece_delete(request, piece_uuid):
|
||||
piece = get_object_or_404(Piece, uid=base62.decode(piece_uuid))
|
||||
return_url = request.GET.get("return_url", None) or "/"
|
||||
if not piece.is_editable_by(request.user):
|
||||
raise PermissionDenied()
|
||||
if request.method == 'GET':
|
||||
review_form = ReviewForm(instance=review)
|
||||
return render(request, 'review_delete.html', {'form': review_form, 'review': review})
|
||||
elif request.method == 'POST':
|
||||
item = review.item
|
||||
review.delete()
|
||||
return redirect(item.url)
|
||||
if request.method == "GET":
|
||||
return render(
|
||||
request, "piece_delete.html", {"piece": piece, "return_url": return_url}
|
||||
)
|
||||
elif request.method == "POST":
|
||||
piece.delete()
|
||||
return redirect(return_url)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
@ -264,57 +353,67 @@ def render_list_not_fount(request):
|
|||
msg = _("相关列表不存在")
|
||||
return render(
|
||||
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)
|
||||
if user is None:
|
||||
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)
|
||||
if type == 'mark':
|
||||
if type == "mark":
|
||||
shelf = user.shelf_manager.get_shelf(item_category, shelf_type)
|
||||
queryset = ShelfMember.objects.filter(owner=user, parent=shelf)
|
||||
elif type == 'tagmember':
|
||||
elif type == "tagmember":
|
||||
tag = Tag.objects.filter(owner=user, title=tag_title).first()
|
||||
if not tag:
|
||||
return render_list_not_fount(request)
|
||||
if tag.visibility != 0 and user != request.user:
|
||||
return render_list_not_fount(request)
|
||||
queryset = TagMember.objects.filter(parent=tag)
|
||||
elif type == 'review':
|
||||
elif type == "review":
|
||||
queryset = Review.objects.filter(owner=user)
|
||||
queryset = queryset.filter(query_item_category(item_category))
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
queryset = queryset.filter(q_visible_to(request.user, user))
|
||||
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)
|
||||
return render(request, f'user_{type}_list.html', {
|
||||
'user': user,
|
||||
'members': members,
|
||||
})
|
||||
return render(
|
||||
request,
|
||||
f"user_{type}_list.html",
|
||||
{
|
||||
"user": user,
|
||||
"members": members,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
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
|
||||
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
|
||||
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
|
||||
|
@ -322,17 +421,23 @@ def user_tag_list(request, user_name):
|
|||
user = User.get(user_name)
|
||||
if user is None:
|
||||
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)
|
||||
tags = Tag.objects.filter(owner=user)
|
||||
tags = user.tag_set.all()
|
||||
if user != request.user:
|
||||
tags = tags.filter(visibility=0)
|
||||
tags = tags.values('title').annotate(total=Count('members')).order_by('-total')
|
||||
return render(request, 'user_tag_list.html', {
|
||||
'user': user,
|
||||
'tags': tags,
|
||||
})
|
||||
tags = tags.values("title").annotate(total=Count("members")).order_by("-total")
|
||||
return render(
|
||||
request,
|
||||
"user_tag_list.html",
|
||||
{
|
||||
"user": user,
|
||||
"tags": tags,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -340,18 +445,24 @@ def user_collection_list(request, user_name):
|
|||
user = User.get(user_name)
|
||||
if user is None:
|
||||
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)
|
||||
collections = Tag.objects.filter(owner=user)
|
||||
collections = Collection.objects.filter(owner=user)
|
||||
if user != request.user:
|
||||
if request.user.is_following(user):
|
||||
collections = collections.filter(visibility__ne=2)
|
||||
else:
|
||||
collections = collections.filter(visibility=0)
|
||||
return render(request, 'user_collection_list.html', {
|
||||
'user': user,
|
||||
'collections': collections,
|
||||
})
|
||||
return render(
|
||||
request,
|
||||
"user_collection_list.html",
|
||||
{
|
||||
"user": user,
|
||||
"collections": collections,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -359,27 +470,37 @@ def user_liked_collection_list(request, user_name):
|
|||
user = User.get(user_name)
|
||||
if user is None:
|
||||
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)
|
||||
collections = Collection.objects.filter(likes__owner=user)
|
||||
if user != request.user:
|
||||
collections = collections.filter(query_visible(request.user))
|
||||
return render(request, 'user_collection_list.html', {
|
||||
'user': user,
|
||||
'collections': collections,
|
||||
})
|
||||
return render(
|
||||
request,
|
||||
"user_collection_list.html",
|
||||
{
|
||||
"user": user,
|
||||
"collections": collections,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def home_anonymous(request, id):
|
||||
login_url = settings.LOGIN_URL + "?next=" + request.get_full_path()
|
||||
try:
|
||||
username = id.split('@')[0]
|
||||
site = id.split('@')[1]
|
||||
return render(request, 'users/home_anonymous.html', {
|
||||
'login_url': login_url,
|
||||
'username': username,
|
||||
'site': site,
|
||||
})
|
||||
username = id.split("@")[0]
|
||||
site = id.split("@")[1]
|
||||
return render(
|
||||
request,
|
||||
"users/home_anonymous.html",
|
||||
{
|
||||
"login_url": login_url,
|
||||
"username": username,
|
||||
"site": site,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
return redirect(login_url)
|
||||
|
||||
|
@ -387,7 +508,7 @@ def home_anonymous(request, id):
|
|||
def home(request, user_name):
|
||||
if not request.user.is_authenticated:
|
||||
return home_anonymous(request, user_name)
|
||||
if request.method != 'GET':
|
||||
if request.method != "GET":
|
||||
return HttpResponseBadRequest()
|
||||
user = User.get(user_name)
|
||||
if user is None:
|
||||
|
@ -395,14 +516,13 @@ def home(request, user_name):
|
|||
|
||||
# access one's own home page
|
||||
if user == request.user:
|
||||
reports = Report.objects.order_by(
|
||||
'-submitted_time').filter(is_read=False)
|
||||
reports = Report.objects.order_by("-submitted_time").filter(is_read=False)
|
||||
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:
|
||||
request.user.read_announcement_index = Announcement.objects.latest(
|
||||
'pk').pk
|
||||
request.user.save(update_fields=['read_announcement_index'])
|
||||
request.user.read_announcement_index = Announcement.objects.latest("pk").pk
|
||||
request.user.save(update_fields=["read_announcement_index"])
|
||||
except ObjectDoesNotExist:
|
||||
# when there is no annoucenment
|
||||
pass
|
||||
|
@ -416,42 +536,51 @@ def home(request, user_name):
|
|||
|
||||
qv = q_visible_to(request.user, user)
|
||||
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:
|
||||
shelf_list[category] = {}
|
||||
for shelf_type in ShelfType:
|
||||
shelf = user.shelf_manager.get_shelf(category, shelf_type)
|
||||
members = shelf.recent_members.filter(qv)
|
||||
shelf_list[category][shelf_type] = {
|
||||
'title': shelf.title,
|
||||
'count': members.count(),
|
||||
'members': members[:5].prefetch_related('item'),
|
||||
"title": shelf.title,
|
||||
"count": members.count(),
|
||||
"members": members[:5].prefetch_related("item"),
|
||||
}
|
||||
reviews = Review.objects.filter(owner=user).filter(qv)
|
||||
shelf_list[category]['reviewed'] = {
|
||||
'title': '评论过的' + category.label,
|
||||
'count': reviews.count(),
|
||||
'members': reviews[:5].prefetch_related('item'),
|
||||
shelf_list[category]["reviewed"] = {
|
||||
"title": "评论过的" + category.label,
|
||||
"count": reviews.count(),
|
||||
"members": reviews[:5].prefetch_related("item"),
|
||||
}
|
||||
collections = Collection.objects.filter(owner=user).filter(qv).order_by("-edited_time")
|
||||
liked_collections = Collection.objects.filter(likes__owner=user).order_by("-edited_time")
|
||||
collections = (
|
||||
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:
|
||||
liked_collections = liked_collections.filter(query_visible(request.user))
|
||||
|
||||
layout = user.get_preference().get_serialized_home_layout()
|
||||
|
||||
return render(
|
||||
request,
|
||||
'profile.html',
|
||||
"profile.html",
|
||||
{
|
||||
'user': user,
|
||||
'shelf_list': shelf_list,
|
||||
'collections': collections[:5],
|
||||
'collections_count': collections.count(),
|
||||
'liked_collections': liked_collections.order_by("-edited_time")[:5],
|
||||
'liked_collections_count': liked_collections.count(),
|
||||
'layout': layout,
|
||||
'reports': reports,
|
||||
'unread_announcements': unread_announcements,
|
||||
}
|
||||
"user": user,
|
||||
"shelf_list": shelf_list,
|
||||
"collections": collections[:5],
|
||||
"collections_count": collections.count(),
|
||||
"liked_collections": liked_collections.order_by("-edited_time")[:5],
|
||||
"liked_collections_count": liked_collections.count(),
|
||||
"layout": layout,
|
||||
"reports": reports,
|
||||
"unread_announcements": unread_announcements,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -14,6 +14,7 @@ from catalog.common import *
|
|||
from catalog.models import *
|
||||
from catalog.sites import *
|
||||
from journal.models import *
|
||||
|
||||
# from social import models as social_models
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.paginator import Paginator
|
||||
|
@ -51,29 +52,53 @@ shelf_map = {
|
|||
}
|
||||
|
||||
tag_map = {
|
||||
BookMark: 'bookmark_tags',
|
||||
MovieMark: 'moviemark_tags',
|
||||
AlbumMark: 'albummark_tags',
|
||||
GameMark: 'gamemark_tags',
|
||||
BookMark: "bookmark_tags",
|
||||
MovieMark: "moviemark_tags",
|
||||
AlbumMark: "albummark_tags",
|
||||
GameMark: "gamemark_tags",
|
||||
}
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Migrate legacy marks to user journal'
|
||||
help = "Migrate legacy marks to user journal"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--book', dest='types', action='append_const', const=BookMark)
|
||||
parser.add_argument('--movie', dest='types', action='append_const', const=MovieMark)
|
||||
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('--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')
|
||||
parser.add_argument(
|
||||
"--book", dest="types", action="append_const", const=BookMark
|
||||
)
|
||||
parser.add_argument(
|
||||
"--movie", dest="types", action="append_const", const=MovieMark
|
||||
)
|
||||
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(
|
||||
"--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):
|
||||
print("Initialize shelves")
|
||||
|
@ -91,7 +116,11 @@ class Command(BaseCommand):
|
|||
def collection(self, options):
|
||||
collection_map = {}
|
||||
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):
|
||||
c = Collection.objects.create(
|
||||
owner_id=entity.owner_id,
|
||||
|
@ -115,11 +144,15 @@ class Command(BaseCommand):
|
|||
if old_id:
|
||||
item_link = LinkModel.objects.get(old_id=old_id)
|
||||
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:
|
||||
# TODO convert song to album
|
||||
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')
|
||||
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")
|
||||
)
|
||||
for entity in tqdm(qs):
|
||||
Like.objects.create(
|
||||
owner_id=entity.owner_id,
|
||||
|
@ -132,12 +165,14 @@ class Command(BaseCommand):
|
|||
for typ in [GameReview, AlbumReview, BookReview, MovieReview]:
|
||||
print(typ)
|
||||
LinkModel = model_link[typ]
|
||||
qs = typ.objects.all().filter(owner__is_active=True).order_by('id')
|
||||
if options['id']:
|
||||
if options['maxid']:
|
||||
qs = qs.filter(id__gte=int(options['id']), id__lte=int(options['maxid']))
|
||||
qs = typ.objects.all().filter(owner__is_active=True).order_by("id")
|
||||
if options["id"]:
|
||||
if options["maxid"]:
|
||||
qs = qs.filter(
|
||||
id__gte=int(options["id"]), id__lte=int(options["maxid"])
|
||||
)
|
||||
else:
|
||||
qs = qs.filter(id=int(options['id']))
|
||||
qs = qs.filter(id=int(options["id"]))
|
||||
pg = Paginator(qs, BATCH_SIZE)
|
||||
for p in tqdm(pg.page_range):
|
||||
with transaction.atomic():
|
||||
|
@ -145,28 +180,42 @@ class Command(BaseCommand):
|
|||
try:
|
||||
item_link = LinkModel.objects.get(old_id=entity.item.id)
|
||||
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:
|
||||
print(f'Convert failed for {typ} {entity.id}: {e}')
|
||||
if options['failstop']:
|
||||
print(f"Convert failed for {typ} {entity.id}: {e}")
|
||||
if options["failstop"]:
|
||||
raise (e)
|
||||
|
||||
def mark(self, options):
|
||||
types = options['types'] or [GameMark, AlbumMark, MovieMark, BookMark]
|
||||
print('Preparing cache')
|
||||
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()}
|
||||
types = options["types"] or [GameMark, AlbumMark, MovieMark, BookMark]
|
||||
print("Preparing cache")
|
||||
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()
|
||||
}
|
||||
|
||||
for typ in types:
|
||||
print(typ)
|
||||
LinkModel = model_link[typ]
|
||||
tag_field = tag_map[typ]
|
||||
qs = typ.objects.all().filter(owner__is_active=True).order_by('id')
|
||||
if options['id']:
|
||||
if options['maxid']:
|
||||
qs = qs.filter(id__gte=int(options['id']), id__lte=int(options['maxid']))
|
||||
qs = typ.objects.all().filter(owner__is_active=True).order_by("id")
|
||||
if options["id"]:
|
||||
if options["maxid"]:
|
||||
qs = qs.filter(
|
||||
id__gte=int(options["id"]), id__lte=int(options["maxid"])
|
||||
)
|
||||
else:
|
||||
qs = qs.filter(id=int(options['id']))
|
||||
qs = qs.filter(id=int(options["id"]))
|
||||
|
||||
pg = Paginator(qs, BATCH_SIZE)
|
||||
for p in tqdm(pg.page_range):
|
||||
|
@ -193,22 +242,42 @@ class Command(BaseCommand):
|
|||
visibility = entity.visibility
|
||||
created_time = entity.created_time
|
||||
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:
|
||||
Comment.objects.create(owner_id=user_id, item_id=item_id, text=entity.text, visibility=visibility)
|
||||
shelf = shelf_cache[f'{user_id}_{item.category}_{entity.status}']
|
||||
Comment.objects.create(
|
||||
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(
|
||||
parent_id=shelf,
|
||||
owner_id=user_id,
|
||||
position=0,
|
||||
item_id=item_id,
|
||||
metadata={'shared_link': entity.shared_link},
|
||||
created_time=created_time)
|
||||
ShelfLogEntry.objects.create(owner_id=user_id, shelf_id=shelf, item_id=item_id, timestamp=created_time)
|
||||
metadata={"shared_link": entity.shared_link},
|
||||
created_time=created_time,
|
||||
)
|
||||
ShelfLogEntry.objects.create(
|
||||
owner_id=user_id,
|
||||
shelf_id=shelf,
|
||||
item_id=item_id,
|
||||
timestamp=created_time,
|
||||
)
|
||||
for title in tags:
|
||||
tag_key = f'{user_id}_{title}'
|
||||
tag_key = f"{user_id}_{title}"
|
||||
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
|
||||
else:
|
||||
tag = tag_cache[tag_key]
|
||||
|
@ -217,28 +286,31 @@ class Command(BaseCommand):
|
|||
owner_id=user_id,
|
||||
position=0,
|
||||
item_id=item_id,
|
||||
created_time=created_time)
|
||||
created_time=created_time,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f'Convert failed for {typ} {entity.id}: {e}')
|
||||
if options['failstop']:
|
||||
print(f"Convert failed for {typ} {entity.id}: {e}")
|
||||
if options["failstop"]:
|
||||
raise (e)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options['initshelf']:
|
||||
if options["initshelf"]:
|
||||
self.initshelf()
|
||||
elif options['collection']:
|
||||
if options['clear']:
|
||||
elif options["collection"]:
|
||||
if options["clear"]:
|
||||
self.clear([Collection, Like])
|
||||
else:
|
||||
self.collection(options)
|
||||
elif options['review']:
|
||||
if options['clear']:
|
||||
elif options["review"]:
|
||||
if options["clear"]:
|
||||
self.clear([Review])
|
||||
else:
|
||||
self.review(options)
|
||||
elif options['mark']:
|
||||
if options['clear']:
|
||||
self.clear([Comment, Rating, TagMember, Tag, ShelfLogEntry, ShelfMember])
|
||||
elif options["mark"]:
|
||||
if options["clear"]:
|
||||
self.clear(
|
||||
[Comment, Rating, TagMember, Tag, ShelfLogEntry, ShelfMember]
|
||||
)
|
||||
else:
|
||||
self.mark(options)
|
||||
self.stdout.write(self.style.SUCCESS(f'Done.'))
|
||||
self.stdout.write(self.style.SUCCESS(f"Done."))
|
||||
|
|
376
mastodon/api.py
376
mastodon/api.py
|
@ -19,47 +19,47 @@ logger = logging.getLogger(__name__)
|
|||
# returns user info
|
||||
# retruns the same info as verify account credentials
|
||||
# GET
|
||||
API_GET_ACCOUNT = '/api/v1/accounts/:id'
|
||||
API_GET_ACCOUNT = "/api/v1/accounts/:id"
|
||||
|
||||
# returns user info if valid, 401 if invalid
|
||||
# GET
|
||||
API_VERIFY_ACCOUNT = '/api/v1/accounts/verify_credentials'
|
||||
API_VERIFY_ACCOUNT = "/api/v1/accounts/verify_credentials"
|
||||
|
||||
# obtain token
|
||||
# GET
|
||||
API_OBTAIN_TOKEN = '/oauth/token'
|
||||
API_OBTAIN_TOKEN = "/oauth/token"
|
||||
|
||||
# obatin auth code
|
||||
# GET
|
||||
API_OAUTH_AUTHORIZE = '/oauth/authorize'
|
||||
API_OAUTH_AUTHORIZE = "/oauth/authorize"
|
||||
|
||||
# revoke token
|
||||
# POST
|
||||
API_REVOKE_TOKEN = '/oauth/revoke'
|
||||
API_REVOKE_TOKEN = "/oauth/revoke"
|
||||
|
||||
# relationships
|
||||
# GET
|
||||
API_GET_RELATIONSHIPS = '/api/v1/accounts/relationships'
|
||||
API_GET_RELATIONSHIPS = "/api/v1/accounts/relationships"
|
||||
|
||||
# toot
|
||||
# POST
|
||||
API_PUBLISH_TOOT = '/api/v1/statuses'
|
||||
API_PUBLISH_TOOT = "/api/v1/statuses"
|
||||
|
||||
# create new app
|
||||
# POST
|
||||
API_CREATE_APP = '/api/v1/apps'
|
||||
API_CREATE_APP = "/api/v1/apps"
|
||||
|
||||
# search
|
||||
# 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"
|
||||
|
||||
|
@ -70,43 +70,38 @@ post = functools.partial(requests.post, timeout=settings.MASTODON_TIMEOUT)
|
|||
|
||||
# low level api below
|
||||
def get_relationships(site, id_list, token): # no longer in use
|
||||
url = 'https://' + site + API_GET_RELATIONSHIPS
|
||||
payload = {'id[]': id_list}
|
||||
headers = {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Authorization': f'Bearer {token}'
|
||||
}
|
||||
url = "https://" + site + API_GET_RELATIONSHIPS
|
||||
payload = {"id[]": id_list}
|
||||
headers = {"User-Agent": USER_AGENT, "Authorization": f"Bearer {token}"}
|
||||
response = get(url, headers=headers, params=payload)
|
||||
return response.json()
|
||||
|
||||
|
||||
def post_toot(site, content, visibility, token, local_only=False, update_id=None):
|
||||
headers = {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Authorization': f'Bearer {token}',
|
||||
'Idempotency-Key': random_string_generator(16)
|
||||
"User-Agent": USER_AGENT,
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Idempotency-Key": random_string_generator(16),
|
||||
}
|
||||
if site == TWITTER_DOMAIN:
|
||||
url = TWITTER_API_POST
|
||||
payload = {
|
||||
'text': content if len(content) <= 150 else content[0:150] + '...'
|
||||
}
|
||||
payload = {"text": content if len(content) <= 150 else content[0:150] + "..."}
|
||||
response = post(url, headers=headers, json=payload)
|
||||
if response.status_code == 201:
|
||||
response.status_code = 200
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Error {url} {response.status_code}")
|
||||
else:
|
||||
url = 'https://' + site + API_PUBLISH_TOOT
|
||||
url = "https://" + site + API_PUBLISH_TOOT
|
||||
payload = {
|
||||
'status': content,
|
||||
'visibility': visibility,
|
||||
"status": content,
|
||||
"visibility": visibility,
|
||||
}
|
||||
if local_only:
|
||||
payload['local_only'] = True
|
||||
payload["local_only"] = True
|
||||
try:
|
||||
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:
|
||||
response = post(url, headers=headers, data=payload)
|
||||
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):
|
||||
if domain_name.lower().strip() == TWITTER_DOMAIN:
|
||||
return TWITTER_DOMAIN, ''
|
||||
return TWITTER_DOMAIN, ""
|
||||
try:
|
||||
url = f'https://{domain_name}/api/v1/instance'
|
||||
response = get(url, headers={'User-Agent': USER_AGENT})
|
||||
url = f"https://{domain_name}/api/v1/instance"
|
||||
response = get(url, headers={"User-Agent": USER_AGENT})
|
||||
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:
|
||||
logger.error(f"Error {url}")
|
||||
return domain_name, ''
|
||||
return domain_name, ""
|
||||
|
||||
|
||||
def create_app(domain_name):
|
||||
# naive protocal strip
|
||||
is_http = False
|
||||
if domain_name.startswith("https://"):
|
||||
domain_name = domain_name.replace("https://", '')
|
||||
domain_name = domain_name.replace("https://", "")
|
||||
elif domain_name.startswith("http://"):
|
||||
is_http = True
|
||||
domain_name = domain_name.replace("http://", '')
|
||||
if domain_name.endswith('/'):
|
||||
domain_name = domain_name.replace("http://", "")
|
||||
if domain_name.endswith("/"):
|
||||
domain_name = domain_name[0:-1]
|
||||
|
||||
if not is_http:
|
||||
url = 'https://' + domain_name + API_CREATE_APP
|
||||
url = "https://" + domain_name + API_CREATE_APP
|
||||
else:
|
||||
url = 'http://' + domain_name + API_CREATE_APP
|
||||
url = "http://" + domain_name + API_CREATE_APP
|
||||
|
||||
payload = {
|
||||
'client_name': settings.CLIENT_NAME,
|
||||
'scopes': settings.MASTODON_CLIENT_SCOPE,
|
||||
'redirect_uris': settings.REDIRECT_URIS,
|
||||
'website': settings.APP_WEBSITE
|
||||
"client_name": settings.CLIENT_NAME,
|
||||
"scopes": settings.MASTODON_CLIENT_SCOPE,
|
||||
"redirect_uris": settings.REDIRECT_URIS,
|
||||
"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
|
||||
|
||||
|
||||
def get_site_id(username, user_site, target_site, token):
|
||||
url = 'https://' + target_site + API_SEARCH
|
||||
url = "https://" + target_site + API_SEARCH
|
||||
payload = {
|
||||
'limit': 1,
|
||||
'type': 'accounts',
|
||||
'resolve': True,
|
||||
'q': f"{username}@{user_site}"
|
||||
}
|
||||
headers = {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Authorization': f'Bearer {token}'
|
||||
"limit": 1,
|
||||
"type": "accounts",
|
||||
"resolve": True,
|
||||
"q": f"{username}@{user_site}",
|
||||
}
|
||||
headers = {"User-Agent": USER_AGENT, "Authorization": f"Bearer {token}"}
|
||||
response = get(url, params=payload, headers=headers)
|
||||
try:
|
||||
data = response.json()
|
||||
except Exception:
|
||||
logger.error(f"Error parsing JSON from {url}")
|
||||
return None
|
||||
if 'accounts' not in data:
|
||||
if "accounts" not in data:
|
||||
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
|
||||
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
|
||||
else:
|
||||
return data['accounts'][0]['id']
|
||||
return data["accounts"][0]["id"]
|
||||
|
||||
|
||||
# high level api below
|
||||
def get_relationship(request_user, target_user, useless_token=None):
|
||||
return [{
|
||||
'blocked_by': target_user.is_blocking(request_user),
|
||||
'following': request_user.is_following(target_user),
|
||||
}]
|
||||
return [
|
||||
{
|
||||
"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):
|
||||
|
@ -209,19 +207,22 @@ def get_cross_site_id(target_user, target_site, token):
|
|||
try:
|
||||
cross_site_info = CrossSiteUserInfo.objects.get(
|
||||
uid=f"{target_user.username}@{target_user.mastodon_site}",
|
||||
target_site=target_site
|
||||
target_site=target_site,
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
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:
|
||||
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
|
||||
cross_site_info = CrossSiteUserInfo.objects.create(
|
||||
uid=f"{target_user.username}@{target_user.mastodon_site}",
|
||||
target_site=target_site,
|
||||
site_id=cross_site_id,
|
||||
local_id=target_user.id
|
||||
local_id=target_user.id,
|
||||
)
|
||||
return cross_site_info.site_id
|
||||
|
||||
|
@ -229,31 +230,41 @@ def get_cross_site_id(target_user, target_site, token):
|
|||
# utils below
|
||||
def random_string_generator(n):
|
||||
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):
|
||||
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:
|
||||
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:
|
||||
logger.error(f"Error {url} {response.status_code}")
|
||||
return response.status_code, None
|
||||
r = response.json()['data']
|
||||
r['display_name'] = r['name']
|
||||
r['note'] = r['description']
|
||||
r['avatar'] = r['profile_image_url']
|
||||
r['avatar_static'] = r['profile_image_url']
|
||||
r['locked'] = r['protected']
|
||||
r['url'] = f'https://{TWITTER_DOMAIN}/{r["username"]}'
|
||||
r = response.json()["data"]
|
||||
r["display_name"] = r["name"]
|
||||
r["note"] = r["description"]
|
||||
r["avatar"] = r["profile_image_url"]
|
||||
r["avatar_static"] = r["profile_image_url"]
|
||||
r["locked"] = r["protected"]
|
||||
r["url"] = f'https://{TWITTER_DOMAIN}/{r["username"]}'
|
||||
return 200, r
|
||||
except Exception:
|
||||
return -1, None
|
||||
url = 'https://' + site + API_VERIFY_ACCOUNT
|
||||
url = "https://" + site + API_VERIFY_ACCOUNT
|
||||
try:
|
||||
response = get(url, headers={'User-Agent': USER_AGENT, 'Authorization': f'Bearer {token}'})
|
||||
return response.status_code, (response.json() if response.status_code == 200 else None)
|
||||
response = get(
|
||||
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:
|
||||
return -1, None
|
||||
|
||||
|
@ -261,94 +272,130 @@ def verify_account(site, token):
|
|||
def get_related_acct_list(site, token, api):
|
||||
if site == TWITTER_DOMAIN:
|
||||
return []
|
||||
url = 'https://' + site + api
|
||||
url = "https://" + site + api
|
||||
results = []
|
||||
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
|
||||
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()))
|
||||
if 'Link' in response.headers:
|
||||
for ls in response.headers['Link'].split(','):
|
||||
li = ls.strip().split(';')
|
||||
results.extend(
|
||||
map(
|
||||
lambda u: (
|
||||
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"':
|
||||
url = li[0].strip().replace('>', '').replace('<', '')
|
||||
url = li[0].strip().replace(">", "").replace("<", "")
|
||||
return results
|
||||
|
||||
|
||||
class TootVisibilityEnum:
|
||||
PUBLIC = 'public'
|
||||
PRIVATE = 'private'
|
||||
DIRECT = 'direct'
|
||||
UNLISTED = 'unlisted'
|
||||
PUBLIC = "public"
|
||||
PRIVATE = "private"
|
||||
DIRECT = "direct"
|
||||
UNLISTED = "unlisted"
|
||||
|
||||
|
||||
def get_mastodon_application(domain):
|
||||
app = MastodonApplication.objects.filter(domain_name=domain).first()
|
||||
if app is not None:
|
||||
return app, ''
|
||||
return app, ""
|
||||
if domain == TWITTER_DOMAIN:
|
||||
return None, 'Twitter未配置'
|
||||
return None, "Twitter未配置"
|
||||
error_msg = None
|
||||
try:
|
||||
response = create_app(domain)
|
||||
except (requests.exceptions.Timeout, ConnectionError):
|
||||
error_msg = "联邦网络请求超时。"
|
||||
logger.error(f'Error creating app for {domain}: Timeout')
|
||||
logger.error(f"Error creating app for {domain}: Timeout")
|
||||
except Exception as e:
|
||||
error_msg = "联邦网络请求失败 " + str(e)
|
||||
logger.error(f'Error creating app for {domain}: {e}')
|
||||
logger.error(f"Error creating app for {domain}: {e}")
|
||||
else:
|
||||
# fill the form with returned data
|
||||
if response.status_code != 200:
|
||||
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:
|
||||
try:
|
||||
data = response.json()
|
||||
except Exception:
|
||||
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:
|
||||
if settings.MASTODON_ALLOW_ANY_SITE:
|
||||
app = MastodonApplication.objects.create(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 '')
|
||||
app = MastodonApplication.objects.create(
|
||||
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:
|
||||
error_msg = "不支持其它实例登录"
|
||||
logger.error(f'Disallowed to create app for {domain}')
|
||||
logger.error(f"Disallowed to create app for {domain}")
|
||||
return app, error_msg
|
||||
|
||||
|
||||
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:
|
||||
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
|
||||
return "https://" + login_domain + "/oauth/authorize?client_id=" + app.client_id + "&scope=" + quote(scope) + "&redirect_uri=" + url + "&response_type=code"
|
||||
scope = (
|
||||
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):
|
||||
"""Returns token if success else None."""
|
||||
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 = {
|
||||
'client_id': mast_app.client_id,
|
||||
'client_secret': mast_app.client_secret,
|
||||
'redirect_uri': redirect_uri,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code
|
||||
"client_id": mast_app.client_id,
|
||||
"client_secret": mast_app.client_secret,
|
||||
"redirect_uri": redirect_uri,
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
}
|
||||
headers = {'User-Agent': USER_AGENT}
|
||||
headers = {"User-Agent": USER_AGENT}
|
||||
auth = None
|
||||
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:
|
||||
url = TWITTER_API_TOKEN
|
||||
auth = (mast_app.client_id, mast_app.client_secret)
|
||||
del payload['client_secret']
|
||||
payload['code_verifier'] = 'challenge'
|
||||
del payload["client_secret"]
|
||||
payload["code_verifier"] = "challenge"
|
||||
else:
|
||||
url = 'https://' + mast_app.domain_name + API_OBTAIN_TOKEN
|
||||
url = "https://" + mast_app.domain_name + API_OBTAIN_TOKEN
|
||||
try:
|
||||
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"}
|
||||
|
@ -359,7 +406,7 @@ def obtain_token(site, request, code):
|
|||
logger.error(f"Error {url} {e}")
|
||||
return None, None
|
||||
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):
|
||||
|
@ -368,34 +415,34 @@ def refresh_access_token(site, refresh_token):
|
|||
mast_app = MastodonApplication.objects.get(domain_name=site)
|
||||
url = TWITTER_API_TOKEN
|
||||
payload = {
|
||||
'client_id': mast_app.client_id,
|
||||
'refresh_token': refresh_token,
|
||||
'grant_type': 'refresh_token',
|
||||
"client_id": mast_app.client_id,
|
||||
"refresh_token": 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)
|
||||
response = post(url, data=payload, headers=headers, auth=auth)
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Error {url} {response.status_code}")
|
||||
return None
|
||||
data = response.json()
|
||||
return data.get('access_token')
|
||||
return data.get("access_token")
|
||||
|
||||
|
||||
def revoke_token(site, token):
|
||||
mast_app = MastodonApplication.objects.get(domain_name=site)
|
||||
|
||||
payload = {
|
||||
'client_id': mast_app.client_id,
|
||||
'client_secret': mast_app.client_secret,
|
||||
'token': token
|
||||
"client_id": mast_app.client_id,
|
||||
"client_secret": mast_app.client_secret,
|
||||
"token": token,
|
||||
}
|
||||
|
||||
if mast_app.is_proxy:
|
||||
url = 'https://' + mast_app.proxy_to + API_REVOKE_TOKEN
|
||||
url = "https://" + mast_app.proxy_to + API_REVOKE_TOKEN
|
||||
else:
|
||||
url = 'https://' + site + API_REVOKE_TOKEN
|
||||
post(url, data=payload, headers={'User-Agent': USER_AGENT})
|
||||
url = "https://" + site + API_REVOKE_TOKEN
|
||||
post(url, data=payload, headers={"User-Agent": USER_AGENT})
|
||||
|
||||
|
||||
def share_mark(mark):
|
||||
|
@ -408,22 +455,38 @@ def share_mark(mark):
|
|||
visibility = TootVisibilityEnum.PUBLIC
|
||||
else:
|
||||
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 ''
|
||||
stars = rating_to_emoji(mark.rating, MastodonApplication.objects.get(domain_name=user.mastodon_site).star_mode)
|
||||
tags = (
|
||||
"\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}"
|
||||
update_id = None
|
||||
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
|
||||
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]:
|
||||
j = response.json()
|
||||
if 'url' in j:
|
||||
mark.shared_link = j['url']
|
||||
elif 'data' in j:
|
||||
mark.shared_link = f"https://twitter.com/{user.username}/status/{j['data']['id']}"
|
||||
if "url" in j:
|
||||
mark.shared_link = j["url"]
|
||||
elif "data" in j:
|
||||
mark.shared_link = (
|
||||
f"https://twitter.com/{user.username}/status/{j['data']['id']}"
|
||||
)
|
||||
if mark.shared_link:
|
||||
mark.save(update_fields=['shared_link'])
|
||||
mark.save(update_fields=["shared_link"])
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
@ -439,21 +502,34 @@ def share_review(review):
|
|||
visibility = TootVisibilityEnum.PUBLIC
|
||||
else:
|
||||
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}"
|
||||
update_id = None
|
||||
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
|
||||
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]:
|
||||
j = response.json()
|
||||
if 'url' in j:
|
||||
review.shared_link = j['url']
|
||||
elif 'data' in j:
|
||||
review.shared_link = f"https://twitter.com/{user.username}/status/{j['data']['id']}"
|
||||
if "url" in j:
|
||||
review.shared_link = j["url"]
|
||||
elif "data" in j:
|
||||
review.shared_link = (
|
||||
f"https://twitter.com/{user.username}/status/{j['data']['id']}"
|
||||
)
|
||||
if review.shared_link:
|
||||
review.save(update_fields=['shared_link'])
|
||||
review.save(update_fields=["shared_link"])
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
@ -468,15 +544,21 @@ def share_collection(collection, comment, user, visibility_no):
|
|||
visibility = TootVisibilityEnum.PUBLIC
|
||||
else:
|
||||
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}"
|
||||
response = post_toot(user.mastodon_site, content, visibility, user.mastodon_token)
|
||||
if response and response.status_code in [200, 201]:
|
||||
j = response.json()
|
||||
if 'url' in j:
|
||||
shared_link = j['url']
|
||||
elif 'data' in j:
|
||||
shared_link = f"https://twitter.com/{user.username}/status/{j['data']['id']}"
|
||||
if "url" in j:
|
||||
shared_link = j["url"]
|
||||
elif "data" in j:
|
||||
shared_link = (
|
||||
f"https://twitter.com/{user.username}/status/{j['data']['id']}"
|
||||
)
|
||||
if shared_link:
|
||||
pass
|
||||
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