Merge pull request #520 from Gluejar/onix-feed

Implement ONIX 3.0 feed
pull/1/head
Raymond Yee 2015-08-28 21:57:18 -04:00
commit d4c2257511
11 changed files with 23671 additions and 6 deletions

290
api/crosswalks.py Normal file
View File

@ -0,0 +1,290 @@
relator_contrib = {
"act" : "E01",
"adp" : "B05",
"adp" : "B99",
"aft" : "A19",
"aft" : "A22",
"ant" : "A37",
"arr" : "B25",
"art" : "A07",
"aui" : "A15",
"aui" : "A16",
"aui" : "A23",
"aui" : "A24",
"aui" : "A29",
"aus" : "A03",
"aut" : "A01",
"aut" : "A38",
"clb" : "A33",
"cmm" : "A21",
"cmm" : "E04",
"cmp" : "A06",
"cnd" : "D03",
"com" : "C01",
"com" : "C02",
"com" : "C99",
"cov" : "A36",
"cre" : "A09",
"cre" : "A10",
"cre" : "A99",
"ctb" : "A02",
"ctb" : "A14",
"ctb" : "A17",
"ctb" : "A18",
"ctb" : "A20",
"ctb" : "A25",
"ctb" : "A27",
"ctb" : "A32",
"ctb" : "A34",
"ctg" : "A39",
"cwt" : "A21",
"dnc" : "E02",
"drt" : "D02",
"drt" : "D99",
"dsr" : "A11",
"edt" : "B01",
"edt" : "B02",
"edt" : "B04",
"edt" : "B09",
"edt" : "B12",
"edt" : "B13",
"edt" : "B14",
"edt" : "B15",
"edt" : "B16",
"edt" : "B19",
"edt" : "B20",
"edt" : "B21",
"edt" : "B23",
"edt" : "B24",
"ill" : "A12",
"ill" : "A35",
"ill" : "A40",
"itr" : "E06",
"ive" : "A44",
"ivr" : "A43",
"lbt" : "A04",
"lyr" : "A05",
"lyr" : "A31",
"nrt" : "B03",
"nrt" : "B07",
"nrt" : "E03",
"nrt" : "E07",
"pbd" : "B11",
"pbl" : "B18",
"pht" : "A08",
"pht" : "A13",
"pht" : "F01",
"prf" : "E08",
"prf" : "E99",
"prg" : "A30",
"pro" : "D01",
"trl" : "B06",
"trl" : "B08",
"trl" : "B10",
"voc" : "E05",
}
iso639 = {
"aa" : "aar",
"ab" : "abk",
"ae" : "ave",
"af" : "afr",
"ak" : "aka",
"am" : "amh",
"an" : "arg",
"ar" : "ara",
"as" : "asm",
"av" : "ava",
"ay" : "aym",
"az" : "aze",
"ba" : "bak",
"be" : "bel",
"bg" : "bul",
"bh" : "bih",
"bi" : "bis",
"bm" : "bam",
"bn" : "ben",
"bo" : "tib",
"br" : "bre",
"bs" : "bos",
"ca" : "cat",
"ce" : "che",
"ch" : "cha",
"co" : "cos",
"cr" : "cre",
"cs" : "cze",
"cs" : "cze",
"cu" : "chu",
"cv" : "chv",
"cy" : "wel",
"cy" : "wel",
"da" : "dan",
"de" : "ger",
"de" : "ger",
"dv" : "div",
"dz" : "dzo",
"ee" : "ewe",
"el" : "gre",
"el" : "gre",
"en" : "eng",
"eo" : "epo",
"es" : "spa",
"et" : "est",
"eu" : "baq",
"eu" : "baq",
"fa" : "per",
"fa" : "per",
"ff" : "ful",
"fi" : "fin",
"fj" : "fij",
"fo" : "fao",
"fr" : "fre",
"fr" : "fre",
"fy" : "fry",
"ga" : "gle",
"gd" : "gla",
"gl" : "glg",
"gn" : "grn",
"gu" : "guj",
"gv" : "glv",
"ha" : "hau",
"he" : "heb",
"hi" : "hin",
"ho" : "hmo",
"hr" : "hrv",
"ht" : "hat",
"hu" : "hun",
"hy" : "arm ",
"hy" : "arm",
"hz" : "her",
"ia" : "ina",
"id" : "ind",
"ie" : "ile",
"ig" : "ibo",
"ii" : "iii",
"ik" : "ipk",
"io" : "ido",
"is" : "ice",
"is" : "ice",
"it" : "ita",
"iu" : "iku",
"ja" : "jpn",
"jv" : "jav",
"ka" : "geo",
"ka" : "geo",
"kg" : "kon",
"ki" : "kik",
"kj" : "kua",
"kk" : "kaz",
"kl" : "kal",
"km" : "khm",
"kn" : "kan",
"ko" : "kor",
"kr" : "kau",
"ks" : "kas",
"ku" : "kur",
"kv" : "kom",
"kw" : "cor",
"ky" : "kir",
"la" : "lat",
"lb" : "ltz",
"lg" : "lug",
"li" : "lim",
"ln" : "lin",
"lo" : "lao",
"lt" : "lit",
"lu" : "lub",
"lv" : "lav",
"mg" : "mlg",
"mh" : "mah",
"mi" : "mao",
"mi" : "mao",
"mk" : "mac",
"mk" : "mac",
"ml" : "mal",
"mn" : "mon",
"mr" : "mar",
"ms" : "may",
"ms" : "may",
"mt" : "mlt",
"my" : "bur",
"my" : "bur",
"na" : "nau",
"nb" : "nob",
"nd" : "nde",
"ne" : "nep",
"ng" : "ndo",
"nl" : "dut",
"nl" : "dut",
"nn" : "nno",
"no" : "nor",
"nr" : "nbl",
"nv" : "nav",
"ny" : "nya",
"oc" : "oci",
"oj" : "oji",
"om" : "orm",
"or" : "ori",
"os" : "oss",
"pa" : "pan",
"pi" : "pli",
"pl" : "pol",
"ps" : "pus",
"pt" : "por",
"qu" : "que",
"rm" : "roh",
"rn" : "run",
"ro" : "rum",
"ro" : "rum",
"ru" : "rus",
"rw" : "kin",
"sa" : "san",
"sc" : "srd",
"sd" : "snd",
"se" : "sme",
"sg" : "sag",
"si" : "sin",
"sk" : "slo",
"sk" : "slo",
"sl" : "slv",
"sm" : "smo",
"sn" : "sna",
"so" : "som",
"sq" : "alb ",
"sq" : "alb",
"sr" : "srp",
"ss" : "ssw",
"st" : "sot",
"su" : "sun",
"sv" : "swe",
"sw" : "swa",
"ta" : "tam",
"te" : "tel",
"tg" : "tgk",
"th" : "tha",
"ti" : "tir",
"tk" : "tuk",
"tl" : "tgl",
"tn" : "tsn",
"to" : "ton",
"tr" : "tur",
"ts" : "tso",
"tt" : "tat",
"tw" : "twi",
"ty" : "tah",
"ug" : "uig",
"uk" : "ukr",
"ur" : "urd",
"uz" : "uzb",
"ve" : "ven",
"vi" : "vie",
"vo" : "vol",
"wa" : "wln",
"wo" : "wol",
"xh" : "xho",
"yi" : "yid",
"yo" : "yor",
"za" : "zha",
"zh" : "chi",
"zh" : "chi",
"zu" : "zul",
}

179
api/onix.py Normal file
View File

@ -0,0 +1,179 @@
import datetime
import pytz
from lxml import etree
from regluit.core import models
from regluit.core.cc import ccinfo
from regluit.bisac import Bisac
from .crosswalks import relator_contrib, iso639
feed_xml = """<?xml version="1.0" encoding="UTF-8"?>
<ONIXMessage release="3.0" xmlns="http://ns.editeur.org/onix/3.0/reference" />
"""
bisac = Bisac()
def text_node(tag, text, attrib={}):
node = etree.Element(tag, attrib=attrib)
node.text = text
return node
def onix_feed(facet, max=None):
feed = etree.fromstring(feed_xml)
feed.append(header(facet))
works = facet.works[0:max] if max else facet.works
for work in works:
editions = models.Edition.objects.filter(work=work,ebooks__isnull=False)
editions = facet.facet_object.filter_model("Edition",editions).distinct()
for edition in editions:
feed.append(product(edition, facet.facet_object))
return etree.tostring(feed, pretty_print=True)
def onix_feed_for_work(work):
feed = etree.fromstring(feed_xml)
feed.append(header(work))
for edition in models.Edition.objects.filter(work=work,ebooks__isnull=False).distinct():
feed.append(product(edition))
return etree.tostring(feed, pretty_print=True)
def header(facet=None):
header_node = etree.Element("Header")
sender_node = etree.Element("Sender")
sender_node.append(text_node("SenderName", "unglue.it"))
sender_node.append(text_node("EmailAddress", "support@gluejar.com"))
header_node.append(sender_node)
header_node.append(text_node("SentDateTime", pytz.utc.localize(datetime.datetime.utcnow()).strftime('%Y%m%dT%H%M%SZ')))
header_node.append(text_node("MessageNote", facet.title if facet else "Unglue.it Editions"))
return header_node
def product(edition, facet=None):
work=edition.work
product_node = etree.Element("Product")
product_node.append(text_node("RecordReference", "it.unglue.work.%s.%s" % (work.id, edition.id)))
product_node.append(text_node("NotificationType", "03" )) # final
ident_node = etree.SubElement(product_node, "ProductIdentifier")
ident_node.append(text_node("ProductIDType", "01" )) #proprietary
ident_node.append(text_node("IDTypeName", "unglue.it edition id" )) #proprietary
ident_node.append(text_node("IDValue", unicode(edition.id) ))
if edition.isbn_13:
ident_node = etree.SubElement(product_node, "ProductIdentifier")
ident_node.append(text_node("ProductIDType", "03" )) #proprietary
ident_node.append(text_node("IDValue", edition.isbn_13 ))
# Descriptive Detail Block
descriptive_node = etree.SubElement(product_node, "DescriptiveDetail")
descriptive_node.append(text_node("ProductComposition", "00" )) # single item
descriptive_node.append(text_node("ProductForm", "ED" )) # download
ebook = None
ebooks=facet.filter_model("Ebook",edition.ebooks.all()) if facet else edition.ebooks.all()
for ebook in ebooks:
if ebook.format=='epub':
descriptive_node.append(text_node("ProductFormDetail", "E101" ))
elif ebook.format=='pdf':
descriptive_node.append(text_node("ProductFormDetail", "E107" ))
elif ebook.format=='mobi':
descriptive_node.append(text_node("ProductFormDetail", "E116" ))
if ebook.rights:
license_node = etree.SubElement(descriptive_node, "EpubLicense")
license_node.append(text_node("EpubLicenseName", ebook.rights ))
lic_expr_node = etree.SubElement(license_node, "EpubLicenseExpression")
lic_expr_node.append(text_node("EpubLicenseExpressionType", '01' )) #human readable
lic_expr_node.append(text_node("EpubLicenseExpressionLink", ccinfo(ebook.rights).url ))
title_node = etree.SubElement(descriptive_node, "TitleDetail")
title_node.append(text_node("TitleType", '01' )) #distinctive title
title_el = etree.SubElement(title_node, "TitleElement")
title_el.append(text_node("TitleElementLevel", '01' ))
title_el.append(text_node("TitleText", edition.title ))
contrib_i = 0
for contrib in edition.relators.all():
contrib_i+=1
contrib_node = etree.SubElement(descriptive_node, "Contributor")
contrib_node.append(text_node("SequenceNumber", unicode(contrib_i )))
contrib_node.append(text_node("ContributorRole", relator_contrib.get(contrib.relation.code,"") ))
contrib_node.append(text_node("PersonName", contrib.author.name))
(lang, locale) = (edition.work.language, None)
if '_' in lang:
(lang, locale) = lang.split('_')
if len(lang)==2:
lang = iso639.get(lang, None)
if lang:
lang_node = etree.SubElement(descriptive_node, "Language")
lang_node.append(text_node("LanguageRole", "01"))
lang_node.append(text_node("LanguageCode", lang))
if locale:
lang_node.append(text_node("CountryCode", locale))
for subject in work.subjects.all():
subj_node = etree.SubElement(descriptive_node, "Subject")
if subject.authority == 'lcsh':
subj_node.append(text_node("SubjectSchemeIdentifier", "04"))
subj_node.append(text_node("SubjectHeadingText", subject.name))
elif subject.authority == 'lcc':
subj_node.append(text_node("SubjectSchemeIdentifier", "03"))
subj_node.append(text_node("SubjectCode", subject.name))
elif subject.authority == 'bisacsh':
subj_node.append(text_node("SubjectSchemeIdentifier", "10"))
subj_node.append(text_node("SubjectCode", bisac.code(subject.name)))
subj_node.append(text_node("SubjectHeadingText", subject.name))
else:
subj_node.append(text_node("SubjectSchemeIdentifier", "20"))
subj_node.append(text_node("SubjectHeadingText", subject.name))
# Collateral Detail Block
coll_node = etree.SubElement(product_node, "CollateralDetail")
desc_node = etree.SubElement(coll_node, "TextContent")
desc_node.append(text_node("TextType", '03')) # description
desc_node.append(text_node("ContentAudience", '00')) #unrestricted
desc = work.description + '<br /><br />Listed by <a href="https://unglue.it/work/%s/">Unglue.it</a>.' % work.id
try :
content = etree.XML("<div>" + desc + "</div>")
content_node = etree.SubElement(desc_node, "Text", attrib={"textformat":"05"}) #xhtml
content_node.append(content)
except etree.XMLSyntaxError:
content_node = etree.SubElement(desc_node, "Text", attrib={"textformat":"02"}) #html
content_node.text = etree.CDATA(desc)
supp_node = etree.SubElement(coll_node, "SupportingResource")
supp_node.append(text_node("ResourceContentType", '01')) #front cover
supp_node.append(text_node("ContentAudience", '00')) #unrestricted
supp_node.append(text_node("ResourceMode", '03')) #image
cover_node = etree.SubElement(supp_node, "ResourceVersion")
cover_node.append(text_node("ResourceForm", '01')) #linkable
coverfeat_node = etree.SubElement(cover_node, "ResourceVersionFeature")
coverfeat_node.append(text_node("ResourceVersionFeatureType", '01')) #image format
coverfeat_node.append(text_node("FeatureValue", 'D502')) #jpeg
cover_node.append(text_node("ResourceLink", edition.cover_image_thumbnail())) #link
# Publishing Detail Block
pubdetail_node = etree.SubElement(product_node, "PublishingDetail")
if edition.publisher_name:
pub_node = etree.SubElement(pubdetail_node, "Publisher")
pub_node.append(text_node("PublishingRole", '01')) #publisher
pub_node.append(text_node("PublisherName", edition.publisher_name.name))
pubdetail_node.append(text_node("PublishingStatus", '00')) #unspecified
if edition.publication_date:
pubdate_node = etree.SubElement(pubdetail_node, "PublishingDate")
pubdate_node.append(text_node("PublishingDateRole", '01')) #nominal pub date
pubdate_node.append(text_node("Date", edition.publication_date.replace('-','')))
# Product Supply Block
supply_node = etree.SubElement(product_node,"ProductSupply")
market_node = etree.SubElement(supply_node,"Market")
terr_node = etree.SubElement(market_node,"Territory")
terr_node.append(text_node("RegionsIncluded", 'WORLD'))
supply_detail_node = etree.SubElement(supply_node,"SupplyDetail")
supplier_node = etree.SubElement(supply_detail_node,"Supplier")
supplier_node.append(text_node("SupplierRole", '11')) #non-exclusive distributer
supplier_node.append(text_node("SupplierName", 'Unglue.it')) #non-exclusive distributer
for ebook in ebooks:
website_node = etree.SubElement(supplier_node,"Website")
website_node.append(text_node("WebsiteRole", '29')) #full content
website_node.append(text_node("WebsiteDescription", '%s file download' % ebook.format, attrib={'textformat':'06'})) #full content
website_node.append(text_node("WebsiteLink", ebook.download_url)) #full content
supply_detail_node.append(text_node("ProductAvailability", '20')) #Available
price_node = etree.SubElement(supply_detail_node,"Price")
price_node.append(text_node("PriceType", '01')) #retail excluding tax
price_node.append(text_node("PriceAmount", '0.00')) #retail excluding tax
price_node.append(text_node("CurrencyCode", 'USD')) #retail excluding tax
return product_node

View File

@ -45,8 +45,15 @@
{% endif %}
<h3>OPDS Catalog Feeds</h3>
<p>We have a basic implementation of <a href="http://opds-spec.org/specs/opds-catalog-1-1-20110627/">OPDS</a> feeds. You don't need a key to use them. The starting point is <a href="{% url 'opds' %}">{% url 'opds' %}</a></p>
<p>We have a basic implementation of <a href="http://opds-spec.org/specs/opds-catalog-1-1-20110627/">OPDS</a> feeds. You don't need a key to use them. The starting point is <code><a href="{% url 'opds' %}">{{base_url}}{% url 'opds' %}</a></code></p>
<p>There's also an OPDS record available for every work on unglue.it. For example, requesting, <code><a href="{% url 'opds_acqusition' 'all'%}?work=13950">{{base_url}}{% url 'opds_acqusition' 'all'%}?work=13950</a></code> get you to the web page or opds record for <i>A Christmas Carol</i>.</p>
<h3>ONIX Catalog Feeds</h3>
<p>There is an <a href="http://www.editeur.org/12/about-release-3.0/">ONIX 3.0</a> feed corresponding to every facet of our <a href="{% url 'free' %}">free ebook lists</a>. You don't need a key to use them. There is a maximum of 100 books per result you can change with the max parameter. For example, here are the <a href="{% url 'onix' 'by-nc-nd/epub' %}">first hundred CC BY-ND-ND licensed books available in EPUB.</a></p>
<p>There's also an ONIX record available for every free ebook on unglue.it. For example, here is <a href="{% url 'onix_all' %}?work=140086"><i>Issues in Open Research Data</i></a>.</p>
<h3>Identifiers with Content type negotiation</h3>
<p>There's a URI to identify every work used in OPDS feeds. HTTP content negotiation is used for these ids, so requesting <code>application/atom+xml;profile=opds-catalog;kind=acquisition</code> for <code><a href="{% url 'work_identifier' '13950'%}">{{base_url}}{% url 'work_identifier' '13950' %}</a></code> get you to the web page or opds record for <i>A Christmas Carol</i>. requesting <code>text/xml</code> gets you the onix record. Otherwise, you get the normal html page.</p>
{% endblock %}

