new data model: /book/<uid>
This commit is contained in:
parent
486dd16e1f
commit
b2af6f3230
45 changed files with 1150 additions and 148 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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+)"]
|
||||
|
|
|
@ -6,7 +6,7 @@ import logging
|
|||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@SiteList.register
|
||||
@SiteManager.register
|
||||
class Bangumi(AbstractSite):
|
||||
ID_TYPE = IdType.Bangumi
|
||||
URL_PATTERNS = [
|
||||
|
|
|
@ -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+)"]
|
||||
|
|
|
@ -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+)/"]
|
||||
|
|
|
@ -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}"]
|
||||
|
|
|
@ -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}"]
|
||||
|
|
|
@ -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}"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=([^&#?]+)",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]+)']
|
||||
|
|
|
@ -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+)"]
|
||||
|
|
|
@ -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+)[^/]*$']
|
||||
|
|
19
catalog/templates/album.html
Normal file
19
catalog/templates/album.html
Normal file
|
@ -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 %}
|
||||
|
||||
<!-- class specific details -->
|
||||
{% block details %}
|
||||
{% endblock %}
|
||||
|
||||
<!-- class specific sidebar -->
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
98
catalog/templates/edition.html
Normal file
98
catalog/templates/edition.html
Normal file
|
@ -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 %}
|
||||
<div class="entity-detail__fields">
|
||||
<div class="entity-detail__rating">
|
||||
{% if item.rating %}
|
||||
<span class="entity-detail__rating-star rating-star" data-rating-score="{{ item.rating | floatformat:"0" }}"></span>
|
||||
<span class="entity-detail__rating-score"> {{ item.rating }} </span>
|
||||
<small>({{ item.rating_count }}人评分)</small>
|
||||
{% else %}
|
||||
<span> {% trans '评分:评分人数不足' %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>{% if item.isbn %}{% trans 'ISBN:' %}{{ item.isbn }}{% endif %}</div>
|
||||
<div>{% if item.authors %}{% trans '作者:' %}
|
||||
{% for author in item.authors %}
|
||||
<span>{{ author }}</span>{% if not forloop.last %} / {% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}</div>
|
||||
<div>{% if item.pub_house %}{% trans '出版社:' %}{{ item.pub_house }}{% endif %}</div>
|
||||
<div>{% if item.subtitle %}{% trans '副标题:' %}{{ item.subtitle }}{% endif %}</div>
|
||||
<div>{% if item.translator %}{% trans '译者:' %}
|
||||
{% for translator in item.translator %}
|
||||
<span>{{ translator }}</span>{% if not forloop.last %} / {% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}</div>
|
||||
<div>{% if item.orig_title %}{% trans '原作名:' %}{{ item.orig_title }}{% endif %}</div>
|
||||
<div>{% if item.language %}{% trans '语言:' %}{{ item.language }}{% endif %}</div>
|
||||
<div>{%if item.pub_year %}{% trans '出版时间:' %}{{ item.pub_year }}{% trans '年' %}{% if item.pub_month %}{{ item.pub_month }}{% trans '月' %}{% endif %}{% endif %}</div>
|
||||
</div>
|
||||
<div class="entity-detail__fields">
|
||||
|
||||
<div>{% if item.binding %}{% trans '装帧:' %}{{ item.binding }}{% endif %}</div>
|
||||
<div>{% if item.price %}{% trans '定价:' %}{{ item.price }}{% endif %}</div>
|
||||
<div>{% if item.pages %}{% trans '页数:' %}{{ item.pages }}{% endif %}</div>
|
||||
{% if item.other_info %}
|
||||
{% for k, v in item.other_info.items %}
|
||||
<div>
|
||||
{{ k }}:{{ v | urlize }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if item.last_editor and item.last_editor.preference.show_last_edit or user.is_staff %}
|
||||
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' item.last_editor.mastodon_username %}">{{ item.last_editor | default:"" }}</a></div>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<a href="{% url 'books:update' item.id %}">{% trans '编辑这本书' %}</a>
|
||||
{% if user.is_staff %}
|
||||
/<a href="{% url 'books:delete' item.id %}"> {% trans '删除' %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block sidebar %}
|
||||
{% if item.get_related_books.count > 0 %}
|
||||
<div class="aside-section-wrapper">
|
||||
<div class="action-panel">
|
||||
<div class="action-panel__label">{% trans '相关书目' %}</div>
|
||||
<div >
|
||||
{% for b in item.get_related_books %}
|
||||
<p>
|
||||
<a href="{% url 'books:retrieve' b.id %}">{{ b.title }}</a>
|
||||
<small>({{ b.pub_house }} {{ b.pub_year }})</small>
|
||||
<span class="source-label source-label__{{ b.source_site }}">{{ b.get_source_site_display }}</span>
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if item.isbn %}
|
||||
<div class="aside-section-wrapper">
|
||||
<div class="action-panel">
|
||||
<div class="action-panel__label">{% trans '借阅或购买' %}</div>
|
||||
<div class="action-panel__button-group">
|
||||
<a class="action-panel__button" target="_blank" href="https://www.worldcat.org/isbn/{{ item.isbn }}">{% trans 'WorldCat' %}</a>
|
||||
<a class="action-panel__button" target="_blank" href="https://openlibrary.org/search?isbn={{ item.isbn }}">{% trans 'Open Library' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
19
catalog/templates/game.html
Normal file
19
catalog/templates/game.html
Normal file
|
@ -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 %}
|
||||
|
||||
<!-- class specific details -->
|
||||
{% block details %}
|
||||
{% endblock %}
|
||||
|
||||
<!-- class specific sidebar -->
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
19
catalog/templates/item.html
Normal file
19
catalog/templates/item.html
Normal file
|
@ -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 %}
|
||||
|
||||
<!-- class specific details -->
|
||||
{% block details %}
|
||||
{% endblock %}
|
||||
|
||||
<!-- class specific sidebar -->
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
353
catalog/templates/item_base.html
Normal file
353
catalog/templates/item_base.html
Normal file
|
@ -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 %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta property="og:title" content="{{ site_name }}{% trans item.category.label %} - {{ item.title }}">
|
||||
<meta property="og:type" content="book">
|
||||
<meta property="og:url" content="{{ request.build_absolute_uri }}">
|
||||
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ item.cover.url }}">
|
||||
<meta property="og:site_name" content="{{ site_name }}">
|
||||
<meta property="og:description" content="{{ item.brief }}">
|
||||
{% if item.author %}
|
||||
<meta property="og:book:author" content="{% for author in item.author %}{{ author }}{% if not forloop.last %},{% endif %}{% endfor %}">
|
||||
{% endif %}
|
||||
{% if item.isbn %}
|
||||
<meta property="og:book:isbn" content="{{ item.isbn }}">
|
||||
{% endif %}
|
||||
|
||||
<title>{{ site_name }} - {% trans item.category.label %} | {{ item.title }}</title>
|
||||
|
||||
{% include "partial/_common_libs.html" with jquery=1 %}
|
||||
|
||||
<script src="{% static 'lib/js/rating-star.js' %}"></script>
|
||||
<script src="{% static 'js/detail.js' %}"></script>
|
||||
</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-detail">
|
||||
|
||||
<a href="{{ item.cover.url }}" class="entity-detail__img-origin" target="_blank" title="{% trans '查看原图' %}">
|
||||
<img src="{{ item.cover|thumb:'normal' }}" class="entity-detail__img" alt="{{ item.title }}">
|
||||
</a>
|
||||
|
||||
<div class="entity-detail__info">
|
||||
<h5 class="entity-detail__title">
|
||||
{{ item.title }}
|
||||
|
||||
{% for res in item.external_resources.all %}
|
||||
<a href="{{ res.url }}">
|
||||
<span class="source-label source-label__{{ res.site_name }}">{{ res.site_name.label }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</h5>
|
||||
|
||||
{% block details %}
|
||||
<div class="entity-detail__fields">
|
||||
<div class="entity-detail__rating">
|
||||
{% if item.rating %}
|
||||
<span class="entity-detail__rating-star rating-star" data-rating-score="{{ item.rating | floatformat:"0" }}"></span>
|
||||
<span class="entity-detail__rating-score"> {{ item.rating }} </span>
|
||||
<small>({{ item.rating_count }}人评分)</small>
|
||||
{% else %}
|
||||
<span> {% trans '评分:评分人数不足' %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>uid: {{item.url_id}}</div>
|
||||
<div>class: {{item.class_name}}</div>
|
||||
<div>category: {{item.category}}</div>
|
||||
<div>id type: {{item.primary_id_type}}</div>
|
||||
<div>id value: {{item.primary_id_value}</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<div class="tag-collection">
|
||||
{% for tag in item.tags %}
|
||||
<span class="tag-collection__tag">
|
||||
<a href="{% url 'common:search' %}?tag={{ tag }}">{{ tag }}</a>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dividing-line"></div>
|
||||
{% if item.brief %}
|
||||
<div class="entity-desc" id="description">
|
||||
<h5 class="entity-desc__title">{% trans '简介' %}</h5>
|
||||
|
||||
<p class="entity-desc__content">{{ item.brief | linebreaksbr }}</p>
|
||||
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
|
||||
<a href="javascript:void(0);">展开全部</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if item.contents %}
|
||||
<div class="entity-desc" id="contents">
|
||||
<h5 class="entity-desc__title">{% trans '目录' %}</h5>
|
||||
<p class="entity-desc__content">{{ item.contents | linebreaksbr }}</p>
|
||||
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
|
||||
<a href="javascript:void(0);">展开全部</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="entity-marks">
|
||||
<h5 class="entity-marks__title">{% trans '标记' %}</h5>
|
||||
<a href="{% url 'books:retrieve_mark_list' item.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
|
||||
<a href="{% url 'books:retrieve_mark_list' item.id 1 %}" class="entity-marks__more-link">关注的人的标记</a>
|
||||
{% include "partial/mark_list.html" with mark_list=mark_list current_item=book %}
|
||||
</div>
|
||||
<div class="entity-reviews">
|
||||
<h5 class="entity-reviews__title">{% trans '评论' %}</h5>
|
||||
{% if review_list_more %}
|
||||
<a href="{% url 'books:retrieve_review_list' item.id %}" class="entity-reviews__more-link">{% trans '全部评论' %}</a>
|
||||
{% endif %}
|
||||
{% if review_list %}
|
||||
<ul class="entity-reviews__review-list">
|
||||
{% for others_review in review_list %}
|
||||
<li class="entity-reviews__review">
|
||||
<a href="{% url 'users:home' others_review.owner.mastodon_username %}" class="entity-reviews__owner-link">{{ others_review.owner.username }}</a>
|
||||
{% if others_review.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 %}
|
||||
<span class="entity-reviews__review-time">{{ others_review.edited_time }}</span>
|
||||
{% if others_review.book != book %}
|
||||
<span class="entity-reviews__review-time source-label"><a class="entity-reviews__review-time" href="{% url 'books:retrieve' others_review.item.id %}">{{ others_review.item.get_source_site_display }}</a></span>
|
||||
{% endif %}
|
||||
<span class="entity-reviews__review-title"> <a href="{% url 'books:retrieve_review' others_review.id %}">{{ others_review.title }}</a></span>
|
||||
<span>{{ others_review.get_plain_content | truncate:100 }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div>{% trans '暂无评论' %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid__aside" id="aside">
|
||||
{% block sidebar_review %}
|
||||
<div class="aside-section-wrapper">
|
||||
{% if mark.shelf_type %}
|
||||
<div class="mark-panel">
|
||||
{% if mark.rating %}
|
||||
<span class="mark-panel__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
|
||||
{% endif %}
|
||||
<span class="mark-panel__status">{% trans '我' %}{% trans mark.shelf_label %}</span>
|
||||
{% if mark.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 %}
|
||||
<span class="mark-panel__actions">
|
||||
<a href="" class="edit">{% trans '修改' %}</a>
|
||||
<form action="{% url 'books:delete_mark' mark.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<a href="" class="delete">{% trans '删除' %}</a>
|
||||
</form>
|
||||
</span>
|
||||
<div class="mark-panel__clear"></div>
|
||||
|
||||
<div class="mark-panel__time">{{ mark.created_time }}</div>
|
||||
|
||||
{% if mark.text %}
|
||||
<p class="mark-panel__text">{{ mark.text }}</p>
|
||||
{% endif %}
|
||||
<div class="tag-collection">
|
||||
|
||||
{% for tag in mark.tags %}
|
||||
<span class="tag-collection__tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="action-panel" id="addMarkPanel">
|
||||
<div class="action-panel__label">{% trans '标记' %}{% trans item.demonstrative %}</div>
|
||||
<div class="action-panel__button-group">
|
||||
<button class="action-panel__button" data-status="{{ status_enum.WISH.value }}" id="wishButton">{% trans '想读' %}</button>
|
||||
<button class="action-panel__button" data-status="{{ status_enum.DO.value }}">{% trans '在读' %}</button>
|
||||
<button class="action-panel__button" data-status="{{ status_enum.COLLECT.value }}">{% trans '读过' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="aside-section-wrapper">
|
||||
{% if review %}
|
||||
<div class="review-panel">
|
||||
|
||||
<span class="review-panel__label">{% trans '我的评论' %}</span>
|
||||
{% if review.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 %}
|
||||
|
||||
<span class="review-panel__actions">
|
||||
<a href="{% url 'books:update_review' review.id %}">{% trans '编辑' %}</a>
|
||||
<a href="{% url 'books:delete_review' review.id %}">{% trans '删除' %}</a>
|
||||
</span>
|
||||
|
||||
<div class="review-panel__time">{{ review.edited_time }}</div>
|
||||
|
||||
<a href="{% url 'books:retrieve_review' review.id %}" class="review-panel__review-title">
|
||||
{{ review.title }}
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div class="action-panel">
|
||||
<div class="action-panel__label">{% trans '我的评论' %}</div>
|
||||
<div class="action-panel__button-group action-panel__button-group--center">
|
||||
|
||||
<a href="{% url 'books:create_review' item.id %}">
|
||||
<button class="action-panel__button">{% trans '去写评论' %}</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar_collection %}
|
||||
{% if collection_list %}
|
||||
<div class="aside-section-wrapper">
|
||||
<div class="action-panel">
|
||||
<div class="action-panel__label">{% trans '相关收藏单' %}</div>
|
||||
<div >
|
||||
{% for c in collection_list %}
|
||||
<p>
|
||||
<a href="{% url 'collection:retrieve' c.id %}">{{ c.title }}</a>
|
||||
</p>
|
||||
{% endfor %}
|
||||
<div class="action-panel__button-group action-panel__button-group--center">
|
||||
<button class="action-panel__button add-to-list" hx-get="{% url 'collection:add_to_list' 'book' item.id %}" hx-target="body" hx-swap="beforeend">{% trans '添加到收藏单' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% include "partial/_footer.html" %}
|
||||
</div>
|
||||
|
||||
<div id="modals">
|
||||
<div class="mark-modal modal">
|
||||
<div class="mark-modal__head">
|
||||
{% if not mark %}
|
||||
<style>
|
||||
.mark-modal__title::after {
|
||||
content: "{% trans item.demonstrative %}";
|
||||
}
|
||||
</style>
|
||||
<span class="mark-modal__title"></span>
|
||||
{% else %}
|
||||
<span class="mark-modal__title">{% trans '我的标记' %}</span>
|
||||
{% endif %}
|
||||
|
||||
<span class="mark-modal__close-button modal-close">
|
||||
<span class="icon-cross">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<polygon
|
||||
points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
|
||||
</polygon>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mark-modal__body">
|
||||
<form action="{% url 'books:create_update_mark' %}" method="post">
|
||||
{{ mark_form.media }}
|
||||
{% csrf_token %}
|
||||
{{ mark_form.id }}
|
||||
{{ mark_form.book }}
|
||||
{% if mark.rating %}
|
||||
{% endif %}
|
||||
|
||||
<div class="mark-modal__rating-star rating-star-edit"></div>
|
||||
{{ mark_form.rating }}
|
||||
<div id="statusSelection" class="mark-modal__status-radio" {% if not mark %}hidden{% endif %}>
|
||||
{{ mark_form.status }}
|
||||
</div>
|
||||
|
||||
<div class="mark-modal__clear"></div>
|
||||
|
||||
{{ mark_form.text }}
|
||||
|
||||
<div class="mark-modal__tag">
|
||||
<label>{{ mark_form.tags.label }}</label>
|
||||
{{ mark_form.tags }}
|
||||
</div>
|
||||
|
||||
<div class="mark-modal__option">
|
||||
<div class="mark-modal__visibility-radio">
|
||||
<span>{{ mark_form.visibility.label }}:
|
||||
{{ mark_form.visibility }}</span>
|
||||
</div>
|
||||
<div class="mark-modal__share-checkbox">
|
||||
{{ mark_form.share_to_mastodon }}{{ mark_form.share_to_mastodon.label }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mark-modal__confirm-button">
|
||||
<input type="submit" class="button float-right" value="{% trans '提交' %}">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="confirm-modal modal">
|
||||
<div class="confirm-modal__head">
|
||||
<span class="confirm-modal__title">{% trans '确定要删除你的标记吗?' %}</span>
|
||||
<span class="confirm-modal__close-button modal-close">
|
||||
<span class="icon-cross">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<polygon
|
||||
points="20 2.61 17.39 0 10 7.39 2.61 0 0 2.61 7.39 10 0 17.39 2.61 20 10 12.61 17.39 20 20 17.39 12.61 10 20 2.61">
|
||||
</polygon>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="confirm-modal__body">
|
||||
<div class="confirm-modal__confirm-button">
|
||||
<input type="submit" class="button float-right" value="{% trans '确认' %}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-mask"></div>
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
19
catalog/templates/movie.html
Normal file
19
catalog/templates/movie.html
Normal file
|
@ -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 %}
|
||||
|
||||
<!-- class specific details -->
|
||||
{% block details %}
|
||||
{% endblock %}
|
||||
|
||||
<!-- class specific sidebar -->
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
19
catalog/templates/performance.html
Normal file
19
catalog/templates/performance.html
Normal file
|
@ -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 %}
|
||||
|
||||
<!-- class specific details -->
|
||||
{% block details %}
|
||||
{% endblock %}
|
||||
|
||||
<!-- class specific sidebar -->
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
19
catalog/templates/podcast.html
Normal file
19
catalog/templates/podcast.html
Normal file
|
@ -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 %}
|
||||
|
||||
<!-- class specific details -->
|
||||
{% block details %}
|
||||
{% endblock %}
|
||||
|
||||
<!-- class specific sidebar -->
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
19
catalog/templates/tvseason.html
Normal file
19
catalog/templates/tvseason.html
Normal file
|
@ -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 %}
|
||||
|
||||
<!-- class specific details -->
|
||||
{% block details %}
|
||||
{% endblock %}
|
||||
|
||||
<!-- class specific sidebar -->
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
19
catalog/templates/tvshow.html
Normal file
19
catalog/templates/tvshow.html
Normal file
|
@ -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 %}
|
||||
|
||||
<!-- class specific details -->
|
||||
{% block details %}
|
||||
{% endblock %}
|
||||
|
||||
<!-- class specific sidebar -->
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
19
catalog/templates/work.html
Normal file
19
catalog/templates/work.html
Normal file
|
@ -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 %}
|
||||
|
||||
<!-- class specific details -->
|
||||
{% block details %}
|
||||
{% endblock %}
|
||||
|
||||
<!-- class specific sidebar -->
|
||||
{% block sidebar %}
|
||||
{% endblock %}
|
|
@ -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)
|
||||
|
|
|
@ -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<uid>[A-Za-z0-9]{21,22})/', retrieve, name='retrieve'),
|
||||
]
|
||||
|
|
118
catalog/views.py
118
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/<id>')
|
||||
return HttpResponseBadRequest()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue