Merge pull request #119 from mouse-reeve/works-editions

Get editions of openlibrary works
This commit is contained in:
Mouse Reeve 2020-03-30 17:16:52 -07:00 committed by GitHub
commit 3cc948a55b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 734 additions and 59 deletions

View file

@ -22,7 +22,7 @@ def get_book(book):
'source_url',
'description',
'language',
'languages',
'series',
'series_number',
'subjects',

View file

@ -7,6 +7,7 @@ import requests
from fedireads import models
from .abstract_connector import AbstractConnector, SearchResult
from .abstract_connector import update_from_mappings, get_date
from .openlibrary_languages import languages
class Connector(AbstractConnector):
@ -43,20 +44,22 @@ class Connector(AbstractConnector):
def get_or_create_book(self, olkey):
''' pull up a book record by whatever means possible '''
if re.match(r'^OL\d+W$', olkey):
model = models.Work
elif re.match(r'^OL\d+M$', olkey):
model = models.Edition
else:
raise ValueError('Invalid OpenLibrary ID')
''' pull up a book record by whatever means possible.
if you give a work key, it should give you the default edition,
annotated with work data. '''
try:
book = model.objects.get(openlibrary_key=olkey)
book = models.Book.objects.select_subclasses().get(
openlibrary_key=olkey
)
return book
except ObjectDoesNotExist:
# no book was found, so we start creating a new one
book = model(openlibrary_key=olkey)
pass
# no book was found, so we start creating a new one
model = models.Edition
if re.match(r'^OL\d+W$', olkey):
model = models.Work
book = model(openlibrary_key=olkey)
return self.update_book(book)
@ -67,10 +70,14 @@ class Connector(AbstractConnector):
response = requests.get('%s/works/%s.json' % (self.url, olkey))
if not response.ok:
response.raise_for_status()
data = response.json()
if not book.source_url:
book.source_url = response.url
return self.update_from_data(book, data)
# great, we can update our book.
def update_from_data(self, book, data, work=None):
''' update a book from a json blob '''
mappings = {
'publish_date': ('published_date', get_date),
'first_publish_date': ('first_published_date', get_date),
@ -78,6 +85,7 @@ class Connector(AbstractConnector):
'isbn_13': ('isbn', None),
'oclc_numbers': ('oclc_number', lambda a: a[0]),
'lccn': ('lccn', lambda a: a[0]),
'languages': ('languages', get_languages),
}
book = update_from_mappings(book, data, mappings)
@ -85,19 +93,22 @@ class Connector(AbstractConnector):
if 'goodreads' in data['identifiers']:
book.goodreads_key = data['identifiers']['goodreads']
if not book.source_url:
book.source_url = response.url
if not book.connector:
book.connector = self.connector
book.save()
# this book sure as heck better be an edition
if data.get('works'):
key = data.get('works')[0]['key']
key = key.split('/')[-1]
work = self.get_or_create_book(key)
if not work:
work = None
if data.get('works'):
key = data.get('works')[0]['key']
key = key.split('/')[-1]
work = self.get_or_create_book(key)
book.parent_work = work
book.parent_work = work
if isinstance(book, models.Work):
# load editions of a work
self.get_editions_of_work(book)
# we also need to know the author get the cover
for author_blob in data.get('authors', []):
@ -113,6 +124,27 @@ class Connector(AbstractConnector):
return book
def get_editions_of_work(self, work):
''' get all editions of a work '''
response = requests.get(
'%s/works/%s/editions.json' % (self.url, work.openlibrary_key))
edition_data = response.json()
for data in edition_data.get('entries', []):
try:
olkey = data['key'].split('/')[-1]
except KeyError:
# bad data I guess?
continue
try:
models.Edition.objects.get(openlibrary_key=olkey)
continue
except ObjectDoesNotExist:
book = models.Edition.objects.create(openlibrary_key=olkey)
self.update_from_data(book, data, work=work)
set_default_edition(work)
def get_or_create_author(self, olkey):
''' load that author '''
if not re.match(r'^OL\d+A$', olkey):
@ -155,9 +187,48 @@ class Connector(AbstractConnector):
return [image_name, image_content]
def set_default_edition(work):
''' pick one edition to be what gets shown by default '''
# check for an existing default work, in which case we're done
# favor recent, hardcover, english editions
editions = models.Edition.objects.filter(
parent_work=work,
).all()
options = [e for e in editions if 'English' in e.languages] or editions
format_prefs = {
'hardcover': 0,
'paperback': 1,
'mass market paperback': 2,
}
options = sorted(
options,
key=lambda e: format_prefs.get(str(e.physical_format).lower(), 3)
)
if options[0].physical_format in format_prefs:
options = [e for e in options if \
e.physical_format == options[0].physical_format]
options = sorted(
options,
key=lambda e: -1 * e.published_date.year
)
options[0].default = True
options[0].save()
def get_description(description_blob):
''' descriptions can be a string or a dict '''
if isinstance(description_blob, dict):
return description_blob.get('value')
return description_blob
def get_languages(language_blob):
''' /language/eng -> English '''
langs = []
for lang in language_blob:
langs.append(
languages.get(lang.get('key', ''), None)
)
return langs