View File

@ -126,7 +126,11 @@ class ApiTests(TestCase):
r = self.client.get('/api/widget/%s/'%self.work_id)
self.assertEqual(r.status_code, 200)
class OPDSTests(TestCase):
class FeedTests(TestCase):
def setUp(self):
edition = bookloader.add_by_isbn_from_google(isbn='0441007465')
ebook = models.Ebook.objects.create(edition=edition, url='http://example.org/', format='epub', rights='CC BY')
self.test_work_id = edition.work.id
def test_opds(self):
r = self.client.get('/api/opds/creative_commons/')
@ -137,6 +141,18 @@ class OPDSTests(TestCase):
self.assertEqual(r.status_code, 200)
r = self.client.get('/api/opds/active_campaigns/?order_by=title')
self.assertEqual(r.status_code, 200)
r = self.client.get('/api/opds/?work=%s' % self.test_work_id)
self.assertEqual(r.status_code, 200)
def test_nix(self):
r = self.client.get('/api/onix/by/')
self.assertEqual(r.status_code, 200)
r = self.client.get('/api/onix/cc0/')
self.assertEqual(r.status_code, 200)
r = self.client.get('/api/onix/epub/?max=10')
self.assertEqual(r.status_code, 200)
r = self.client.get('/api/onix/?work=%s' % self.test_work_id)
self.assertEqual(r.status_code, 200)
class AllowedRepoTests(TestCase):
def setUp(self):

