From b2af6f3230bd079f238028b73e82aac8e98fa444 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 15 Dec 2022 17:29:35 -0500 Subject: [PATCH] new data model: /book/ --- catalog/apps.py | 6 + catalog/book/models.py | 2 + catalog/book/tests.py | 47 ++-- catalog/common/__init__.py | 2 +- catalog/common/models.py | 23 +- catalog/common/sites.py | 23 +- catalog/game/tests.py | 28 +-- catalog/management/commands/cat.py | 4 +- catalog/movie/models.py | 39 ++++ catalog/movie/tests.py | 24 +- catalog/music/tests.py | 16 +- catalog/performance/tests.py | 6 +- catalog/podcast/tests.py | 6 +- catalog/sites/__init__.py | 2 +- catalog/sites/apple_podcast.py | 2 +- catalog/sites/bangumi.py | 2 +- catalog/sites/douban_book.py | 5 +- catalog/sites/douban_drama.py | 2 +- catalog/sites/douban_game.py | 2 +- catalog/sites/douban_movie.py | 2 +- catalog/sites/douban_music.py | 2 +- catalog/sites/goodreads.py | 6 +- catalog/sites/google_books.py | 5 +- catalog/sites/igdb.py | 4 +- catalog/sites/imdb.py | 4 +- catalog/sites/spotify.py | 2 +- catalog/sites/steam.py | 2 +- catalog/sites/tmdb.py | 6 +- catalog/templates/album.html | 19 ++ catalog/templates/edition.html | 98 ++++++++ catalog/templates/game.html | 19 ++ catalog/templates/item.html | 19 ++ catalog/templates/item_base.html | 353 +++++++++++++++++++++++++++++ catalog/templates/movie.html | 19 ++ catalog/templates/performance.html | 19 ++ catalog/templates/podcast.html | 19 ++ catalog/templates/tvseason.html | 19 ++ catalog/templates/tvshow.html | 19 ++ catalog/templates/work.html | 19 ++ catalog/tv/tests.py | 40 ++-- catalog/urls.py | 5 +- catalog/views.py | 118 +++++++++- journal/models.py | 174 ++++++++++++-- journal/tests.py | 58 ++++- social/apps.py | 7 + 45 files changed, 1150 insertions(+), 148 deletions(-) create mode 100644 catalog/templates/album.html create mode 100644 catalog/templates/edition.html create mode 100644 catalog/templates/game.html create mode 100644 catalog/templates/item.html create mode 100644 catalog/templates/item_base.html create mode 100644 catalog/templates/movie.html create mode 100644 catalog/templates/performance.html create mode 100644 catalog/templates/podcast.html create mode 100644 catalog/templates/tvseason.html create mode 100644 catalog/templates/tvshow.html create mode 100644 catalog/templates/work.html diff --git a/catalog/apps.py b/catalog/apps.py index a5993c68..62a2dd40 100644 --- a/catalog/apps.py +++ b/catalog/apps.py @@ -4,3 +4,9 @@ from django.apps import AppConfig class CatalogConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'catalog' + + def ready(self): + # load key modules in proper order, make sure class inject and signal works as expected + from catalog import models + from catalog import sites + from journal import models as journal_models diff --git a/catalog/book/models.py b/catalog/book/models.py index 8c0390d6..fecfd9a1 100644 --- a/catalog/book/models.py +++ b/catalog/book/models.py @@ -26,6 +26,8 @@ from .utils import * class Edition(Item): category = ItemCategory.Book url_path = 'book' + demonstrative = _('这本书') + isbn = PrimaryLookupIdDescriptor(IdType.ISBN) asin = PrimaryLookupIdDescriptor(IdType.ASIN) cubn = PrimaryLookupIdDescriptor(IdType.CUBN) diff --git a/catalog/book/tests.py b/catalog/book/tests.py index 942db2bb..4229bc1c 100644 --- a/catalog/book/tests.py +++ b/catalog/book/tests.py @@ -63,8 +63,8 @@ class GoodreadsTestCase(TestCase): t_id = '77566' t_url = 'https://www.goodreads.com/zh/book/show/77566.Hyperion' t_url2 = 'https://www.goodreads.com/book/show/77566' - p1 = SiteList.get_site_by_id_type(t_type) - p2 = SiteList.get_site_by_url(t_url) + p1 = SiteManager.get_site_by_id_type(t_type) + p2 = SiteManager.get_site_by_url(t_url) self.assertEqual(p1.id_to_url(t_id), t_url2) self.assertEqual(p2.url_to_id(t_url), t_id) @@ -73,7 +73,7 @@ class GoodreadsTestCase(TestCase): t_url = 'https://www.goodreads.com/book/show/77566.Hyperion' t_url2 = 'https://www.goodreads.com/book/show/77566' isbn = '9780553283686' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) self.assertEqual(site.url, t_url2) site.get_resource() @@ -93,7 +93,7 @@ class GoodreadsTestCase(TestCase): self.assertEqual(edition.title, 'Hyperion') edition.delete() - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) self.assertEqual(site.url, t_url2) site.get_resource() @@ -102,7 +102,7 @@ class GoodreadsTestCase(TestCase): @use_local_response def test_asin(self): t_url = 'https://www.goodreads.com/book/show/45064996-hyperion' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) site.get_resource_ready() self.assertEqual(site.resource.item.title, 'Hyperion') self.assertEqual(site.resource.item.asin, 'B004G60EHS') @@ -110,12 +110,12 @@ class GoodreadsTestCase(TestCase): @use_local_response def test_work(self): url = 'https://www.goodreads.com/work/editions/153313' - p = SiteList.get_site_by_url(url).get_resource_ready() + p = SiteManager.get_site_by_url(url).get_resource_ready() self.assertEqual(p.item.title, '1984') url1 = 'https://www.goodreads.com/book/show/3597767-rok-1984' url2 = 'https://www.goodreads.com/book/show/40961427-1984' - p1 = SiteList.get_site_by_url(url1).get_resource_ready() - p2 = SiteList.get_site_by_url(url2).get_resource_ready() + p1 = SiteManager.get_site_by_url(url1).get_resource_ready() + p2 = SiteManager.get_site_by_url(url2).get_resource_ready() w1 = p1.item.works.all().first() w2 = p2.item.works.all().first() self.assertEqual(w1, w2) @@ -127,8 +127,8 @@ class GoogleBooksTestCase(TestCase): t_id = 'hV--zQEACAAJ' t_url = 'https://books.google.com.bn/books?id=hV--zQEACAAJ&hl=ms' t_url2 = 'https://books.google.com/books?id=hV--zQEACAAJ' - p1 = SiteList.get_site_by_url(t_url) - p2 = SiteList.get_site_by_url(t_url2) + p1 = SiteManager.get_site_by_url(t_url) + p2 = SiteManager.get_site_by_url(t_url2) self.assertIsNotNone(p1) self.assertEqual(p1.url, t_url2) self.assertEqual(p1.ID_TYPE, t_type) @@ -138,7 +138,7 @@ class GoogleBooksTestCase(TestCase): @use_local_response def test_scrape(self): t_url = 'https://books.google.com.bn/books?id=hV--zQEACAAJ' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) site.get_resource_ready() self.assertEqual(site.ready, True) @@ -159,8 +159,8 @@ class DoubanBookTestCase(TestCase): t_id = '35902899' t_url = 'https://m.douban.com/book/subject/35902899/' t_url2 = 'https://book.douban.com/subject/35902899/' - p1 = SiteList.get_site_by_url(t_url) - p2 = SiteList.get_site_by_url(t_url2) + p1 = SiteManager.get_site_by_url(t_url) + p2 = SiteManager.get_site_by_url(t_url2) self.assertEqual(p1.url, t_url2) self.assertEqual(p1.ID_TYPE, t_type) self.assertEqual(p1.id_value, t_id) @@ -169,10 +169,11 @@ class DoubanBookTestCase(TestCase): @use_local_response def test_scrape(self): t_url = 'https://book.douban.com/subject/35902899/' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) site.get_resource_ready() self.assertEqual(site.ready, True) + self.assertEqual(site.resource.site_name, SiteName.Douban) self.assertEqual(site.resource.metadata.get('title'), '1984 Nineteen Eighty-Four') self.assertEqual(site.resource.metadata.get('isbn'), '9781847498571') self.assertEqual(site.resource.id_type, IdType.DoubanBook) @@ -185,8 +186,8 @@ class DoubanBookTestCase(TestCase): # url = 'https://www.goodreads.com/work/editions/153313' url1 = 'https://book.douban.com/subject/1089243/' url2 = 'https://book.douban.com/subject/2037260/' - p1 = SiteList.get_site_by_url(url1).get_resource_ready() - p2 = SiteList.get_site_by_url(url2).get_resource_ready() + p1 = SiteManager.get_site_by_url(url1).get_resource_ready() + p2 = SiteManager.get_site_by_url(url2).get_resource_ready() w1 = p1.item.works.all().first() w2 = p2.item.works.all().first() self.assertEqual(w1.title, '黄金时代') @@ -205,9 +206,9 @@ class MultiBookSitesTestCase(TestCase): url1 = 'https://www.goodreads.com/book/show/56821625-1984' url2 = 'https://book.douban.com/subject/35902899/' url3 = 'https://books.google.com/books?id=hV--zQEACAAJ' - p1 = SiteList.get_site_by_url(url1).get_resource_ready() - p2 = SiteList.get_site_by_url(url2).get_resource_ready() - p3 = SiteList.get_site_by_url(url3).get_resource_ready() + p1 = SiteManager.get_site_by_url(url1).get_resource_ready() + p2 = SiteManager.get_site_by_url(url2).get_resource_ready() + p3 = SiteManager.get_site_by_url(url3).get_resource_ready() self.assertEqual(p1.item.id, p2.item.id) self.assertEqual(p2.item.id, p3.item.id) @@ -218,16 +219,16 @@ class MultiBookSitesTestCase(TestCase): url2 = 'https://book.douban.com/subject/2037260/' url3 = 'https://www.goodreads.com/book/show/59952545-golden-age' url4 = 'https://www.goodreads.com/book/show/11798823' - p1 = SiteList.get_site_by_url(url1).get_resource_ready() # lxml bug may break this + p1 = SiteManager.get_site_by_url(url1).get_resource_ready() # lxml bug may break this w1 = p1.item.works.all().first() - p2 = SiteList.get_site_by_url(url2).get_resource_ready() + p2 = SiteManager.get_site_by_url(url2).get_resource_ready() w2 = p2.item.works.all().first() self.assertEqual(w1, w2) self.assertEqual(p1.item.works.all().count(), 1) - p3 = SiteList.get_site_by_url(url3).get_resource_ready() + p3 = SiteManager.get_site_by_url(url3).get_resource_ready() w3 = p3.item.works.all().first() self.assertNotEqual(w3, w2) - p4 = SiteList.get_site_by_url(url4).get_resource_ready() + p4 = SiteManager.get_site_by_url(url4).get_resource_ready() self.assertEqual(p4.item.works.all().count(), 2) self.assertEqual(p1.item.works.all().count(), 2) w2e = w2.editions.all().order_by('title') diff --git a/catalog/common/__init__.py b/catalog/common/__init__.py index ab4fe928..105be222 100644 --- a/catalog/common/__init__.py +++ b/catalog/common/__init__.py @@ -5,4 +5,4 @@ from .scrapers import * from . import jsondata -__all__ = ('IdType', 'ItemCategory', 'Item', 'ExternalResource', 'ResourceContent', 'ParseError', 'AbstractSite', 'SiteList', 'jsondata', 'PrimaryLookupIdDescriptor', 'LookupIdDescriptor', 'get_mock_mode', 'get_mock_file', 'use_local_response', 'RetryDownloader', 'BasicDownloader', 'ProxiedDownloader', 'BasicImageDownloader', 'RESPONSE_OK', 'RESPONSE_NETWORK_ERROR', 'RESPONSE_INVALID_CONTENT', 'RESPONSE_CENSORSHIP') +__all__ = ('IdType', 'SiteName', 'ItemCategory', 'Item', 'ExternalResource', 'ResourceContent', 'ParseError', 'AbstractSite', 'SiteManager', 'jsondata', 'PrimaryLookupIdDescriptor', 'LookupIdDescriptor', 'get_mock_mode', 'get_mock_file', 'use_local_response', 'RetryDownloader', 'BasicDownloader', 'ProxiedDownloader', 'BasicImageDownloader', 'RESPONSE_OK', 'RESPONSE_NETWORK_ERROR', 'RESPONSE_INVALID_CONTENT', 'RESPONSE_CENSORSHIP') diff --git a/catalog/common/models.py b/catalog/common/models.py index f608d304..fb803469 100644 --- a/catalog/common/models.py +++ b/catalog/common/models.py @@ -13,6 +13,20 @@ from .mixins import SoftDeleteMixin # from django.conf import settings +class SiteName(models.TextChoices): + Douban = 'douban', _('豆瓣') + Goodreads = 'goodreads', _('Goodreads') + GoogleBooks = 'googlebooks', _('谷歌图书') + IMDB = 'imdb', _('IMDB') + TMDB = 'tmdb', _('The Movie Database') + Bandcamp = 'bandcamp', _('Bandcamp') + Spotify_Album = 'spotify', _('Spotify') + IGDB = 'igdb', _('IGDB') + Steam = 'steam', _('Steam') + Bangumi = 'bangumi', _('Bangumi') + ApplePodcast = 'apple_podcast', _('苹果播客') + + class IdType(models.TextChoices): WikiData = 'wikidata', _('维基数据') ISBN10 = 'isbn10', _('ISBN10') @@ -156,9 +170,10 @@ class LookupIdDescriptor(object): # TODO make it mixin of Field # return sid[0] in IdType.values() -class Item(PolymorphicModel, SoftDeleteMixin): +class Item(SoftDeleteMixin, PolymorphicModel): url_path = None # subclass must specify this category = None # subclass must specify this + demonstrative = None # subclass must specify this uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True) # item_type = models.CharField(_("类型"), choices=ItemType.choices, blank=False, max_length=50) title = models.CharField(_("title in primary language"), max_length=1000, default="") @@ -218,6 +233,10 @@ class Item(PolymorphicModel, SoftDeleteMixin): def url(self): return f'/{self.url_path}/{self.url_id}' + @property + def class_name(self): + return self.__class__.__name__.lower() + @classmethod def get_by_url(cls, url_or_b62): b62 = url_or_b62.split('/')[-1] @@ -293,7 +312,7 @@ class ExternalResource(models.Model): @property def site_name(self): - return self.id_type # TODO change to localized name + return self.get_site().SITE_NAME def update_content(self, resource_content): self.other_lookup_ids = resource_content.lookup_ids diff --git a/catalog/common/sites.py b/catalog/common/sites.py index d23db01e..d873b120 100644 --- a/catalog/common/sites.py +++ b/catalog/common/sites.py @@ -1,5 +1,5 @@ """ -Site and SiteList +Site and SiteManager Site should inherite from AbstractSite a Site should map to a unique set of url patterns. @@ -28,6 +28,7 @@ class AbstractSite: """ Abstract class to represent a site """ + SITE_NAME = None ID_TYPE = None WIKI_PROPERTY_ID = 'P0undefined0' DEFAULT_MODEL = None @@ -74,7 +75,11 @@ class AbstractSite: def get_item(self): p = self.get_resource() if not p: - raise ValueError(f'resource not available for {self.url}') + # raise ValueError(f'resource not available for {self.url}') + return None + if not p.ready: + # raise ValueError(f'resource not ready for {self.url}') + return None model = p.get_preferred_model() if not model: model = self.DEFAULT_MODEL @@ -93,7 +98,7 @@ class AbstractSite: return bool(self.resource and self.resource.ready) def get_resource_ready(self, auto_save=True, auto_create=True, auto_link=True, data_from_link=None): - """return a resource scraped, or scrape if not yet""" + """return a resource scraped, or scrape if not yet""" if auto_link: auto_create = True if auto_create: @@ -119,7 +124,7 @@ class AbstractSite: p.item.save() if auto_link: for linked_resources in p.required_resources: - linked_site = SiteList.get_site_by_url(linked_resources['url']) + linked_site = SiteManager.get_site_by_url(linked_resources['url']) if linked_site: linked_site.get_resource_ready(auto_link=False) else: @@ -129,7 +134,7 @@ class AbstractSite: return p -class SiteList: +class SiteManager: registry = {} @classmethod @@ -153,3 +158,11 @@ class SiteList: def get_id_by_url(cls, url: str): site = cls.get_site_by_url(url) return site.url_to_id(url) if site else None + + @staticmethod + def get_site_by_resource(resource): + return SiteManager.get_site_by_id_type(resource.id_type) + + +ExternalResource.get_site = lambda resource: SiteManager.get_site_by_id_type(resource.id_type) +# ExternalResource.get_site = SiteManager.get_site_by_resource diff --git a/catalog/game/tests.py b/catalog/game/tests.py index cf7437d6..bef6d4cf 100644 --- a/catalog/game/tests.py +++ b/catalog/game/tests.py @@ -8,17 +8,17 @@ class IGDBTestCase(TestCase): t_id_type = IdType.IGDB t_id_value = 'portal-2' t_url = 'https://www.igdb.com/games/portal-2' - site = SiteList.get_site_by_id_type(t_id_type) + site = SiteManager.get_site_by_id_type(t_id_type) self.assertIsNotNone(site) self.assertEqual(site.validate_url(t_url), True) - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.url, t_url) self.assertEqual(site.id_value, t_id_value) @use_local_response def test_scrape(self): t_url = 'https://www.igdb.com/games/portal-2' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) site.get_resource_ready() self.assertEqual(site.ready, True) @@ -29,7 +29,7 @@ class IGDBTestCase(TestCase): @use_local_response def test_scrape_non_steam(self): t_url = 'https://www.igdb.com/games/the-legend-of-zelda-breath-of-the-wild' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) site.get_resource_ready() self.assertEqual(site.ready, True) @@ -45,17 +45,17 @@ class SteamTestCase(TestCase): t_id_value = '620' t_url = 'https://store.steampowered.com/app/620/Portal_2/' t_url2 = 'https://store.steampowered.com/app/620' - site = SiteList.get_site_by_id_type(t_id_type) + site = SiteManager.get_site_by_id_type(t_id_type) self.assertIsNotNone(site) self.assertEqual(site.validate_url(t_url), True) - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.url, t_url2) self.assertEqual(site.id_value, t_id_value) @use_local_response def test_scrape(self): t_url = 'https://store.steampowered.com/app/620/Portal_2/' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) site.get_resource_ready() self.assertEqual(site.ready, True) @@ -70,17 +70,17 @@ class DoubanGameTestCase(TestCase): t_id_type = IdType.DoubanGame t_id_value = '10734307' t_url = 'https://www.douban.com/game/10734307/' - site = SiteList.get_site_by_id_type(t_id_type) + site = SiteManager.get_site_by_id_type(t_id_type) self.assertIsNotNone(site) self.assertEqual(site.validate_url(t_url), True) - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.url, t_url) self.assertEqual(site.id_value, t_id_value) @use_local_response def test_scrape(self): t_url = 'https://www.douban.com/game/10734307/' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) site.get_resource_ready() self.assertEqual(site.ready, True) @@ -94,10 +94,10 @@ class BangumiGameTestCase(TestCase): t_id_type = IdType.Bangumi t_id_value = '15912' t_url = 'https://bgm.tv/subject/15912' - site = SiteList.get_site_by_id_type(t_id_type) + site = SiteManager.get_site_by_id_type(t_id_type) self.assertIsNotNone(site) self.assertEqual(site.validate_url(t_url), True) - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.url, t_url) self.assertEqual(site.id_value, t_id_value) @@ -112,6 +112,6 @@ class MultiGameSitesTestCase(TestCase): def test_games(self): url1 = 'https://www.igdb.com/games/portal-2' url2 = 'https://store.steampowered.com/app/620/Portal_2/' - p1 = SiteList.get_site_by_url(url1).get_resource_ready() - p2 = SiteList.get_site_by_url(url2).get_resource_ready() + p1 = SiteManager.get_site_by_url(url1).get_resource_ready() + p2 = SiteManager.get_site_by_url(url2).get_resource_ready() self.assertEqual(p1.item.id, p2.item.id) diff --git a/catalog/management/commands/cat.py b/catalog/management/commands/cat.py index 9d714693..f7e162f0 100644 --- a/catalog/management/commands/cat.py +++ b/catalog/management/commands/cat.py @@ -1,6 +1,6 @@ from django.core.management.base import BaseCommand import pprint -from catalog.common import SiteList +from catalog.common import SiteManager from catalog.sites import * @@ -17,7 +17,7 @@ class Command(BaseCommand): def handle(self, *args, **options): url = str(options['url']) - site = SiteList.get_site_by_url(url) + site = SiteManager.get_site_by_url(url) if site is None: self.stdout.write(self.style.ERROR(f'Unknown site for {url}')) return diff --git a/catalog/movie/models.py b/catalog/movie/models.py index 92762de4..af180e24 100644 --- a/catalog/movie/models.py +++ b/catalog/movie/models.py @@ -1,4 +1,6 @@ from catalog.common import * +from django.utils.translation import gettext_lazy as _ +from django.db import models class Movie(Item): @@ -8,3 +10,40 @@ class Movie(Item): tmdb_movie = PrimaryLookupIdDescriptor(IdType.TMDB_Movie) douban_movie = PrimaryLookupIdDescriptor(IdType.DoubanMovie) duration = jsondata.IntegerField(blank=True, default=None) + + METADATA_COPY_LIST = [ + 'title', + 'orig_title', + 'other_title', + 'imdb_code', + 'director', + 'playwright', + 'actor', + 'genre', + 'showtime', + 'site', + 'area', + 'language', + 'year', + 'duration', + 'season_number', + 'episodes', + 'single_episode_length', + 'brief', + ] + orig_title = jsondata.CharField(_("original title"), blank=True, default='', max_length=500) + other_title = jsondata.ArrayField(models.CharField(_("other title"), blank=True, default='', max_length=500), null=True, blank=True, default=list, ) + imdb_code = jsondata.CharField(blank=True, max_length=10, null=False, db_index=True, default='') + director = jsondata.ArrayField(models.CharField(_("director"), blank=True, default='', max_length=200), null=True, blank=True, default=list, ) + playwright = jsondata.ArrayField(models.CharField(_("playwright"), blank=True, default='', max_length=200), null=True, blank=True, default=list, ) + actor = jsondata.ArrayField(models.CharField(_("actor"), blank=True, default='', max_length=200), null=True, blank=True, default=list, ) + genre = jsondata.ArrayField(models.CharField(_("genre"), blank=True, default='', max_length=50), null=True, blank=True, default=list, ) # , choices=MovieGenreEnum.choices + showtime = jsondata.ArrayField(null=True, blank=True, default=list, ) + site = jsondata.URLField(_('site url'), blank=True, default='', max_length=200) + area = jsondata.ArrayField(models.CharField(_("country or region"), blank=True, default='', max_length=100, ), null=True, blank=True, default=list, ) + language = jsondata.ArrayField(models.CharField(blank=True, default='', max_length=100, ), null=True, blank=True, default=list, ) + year = jsondata.IntegerField(null=True, blank=True) + season_number = jsondata.IntegerField(null=True, blank=True) + episodes = jsondata.IntegerField(null=True, blank=True) + single_episode_length = jsondata.IntegerField(null=True, blank=True) + duration = jsondata.CharField(blank=True, default='', max_length=200) diff --git a/catalog/movie/tests.py b/catalog/movie/tests.py index b3deacce..44ab58c1 100644 --- a/catalog/movie/tests.py +++ b/catalog/movie/tests.py @@ -6,17 +6,17 @@ class DoubanMovieTestCase(TestCase): def test_parse(self): t_id = '3541415' t_url = 'https://movie.douban.com/subject/3541415/' - p1 = SiteList.get_site_by_id_type(IdType.DoubanMovie) + p1 = SiteManager.get_site_by_id_type(IdType.DoubanMovie) self.assertIsNotNone(p1) self.assertEqual(p1.validate_url(t_url), True) - p2 = SiteList.get_site_by_url(t_url) + p2 = SiteManager.get_site_by_url(t_url) self.assertEqual(p1.id_to_url(t_id), t_url) self.assertEqual(p2.url_to_id(t_url), t_id) @use_local_response def test_scrape(self): t_url = 'https://movie.douban.com/subject/3541415/' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) self.assertEqual(site.id_value, '3541415') site.get_resource_ready() @@ -31,18 +31,18 @@ class TMDBMovieTestCase(TestCase): t_id = '293767' t_url = 'https://www.themoviedb.org/movie/293767-billy-lynn-s-long-halftime-walk' t_url2 = 'https://www.themoviedb.org/movie/293767' - p1 = SiteList.get_site_by_id_type(IdType.TMDB_Movie) + p1 = SiteManager.get_site_by_id_type(IdType.TMDB_Movie) self.assertIsNotNone(p1) self.assertEqual(p1.validate_url(t_url), True) self.assertEqual(p1.validate_url(t_url2), True) - p2 = SiteList.get_site_by_url(t_url) + p2 = SiteManager.get_site_by_url(t_url) self.assertEqual(p1.id_to_url(t_id), t_url2) self.assertEqual(p2.url_to_id(t_url), t_id) @use_local_response def test_scrape(self): t_url = 'https://www.themoviedb.org/movie/293767' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) self.assertEqual(site.id_value, '293767') site.get_resource_ready() @@ -57,18 +57,18 @@ class IMDBMovieTestCase(TestCase): t_id = 'tt1375666' t_url = 'https://www.imdb.com/title/tt1375666/' t_url2 = 'https://www.imdb.com/title/tt1375666/' - p1 = SiteList.get_site_by_id_type(IdType.IMDB) + p1 = SiteManager.get_site_by_id_type(IdType.IMDB) self.assertIsNotNone(p1) self.assertEqual(p1.validate_url(t_url), True) self.assertEqual(p1.validate_url(t_url2), True) - p2 = SiteList.get_site_by_url(t_url) + p2 = SiteManager.get_site_by_url(t_url) self.assertEqual(p1.id_to_url(t_id), t_url2) self.assertEqual(p2.url_to_id(t_url), t_id) @use_local_response def test_scrape(self): t_url = 'https://www.imdb.com/title/tt1375666/' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) self.assertEqual(site.id_value, 'tt1375666') site.get_resource_ready() @@ -83,8 +83,8 @@ class MultiMovieSitesTestCase(TestCase): url1 = 'https://www.themoviedb.org/movie/27205-inception' url2 = 'https://movie.douban.com/subject/3541415/' url3 = 'https://www.imdb.com/title/tt1375666/' - p1 = SiteList.get_site_by_url(url1).get_resource_ready() - p2 = SiteList.get_site_by_url(url2).get_resource_ready() - p3 = SiteList.get_site_by_url(url3).get_resource_ready() + p1 = SiteManager.get_site_by_url(url1).get_resource_ready() + p2 = SiteManager.get_site_by_url(url2).get_resource_ready() + p3 = SiteManager.get_site_by_url(url3).get_resource_ready() self.assertEqual(p1.item.id, p2.item.id) self.assertEqual(p2.item.id, p3.item.id) diff --git a/catalog/music/tests.py b/catalog/music/tests.py index d035382d..a171acb7 100644 --- a/catalog/music/tests.py +++ b/catalog/music/tests.py @@ -8,17 +8,17 @@ class SpotifyTestCase(TestCase): t_id_type = IdType.Spotify_Album t_id_value = '65KwtzkJXw7oT819NFWmEP' t_url = 'https://open.spotify.com/album/65KwtzkJXw7oT819NFWmEP' - site = SiteList.get_site_by_id_type(t_id_type) + site = SiteManager.get_site_by_id_type(t_id_type) self.assertIsNotNone(site) self.assertEqual(site.validate_url(t_url), True) - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.url, t_url) self.assertEqual(site.id_value, t_id_value) @use_local_response def test_scrape(self): t_url = 'https://open.spotify.com/album/65KwtzkJXw7oT819NFWmEP' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) site.get_resource_ready() self.assertEqual(site.ready, True) @@ -32,17 +32,17 @@ class DoubanMusicTestCase(TestCase): t_id_type = IdType.DoubanMusic t_id_value = '33551231' t_url = 'https://music.douban.com/subject/33551231/' - site = SiteList.get_site_by_id_type(t_id_type) + site = SiteManager.get_site_by_id_type(t_id_type) self.assertIsNotNone(site) self.assertEqual(site.validate_url(t_url), True) - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.url, t_url) self.assertEqual(site.id_value, t_id_value) @use_local_response def test_scrape(self): t_url = 'https://music.douban.com/subject/33551231/' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) site.get_resource_ready() self.assertEqual(site.ready, True) @@ -56,6 +56,6 @@ class MultiMusicSitesTestCase(TestCase): def test_albums(self): url1 = 'https://music.douban.com/subject/33551231/' url2 = 'https://open.spotify.com/album/65KwtzkJXw7oT819NFWmEP' - p1 = SiteList.get_site_by_url(url1).get_resource_ready() - p2 = SiteList.get_site_by_url(url2).get_resource_ready() + p1 = SiteManager.get_site_by_url(url1).get_resource_ready() + p2 = SiteManager.get_site_by_url(url2).get_resource_ready() self.assertEqual(p1.item.id, p2.item.id) diff --git a/catalog/performance/tests.py b/catalog/performance/tests.py index 9d3302ea..8e765743 100644 --- a/catalog/performance/tests.py +++ b/catalog/performance/tests.py @@ -9,9 +9,9 @@ class DoubanDramaTestCase(TestCase): def test_parse(self): t_id = '24849279' t_url = 'https://www.douban.com/location/drama/24849279/' - p1 = SiteList.get_site_by_id_type(IdType.DoubanDrama) + p1 = SiteManager.get_site_by_id_type(IdType.DoubanDrama) self.assertIsNotNone(p1) - p1 = SiteList.get_site_by_url(t_url) + p1 = SiteManager.get_site_by_url(t_url) self.assertIsNotNone(p1) self.assertEqual(p1.validate_url(t_url), True) self.assertEqual(p1.id_to_url(t_id), t_url) @@ -20,7 +20,7 @@ class DoubanDramaTestCase(TestCase): @use_local_response def test_scrape(self): t_url = 'https://www.douban.com/location/drama/24849279/' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) resource = site.get_resource_ready() self.assertEqual(site.ready, True) diff --git a/catalog/podcast/tests.py b/catalog/podcast/tests.py index 0e70b3e1..615b8925 100644 --- a/catalog/podcast/tests.py +++ b/catalog/podcast/tests.py @@ -11,17 +11,17 @@ class ApplePodcastTestCase(TestCase): t_id = '657765158' t_url = 'https://podcasts.apple.com/us/podcast/%E5%A4%A7%E5%86%85%E5%AF%86%E8%B0%88/id657765158' t_url2 = 'https://podcasts.apple.com/us/podcast/id657765158' - p1 = SiteList.get_site_by_id_type(IdType.ApplePodcast) + p1 = SiteManager.get_site_by_id_type(IdType.ApplePodcast) self.assertIsNotNone(p1) self.assertEqual(p1.validate_url(t_url), True) - p2 = SiteList.get_site_by_url(t_url) + p2 = SiteManager.get_site_by_url(t_url) self.assertEqual(p1.id_to_url(t_id), t_url2) self.assertEqual(p2.url_to_id(t_url), t_id) @use_local_response def test_scrape(self): t_url = 'https://podcasts.apple.com/gb/podcast/the-new-yorker-radio-hour/id1050430296' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) self.assertEqual(site.id_value, '1050430296') site.get_resource_ready() diff --git a/catalog/sites/__init__.py b/catalog/sites/__init__.py index 5f103943..fbce5de5 100644 --- a/catalog/sites/__init__.py +++ b/catalog/sites/__init__.py @@ -1,4 +1,4 @@ -from ..common.sites import SiteList +from ..common.sites import SiteManager from .apple_podcast import ApplePodcast from .douban_book import DoubanBook from .douban_movie import DoubanMovie diff --git a/catalog/sites/apple_podcast.py b/catalog/sites/apple_podcast.py index ae2b3b54..17a3917a 100644 --- a/catalog/sites/apple_podcast.py +++ b/catalog/sites/apple_podcast.py @@ -6,7 +6,7 @@ import logging _logger = logging.getLogger(__name__) -@SiteList.register +@SiteManager.register class ApplePodcast(AbstractSite): ID_TYPE = IdType.ApplePodcast URL_PATTERNS = [r"https://[^.]+.apple.com/\w+/podcast/*[^/?]*/id(\d+)"] diff --git a/catalog/sites/bangumi.py b/catalog/sites/bangumi.py index 21875536..607c6398 100644 --- a/catalog/sites/bangumi.py +++ b/catalog/sites/bangumi.py @@ -6,7 +6,7 @@ import logging _logger = logging.getLogger(__name__) -@SiteList.register +@SiteManager.register class Bangumi(AbstractSite): ID_TYPE = IdType.Bangumi URL_PATTERNS = [ diff --git a/catalog/sites/douban_book.py b/catalog/sites/douban_book.py index 19aeecaf..9313130c 100644 --- a/catalog/sites/douban_book.py +++ b/catalog/sites/douban_book.py @@ -8,8 +8,9 @@ import logging _logger = logging.getLogger(__name__) -@SiteList.register +@SiteManager.register class DoubanBook(AbstractSite): + SITE_NAME = SiteName.Douban ID_TYPE = IdType.DoubanBook URL_PATTERNS = [r"\w+://book\.douban\.com/subject/(\d+)/{0,1}", r"\w+://m.douban.com/book/subject/(\d+)/{0,1}"] WIKI_PROPERTY_ID = '?' @@ -181,7 +182,7 @@ class DoubanBook(AbstractSite): return pd -@SiteList.register +@SiteManager.register class DoubanBook_Work(AbstractSite): ID_TYPE = IdType.DoubanBook_Work URL_PATTERNS = [r"\w+://book\.douban\.com/works/(\d+)"] diff --git a/catalog/sites/douban_drama.py b/catalog/sites/douban_drama.py index 4a0c27b7..812c8c7f 100644 --- a/catalog/sites/douban_drama.py +++ b/catalog/sites/douban_drama.py @@ -7,7 +7,7 @@ import logging _logger = logging.getLogger(__name__) -@SiteList.register +@SiteManager.register class DoubanDrama(AbstractSite): ID_TYPE = IdType.DoubanDrama URL_PATTERNS = [r"\w+://www.douban.com/location/drama/(\d+)/"] diff --git a/catalog/sites/douban_game.py b/catalog/sites/douban_game.py index 4c50a42e..0b8d247e 100644 --- a/catalog/sites/douban_game.py +++ b/catalog/sites/douban_game.py @@ -8,7 +8,7 @@ import logging _logger = logging.getLogger(__name__) -@SiteList.register +@SiteManager.register class DoubanGame(AbstractSite): ID_TYPE = IdType.DoubanGame URL_PATTERNS = [r"\w+://www\.douban\.com/game/(\d+)/{0,1}", r"\w+://m.douban.com/game/subject/(\d+)/{0,1}"] diff --git a/catalog/sites/douban_movie.py b/catalog/sites/douban_movie.py index d2a971c5..eb2a1878 100644 --- a/catalog/sites/douban_movie.py +++ b/catalog/sites/douban_movie.py @@ -54,7 +54,7 @@ class MovieGenreEnum(models.TextChoices): # MovieGenreTranslator = ChoicesDictGenerator(MovieGenreEnum) -@SiteList.register +@SiteManager.register class DoubanMovie(AbstractSite): ID_TYPE = IdType.DoubanMovie URL_PATTERNS = [r"\w+://movie\.douban\.com/subject/(\d+)/{0,1}", r"\w+://m.douban.com/movie/subject/(\d+)/{0,1}"] diff --git a/catalog/sites/douban_music.py b/catalog/sites/douban_music.py index 1aa157f2..3ae28b48 100644 --- a/catalog/sites/douban_music.py +++ b/catalog/sites/douban_music.py @@ -8,7 +8,7 @@ import logging _logger = logging.getLogger(__name__) -@SiteList.register +@SiteManager.register class DoubanMusic(AbstractSite): ID_TYPE = IdType.DoubanMusic URL_PATTERNS = [r"\w+://music\.douban\.com/subject/(\d+)/{0,1}", r"\w+://m.douban.com/music/subject/(\d+)/{0,1}"] diff --git a/catalog/sites/goodreads.py b/catalog/sites/goodreads.py index be3d4c26..42c54ce1 100644 --- a/catalog/sites/goodreads.py +++ b/catalog/sites/goodreads.py @@ -23,8 +23,9 @@ class GoodreadsDownloader(RetryDownloader): return RESPONSE_INVALID_CONTENT -@SiteList.register +@SiteManager.register class Goodreads(AbstractSite): + SITE_NAME = SiteName.Goodreads ID_TYPE = IdType.Goodreads WIKI_PROPERTY_ID = 'P2968' DEFAULT_MODEL = Edition @@ -87,8 +88,9 @@ class Goodreads(AbstractSite): return pd -@SiteList.register +@SiteManager.register class Goodreads_Work(AbstractSite): + SITE_NAME = SiteName.Goodreads ID_TYPE = IdType.Goodreads_Work WIKI_PROPERTY_ID = '' DEFAULT_MODEL = Work diff --git a/catalog/sites/google_books.py b/catalog/sites/google_books.py index 0554d3d8..806056a6 100644 --- a/catalog/sites/google_books.py +++ b/catalog/sites/google_books.py @@ -7,10 +7,11 @@ import logging _logger = logging.getLogger(__name__) -@SiteList.register +@SiteManager.register class GoogleBooks(AbstractSite): + SITE_NAME = SiteName.GoogleBooks ID_TYPE = IdType.GoogleBooks - URL_PATTERNS = [ + URL_PATTERNS = [ r"https://books\.google\.co[^/]+/books\?id=([^&#]+)", r"https://www\.google\.co[^/]+/books/edition/[^/]+/([^&#?]+)", r"https://books\.google\.co[^/]+/books/about/[^?]+?id=([^&#?]+)", diff --git a/catalog/sites/igdb.py b/catalog/sites/igdb.py index fc4eaa35..bd90962d 100644 --- a/catalog/sites/igdb.py +++ b/catalog/sites/igdb.py @@ -37,7 +37,7 @@ def search_igdb_by_3p_url(steam_url): return IGDB(url=r[0]['game']['url']) -@SiteList.register +@SiteManager.register class IGDB(AbstractSite): ID_TYPE = IdType.IGDB URL_PATTERNS = [r"\w+://www\.igdb\.com/games/([a-zA-Z0-9\-_]+)"] @@ -102,7 +102,7 @@ class IGDB(AbstractSite): 'cover_image_url': 'https:' + r['cover']['url'].replace('t_thumb', 't_cover_big'), }) if steam_url: - pd.lookup_ids[IdType.Steam] = SiteList.get_site_by_id_type(IdType.Steam).url_to_id(steam_url) + pd.lookup_ids[IdType.Steam] = SiteManager.get_site_by_id_type(IdType.Steam).url_to_id(steam_url) if pd.metadata["cover_image_url"]: imgdl = BasicImageDownloader(pd.metadata["cover_image_url"], self.url) try: diff --git a/catalog/sites/imdb.py b/catalog/sites/imdb.py index a6064afd..42a20f75 100644 --- a/catalog/sites/imdb.py +++ b/catalog/sites/imdb.py @@ -8,7 +8,7 @@ import logging _logger = logging.getLogger(__name__) -@SiteList.register +@SiteManager.register class IMDB(AbstractSite): ID_TYPE = IdType.IMDB URL_PATTERNS = [r'\w+://www.imdb.com/title/(tt\d+)'] @@ -42,7 +42,7 @@ class IMDB(AbstractSite): raise ParseError(self, "IMDB id matching TMDB but not first episode, this is not supported") else: raise ParseError(self, "IMDB id not found in TMDB") - tmdb = SiteList.get_site_by_url(url) + tmdb = SiteManager.get_site_by_url(url) pd = tmdb.scrape() pd.metadata['preferred_model'] = tmdb.DEFAULT_MODEL.__name__ return pd diff --git a/catalog/sites/spotify.py b/catalog/sites/spotify.py index c29c32dd..0d38f08d 100644 --- a/catalog/sites/spotify.py +++ b/catalog/sites/spotify.py @@ -19,7 +19,7 @@ spotify_token = None spotify_token_expire_time = time.time() -@SiteList.register +@SiteManager.register class Spotify(AbstractSite): ID_TYPE = IdType.Spotify_Album URL_PATTERNS = [r'\w+://open\.spotify\.com/album/([a-zA-Z0-9]+)'] diff --git a/catalog/sites/steam.py b/catalog/sites/steam.py index c80bc769..b5c42227 100644 --- a/catalog/sites/steam.py +++ b/catalog/sites/steam.py @@ -8,7 +8,7 @@ import logging _logger = logging.getLogger(__name__) -@SiteList.register +@SiteManager.register class Steam(AbstractSite): ID_TYPE = IdType.Steam URL_PATTERNS = [r"\w+://store\.steampowered\.com/app/(\d+)"] diff --git a/catalog/sites/tmdb.py b/catalog/sites/tmdb.py index f0c65c90..3cfa87c0 100644 --- a/catalog/sites/tmdb.py +++ b/catalog/sites/tmdb.py @@ -58,7 +58,7 @@ genre_map = { } -@SiteList.register +@SiteManager.register class TMDB_Movie(AbstractSite): ID_TYPE = IdType.TMDB_Movie URL_PATTERNS = [r'\w+://www.themoviedb.org/movie/(\d+)'] @@ -159,7 +159,7 @@ class TMDB_Movie(AbstractSite): return pd -@SiteList.register +@SiteManager.register class TMDB_TV(AbstractSite): ID_TYPE = IdType.TMDB_TV URL_PATTERNS = [r'\w+://www.themoviedb.org/tv/(\d+)[^/]*$', r'\w+://www.themoviedb.org/tv/(\d+)[^/]*/seasons'] @@ -268,7 +268,7 @@ class TMDB_TV(AbstractSite): return pd -@SiteList.register +@SiteManager.register class TMDB_TVSeason(AbstractSite): ID_TYPE = IdType.TMDB_TVSeason URL_PATTERNS = [r'\w+://www.themoviedb.org/tv/(\d+)[^/]*/season/(\d+)[^/]*$'] diff --git a/catalog/templates/album.html b/catalog/templates/album.html new file mode 100644 index 00000000..16df8f0e --- /dev/null +++ b/catalog/templates/album.html @@ -0,0 +1,19 @@ +{% extends "item_base.html" %} +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load strip_scheme %} +{% load thumb %} + + +{% block details %} +{% endblock %} + + +{% block sidebar %} +{% endblock %} diff --git a/catalog/templates/edition.html b/catalog/templates/edition.html new file mode 100644 index 00000000..22ad05b5 --- /dev/null +++ b/catalog/templates/edition.html @@ -0,0 +1,98 @@ +{% extends "item_base.html" %} +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load strip_scheme %} +{% load thumb %} + +{% block details %} +
+
+ {% if item.rating %} + + {{ item.rating }} + ({{ item.rating_count }}人评分) + {% else %} + {% trans '评分:评分人数不足' %} + {% endif %} +
+
{% if item.isbn %}{% trans 'ISBN:' %}{{ item.isbn }}{% endif %}
+
{% if item.authors %}{% trans '作者:' %} + {% for author in item.authors %} + {{ author }}{% if not forloop.last %} / {% endif %} + {% endfor %} + {% endif %}
+
{% if item.pub_house %}{% trans '出版社:' %}{{ item.pub_house }}{% endif %}
+
{% if item.subtitle %}{% trans '副标题:' %}{{ item.subtitle }}{% endif %}
+
{% if item.translator %}{% trans '译者:' %} + {% for translator in item.translator %} + {{ translator }}{% if not forloop.last %} / {% endif %} + {% endfor %} + {% endif %}
+
{% if item.orig_title %}{% trans '原作名:' %}{{ item.orig_title }}{% endif %}
+
{% if item.language %}{% trans '语言:' %}{{ item.language }}{% endif %}
+
{%if item.pub_year %}{% trans '出版时间:' %}{{ item.pub_year }}{% trans '年' %}{% if item.pub_month %}{{ item.pub_month }}{% trans '月' %}{% endif %}{% endif %}
+
+
+ +
{% if item.binding %}{% trans '装帧:' %}{{ item.binding }}{% endif %}
+
{% if item.price %}{% trans '定价:' %}{{ item.price }}{% endif %}
+
{% if item.pages %}{% trans '页数:' %}{{ item.pages }}{% endif %}
+ {% if item.other_info %} + {% for k, v in item.other_info.items %} +
+ {{ k }}:{{ v | urlize }} +
+ {% endfor %} + {% endif %} + + + {% if item.last_editor and item.last_editor.preference.show_last_edit or user.is_staff %} +
{% trans '最近编辑者:' %}{{ item.last_editor | default:"" }}
+ {% endif %} + +
+ {% trans '编辑这本书' %} + {% if user.is_staff %} + / {% trans '删除' %} + {% endif %} +
+
+{% endblock %} + + +{% block sidebar %} +{% if item.get_related_books.count > 0 %} +
+
+
{% trans '相关书目' %}
+
+ {% for b in item.get_related_books %} +