View file

@ -0,0 +1,467 @@
''' key lookups for openlibrary languages '''
languages = {
'/languages/eng': 'English',
'/languages/fre': 'French',
'/languages/spa': 'Spanish',
'/languages/ger': 'German',
'/languages/rus': 'Russian',
'/languages/ita': 'Italian',
'/languages/chi': 'Chinese',
'/languages/jpn': 'Japanese',
'/languages/por': 'Portuguese',
'/languages/ara': 'Arabic',
'/languages/pol': 'Polish',
'/languages/heb': 'Hebrew',
'/languages/kor': 'Korean',
'/languages/dut': 'Dutch',
'/languages/ind': 'Indonesian',
'/languages/lat': 'Latin',
'/languages/und': 'Undetermined',
'/languages/cmn': 'Mandarin',
'/languages/hin': 'Hindi',
'/languages/swe': 'Swedish',
'/languages/dan': 'Danish',
'/languages/urd': 'Urdu',
'/languages/hun': 'Hungarian',
'/languages/cze': 'Czech',
'/languages/tur': 'Turkish',
'/languages/ukr': 'Ukrainian',
'/languages/gre': 'Greek',
'/languages/vie': 'Vietnamese',
'/languages/bul': 'Bulgarian',
'/languages/ben': 'Bengali',
'/languages/rum': 'Romanian',
'/languages/cat': 'Catalan',
'/languages/nor': 'Norwegian',
'/languages/tha': 'Thai',
'/languages/per': 'Persian',
'/languages/scr': 'Croatian',
'/languages/mul': 'Multiple languages',
'/languages/fin': 'Finnish',
'/languages/tam': 'Tamil',
'/languages/guj': 'Gujarati',
'/languages/mar': 'Marathi',
'/languages/scc': 'Serbian',
'/languages/pan': 'Panjabi',
'/languages/wel': 'Welsh',
'/languages/tel': 'Telugu',
'/languages/yid': 'Yiddish',
'/languages/kan': 'Kannada',
'/languages/slo': 'Slovak',
'/languages/san': 'Sanskrit',
'/languages/arm': 'Armenian',
'/languages/mal': 'Malayalam',
'/languages/may': 'Malay',
'/languages/bur': 'Burmese',
'/languages/slv': 'Slovenian',
'/languages/lit': 'Lithuanian',
'/languages/tib': 'Tibetan',
'/languages/lav': 'Latvian',
'/languages/est': 'Estonian',
'/languages/nep': 'Nepali',
'/languages/ori': 'Oriya',
'/languages/mon': 'Mongolian',
'/languages/alb': 'Albanian',
'/languages/iri': 'Irish',
'/languages/geo': 'Georgian',
'/languages/afr': 'Afrikaans',
'/languages/grc': 'Ancient Greek',
'/languages/mac': 'Macedonian',
'/languages/bel': 'Belarusian',
'/languages/ice': 'Icelandic',
'/languages/srp': 'Serbian',
'/languages/snh': 'Sinhalese',
'/languages/snd': 'Sindhi',
'/languages/ota': 'Turkish, Ottoman',
'/languages/kur': 'Kurdish',
'/languages/aze': 'Azerbaijani',
'/languages/pus': 'Pushto',
'/languages/amh': 'Amharic',
'/languages/gag': 'Galician',
'/languages/hrv': 'Croatian',
'/languages/sin': 'Sinhalese',
'/languages/asm': 'Assamese',
'/languages/uzb': 'Uzbek',
'/languages/gae': 'Scottish Gaelix',
'/languages/kaz': 'Kazakh',
'/languages/swa': 'Swahili',
'/languages/bos': 'Bosnian',
'/languages/glg': 'Galician ',
'/languages/baq': 'Basque',
'/languages/tgl': 'Tagalog',
'/languages/raj': 'Rajasthani',
'/languages/gle': 'Irish',
'/languages/lao': 'Lao',
'/languages/jav': 'Javanese',
'/languages/mai': 'Maithili',
'/languages/tgk': 'Tajik ',
'/languages/khm': 'Khmer',
'/languages/roh': 'Raeto-Romance',
'/languages/kok': 'Konkani ',
'/languages/sit': 'Sino-Tibetan (Other)',
'/languages/mol': 'Moldavian',
'/languages/kir': 'Kyrgyz',
'/languages/new': 'Newari',
'/languages/inc': 'Indic (Other)',
'/languages/frm': 'French, Middle (ca. 1300-1600)',
'/languages/esp': 'Esperanto',
'/languages/hau': 'Hausa',
'/languages/tag': 'Tagalog',
'/languages/tuk': 'Turkmen',
'/languages/enm': 'English, Middle (1100-1500)',
'/languages/map': 'Austronesian (Other)',
'/languages/pli': 'Pali',
'/languages/fro': 'French, Old (ca. 842-1300)',
'/languages/nic': 'Niger-Kordofanian (Other)',
'/languages/tir': 'Tigrinya',
'/languages/wen': 'Sorbian (Other)',
'/languages/bho': 'Bhojpuri',
'/languages/roa': 'Romance (Other)',
'/languages/tut': 'Altaic (Other)',
'/languages/bra': 'Braj',
'/languages/sun': 'Sundanese',
'/languages/fiu': 'Finno-Ugrian (Other)',
'/languages/far': 'Faroese',
'/languages/ban': 'Balinese',
'/languages/tar': 'Tatar',
'/languages/bak': 'Bashkir',
'/languages/tat': 'Tatar',
'/languages/chu': 'Church Slavic',
'/languages/dra': 'Dravidian (Other)',
'/languages/pra': 'Prakrit languages',
'/languages/paa': 'Papuan (Other)',
'/languages/doi': 'Dogri',
'/languages/lah': 'Lahndā',
'/languages/mni': 'Manipuri',
'/languages/yor': 'Yoruba',
'/languages/gmh': 'German, Middle High (ca. 1050-1500)',
'/languages/kas': 'Kashmiri',
'/languages/fri': 'Frisian',
'/languages/mla': 'Malagasy',
'/languages/egy': 'Egyptian',
'/languages/rom': 'Romani',
'/languages/syr': 'Syriac, Modern',
'/languages/cau': 'Caucasian (Other)',
'/languages/hbs': 'Serbo-Croatian',
'/languages/sai': 'South American Indian (Other)',
'/languages/pro': 'Provençal (to 1500)',
'/languages/cpf': 'Creoles and Pidgins, French-based (Other)',
'/languages/ang': 'English, Old (ca. 450-1100)',
'/languages/bal': 'Baluchi',
'/languages/gla': 'Scottish Gaelic',
'/languages/chv': 'Chuvash',
'/languages/kin': 'Kinyarwanda',
'/languages/zul': 'Zulu',
'/languages/sla': 'Slavic (Other)',
'/languages/som': 'Somali',
'/languages/mlt': 'Maltese',
'/languages/uig': 'Uighur',
'/languages/mlg': 'Malagasy',
'/languages/sho': 'Shona',
'/languages/lan': 'Occitan (post 1500)',
'/languages/bre': 'Breton',
'/languages/sco': 'Scots',
'/languages/sso': 'Sotho',
'/languages/myn': 'Mayan languages',
'/languages/xho': 'Xhosa',
'/languages/gem': 'Germanic (Other)',
'/languages/esk': 'Eskimo languages',
'/languages/akk': 'Akkadian',
'/languages/div': 'Maldivian',
'/languages/sah': 'Yakut',
'/languages/tsw': 'Tswana',
'/languages/nso': 'Northern Sotho',
'/languages/pap': 'Papiamento',
'/languages/bnt': 'Bantu (Other)',
'/languages/oss': 'Ossetic',
'/languages/cre': 'Cree',
'/languages/ibo': 'Igbo',
'/languages/fao': 'Faroese',
'/languages/nai': 'North American Indian (Other)',
'/languages/mag': 'Magahi',
'/languages/arc': 'Aramaic',
'/languages/epo': 'Esperanto',
'/languages/kha': 'Khasi',
'/languages/oji': 'Ojibwa',
'/languages/que': 'Quechua',
'/languages/lug': 'Ganda',
'/languages/mwr': 'Marwari',
'/languages/awa': 'Awadhi ',
'/languages/cor': 'Cornish',
'/languages/lad': 'Ladino',
'/languages/dzo': 'Dzongkha',
'/languages/cop': 'Coptic',
'/languages/nah': 'Nahuatl',
'/languages/cai': 'Central American Indian (Other)',
'/languages/phi': 'Philippine (Other)',
'/languages/moh': 'Mohawk',
'/languages/crp': 'Creoles and Pidgins (Other)',
'/languages/nya': 'Nyanja',
'/languages/wol': 'Wolof ',
'/languages/haw': 'Hawaiian',
'/languages/eth': 'Ethiopic',
'/languages/mis': 'Miscellaneous languages',
'/languages/mkh': 'Mon-Khmer (Other)',
'/languages/alg': 'Algonquian (Other)',
'/languages/nde': 'Ndebele (Zimbabwe)',
'/languages/ssa': 'Nilo-Saharan (Other)',
'/languages/chm': 'Mari',
'/languages/che': 'Chechen',
'/languages/gez': 'Ethiopic',
'/languages/ven': 'Venda',
'/languages/cam': 'Khmer',
'/languages/fur': 'Friulian',
'/languages/ful': 'Fula',
'/languages/gal': 'Oromo',
'/languages/jrb': 'Judeo-Arabic',
'/languages/bua': 'Buriat',
'/languages/ady': 'Adygei',
'/languages/bem': 'Bemba',
'/languages/kar': 'Karen languages',
'/languages/sna': 'Shona',
'/languages/twi': 'Twi',
'/languages/btk': 'Batak',
'/languages/kaa': 'Kara-Kalpak',
'/languages/kom': 'Komi',
'/languages/sot': 'Sotho',
'/languages/tso': 'Tsonga',
'/languages/cpe': 'Creoles and Pidgins, English-based (Other)',
'/languages/gua': 'Guarani',
'/languages/mao': 'Maori',
'/languages/mic': 'Micmac',
'/languages/swz': 'Swazi',
'/languages/taj': 'Tajik',
'/languages/smo': 'Samoan',
'/languages/ace': 'Achinese',
'/languages/afa': 'Afroasiatic (Other)',
'/languages/lap': 'Sami',
'/languages/min': 'Minangkabau',
'/languages/oci': 'Occitan (post 1500)',
'/languages/tsn': 'Tswana',
'/languages/pal': 'Pahlavi',
'/languages/sux': 'Sumerian',
'/languages/ewe': 'Ewe',
'/languages/him': 'Himachali',
'/languages/kaw': 'Kawi',
'/languages/lus': 'Lushai',
'/languages/ceb': 'Cebuano',
'/languages/chr': 'Cherokee',
'/languages/fil': 'Filipino',
'/languages/ndo': 'Ndonga',
'/languages/ilo': 'Iloko',
'/languages/kbd': 'Kabardian',
'/languages/orm': 'Oromo',
'/languages/dum': 'Dutch, Middle (ca. 1050-1350)',
'/languages/bam': 'Bambara',
'/languages/goh': 'Old High German',
'/languages/got': 'Gothic',
'/languages/kon': 'Kongo',
'/languages/mun': 'Munda (Other)',
'/languages/kru': 'Kurukh',
'/languages/pam': 'Pampanga',
'/languages/grn': 'Guarani',
'/languages/gaa': '',
'/languages/fry': 'Frisian',
'/languages/iba': 'Iban',
'/languages/mak': 'Makasar',
'/languages/kik': 'Kikuyu',
'/languages/cho': 'Choctaw',
'/languages/cpp': 'Creoles and Pidgins, Portuguese-based (Other)',
'/languages/dak': 'Dakota',
'/languages/udm': 'Udmurt ',
'/languages/hat': 'Haitian French Creole',
'/languages/mus': 'Creek',
'/languages/ber': 'Berber (Other)',
'/languages/hil': 'Hiligaynon',
'/languages/iro': 'Iroquoian (Other)',
'/languages/kua': 'Kuanyama',
'/languages/mno': 'Manobo languages',
'/languages/run': 'Rundi',
'/languages/sat': 'Santali',
'/languages/shn': 'Shan',
'/languages/tyv': 'Tuvinian',
'/languages/chg': 'Chagatai',
'/languages/syc': 'Syriac',
'/languages/ath': 'Athapascan (Other)',
'/languages/aym': 'Aymara',
'/languages/bug': 'Bugis',
'/languages/cel': 'Celtic (Other)',
'/languages/int': 'Interlingua (International Auxiliary Language Association)',
'/languages/xal': 'Oirat',
'/languages/ava': 'Avaric',
'/languages/son': 'Songhai',
'/languages/tah': 'Tahitian',
'/languages/tet': 'Tetum',
'/languages/ira': 'Iranian (Other)',
'/languages/kac': 'Kachin',
'/languages/nob': 'Norwegian (Bokmål)',
'/languages/vai': 'Vai',
'/languages/bik': 'Bikol',
'/languages/mos': 'Mooré',
'/languages/tig': 'Tigré',
'/languages/fat': 'Fanti',
'/languages/her': 'Herero',
'/languages/kal': 'Kalâtdlisut',
'/languages/mad': 'Madurese',
'/languages/yue': 'Cantonese',
'/languages/chn': 'Chinook jargon',
'/languages/hmn': 'Hmong',
'/languages/lin': 'Lingala',
'/languages/man': 'Mandingo',
'/languages/nds': 'Low German',
'/languages/bas': 'Basa',
'/languages/gay': 'Gayo',
'/languages/gsw': 'gsw',
'/languages/ine': 'Indo-European (Other)',
'/languages/kro': 'Kru (Other)',
'/languages/kum': 'Kumyk',
'/languages/tsi': 'Tsimshian',
'/languages/zap': 'Zapotec',
'/languages/ach': 'Acoli',
'/languages/ada': 'Adangme',
'/languages/aka': 'Akan',
'/languages/khi': 'Khoisan (Other)',
'/languages/srd': 'Sardinian',
'/languages/arn': 'Mapuche',
'/languages/dyu': 'Dyula',
'/languages/loz': 'Lozi',
'/languages/ltz': 'Luxembourgish',
'/languages/sag': 'Sango (Ubangi Creole)',
'/languages/lez': 'Lezgian',
'/languages/luo': 'Luo (Kenya and Tanzania)',
'/languages/ssw': 'Swazi ',
'/languages/krc': 'Karachay-Balkar',
'/languages/nyn': 'Nyankole',
'/languages/sal': 'Salishan languages',
'/languages/jpr': 'Judeo-Persian',
'/languages/pau': 'Palauan',
'/languages/smi': 'Sami',
'/languages/aar': 'Afar',
'/languages/abk': 'Abkhaz',
'/languages/gon': 'Gondi',
'/languages/nzi': 'Nzima',
'/languages/sam': 'Samaritan Aramaic',
'/languages/sao': 'Samoan',
'/languages/srr': 'Serer',
'/languages/apa': 'Apache languages',
'/languages/crh': 'Crimean Tatar',
'/languages/efi': 'Efik',
'/languages/iku': 'Inuktitut',
'/languages/nav': 'Navajo',
'/languages/pon': 'Ponape',
'/languages/tmh': 'Tamashek',
'/languages/aus': 'Australian languages',
'/languages/oto': 'Otomian languages',
'/languages/war': 'Waray',
'/languages/ypk': 'Yupik languages',
'/languages/ave': 'Avestan',
'/languages/cus': 'Cushitic (Other)',
'/languages/del': 'Delaware',
'/languages/fon': 'Fon',
'/languages/ina': 'Interlingua (International Auxiliary Language Association)',
'/languages/myv': 'Erzya',
'/languages/pag': 'Pangasinan',
'/languages/peo': 'Old Persian (ca. 600-400 B.C.)',
'/languages/vls': 'Flemish',
'/languages/bai': 'Bamileke languages',
'/languages/bla': 'Siksika',
'/languages/day': 'Dayak',
'/languages/men': 'Mende',
'/languages/tai': 'Tai',
'/languages/ton': 'Tongan',
'/languages/uga': 'Ugaritic',
'/languages/yao': 'Yao (Africa)',
'/languages/zza': 'Zaza',
'/languages/bin': 'Edo',
'/languages/frs': 'East Frisian',
'/languages/inh': 'Ingush',
'/languages/mah': 'Marshallese',
'/languages/sem': 'Semitic (Other)',
'/languages/art': 'Artificial (Other)',
'/languages/chy': 'Cheyenne',
'/languages/cmc': 'Chamic languages',
'/languages/dar': 'Dargwa',
'/languages/dua': 'Duala',
'/languages/elx': 'Elamite',
'/languages/fan': 'Fang',
'/languages/fij': 'Fijian',
'/languages/gil': 'Gilbertese',
'/languages/ijo': 'Ijo',
'/languages/kam': 'Kamba',
'/languages/nog': 'Nogai',
'/languages/non': 'Old Norse',
'/languages/tem': 'Temne',
'/languages/arg': 'Aragonese',
'/languages/arp': 'Arapaho',
'/languages/arw': 'Arawak',
'/languages/din': 'Dinka',
'/languages/grb': 'Grebo',
'/languages/kos': 'Kusaie',
'/languages/lub': 'Luba-Katanga',
'/languages/mnc': 'Manchu',
'/languages/nyo': 'Nyoro',
'/languages/rar': 'Rarotongan',
'/languages/sel': 'Selkup',
'/languages/tkl': 'Tokelauan',
'/languages/tog': 'Tonga (Nyasa)',
'/languages/tum': 'Tumbuka',
'/languages/alt': 'Altai',
'/languages/ase': 'American Sign Language',
'/languages/ast': 'Asturian',
'/languages/chk': 'Chuukese',
'/languages/cos': 'Corsican',
'/languages/ewo': 'Ewondo',
'/languages/gor': 'Gorontalo',
'/languages/hmo': 'Hiri Motu',
'/languages/lol': 'Mongo-Nkundu',
'/languages/lun': 'Lunda',
'/languages/mas': 'Masai',
'/languages/niu': 'Niuean',
'/languages/rup': 'Aromanian',
'/languages/sas': 'Sasak',
'/languages/sio': 'Siouan (Other)',
'/languages/sus': 'Susu',
'/languages/zun': 'Zuni',
'/languages/bat': 'Baltic (Other)',
'/languages/car': 'Carib',
'/languages/cha': 'Chamorro',
'/languages/kab': 'Kabyle',
'/languages/kau': 'Kanuri',
'/languages/kho': 'Khotanese',
'/languages/lua': 'Luba-Lulua',
'/languages/mdf': 'Moksha',
'/languages/nbl': 'Ndebele (South Africa)',
'/languages/umb': 'Umbundu',
'/languages/wak': 'Wakashan languages',
'/languages/wal': 'Wolayta',
'/languages/ale': 'Aleut',
'/languages/bis': 'Bislama',
'/languages/gba': 'Gbaya',
'/languages/glv': 'Manx',
'/languages/gul': 'Gullah',
'/languages/ipk': 'Inupiaq',
'/languages/krl': 'Karelian',
'/languages/lam': 'Lamba (Zambia and Congo)',
'/languages/sad': 'Sandawe',
'/languages/sid': 'Sidamo',
'/languages/snk': 'Soninke',
'/languages/srn': 'Sranan',
'/languages/suk': 'Sukuma',
'/languages/ter': 'Terena',
'/languages/tiv': 'Tiv',
'/languages/tli': 'Tlingit',
'/languages/tpi': 'Tok Pisin',
'/languages/tvl': 'Tuvaluan',
'/languages/yap': 'Yapese',
'/languages/eka': 'Ekajuk',
'/languages/hsb': 'Upper Sorbian',
'/languages/ido': 'Ido',
'/languages/kmb': 'Kimbundu',
'/languages/kpe': 'Kpelle',
'/languages/mwl': 'Mirandese',
'/languages/nno': 'Nynorsk',
'/languages/nub': 'Nubian languages',
'/languages/osa': 'Osage',
'/languages/sme': 'Northern Sami',
'/languages/znd': 'Zande languages',
}