View File

@ -6,6 +6,7 @@ from django.views.generic.base import TemplateView
from regluit.api import resources
from regluit.api.views import ApiHelpView
from regluit.api.views import OPDSNavigationView, OPDSAcquisitionView
from regluit.api.views import OnixView
v1_api = Api(api_name='v1')
@ -24,6 +25,8 @@ urlpatterns = patterns('',
url(r'^widget/(?P<isbn>\w+)/$','regluit.api.views.widget', name="widget"),
url(r"^opds/$", OPDSNavigationView.as_view(template_name="opds.xml"), name="opds"),
url(r"^opds/(?P<facet>.*)/$", OPDSAcquisitionView.as_view(), name="opds_acqusition"),
url(r"^onix/(?P<facet>.*)/$", OnixView.as_view(), name="onix"),
url(r"^onix/$", OnixView.as_view(), name="onix_all"),
url(r'^id/work/(?P<work_id>\w+)/$', 'regluit.api.views.negotiate_content', name="work_identifier"),
url(r'^loader/yaml$','regluit.api.views.load_yaml', name="load_yaml"),
(r'^', include(v1_api.urls)),

View File

@ -12,11 +12,12 @@ from django.http import (
HttpResponse,
HttpResponseNotFound,
HttpResponseRedirect,
Http404,
)
import regluit.core.isbn
from regluit.core.bookloader import load_from_yaml
from regluit.api import opds
from regluit.api import opds, onix
from regluit.api.models import repo_allowed
from regluit.core import models
@ -45,6 +46,9 @@ def negotiate_content(request,work_id):
if request.META.get('HTTP_ACCEPT', None):
if "opds-catalog" in request.META['HTTP_ACCEPT']:
return HttpResponseRedirect(reverse('opds_acqusition',args=['all'])+'?work='+work_id)
elif "text/xml" in request.META['HTTP_ACCEPT']:
return HttpResponseRedirect(reverse('onix',args=['all'])+'?work='+work_id)
return HttpResponseRedirect(reverse('work', kwargs={'work_id': work_id}))
def widget(request,isbn):
@ -142,3 +146,26 @@ class OPDSAcquisitionView(View):
facet_class = opds.get_facet_class(facet)()
return HttpResponse(facet_class.feed(page,order_by),
content_type="application/atom+xml;profile=opds-catalog;kind=acquisition")
class OnixView(View):
def get(self, request, *args, **kwargs):
work = request.GET.get('work', None)
if work:
try:
work=models.safe_get_work(work)
except models.Work.DoesNotExist:
raise Http404
return HttpResponse(onix.onix_feed_for_work(work),
content_type="text/xml")
facet = kwargs.get('facet', 'all')
if facet:
max = request.GET.get('max', 100)
try:
max = int(max)
except:
max = None
facet_class = opds.get_facet_class(facet)()
return HttpResponse(onix.onix_feed(facet_class, max),
content_type="text/xml")

23121
bisac/__init__.py Normal file

File diff suppressed because it is too large Load Diff

9
bisac/tests.py Normal file
View File

@ -0,0 +1,9 @@
import unittest
from . import Bisac
class TestBisac(unittest.TestCase):
def setUp(self):
self.bisac=Bisac()
def test_code(self):
self.assertEqual(self.bisac.code('Religion'),'REL000000')

View File

@ -112,7 +112,9 @@ class FormatFacetGroup(FacetGroup):
self.facet_name=facet_name
def format_filter(query_set):
return query_set.filter(format=facet_name)
model_filters = {"Ebook": format_filter}
def edition_format_filter(query_set):
return query_set.filter(ebooks__format=facet_name)
model_filters = {"Ebook": format_filter, "Edition": edition_format_filter}
def get_query_set(self):
return self._get_query_set().filter(editions__ebooks__format=self.facet_name)
def template(self):
@ -141,7 +143,9 @@ class LicenseFacetGroup(FacetGroup):
self.license = cc.ccinfo(facet_name)
def license_filter(query_set):
return query_set.filter(rights=cc.ccinfo(facet_name))
model_filters = {"Ebook": license_filter}
def edition_license_filter(query_set):
return query_set.filter(ebooks__rights=cc.ccinfo(facet_name))
model_filters = {"Ebook": license_filter, "Edition": edition_license_filter}
def get_query_set(self):
return self._get_query_set().filter(editions__ebooks__rights=self.license.license)
def template(self):
@ -224,7 +228,9 @@ class PublisherFacetGroup(FacetGroup):
self.publisher = None
def pub_filter(query_set):
return query_set.filter(edition__publisher_name__publisher__id=facet_name[4:])
model_filters = {"Ebook": pub_filter}
def edition_pub_filter(query_set):
return query_set.filter(publisher_name__publisher__id=facet_name[4:])
model_filters = {"Ebook": pub_filter, "Edition": edition_pub_filter }
def get_query_set(self):
return self._get_query_set().filter(editions__publisher_name__publisher=self.publisher)
def template(self):

View File

@ -1902,6 +1902,9 @@ def safe_get_work(work_id):
work = WasWork.objects.get(was = work_id).work
except WasWork.DoesNotExist:
raise Work.DoesNotExist()
except ValueError:
#work_id is not a number
raise Work.DoesNotExist()
return work
FORMAT_CHOICES = (('pdf','PDF'),( 'epub','EPUB'), ('html','HTML'), ('text','TEXT'), ('mobi','MOBI'))

View File

@ -21,6 +21,10 @@
href="/api/opds/{{ path }}/?order_by={{ order_by }}"
type="application/atom+xml;profile=opds-catalog"
title="OPDS Catalog for Unglue.it Free Books" />
<link rel="related"
href="/api/onix/{{ path }}/?max=100"
type="text/xml"
title="ONIX feed for Unglue.it Free Books" />
{% endblock %}
{% block topsection %}
<div id="locationhash">#1</div>