new data model: /book/<uid>

This commit is contained in:
Your Name 2022-12-15 17:29:35 -05:00
parent 486dd16e1f
commit b2af6f3230
45 changed files with 1150 additions and 148 deletions

View file

@ -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

View file

@ -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)

View file

@ -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')

View file

@ -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')

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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

View file

@ -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+)"]

View file

@ -6,7 +6,7 @@ import logging
_logger = logging.getLogger(__name__)
@SiteList.register
@SiteManager.register
class Bangumi(AbstractSite):
ID_TYPE = IdType.Bangumi
URL_PATTERNS = [

View file

@ -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+)"]

View file

@ -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+)/"]

View file

@ -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}"]

View file

@ -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}"]

View file

@ -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}"]

View file

@ -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

View file

@ -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=([^&#?]+)",

View file

@ -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:

View file

@ -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

View file

@ -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]+)']

View file

@ -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+)"]

View file

@ -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+)[^/]*$']

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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>

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View file

@ -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)

View file

@ -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'),
]

View file

@ -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()

View file

@ -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

View file

@ -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'])

View file

@ -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