View file

@ -0,0 +1,82 @@
# Generated by Django 3.0.3 on 2020-03-30 22:32
from django.db import migrations, models
import django.db.models.deletion
import fedireads.utils.fields
class Migration(migrations.Migration):
dependencies = [
('fedireads', '0026_auto_20200330_1456'),
]
operations = [
migrations.RemoveField(
model_name='book',
name='language',
),
migrations.RemoveField(
model_name='book',
name='parent_work',
),
migrations.RemoveField(
model_name='book',
name='shelves',
),
migrations.AddField(
model_name='book',
name='languages',
field=fedireads.utils.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None),
),
migrations.AddField(
model_name='edition',
name='default',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='edition',
name='parent_work',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='fedireads.Work'),
),
migrations.AddField(
model_name='edition',
name='shelves',
field=models.ManyToManyField(through='fedireads.ShelfBook', to='fedireads.Shelf'),
),
migrations.AlterField(
model_name='comment',
name='book',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Edition'),
),
migrations.AlterField(
model_name='notification',
name='related_book',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='fedireads.Edition'),
),
migrations.AlterField(
model_name='review',
name='book',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Edition'),
),
migrations.AlterField(
model_name='shelf',
name='books',
field=models.ManyToManyField(through='fedireads.ShelfBook', to='fedireads.Edition'),
),
migrations.AlterField(
model_name='shelfbook',
name='book',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Edition'),
),
migrations.AlterField(
model_name='status',
name='mention_books',
field=models.ManyToManyField(related_name='mention_book', to='fedireads.Edition'),
),
migrations.AlterField(
model_name='tag',
name='book',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Edition'),
),
]