+ {{ b.title }} + ({{ b.pub_house }} {{ b.pub_year }}) + {{ b.get_source_site_display }} +

+ {% endfor %} +
+
+
+{% endif %} + +{% if item.isbn %} +
+
+
{% trans '借阅或购买' %}
+ +
+
+{% endif %} +{% endblock %} diff --git a/catalog/templates/game.html b/catalog/templates/game.html new file mode 100644 index 00000000..16df8f0e --- /dev/null +++ b/catalog/templates/game.html @@ -0,0 +1,19 @@ +{% extends "item_base.html" %} +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load strip_scheme %} +{% load thumb %} + + +{% block details %} +{% endblock %} + + +{% block sidebar %} +{% endblock %} diff --git a/catalog/templates/item.html b/catalog/templates/item.html new file mode 100644 index 00000000..16df8f0e --- /dev/null +++ b/catalog/templates/item.html @@ -0,0 +1,19 @@ +{% extends "item_base.html" %} +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load strip_scheme %} +{% load thumb %} + + +{% block details %} +{% endblock %} + + +{% block sidebar %} +{% endblock %} diff --git a/catalog/templates/item_base.html b/catalog/templates/item_base.html new file mode 100644 index 00000000..1d45d172 --- /dev/null +++ b/catalog/templates/item_base.html @@ -0,0 +1,353 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load strip_scheme %} +{% load thumb %} + + + + + + + + + + + + + + {% if item.author %} + + {% endif %} + {% if item.isbn %} + + {% endif %} + + {{ site_name }} - {% trans item.category.label %} | {{ item.title }} + + {% include "partial/_common_libs.html" with jquery=1 %} + + + + + + +
+
+ {% include "partial/_navbar.html" %} + +
+
+
+
+
+ + + {{ item.title }} + + +
+
+ {{ item.title }} + + {% for res in item.external_resources.all %} + + {{ res.site_name.label }} + + {% endfor %} +
+ + {% block details %} +
+
+ {% if item.rating %} + + {{ item.rating }} + ({{ item.rating_count }}人评分) + {% else %} + {% trans '评分:评分人数不足' %} + {% endif %} +
+
uid: {{item.url_id}}
+
class: {{item.class_name}}
+
category: {{item.category}}
+
id type: {{item.primary_id_type}}
+
id value: {{item.primary_id_value}
+
+ {% endblock %} + +
+ {% for tag in item.tags %} + + {{ tag }} + + {% endfor %} +
+
+
+
+ {% if item.brief %} +
+
{% trans '简介' %}
+ +

{{ item.brief | linebreaksbr }}

+ +
+ {% endif %} + + {% if item.contents %} +
+
{% trans '目录' %}
+

{{ item.contents | linebreaksbr }}

+ +
+ {% endif %} + +
+
{% trans '标记' %}
+ {% trans '全部标记' %} + 关注的人的标记 + {% include "partial/mark_list.html" with mark_list=mark_list current_item=book %} +
+
+
{% trans '评论' %}
+ {% if review_list_more %} + {% trans '全部评论' %} + {% endif %} + {% if review_list %} + + {% else %} +
{% trans '暂无评论' %}
+ {% endif %} +
+
+
+ +
+ {% block sidebar_review %} +
+ {% if mark.shelf_type %} +
+ {% if mark.rating %} + + {% endif %} + {% trans '我' %}{% trans mark.shelf_label %} + {% if mark.visibility > 0 %} + + {% endif %} + + {% trans '修改' %} +
+ {% csrf_token %} + {% trans '删除' %} +
+
+
+ +
{{ mark.created_time }}
+ + {% if mark.text %} +

{{ mark.text }}

+ {% endif %} +
+ + {% for tag in mark.tags %} + {{ tag }} + {% endfor %} + +
+
+ {% else %} +
+
{% trans '标记' %}{% trans item.demonstrative %}
+
+ + + +
+
+ {% endif %} +
+ +
+ {% if review %} +
+ + {% trans '我的评论' %} + {% if review.visibility > 0 %} + + {% endif %} + + + {% trans '编辑' %} + {% trans '删除' %} + + +
{{ review.edited_time }}
+ + + {{ review.title }} + +
+ {% else %} + +
+
{% trans '我的评论' %}
+ +
+ + {% endif %} +
+ {% endblock %} + + {% block sidebar %} + {% endblock %} + + {% block sidebar_collection %} + {% if collection_list %} +
+
+
{% trans '相关收藏单' %}
+
+ {% for c in collection_list %} +

+ {{ c.title }} +

+ {% endfor %} +
+ +
+
+
+
+ {% endif %} + {% endblock %} +
+
+
+
+ {% include "partial/_footer.html" %} +
+ +
+ + + +
+
+ + + + + + diff --git a/catalog/templates/movie.html b/catalog/templates/movie.html new file mode 100644 index 00000000..16df8f0e --- /dev/null +++ b/catalog/templates/movie.html @@ -0,0 +1,19 @@ +{% extends "item_base.html" %} +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load strip_scheme %} +{% load thumb %} + + +{% block details %} +{% endblock %} + + +{% block sidebar %} +{% endblock %} diff --git a/catalog/templates/performance.html b/catalog/templates/performance.html new file mode 100644 index 00000000..16df8f0e --- /dev/null +++ b/catalog/templates/performance.html @@ -0,0 +1,19 @@ +{% extends "item_base.html" %} +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load strip_scheme %} +{% load thumb %} + + +{% block details %} +{% endblock %} + + +{% block sidebar %} +{% endblock %} diff --git a/catalog/templates/podcast.html b/catalog/templates/podcast.html new file mode 100644 index 00000000..16df8f0e --- /dev/null +++ b/catalog/templates/podcast.html @@ -0,0 +1,19 @@ +{% extends "item_base.html" %} +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load strip_scheme %} +{% load thumb %} + + +{% block details %} +{% endblock %} + + +{% block sidebar %} +{% endblock %} diff --git a/catalog/templates/tvseason.html b/catalog/templates/tvseason.html new file mode 100644 index 00000000..16df8f0e --- /dev/null +++ b/catalog/templates/tvseason.html @@ -0,0 +1,19 @@ +{% extends "item_base.html" %} +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load strip_scheme %} +{% load thumb %} + + +{% block details %} +{% endblock %} + + +{% block sidebar %} +{% endblock %} diff --git a/catalog/templates/tvshow.html b/catalog/templates/tvshow.html new file mode 100644 index 00000000..16df8f0e --- /dev/null +++ b/catalog/templates/tvshow.html @@ -0,0 +1,19 @@ +{% extends "item_base.html" %} +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load strip_scheme %} +{% load thumb %} + + +{% block details %} +{% endblock %} + + +{% block sidebar %} +{% endblock %} diff --git a/catalog/templates/work.html b/catalog/templates/work.html new file mode 100644 index 00000000..16df8f0e --- /dev/null +++ b/catalog/templates/work.html @@ -0,0 +1,19 @@ +{% extends "item_base.html" %} +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load strip_scheme %} +{% load thumb %} + + +{% block details %} +{% endblock %} + + +{% block sidebar %} +{% endblock %} diff --git a/catalog/tv/tests.py b/catalog/tv/tests.py index a25c45aa..b43e0f98 100644 --- a/catalog/tv/tests.py +++ b/catalog/tv/tests.py @@ -9,22 +9,22 @@ class TMDBTVTestCase(TestCase): t_url = 'https://www.themoviedb.org/tv/57243-doctor-who' t_url1 = 'https://www.themoviedb.org/tv/57243-doctor-who/seasons' t_url2 = 'https://www.themoviedb.org/tv/57243' - p1 = SiteList.get_site_by_id_type(IdType.TMDB_TV) + p1 = SiteManager.get_site_by_id_type(IdType.TMDB_TV) self.assertIsNotNone(p1) self.assertEqual(p1.validate_url(t_url), True) self.assertEqual(p1.validate_url(t_url1), True) self.assertEqual(p1.validate_url(t_url2), True) - p2 = SiteList.get_site_by_url(t_url) + p2 = SiteManager.get_site_by_url(t_url) self.assertEqual(p1.id_to_url(t_id), t_url2) self.assertEqual(p2.url_to_id(t_url), t_id) wrong_url = 'https://www.themoviedb.org/tv/57243-doctor-who/season/13' - s1 = SiteList.get_site_by_url(wrong_url) + s1 = SiteManager.get_site_by_url(wrong_url) self.assertNotIsInstance(s1, TVShow) @use_local_response def test_scrape(self): t_url = 'https://www.themoviedb.org/tv/57243-doctor-who' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) self.assertEqual(site.id_value, '57243') site.get_resource_ready() @@ -40,18 +40,18 @@ class TMDBTVSeasonTestCase(TestCase): t_id = '57243-11' t_url = 'https://www.themoviedb.org/tv/57243-doctor-who/season/11' t_url_unique = 'https://www.themoviedb.org/tv/57243/season/11' - p1 = SiteList.get_site_by_id_type(IdType.TMDB_TVSeason) + p1 = SiteManager.get_site_by_id_type(IdType.TMDB_TVSeason) self.assertIsNotNone(p1) self.assertEqual(p1.validate_url(t_url), True) self.assertEqual(p1.validate_url(t_url_unique), True) - p2 = SiteList.get_site_by_url(t_url) + p2 = SiteManager.get_site_by_url(t_url) self.assertEqual(p1.id_to_url(t_id), t_url_unique) self.assertEqual(p2.url_to_id(t_url), t_id) @use_local_response def test_scrape(self): t_url = 'https://www.themoviedb.org/tv/57243-doctor-who/season/4' - site = SiteList.get_site_by_url(t_url) + site = SiteManager.get_site_by_url(t_url) self.assertEqual(site.ready, False) self.assertEqual(site.id_value, '57243-4') site.get_resource_ready() @@ -68,7 +68,7 @@ class DoubanMovieTVTestCase(TestCase): @use_local_response def test_scrape(self): url3 = 'https://movie.douban.com/subject/3627919/' - p3 = SiteList.get_site_by_url(url3).get_resource_ready() + p3 = SiteManager.get_site_by_url(url3).get_resource_ready() self.assertEqual(p3.item.__class__.__name__, 'TVSeason') self.assertIsNotNone(p3.item.show) self.assertEqual(p3.item.show.imdb, 'tt0436992') @@ -76,7 +76,7 @@ class DoubanMovieTVTestCase(TestCase): @use_local_response def test_scrape_singleseason(self): url3 = 'https://movie.douban.com/subject/26895436/' - p3 = SiteList.get_site_by_url(url3).get_resource_ready() + p3 = SiteManager.get_site_by_url(url3).get_resource_ready() self.assertEqual(p3.item.__class__.__name__, 'TVShow') @@ -86,9 +86,9 @@ class MultiTVSitesTestCase(TestCase): url1 = 'https://www.themoviedb.org/tv/57243-doctor-who' url2 = 'https://www.imdb.com/title/tt0436992/' # url3 = 'https://movie.douban.com/subject/3541415/' - p1 = SiteList.get_site_by_url(url1).get_resource_ready() - p2 = SiteList.get_site_by_url(url2).get_resource_ready() - # p3 = SiteList.get_site_by_url(url3).get_resource_ready() + p1 = SiteManager.get_site_by_url(url1).get_resource_ready() + p2 = SiteManager.get_site_by_url(url2).get_resource_ready() + # p3 = SiteManager.get_site_by_url(url3).get_resource_ready() self.assertEqual(p1.item.id, p2.item.id) # self.assertEqual(p2.item.id, p3.item.id) @@ -97,9 +97,9 @@ class MultiTVSitesTestCase(TestCase): url1 = 'https://www.themoviedb.org/tv/57243-doctor-who/season/4' url2 = 'https://www.imdb.com/title/tt1159991/' url3 = 'https://movie.douban.com/subject/3627919/' - p1 = SiteList.get_site_by_url(url1).get_resource_ready() - p2 = SiteList.get_site_by_url(url2).get_resource_ready() - p3 = SiteList.get_site_by_url(url3).get_resource_ready() + p1 = SiteManager.get_site_by_url(url1).get_resource_ready() + p2 = SiteManager.get_site_by_url(url2).get_resource_ready() + p3 = SiteManager.get_site_by_url(url3).get_resource_ready() self.assertEqual(p1.item.imdb, p2.item.imdb) self.assertEqual(p2.item.imdb, p3.item.imdb) self.assertEqual(p1.item.id, p2.item.id) @@ -109,8 +109,8 @@ class MultiTVSitesTestCase(TestCase): def test_miniseries(self): url1 = 'https://www.themoviedb.org/tv/86941-the-north-water' url3 = 'https://movie.douban.com/subject/26895436/' - p1 = SiteList.get_site_by_url(url1).get_resource_ready() - p3 = SiteList.get_site_by_url(url3).get_resource_ready() + p1 = SiteManager.get_site_by_url(url1).get_resource_ready() + p3 = SiteManager.get_site_by_url(url3).get_resource_ready() self.assertEqual(p3.item.__class__.__name__, 'TVShow') self.assertEqual(p1.item.id, p3.item.id) @@ -119,9 +119,9 @@ class MultiTVSitesTestCase(TestCase): url1 = 'https://www.themoviedb.org/movie/282758-doctor-who-the-runaway-bride' url2 = 'hhttps://www.imdb.com/title/tt0827573/' url3 = 'https://movie.douban.com/subject/4296866/' - p1 = SiteList.get_site_by_url(url1).get_resource_ready() - p2 = SiteList.get_site_by_url(url2).get_resource_ready() - p3 = SiteList.get_site_by_url(url3).get_resource_ready() + p1 = SiteManager.get_site_by_url(url1).get_resource_ready() + p2 = SiteManager.get_site_by_url(url2).get_resource_ready() + p3 = SiteManager.get_site_by_url(url3).get_resource_ready() self.assertEqual(p1.item.imdb, p2.item.imdb) self.assertEqual(p2.item.imdb, p3.item.imdb) self.assertEqual(p1.item.id, p2.item.id) diff --git a/catalog/urls.py b/catalog/urls.py index 6a2855b5..744074f1 100644 --- a/catalog/urls.py +++ b/catalog/urls.py @@ -1,6 +1,9 @@ -from django.urls import path +from django.urls import path, re_path from .api import api +from .views import * + urlpatterns = [ path("", api.urls), + re_path('book/(?P[A-Za-z0-9]{21,22})/', retrieve, name='retrieve'), ] diff --git a/catalog/views.py b/catalog/views.py index 91ea44a2..2a5df161 100644 --- a/catalog/views.py +++ b/catalog/views.py @@ -1,3 +1,117 @@ -from django.shortcuts import render +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 HttpResponseBadRequest, HttpResponseServerError, HttpResponse +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 +from mastodon import mastodon_request_included +from mastodon.models import MastodonApplication +from mastodon.api import share_mark, share_review +from common.utils import PageLinksGenerator +from common.views import PAGE_LINK_NUMBER, jump_or_scrape, go_relogin +from common.models import SourceSiteEnum +from .models import * +# from .forms import * +# from .forms import BookMarkStatusTranslator +from django.conf import settings +from collection.models import CollectionItem +from common.scraper import get_scraper_by_url, get_normalized_url +from django.utils.baseconv import base62 +from journal.models import Mark -# Create your views here. + +_logger = logging.getLogger(__name__) + + +def retrieve(request, uid): + if request.method == 'GET': + item = get_object_or_404(Edition, uid=base62.decode(uid)) + mark = None + review = None + mark_list = None + review_list = None + mark_list_more = None + review_list_more = None + collection_list = [] + mark_form = None + if request.user.is_authenticated: + mark = Mark(request.user, item) + review = mark.review + + # # retreive tags + # book_tag_list = book.book_tags.values('content').annotate( + # tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER] + + # # retrieve user mark and initialize mark form + # try: + # if request.user.is_authenticated: + # mark = BookMark.objects.get(owner=request.user, book=book) + # except ObjectDoesNotExist: + # mark = None + # if mark: + # mark_tags = mark.bookmark_tags.all() + # mark.get_status_display = BookMarkStatusTranslator(mark.status) + # mark_form = BookMarkForm(instance=mark, initial={ + # 'tags': mark_tags + # }) + # else: + # mark_form = BookMarkForm(initial={ + # 'book': book, + # 'visibility': request.user.get_preference().default_visibility if request.user.is_authenticated else 0, + # 'tags': mark_tags + # }) + + # # retrieve user review + # try: + # if request.user.is_authenticated: + # review = BookReview.objects.get(owner=request.user, book=book) + # except ObjectDoesNotExist: + # review = None + + # # retrieve other related reviews and marks + # if request.user.is_anonymous: + # # hide all marks and reviews for anonymous user + # mark_list = None + # review_list = None + # mark_list_more = None + # review_list_more = None + # else: + # mark_list = BookMark.get_available_for_identicals(book, request.user) + # review_list = BookReview.get_available_for_identicals(book, request.user) + # mark_list_more = True if len(mark_list) > MARK_NUMBER else False + # mark_list = mark_list[:MARK_NUMBER] + # for m in mark_list: + # m.get_status_display = BookMarkStatusTranslator(m.status) + # review_list_more = True if len( + # review_list) > REVIEW_NUMBER else False + # review_list = review_list[:REVIEW_NUMBER] + # all_collections = CollectionItem.objects.filter(book=book).annotate(num_marks=Count('collection__collection_marks')).order_by('-num_marks')[:20] + # collection_list = filter(lambda c: c.is_visible_to(request.user), map(lambda i: i.collection, all_collections)) + + # def strip_html_tags(text): + # import re + # regex = re.compile('<.*?>') + # return re.sub(regex, '', text) + + # for r in review_list: + # r.content = strip_html_tags(r.content) + + return render(request, item.class_name + '.html', { + 'item': item, + 'mark': mark, + 'review': review, + 'mark_form': mark_form, + 'mark_list': mark_list, + 'mark_list_more': mark_list_more, + 'review_list': review_list, + 'review_list_more': review_list_more, + 'collection_list': collection_list, + } + ) + else: + logger.warning('non-GET method at /book/') + return HttpResponseBadRequest() diff --git a/journal/models.py b/journal/models.py index 8aad9964..0ebdc6f5 100644 --- a/journal/models.py +++ b/journal/models.py @@ -2,7 +2,6 @@ from django.db import models from polymorphic.models import PolymorphicModel from users.models import User from catalog.common.models import Item, ItemCategory -from catalog.common.mixins import SoftDeleteMixin from .mixins import UserOwnedObjectMixin from catalog.collection.models import Collection as CatalogCollection from decimal import * @@ -24,51 +23,105 @@ class Piece(PolymorphicModel, UserOwnedObjectMixin): visibility = models.PositiveSmallIntegerField(default=0) # 0: Public / 1: Follower only / 2: Self only created_time = models.DateTimeField(auto_now_add=True) edited_time = models.DateTimeField(auto_now=True) - is_deleted = models.BooleanField(default=False, db_index=True) metadata = models.JSONField(default=dict) attached_to = models.ForeignKey(User, null=True, default=None, on_delete=models.SET_NULL, related_name="attached_with") -class Content(SoftDeleteMixin, Piece): +class Content(Piece): item = models.ForeignKey(Item, on_delete=models.PROTECT) def __str__(self): return f"{self.id}({self.item})" + class Meta: + abstract = True + class Note(Content): pass +class Comment(Content): + text = models.TextField(blank=False, null=False) + + @staticmethod + def comment_item_by_user(item, user, text, visibility=0): + comment = Comment.objects.filter(owner=user, item=item).first() + if text is None: + if comment is not None: + comment.delete() + comment = None + elif comment is None: + comment = Comment.objects.create(owner=user, item=item, text=text, visibility=visibility) + else: + comment.text = text + comment.visibility = visibility + comment.save() + return comment + + class Review(Content): - warning = models.BooleanField(default=False) - title = models.CharField(max_length=500, blank=False, null=True) + title = models.CharField(max_length=500, blank=False, null=False) body = MarkdownxField() - pass + + @staticmethod + def review_item_by_user(item, user, title, body, visibility=0): + # allow multiple reviews per item per user. + review = Review.objects.create(owner=user, item=item, title=title, body=body, visibility=visibility) + """ + review = Review.objects.filter(owner=user, item=item).first() + if title is None: + if review is not None: + review.delete() + review = None + elif review is None: + review = Review.objects.create(owner=user, item=item, title=title, body=body, visibility=visibility) + else: + review.title = title + review.body = body + review.visibility = visibility + review.save() + """ + return review class Rating(Content): - grade = models.IntegerField(default=0, validators=[MaxValueValidator(10), MinValueValidator(0)]) + grade = models.PositiveSmallIntegerField(default=0, validators=[MaxValueValidator(10), MinValueValidator(1)], null=True) - -class RatingManager: @staticmethod def get_rating_for_item(item): - stat = Rating.objects.filter(item=item).aggregate(average=Avg('grade'), count=Count('item')) - return math.ceil(stat['average']) if stat['count'] >= 5 else 0 + stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(average=Avg('grade'), count=Count('item')) + return math.ceil(stat['average']) if stat['count'] >= 5 else None @staticmethod def get_rating_count_for_item(item): - stat = Rating.objects.filter(item=item).aggregate(count=Count('item')) + stat = Rating.objects.filter(item=item, grade__isnull=False).aggregate(count=Count('item')) return stat['count'] + @staticmethod + def set_item_rating_by_user(item, rating_grade, user, visibility=0): + if rating_grade is not None and (rating_grade < 1 or rating_grade > 10): + raise ValueError(f'Invalid rating grade: {rating_grade}') + rating = Rating.objects.filter(owner=user, item=item).first() + if not rating: + rating = Rating.objects.create(owner=user, item=item, grade=rating_grade, visibility=visibility) + else: + rating.visibility = visibility + rating.grade = rating_grade + rating.save() -Item.rating = property(RatingManager.get_rating_for_item) -Item.rating_count = property(RatingManager.get_rating_count_for_item) + @staticmethod + def get_item_rating_by_user(item, user): + rating = Rating.objects.filter(owner=user, item=item).first() + return rating.grade if rating else None + + +Item.rating = property(Rating.get_rating_for_item) +Item.rating_count = property(Rating.get_rating_count_for_item) class Reply(Content): - reply_to_content = models.ForeignKey(Content, on_delete=models.PROTECT, related_name='replies') + reply_to_content = models.ForeignKey(Piece, on_delete=models.PROTECT, related_name='replies') title = models.CharField(max_length=500, null=True) body = MarkdownxField() pass @@ -219,12 +272,12 @@ class Shelf(List): return f'{self.id} {self.title}' @cached_property - def shelf_type_name(self): + def shelf_label(self): return next(iter([n[2] for n in iter(ShelfTypeNames) if n[0] == self.item_category and n[1] == self.shelf_type]), self.shelf_type) @cached_property def title(self): - q = _("{item_category} {shelf_type_name} list").format(shelf_type_name=self.shelf_type_name, item_category=self.item_category) + q = _("{item_category} {shelf_label} list").format(shelf_label=self.shelf_label, item_category=self.item_category) return _("{user}'s {shelf_name}").format(user=self.owner.mastodon_username, shelf_name=q) @@ -265,6 +318,10 @@ class ShelfManager: return None 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() + return member # ._shelf if member else None + def move_item(self, item, shelf_type, visibility=0, metadata=None): # shelf_type=None means remove from current shelf # metadata=None means no change @@ -364,6 +421,8 @@ class Tag(List): def cleanup_title(title): return title.strip().lower() + +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') @@ -382,6 +441,83 @@ class Tag(List): tag = Tag.objects.create(owner=user, title=title, visibility=default_visibility) tag.append_item(item) + @staticmethod + def get_manager_for_user(user): + return TagManager(user) -Item.tags = property(Tag.public_tags_for_item) -User.tags = property(Tag.all_tags_for_user) + def __init__(self, user): + self.owner = user + + def all_tags(self): + return TagManager.all_tags_for_user(self.owner) + + def add_item_tags(self, item, tags, visibility=0): + for tag in tags: + TagManager.add_tag_by_user(item, tag, self.owner, visibility) + + def get_item_tags(self, item): + return [m['_tag__title'] for m in TagMember.objects.filter(_tag__owner=self.owner, item=item).values('_tag__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') + + +class Mark: + """ this mimics previous mark behaviour """ + + def __init__(self, user, item): + self.owner = user + self.item = item + + @cached_property + def shelfmember(self): + return self.owner.shelf_manager.locate_item(self.item) + + @property + def id(self): + return self.item.id if self.shelfmember else None + + @property + def shelf_type(self): + return self.shelfmember._shelf.shelf_type if self.shelfmember else None + + @property + def shelf_label(self): + return self.shelfmember._shelf.shelf_label if self.shelfmember else None + + @property + def visibility(self): + return self.shelfmember.visibility if self.shelfmember else None + + @cached_property + def tags(self): + return self.owner.tag_manager.get_item_tags(self.item) + + @cached_property + def rating(self): + return Rating.get_item_rating_by_user(self.item, self.owner) + + @cached_property + def comment(self): + return Comment.objects.filter(owner=self.owner, item=self.item).first() + + @property + def text(self): + return self.comment.text if self.comment else None + + @cached_property + def review(self): + return Review.objects.filter(owner=self.owner, item=self.item).first() + + def update(self, shelf_type, comment_text, rating_grade, visibility): + if shelf_type != self.shelf_type or visibility != self.visibility: + self.owner.shelf_manager.move_item(self.item, shelf_type, visibility=visibility) + del self.shelfmember + if comment_text != self.text or visibility != self.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.set_item_rating_by_user(self.item, rating_grade, self.owner, visibility) + self.rating = rating_grade diff --git a/journal/tests.py b/journal/tests.py index 9f975968..08460112 100644 --- a/journal/tests.py +++ b/journal/tests.py @@ -81,19 +81,55 @@ class TagTest(TestCase): t1 = 'sci-fi' t2 = 'private' t3 = 'public' - Tag.add_tag_by_user(self.book1, t3, self.user2) - Tag.add_tag_by_user(self.book1, t1, self.user1) - Tag.add_tag_by_user(self.book1, t1, self.user2) - Tag.add_tag_by_user(self.book1, t2, self.user1, default_visibility=2) + 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) + TagManager.add_tag_by_user(self.book1, t2, self.user1, default_visibility=2) self.assertEqual(self.book1.tags, [t1, t3]) - Tag.add_tag_by_user(self.book1, t3, self.user1) - Tag.add_tag_by_user(self.book1, t3, self.user3) + TagManager.add_tag_by_user(self.book1, t3, self.user1) + TagManager.add_tag_by_user(self.book1, t3, self.user3) self.assertEqual(self.book1.tags, [t3, t1]) - Tag.add_tag_by_user(self.book1, t3, self.user3) - Tag.add_tag_by_user(self.book1, t3, self.user3) + TagManager.add_tag_by_user(self.book1, t3, self.user3) + TagManager.add_tag_by_user(self.book1, t3, self.user3) self.assertEqual(Tag.objects.count(), 6) - Tag.add_tag_by_user(self.book2, t1, self.user2) + TagManager.add_tag_by_user(self.book2, t1, self.user2) self.assertEqual(self.user2.tags, [t1, t3]) - Tag.add_tag_by_user(self.book2, t3, self.user2) - Tag.add_tag_by_user(self.movie1, t3, self.user2) + TagManager.add_tag_by_user(self.book2, t3, self.user2) + TagManager.add_tag_by_user(self.movie1, t3, self.user2) self.assertEqual(self.user2.tags, [t3, t1]) + + +class MarkTest(TestCase): + def setUp(self): + self.book1 = Edition.objects.create(title="Hyperion") + self.user1 = User.objects.create(mastodon_site="site", username="name") + self.user1.shelf_manager.initialize() + pass + + def test_mark(self): + mark = Mark(self.user1, self.book1) + self.assertEqual(mark.shelf_type, None) + self.assertEqual(mark.shelf_label, None) + self.assertEqual(mark.text, None) + self.assertEqual(mark.rating, None) + self.assertEqual(mark.visibility, None) + self.assertEqual(mark.review, None) + self.assertEqual(mark.tags, []) + mark.update(ShelfType.WISHED, 'a gentle comment', 9, 1) + + mark = Mark(self.user1, self.book1) + self.assertEqual(mark.shelf_type, ShelfType.WISHED) + 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') + mark = Mark(self.user1, self.book1) + self.assertEqual(mark.review, review) + + self.user1.tag_manager.add_item_tags(self.book1, [' Sci-Fi ', ' fic ']) + mark = Mark(self.user1, self.book1) + self.assertEqual(mark.tags, ['sci-fi', 'fic']) diff --git a/social/apps.py b/social/apps.py index 0567a2c3..8af48774 100644 --- a/social/apps.py +++ b/social/apps.py @@ -4,3 +4,10 @@ from django.apps import AppConfig class SocialConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'social' + + def ready(self): + # load key modules in proper order, make sure class inject and signal works as expected + from catalog import models as catalog_models + from catalog import sites as catalog_sites + from journal import models as journal_models + from social import models as social_models