View file

@ -68,7 +68,9 @@ class Book(FedireadsModel):
sort_title = models.CharField(max_length=255, blank=True, null=True)
subtitle = models.CharField(max_length=255, blank=True, null=True)
description = models.TextField(blank=True, null=True)
language = models.CharField(max_length=255, blank=True, null=True)
languages = ArrayField(
models.CharField(max_length=255), blank=True, default=list
)
series = models.CharField(max_length=255, blank=True, null=True)
series_number = models.CharField(max_length=255, blank=True, null=True)
subjects = ArrayField(
@ -82,13 +84,6 @@ class Book(FedireadsModel):
cover = models.ImageField(upload_to='covers/', blank=True, null=True)
first_published_date = models.DateTimeField(blank=True, null=True)
published_date = models.DateTimeField(blank=True, null=True)
shelves = models.ManyToManyField(
'Shelf',
symmetrical=False,
through='ShelfBook',
through_fields=('book', 'shelf')
)
parent_work = models.ForeignKey('Work', on_delete=models.PROTECT, null=True)
objects = InheritanceManager()
@property
@ -96,7 +91,13 @@ class Book(FedireadsModel):
''' constructs the absolute reference to any db object '''
base_path = 'https://%s' % DOMAIN
model_name = type(self).__name__.lower()
return '%s/%s/%s' % (base_path, model_name, self.openlibrary_key)
return '%s/book/%s' % (base_path, self.openlibrary_key)
def save(self, *args, **kwargs):
''' can't be abstract for query reasons, but you shouldn't USE it '''
if not isinstance(self, Edition) and not isinstance(self, Work):
raise ValueError('Books should be added as Editions or Works')
super().save(*args, **kwargs)
def __repr__(self):
return "<{} key={!r} title={!r}>".format(
@ -111,9 +112,17 @@ class Work(Book):
# library of congress catalog control number
lccn = models.CharField(max_length=255, blank=True, null=True)
@property
def default_edition(self):
ed = Edition.objects.filter(parent_work=self, default=True).first()
if not ed:
ed = Edition.objects.filter(parent_work=self).first()
return ed
class Edition(Book):
''' an edition of a book '''
default = models.BooleanField(default=False)
# these identifiers only apply to work
isbn = models.CharField(max_length=255, blank=True, null=True)
oclc_number = models.CharField(max_length=255, blank=True, null=True)
@ -122,6 +131,13 @@ class Edition(Book):
publishers = ArrayField(
models.CharField(max_length=255), blank=True, default=list
)
shelves = models.ManyToManyField(
'Shelf',
symmetrical=False,
through='ShelfBook',
through_fields=('book', 'shelf')
)
parent_work = models.ForeignKey('Work', on_delete=models.PROTECT, null=True)
class Author(FedireadsModel):

View file

@ -10,7 +10,7 @@ class Shelf(FedireadsModel):
user = models.ForeignKey('User', on_delete=models.PROTECT)
editable = models.BooleanField(default=True)
books = models.ManyToManyField(
'Book',
'Edition',
symmetrical=False,
through='ShelfBook',
through_fields=('shelf', 'book')
@ -29,7 +29,7 @@ class Shelf(FedireadsModel):
class ShelfBook(FedireadsModel):
# many to many join table for books and shelves
book = models.ForeignKey('Book', on_delete=models.PROTECT)
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
shelf = models.ForeignKey('Shelf', on_delete=models.PROTECT)
added_by = models.ForeignKey(
'User',

View file

@ -15,7 +15,7 @@ class Status(FedireadsModel):
status_type = models.CharField(max_length=255, default='Note')
content = models.TextField(blank=True, null=True)
mention_users = models.ManyToManyField('User', related_name='mention_user')
mention_books = models.ManyToManyField('Book', related_name='mention_book')
mention_books = models.ManyToManyField('Edition', related_name='mention_book')
activity_type = models.CharField(max_length=255, default='Note')
local = models.BooleanField(default=True)
privacy = models.CharField(max_length=255, default='public')
@ -49,7 +49,7 @@ class Status(FedireadsModel):
class Comment(Status):
''' like a review but without a rating and transient '''
name = models.CharField(max_length=255)
book = models.ForeignKey('Book', on_delete=models.PROTECT)
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
def save(self, *args, **kwargs):
self.status_type = 'Comment'
@ -60,7 +60,7 @@ class Comment(Status):
class Review(Status):
''' a book review '''
name = models.CharField(max_length=255)
book = models.ForeignKey('Book', on_delete=models.PROTECT)
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
rating = models.IntegerField(
default=0,
validators=[MinValueValidator(0), MaxValueValidator(5)]
@ -107,7 +107,7 @@ class Boost(Status):
class Tag(FedireadsModel):
''' freeform tags for books '''
user = models.ForeignKey('User', on_delete=models.PROTECT)
book = models.ForeignKey('Book', on_delete=models.PROTECT)
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
name = models.CharField(max_length=100)
identifier = models.CharField(max_length=100)
@ -128,7 +128,7 @@ class Notification(FedireadsModel):
''' you've been tagged, liked, followed, etc '''
user = models.ForeignKey('User', on_delete=models.PROTECT)
related_book = models.ForeignKey(
'Book', on_delete=models.PROTECT, null=True)
'Edition', on_delete=models.PROTECT, null=True)
related_user = models.ForeignKey(
'User',
on_delete=models.PROTECT, null=True, related_name='related_user')

View file

@ -14,13 +14,12 @@
{% endif %}
</h2>
<div>
{% if book.parent_work %}<p>Edition of <a href="/book/{{ book.parent_work.fedireads_key }}">{{ book.parent_work.title }}</a></p>{% endif %}
<div class="book-preview">
{% include 'snippets/book_cover.html' with book=book size=large %}
<p>{{ active_tab }} rating: {{ rating | stars }}</p>
{% if book.description %}
<blockquote>{{ book.description | description }}</blockquote>
{% if book.parent_work.description %}
<blockquote>{{ book.parent_work.description | description }}</blockquote>
{% endif %}
<div>
<div id="tag-cloud">
@ -29,6 +28,7 @@
{% endfor %}
</div>
</div>
<p><a href="/editions/{{ book.parent_work.id }}">{{ book.parent_work.edition_set.count }} other editions</a></p>
{% include 'snippets/shelve_button.html' %}

View file

@ -0,0 +1,18 @@
{% extends 'layout.html' %}
{% load fr_display %}
{% block content %}
<div class="content-container">
<h2>Editions of <a href="/book/{{ work.fedireads_key }}">"{{ work.title }}"</a></h2>
<div class="book-grid row wrap">
{% for book in editions %}
<div class="book-preview">
<a href="{{ book.absolute_id }}">
{% include 'snippets/book_cover.html' with book=book %}
</a>
{% include 'snippets/shelve_button.html' with book=book %}
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View file

@ -60,6 +60,7 @@ urlpatterns = [
re_path(r'%s(.json)?/?$' % book_path, views.book_page),
re_path(r'%s/(?P<tab>friends|local|federated)?$' % book_path, views.book_page),
re_path(r'%s/edit/?$' % book_path, views.edit_book_page),
re_path(r'^editions/(?P<work_id>\d+)/?$', views.editions_page),
re_path(r'^author/(?P<author_identifier>[\w\-]+)/?$', views.author_page),
re_path(r'^tag/(?P<tag_id>.+)/?$', views.tag_page),

View file

@ -135,7 +135,10 @@ def upload_cover(request, book_id):
@login_required
def shelve(request):
''' put a on a user's shelf '''
book = models.Book.objects.get(id=request.POST['book'])
book = models.Book.objects.select_subclasses().get(id=request.POST['book'])
if isinstance(book, models.Work):
book = book.default_edition
desired_shelf = models.Shelf.objects.filter(
identifier=request.POST['shelf'],
user=request.user
@ -145,7 +148,7 @@ def shelve(request):
try:
current_shelf = models.Shelf.objects.get(
user=request.user,
book=book
edition=book
)
outgoing.handle_unshelve(request.user, book, current_shelf)
except models.Shelf.DoesNotExist:

View file

@ -52,17 +52,20 @@ def home_tab(request, tab):
size = sum(len(s['books']) for s in shelves)
# books new to the instance, for discovery
if size < 6:
recent_books = models.Work.objects.order_by(
'-created_date'
)[:6 - size]
recent_books = [b.default_edition for b in recent_books]
shelves.append({
'name': 'Recently added',
'identifier': None,
'books': models.Book.objects.order_by(
'-created_date'
)[:6 - size],
'books': recent_books,
'count': 6 - size,
})
# allows us to check if a user has shelved a book
user_books = models.Book.objects.filter(shelves__user=request.user).all()
user_books = models.Edition.objects.filter(shelves__user=request.user).all()
# status updates for your follow network
following = models.User.objects.filter(
@ -107,18 +110,21 @@ def home_tab(request, tab):
def books_page(request):
''' discover books '''
recent_books = models.Book.objects
if request.user.is_authenticated:
recent_books = recent_books.filter(
~Q(shelfbook__shelf__user=request.user)
)
recent_books = models.Work.objects
recent_books = recent_books.order_by('-created_date')[:50]
recent_books = [b.default_edition for b in recent_books]
if request.user.is_authenticated:
recent_books = models.Edition.objects.filter(
~Q(shelfbook__shelf__user=request.user),
id__in=[b.id for b in recent_books],
)
data = {
'books': recent_books,
}
return TemplateResponse(request, 'books.html', data)
@login_required
def import_page(request):
''' import history from goodreads '''
@ -322,11 +328,11 @@ def book_page(request, book_identifier, tab='friends'):
return JsonResponse(activitypub.get_book(book))
if isinstance(book, models.Work):
book_reviews = models.Review.objects.filter(
Q(book=book) | Q(book__parent_work=book),
)
else:
book_reviews = models.Review.objects.filter(book=book)
book = book.default_edition
work = book.parent_work
book_reviews = models.Review.objects.filter(book__in=work.edition_set.all())
if request.user.is_authenticated:
user_reviews = book_reviews.filter(
@ -355,7 +361,7 @@ def book_page(request, book_identifier, tab='friends'):
# TODO: books can be on multiple shelves
shelf = models.Shelf.objects.filter(
user=request.user,
book=book
edition=book
).first()
except models.Shelf.DoesNotExist:
shelf = None
@ -417,6 +423,17 @@ def edit_book_page(request, book_identifier):
return TemplateResponse(request, 'edit_book.html', data)
def editions_page(request, work_id):
''' list of editions of a book '''
work = models.Work.objects.get(id=work_id)
editions = models.Edition.objects.filter(parent_work=work).all()
data = {
'editions': editions,
'work': work,
}
return TemplateResponse(request, 'editions.html', data)
def author_page(request, author_identifier):
''' landing page for an author '''
try:
@ -424,10 +441,10 @@ def author_page(request, author_identifier):
except ValueError:
return HttpResponseNotFound()
books = models.Book.objects.filter(authors=author)
books = models.Work.objects.filter(authors=author)
data = {
'author': author,
'books': books,
'books': [b.default_edition for b in books],
}
return TemplateResponse(request, 'author.html', data)
@ -435,7 +452,7 @@ def author_page(request, author_identifier):
def tag_page(request, tag_id):
''' books related to a tag '''
tag_obj = models.Tag.objects.filter(identifier=tag_id).first()
books = models.Book.objects.filter(tag__identifier=tag_id).distinct()
books = models.Edition.objects.filter(tag__identifier=tag_id).distinct()
data = {
'books': books,
'tag': tag_obj,