commit
6bd1aec374
|
@ -11,7 +11,7 @@
|
|||
{% if editions %}
|
||||
<ul>
|
||||
{% for edition in editions %}
|
||||
<li> <img src="http://covers.openlibrary.org/b/isbn/{{edition.isbn_10}}-S.jpg" /> {{edition.id}} | {{edition.title}} |
|
||||
<li> <img src="https://covers.openlibrary.org/b/isbn/{{edition.isbn_10}}-S.jpg" /> {{edition.id}} | {{edition.title}} |
|
||||
<a href="{% url 'isbn' isbn=edition.isbn_10 %}">{{edition.isbn_10}}</a> |
|
||||
<a href="{% url 'isbn' isbn=edition.isbn_13 %}">{{edition.isbn_13}}</a>
|
||||
{% endfor %}
|
||||
|
|
|
@ -168,7 +168,7 @@ class ApiHelpView(TemplateView):
|
|||
|
||||
class OPDSNavigationView(TemplateView):
|
||||
json=False
|
||||
# http://stackoverflow.com/a/6867976: secret to how to change content-type
|
||||
# https://stackoverflow.com/a/6867976: secret to how to change content-type
|
||||
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
if json:
|
||||
|
|
|
@ -5,6 +5,7 @@ from django import forms
|
|||
from django.contrib.admin import ModelAdmin
|
||||
from django.contrib.admin import site as admin_site
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from selectable.forms import (
|
||||
AutoCompleteSelectWidget,
|
||||
AutoCompleteSelectField,
|
||||
|
@ -170,12 +171,27 @@ class EbookAdmin(ModelAdmin):
|
|||
exclude = ('edition','user', 'filesize')
|
||||
|
||||
class EbookFileAdmin(ModelAdmin):
|
||||
search_fields = ('ebook__edition__title',) # search by provider using leading url
|
||||
list_display = ('created', 'format', 'edition', 'asking')
|
||||
search_fields = ('ebook__edition__title', 'source') # search by provider using leading url
|
||||
list_display = ('created', 'format', 'ebook_link', 'asking')
|
||||
date_hierarchy = 'created'
|
||||
ordering = ('edition__work',)
|
||||
fields = ('file', 'format','edition', 'asking', 'ebook')
|
||||
readonly_fields = ('file', 'edition', 'asking', 'ebook')
|
||||
fields = ('file', 'format', 'edition_link', 'ebook_link', 'source')
|
||||
readonly_fields = ('file', 'edition_link', 'ebook_link', 'ebook')
|
||||
def edition_link(self, obj):
|
||||
if obj.edition:
|
||||
link = reverse("admin:core_edition_change", args=[obj.edition.id])
|
||||
return u'<a href="%s">%s</a>' % (link,obj.edition)
|
||||
else:
|
||||
return u''
|
||||
def ebook_link(self, obj):
|
||||
if obj.ebook:
|
||||
link = reverse("admin:core_ebook_change", args=[obj.ebook.id])
|
||||
return u'<a href="%s">%s</a>' % (link,obj.ebook)
|
||||
else:
|
||||
return u''
|
||||
edition_link.allow_tags=True
|
||||
ebook_link.allow_tags=True
|
||||
|
||||
|
||||
class WishlistAdmin(ModelAdmin):
|
||||
date_hierarchy = 'created'
|
||||
|
|
|
@ -5,36 +5,40 @@ import json
|
|||
import logging
|
||||
import re
|
||||
import requests
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from regluit.core.validation import test_file
|
||||
|
||||
from datetime import timedelta
|
||||
from itertools import (izip, islice)
|
||||
from xml.etree import ElementTree
|
||||
from urlparse import (urljoin, urlparse)
|
||||
|
||||
|
||||
"""
|
||||
django imports
|
||||
"""
|
||||
# django imports
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django_comments.models import Comment
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Q
|
||||
from django.forms import ValidationError
|
||||
|
||||
from github3 import (login, GitHub)
|
||||
from github3.repos.release import Release
|
||||
|
||||
from gitenberg.metadata.pandata import Pandata
|
||||
from ..marc.models import inverse_marc_rels
|
||||
|
||||
"""
|
||||
regluit imports
|
||||
"""
|
||||
# regluit imports
|
||||
|
||||
import regluit
|
||||
import regluit.core.isbn
|
||||
|
||||
from regluit.core import models
|
||||
from regluit.marc.models import inverse_marc_rels
|
||||
from regluit.utils.localdatetime import now
|
||||
import regluit.core.cc as cc
|
||||
|
||||
from . import cc
|
||||
from . import models
|
||||
from .parameters import WORK_IDENTIFIERS
|
||||
from .validation import identifier_cleaner
|
||||
from .loaders.scrape import BaseScraper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
request_log = logging.getLogger("requests")
|
||||
|
@ -62,7 +66,7 @@ def add_by_oclc_from_google(oclc):
|
|||
if not results.has_key('items') or len(results['items']) == 0:
|
||||
logger.warn("no google hits for %s" , oclc)
|
||||
return None
|
||||
|
||||
|
||||
try:
|
||||
e = add_by_googlebooks_id(results['items'][0]['id'], results=results['items'][0])
|
||||
models.Identifier(type='oclc', value=oclc, edition=e, work=e.work).save()
|
||||
|
@ -75,13 +79,10 @@ def add_by_oclc_from_google(oclc):
|
|||
|
||||
def valid_isbn(isbn):
|
||||
try:
|
||||
isbn = regluit.core.isbn.ISBN(isbn)
|
||||
return identifier_cleaner('isbn')(isbn)
|
||||
except:
|
||||
logger.exception("invalid isbn: %s", isbn)
|
||||
return None
|
||||
if not isbn.valid:
|
||||
return None
|
||||
return isbn.to_string()
|
||||
|
||||
def add_by_isbn(isbn, work=None, language='xx', title=''):
|
||||
if not isbn:
|
||||
|
@ -94,11 +95,11 @@ def add_by_isbn(isbn, work=None, language='xx', title=''):
|
|||
return None
|
||||
if e:
|
||||
return e
|
||||
|
||||
|
||||
logger.info("null came back from add_by_isbn_from_google: %s", isbn)
|
||||
|
||||
# if there's a a title, we want to create stub editions and
|
||||
# works, even if google doesn't know about it # but if it's not valid,
|
||||
# if there's a a title, we want to create stub editions and
|
||||
# works, even if google doesn't know about it # but if it's not valid,
|
||||
# forget it!
|
||||
|
||||
if work:
|
||||
|
@ -107,12 +108,12 @@ def add_by_isbn(isbn, work=None, language='xx', title=''):
|
|||
return None
|
||||
if not title:
|
||||
return None
|
||||
|
||||
|
||||
isbn = valid_isbn(isbn)
|
||||
if not isbn:
|
||||
return None
|
||||
|
||||
if not language or language == 'xx': # don't add unknown language
|
||||
|
||||
if not language or language == 'xx': # don't add unknown language
|
||||
# we don't know the language ->'xx'
|
||||
work = models.Work(title=title, language='xx')
|
||||
work.save()
|
||||
|
@ -137,7 +138,7 @@ def get_google_isbn_results(isbn):
|
|||
return None
|
||||
else:
|
||||
return results
|
||||
|
||||
|
||||
def add_ebooks(item, edition):
|
||||
access_info = item.get('accessInfo')
|
||||
if access_info:
|
||||
|
@ -150,8 +151,8 @@ def add_ebooks(item, edition):
|
|||
ebook.save()
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
pdf = access_info.get('pdf')
|
||||
if pdf and pdf.get('downloadLink'):
|
||||
ebook = models.Ebook(edition=edition, format='pdf',
|
||||
|
@ -170,68 +171,68 @@ def update_edition(edition):
|
|||
|
||||
# if there is no ISBN associated with edition, just return the input edition
|
||||
try:
|
||||
isbn=edition.identifiers.filter(type='isbn')[0].value
|
||||
isbn = edition.identifiers.filter(type='isbn')[0].value
|
||||
except (models.Identifier.DoesNotExist, IndexError):
|
||||
return edition
|
||||
|
||||
# do a Google Books lookup on the isbn associated with the edition (there should be either 0 or 1 isbns associated
|
||||
# with an edition because of integrity constraint in Identifier)
|
||||
|
||||
|
||||
# if we get some data about this isbn back from Google, update the edition data accordingly
|
||||
results=get_google_isbn_results(isbn)
|
||||
results = get_google_isbn_results(isbn)
|
||||
if not results:
|
||||
return edition
|
||||
item=results['items'][0]
|
||||
googlebooks_id=item['id']
|
||||
item = results['items'][0]
|
||||
googlebooks_id = item['id']
|
||||
d = item['volumeInfo']
|
||||
if d.has_key('title'):
|
||||
title = d['title']
|
||||
else:
|
||||
title=''
|
||||
if len(title)==0:
|
||||
title = ''
|
||||
if len(title) == 0:
|
||||
# need a title to make an edition record; some crap records in GB. use title from parent if available
|
||||
title=edition.work.title
|
||||
title = edition.work.title
|
||||
|
||||
# check for language change
|
||||
language = d['language']
|
||||
# allow variants in main language (e.g., 'zh-tw')
|
||||
if len(language)>5:
|
||||
language= language[0:5]
|
||||
|
||||
# if the language of the edition no longer matches that of the parent work, attach edition to the
|
||||
if len(language) > 5:
|
||||
language = language[0:5]
|
||||
|
||||
# if the language of the edition no longer matches that of the parent work, attach edition to the
|
||||
if edition.work.language != language:
|
||||
logger.info("reconnecting %s since it is %s instead of %s" %(googlebooks_id, language, edition.work.language))
|
||||
old_work=edition.work
|
||||
|
||||
old_work = edition.work
|
||||
|
||||
new_work = models.Work(title=title, language=language)
|
||||
new_work.save()
|
||||
edition.work = new_work
|
||||
edition.save()
|
||||
for identifier in edition.identifiers.all():
|
||||
logger.info("moving identifier %s" % identifier.value)
|
||||
identifier.work=new_work
|
||||
identifier.work = new_work
|
||||
identifier.save()
|
||||
if old_work and old_work.editions.count()==0:
|
||||
#a dangling work; make sure nothing else is attached!
|
||||
merge_works(new_work,old_work)
|
||||
|
||||
merge_works(new_work, old_work)
|
||||
|
||||
# update the edition
|
||||
edition.title = title
|
||||
edition.publication_date = d.get('publishedDate', '')
|
||||
edition.set_publisher(d.get('publisher'))
|
||||
edition.save()
|
||||
|
||||
|
||||
# create identifier if needed
|
||||
models.Identifier.get_or_add(type='goog',value=googlebooks_id,edition=edition,work=edition.work)
|
||||
models.Identifier.get_or_add(type='goog', value=googlebooks_id, edition=edition, work=edition.work)
|
||||
|
||||
for a in d.get('authors', []):
|
||||
edition.add_author(a)
|
||||
|
||||
|
||||
add_ebooks(item, edition)
|
||||
|
||||
|
||||
return edition
|
||||
|
||||
|
||||
|
||||
def add_by_isbn_from_google(isbn, work=None):
|
||||
"""add a book to the UnglueIt database from google based on ISBN. The work parameter
|
||||
is optional, and if not supplied the edition will be associated with
|
||||
|
@ -241,16 +242,16 @@ def add_by_isbn_from_google(isbn, work=None):
|
|||
return None
|
||||
if len(isbn)==10:
|
||||
isbn = regluit.core.isbn.convert_10_to_13(isbn)
|
||||
|
||||
|
||||
|
||||
# check if we already have this isbn
|
||||
edition = get_edition_by_id(type='isbn',value=isbn)
|
||||
edition = get_edition_by_id(type='isbn', value=isbn)
|
||||
if edition:
|
||||
edition.new = False
|
||||
return edition
|
||||
|
||||
logger.info("adding new book by isbn %s", isbn)
|
||||
results=get_google_isbn_results(isbn)
|
||||
results = get_google_isbn_results(isbn)
|
||||
if results:
|
||||
try:
|
||||
return add_by_googlebooks_id(results['items'][0]['id'], work=work, results=results['items'][0], isbn=isbn)
|
||||
|
@ -261,30 +262,30 @@ def add_by_isbn_from_google(isbn, work=None):
|
|||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_work_by_id(type,value):
|
||||
|
||||
def get_work_by_id(type, value):
|
||||
if value:
|
||||
try:
|
||||
return models.Identifier.objects.get(type=type,value=value).work
|
||||
return models.Identifier.objects.get(type=type, value=value).work
|
||||
except models.Identifier.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_edition_by_id(type,value):
|
||||
def get_edition_by_id(type, value):
|
||||
if value:
|
||||
try:
|
||||
return models.Identifier.objects.get(type=type,value=value).edition
|
||||
return models.Identifier.objects.get(type=type, value=value).edition
|
||||
except models.Identifier.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
def add_by_googlebooks_id(googlebooks_id, work=None, results=None, isbn=None):
|
||||
"""add a book to the UnglueIt database based on the GoogleBooks ID. The
|
||||
work parameter is optional, and if not supplied the edition will be
|
||||
associated with a stub work. isbn can be passed because sometimes passed data won't include it
|
||||
|
||||
work parameter is optional, and if not supplied the edition will be
|
||||
associated with a stub work. isbn can be passed because sometimes passed data won't include it
|
||||
|
||||
"""
|
||||
isbn = valid_isbn(isbn)
|
||||
|
||||
|
||||
# don't ping google again if we already know about the edition
|
||||
try:
|
||||
edition = models.Identifier.objects.get(type='goog', value=googlebooks_id).edition
|
||||
|
@ -292,45 +293,45 @@ def add_by_googlebooks_id(googlebooks_id, work=None, results=None, isbn=None):
|
|||
if isbn:
|
||||
# check that the isbn is in db; if not, then there are two isbns for the edition
|
||||
try:
|
||||
isbn_edition = models.Identifier.objects.get(type='isbn', value=isbn).edition
|
||||
models.Identifier.objects.get(type='isbn', value=isbn).edition
|
||||
# not going to worry about isbn_edition != edition
|
||||
except models.Identifier.DoesNotExist:
|
||||
models.Identifier.objects.create(type='isbn', value=isbn, edition=edition, work=edition.work)
|
||||
return edition
|
||||
except models.Identifier.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
# if google has been queried by caller, don't call again
|
||||
if results:
|
||||
item =results
|
||||
item = results
|
||||
else:
|
||||
logger.info("loading metadata from google for %s", googlebooks_id)
|
||||
url = "https://www.googleapis.com/books/v1/volumes/%s" % googlebooks_id
|
||||
item = _get_json(url)
|
||||
item = _get_json(url)
|
||||
d = item['volumeInfo']
|
||||
|
||||
|
||||
if d.has_key('title'):
|
||||
title = d['title']
|
||||
else:
|
||||
title=''
|
||||
title = ''
|
||||
if len(title)==0:
|
||||
# need a title to make an edition record; some crap records in GB. use title from parent if available
|
||||
if work:
|
||||
title=work.title
|
||||
title = work.title
|
||||
else:
|
||||
return None
|
||||
|
||||
# don't add the edition to a work with a different language
|
||||
# https://www.pivotaltracker.com/story/show/17234433
|
||||
language = d['language']
|
||||
if len(language)>5:
|
||||
language= language[0:5]
|
||||
if len(language) > 5:
|
||||
language = language[0:5]
|
||||
if work and work.language != language:
|
||||
logger.info("not connecting %s since it is %s instead of %s" %
|
||||
(googlebooks_id, language, work.language))
|
||||
work = None
|
||||
# isbn = None
|
||||
if not isbn:
|
||||
if not isbn:
|
||||
for i in d.get('industryIdentifiers', []):
|
||||
if i['type'] == 'ISBN_10' and not isbn:
|
||||
isbn = regluit.core.isbn.convert_10_to_13(i['identifier'])
|
||||
|
@ -341,7 +342,7 @@ def add_by_googlebooks_id(googlebooks_id, work=None, results=None, isbn=None):
|
|||
if work:
|
||||
work.new = False
|
||||
if isbn and not work:
|
||||
work = get_work_by_id(type='isbn',value=isbn)
|
||||
work = get_work_by_id(type='isbn', value=isbn)
|
||||
if work:
|
||||
work.new = False
|
||||
if not work:
|
||||
|
@ -360,7 +361,7 @@ def add_by_googlebooks_id(googlebooks_id, work=None, results=None, isbn=None):
|
|||
return e
|
||||
except models.Identifier.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
# because this is a new google id, we have to create a new edition
|
||||
e = models.Edition(work=work)
|
||||
e.title = title
|
||||
|
@ -368,23 +369,23 @@ def add_by_googlebooks_id(googlebooks_id, work=None, results=None, isbn=None):
|
|||
e.set_publisher(d.get('publisher'))
|
||||
e.save()
|
||||
e.new = True
|
||||
|
||||
|
||||
# create identifier where needed
|
||||
models.Identifier(type='goog',value=googlebooks_id,edition=e,work=work).save()
|
||||
models.Identifier(type='goog', value=googlebooks_id, edition=e, work=work).save()
|
||||
if isbn:
|
||||
models.Identifier.get_or_add(type='isbn',value=isbn,edition=e,work=work)
|
||||
models.Identifier.get_or_add(type='isbn', value=isbn, edition=e, work=work)
|
||||
|
||||
for a in d.get('authors', []):
|
||||
a, created = models.Author.objects.get_or_create(name=a)
|
||||
e.add_author(a)
|
||||
|
||||
add_ebooks(item, e)
|
||||
|
||||
|
||||
return e
|
||||
|
||||
|
||||
def relate_isbn(isbn, cluster_size=1):
|
||||
"""add a book by isbn and then see if there's an existing work to add it to so as to make a cluster
|
||||
"""add a book by isbn and then see if there's an existing work to add it to so as to make a cluster
|
||||
bigger than cluster_size.
|
||||
"""
|
||||
logger.info("finding a related work for %s", isbn)
|
||||
|
@ -412,7 +413,7 @@ def relate_isbn(isbn, cluster_size=1):
|
|||
elif related_edition.work.id != edition.work.id:
|
||||
logger.debug("merge_works path 1 %s %s", edition.work.id, related_edition.work.id )
|
||||
merge_works(related_edition.work, edition.work)
|
||||
|
||||
|
||||
if related_edition.work.editions.count()>cluster_size:
|
||||
return related_edition.work
|
||||
return edition.work
|
||||
|
@ -423,7 +424,7 @@ def add_related(isbn):
|
|||
"""
|
||||
# make sure the seed edition is there
|
||||
logger.info("adding related editions for %s", isbn)
|
||||
|
||||
|
||||
new_editions = []
|
||||
|
||||
edition = add_by_isbn(isbn)
|
||||
|
@ -456,7 +457,7 @@ def add_related(isbn):
|
|||
if other_editions.has_key(related_language):
|
||||
other_editions[related_language].append(related_edition)
|
||||
else:
|
||||
other_editions[related_language]=[related_edition]
|
||||
other_editions[related_language] = [related_edition]
|
||||
|
||||
# group the other language editions together
|
||||
for lang_group in other_editions.itervalues():
|
||||
|
@ -470,16 +471,16 @@ def add_related(isbn):
|
|||
logger.debug("merge_works path 2 %s %s", lang_edition.work.id, w.id )
|
||||
merge_works(lang_edition.work, w)
|
||||
models.WorkRelation.objects.get_or_create(to_work=lang_edition.work, from_work=work, relation='translation')
|
||||
|
||||
|
||||
return new_editions
|
||||
|
||||
|
||||
|
||||
def thingisbn(isbn):
|
||||
"""given an ISBN return a list of related edition ISBNs, according to
|
||||
"""given an ISBN return a list of related edition ISBNs, according to
|
||||
Library Thing. (takes isbn_10 or isbn_13, returns isbn_10, except for 979 isbns, which come back as isbn_13')
|
||||
"""
|
||||
logger.info("looking up %s at ThingISBN" , isbn)
|
||||
url = "http://www.librarything.com/api/thingISBN/%s" % isbn
|
||||
url = "https://www.librarything.com/api/thingISBN/%s" % isbn
|
||||
xml = requests.get(url, headers={"User-Agent": settings.USER_AGENT}).content
|
||||
doc = ElementTree.fromstring(xml)
|
||||
return [e.text for e in doc.findall('isbn')]
|
||||
|
@ -546,20 +547,20 @@ def merge_works(w1, w2, user=None):
|
|||
if subject not in w1.subjects.all():
|
||||
w1.subjects.add(subject)
|
||||
|
||||
|
||||
|
||||
w2.delete()
|
||||
|
||||
|
||||
def detach_edition(e):
|
||||
"""will detach edition from its work, creating a new stub work. if remerge=true, will see if there's another work to attach to
|
||||
"""
|
||||
logger.info("splitting edition %s from %s", e, e.work)
|
||||
w = models.Work(title=e.title, language = e.work.language)
|
||||
w.save()
|
||||
|
||||
|
||||
for identifier in e.identifiers.all():
|
||||
identifier.work = w
|
||||
identifier.save()
|
||||
|
||||
|
||||
e.work = w
|
||||
e.save()
|
||||
|
||||
|
@ -567,11 +568,11 @@ def despam_description(description):
|
|||
""" a lot of descriptions from openlibrary have free-book promotion text; this removes some of it."""
|
||||
if description.find("GeneralBooksClub.com")>-1 or description.find("AkashaPublishing.Com")>-1:
|
||||
return ""
|
||||
pieces=description.split("1stWorldLibrary.ORG -")
|
||||
if len(pieces)>1:
|
||||
pieces = description.split("1stWorldLibrary.ORG -")
|
||||
if len(pieces) > 1:
|
||||
return pieces[1]
|
||||
pieces=description.split("a million books for free.")
|
||||
if len(pieces)>1:
|
||||
pieces = description.split("a million books for free.")
|
||||
if len(pieces) > 1:
|
||||
return pieces[1]
|
||||
return description
|
||||
|
||||
|
@ -579,18 +580,18 @@ def add_openlibrary(work, hard_refresh = False):
|
|||
if (not hard_refresh) and work.openlibrary_lookup is not None:
|
||||
# don't hit OL if we've visited in the past month or so
|
||||
if now()- work.openlibrary_lookup < timedelta(days=30):
|
||||
return
|
||||
return
|
||||
work.openlibrary_lookup = now()
|
||||
work.save()
|
||||
|
||||
# find the first ISBN match in OpenLibrary
|
||||
logger.info("looking up openlibrary data for work %s", work.id)
|
||||
found = False
|
||||
|
||||
e = None # openlibrary edition json
|
||||
w = None # openlibrary work json
|
||||
|
||||
# get the 1st openlibrary match by isbn that has an associated work
|
||||
url = "http://openlibrary.org/api/books"
|
||||
url = "https://openlibrary.org/api/books"
|
||||
params = {"format": "json", "jscmd": "details"}
|
||||
subjects = []
|
||||
for edition in work.editions.all():
|
||||
|
@ -605,29 +606,29 @@ def add_openlibrary(work, hard_refresh = False):
|
|||
if e[isbn_key].has_key('details'):
|
||||
if e[isbn_key]['details'].has_key('oclc_numbers'):
|
||||
for oclcnum in e[isbn_key]['details']['oclc_numbers']:
|
||||
models.Identifier.get_or_add(type='oclc',value=oclcnum,work=work, edition=edition)
|
||||
models.Identifier.get_or_add(type='oclc', value=oclcnum, work=work, edition=edition)
|
||||
if e[isbn_key]['details'].has_key('identifiers'):
|
||||
ids = e[isbn_key]['details']['identifiers']
|
||||
if ids.has_key('goodreads'):
|
||||
models.Identifier.get_or_add(type='gdrd',value=ids['goodreads'][0],work=work,edition=edition)
|
||||
models.Identifier.get_or_add(type='gdrd', value=ids['goodreads'][0], work=work, edition=edition)
|
||||
if ids.has_key('librarything'):
|
||||
models.Identifier.get_or_add(type='ltwk',value=ids['librarything'][0],work=work)
|
||||
models.Identifier.get_or_add(type='ltwk', value=ids['librarything'][0], work=work)
|
||||
if ids.has_key('google'):
|
||||
models.Identifier.get_or_add(type='goog',value=ids['google'][0],work=work)
|
||||
models.Identifier.get_or_add(type='goog', value=ids['google'][0], work=work)
|
||||
if ids.has_key('project_gutenberg'):
|
||||
models.Identifier.get_or_add(type='gute',value=ids['project_gutenberg'][0],work=work)
|
||||
models.Identifier.get_or_add(type='gute', value=ids['project_gutenberg'][0], work=work)
|
||||
if e[isbn_key]['details'].has_key('works'):
|
||||
work_key = e[isbn_key]['details']['works'].pop(0)['key']
|
||||
logger.info("got openlibrary work %s for isbn %s", work_key, isbn_key)
|
||||
models.Identifier.get_or_add(type='olwk',value=work_key,work=work)
|
||||
models.Identifier.get_or_add(type='olwk', value=work_key, work=work)
|
||||
try:
|
||||
w = _get_json("http://openlibrary.org" + work_key,type='ol')
|
||||
w = _get_json("https://openlibrary.org" + work_key, type='ol')
|
||||
if w.has_key('description'):
|
||||
description=w['description']
|
||||
if isinstance(description,dict):
|
||||
description = w['description']
|
||||
if isinstance(description, dict):
|
||||
if description.has_key('value'):
|
||||
description=description['value']
|
||||
description=despam_description(description)
|
||||
description = description['value']
|
||||
description = despam_description(description)
|
||||
if not work.description or work.description.startswith('{') or len(description) > len(work.description):
|
||||
work.description = description
|
||||
work.save()
|
||||
|
@ -637,7 +638,7 @@ def add_openlibrary(work, hard_refresh = False):
|
|||
logger.exception("OL lookup failed for %s", work_key)
|
||||
if not subjects:
|
||||
logger.warn("unable to find work %s at openlibrary", work.id)
|
||||
return
|
||||
return
|
||||
|
||||
# add the subjects to the Work
|
||||
for s in subjects:
|
||||
|
@ -645,7 +646,7 @@ def add_openlibrary(work, hard_refresh = False):
|
|||
logger.info("adding subject %s to work %s", s, work.id)
|
||||
subject, created = models.Subject.objects.get_or_create(name=s)
|
||||
work.subjects.add(subject)
|
||||
|
||||
|
||||
work.save()
|
||||
|
||||
def valid_xml_char_ordinal(c):
|
||||
|
@ -668,12 +669,12 @@ def valid_subject( subject_name ):
|
|||
if num_commas > 2:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _get_json(url, params={}, type='gb'):
|
||||
# TODO: should X-Forwarded-For change based on the request from client?
|
||||
headers = {'User-Agent': settings.USER_AGENT,
|
||||
headers = {'User-Agent': settings.USER_AGENT,
|
||||
'Accept': 'application/json',
|
||||
'X-Forwarded-For': '69.174.114.214'}
|
||||
if type == 'gb':
|
||||
|
@ -690,11 +691,11 @@ def _get_json(url, params={}, type='gb'):
|
|||
|
||||
|
||||
def load_gutenberg_edition(title, gutenberg_etext_id, ol_work_id, seed_isbn, url, format, license, lang, publication_date):
|
||||
|
||||
|
||||
# let's start with instantiating the relevant Work and Edition if they don't already exist
|
||||
|
||||
|
||||
try:
|
||||
work = models.Identifier.objects.get(type='olwk',value=ol_work_id).work
|
||||
work = models.Identifier.objects.get(type='olwk', value=ol_work_id).work
|
||||
except models.Identifier.DoesNotExist: # try to find an Edition with the seed_isbn and use that work to hang off of
|
||||
sister_edition = add_by_isbn(seed_isbn)
|
||||
if sister_edition.new:
|
||||
|
@ -703,152 +704,143 @@ def load_gutenberg_edition(title, gutenberg_etext_id, ol_work_id, seed_isbn, url
|
|||
work = sister_edition.work
|
||||
# attach the olwk identifier to this work if it's not none.
|
||||
if ol_work_id is not None:
|
||||
work_id = models.Identifier.get_or_add(type='olwk',value=ol_work_id, work=work)
|
||||
models.Identifier.get_or_add(type='olwk', value=ol_work_id, work=work)
|
||||
|
||||
# Now pull out any existing Gutenberg editions tied to the work with the proper Gutenberg ID
|
||||
try:
|
||||
edition = models.Identifier.objects.get( type='gtbg', value=gutenberg_etext_id ).edition
|
||||
edition = models.Identifier.objects.get(type='gtbg', value=gutenberg_etext_id ).edition
|
||||
except models.Identifier.DoesNotExist:
|
||||
edition = models.Edition()
|
||||
edition.title = title
|
||||
edition.work = work
|
||||
|
||||
|
||||
edition.save()
|
||||
edition_id = models.Identifier.get_or_add(type='gtbg',value=gutenberg_etext_id, edition=edition, work=work)
|
||||
|
||||
models.Identifier.get_or_add(type='gtbg', value=gutenberg_etext_id, edition=edition, work=work)
|
||||
|
||||
# check to see whether the Edition hasn't already been loaded first
|
||||
# search by url
|
||||
ebooks = models.Ebook.objects.filter(url=url)
|
||||
|
||||
|
||||
# format: what's the controlled vocab? -- from Google -- alternative would be mimetype
|
||||
|
||||
if len(ebooks):
|
||||
|
||||
if len(ebooks):
|
||||
ebook = ebooks[0]
|
||||
elif len(ebooks) == 0: # need to create new ebook
|
||||
ebook = models.Ebook()
|
||||
|
||||
if len(ebooks) > 1:
|
||||
warnings.warn("There is more than one Ebook matching url {0}".format(url))
|
||||
|
||||
|
||||
logger.warning("There is more than one Ebook matching url {0}".format(url))
|
||||
|
||||
|
||||
ebook.format = format
|
||||
ebook.provider = 'Project Gutenberg'
|
||||
ebook.url = url
|
||||
ebook.rights = license
|
||||
|
||||
|
||||
# is an Ebook instantiable without a corresponding Edition? (No, I think)
|
||||
|
||||
|
||||
ebook.edition = edition
|
||||
ebook.save()
|
||||
|
||||
|
||||
return ebook
|
||||
|
||||
def add_missing_isbn_to_editions(max_num=None, confirm=False):
|
||||
"""For each of the editions with Google Books ids, do a lookup and attach ISBNs. Set confirm to True to check db changes made correctly"""
|
||||
logger.info("Number of editions with Google Books IDs but not ISBNs (before): %d",
|
||||
models.Edition.objects.filter(identifiers__type='goog').exclude(identifiers__type='isbn').count())
|
||||
|
||||
from regluit.experimental import bookdata
|
||||
|
||||
gb = bookdata.GoogleBooks(key=settings.GOOGLE_BOOKS_API_KEY)
|
||||
|
||||
new_isbns = []
|
||||
google_id_not_found = []
|
||||
no_isbn_found = []
|
||||
editions_to_merge = []
|
||||
exceptions = []
|
||||
|
||||
|
||||
for (i, ed) in enumerate(islice(models.Edition.objects.filter(identifiers__type='goog').exclude(identifiers__type='isbn'), max_num)):
|
||||
try:
|
||||
g_id = ed.identifiers.get(type='goog').value
|
||||
except Exception, e:
|
||||
# we might get an exception if there is, for example, more than one Google id attached to this Edition
|
||||
logger.exception("add_missing_isbn_to_editions for edition.id %s: %s", ed.id, e)
|
||||
exceptions.append((ed.id, e))
|
||||
continue
|
||||
|
||||
# try to get ISBN from Google Books
|
||||
try:
|
||||
vol_id = gb.volumeid(g_id)
|
||||
if vol_id is None:
|
||||
google_id_not_found.append((ed.id, g_id))
|
||||
logger.debug("g_id not found: %s", g_id)
|
||||
else:
|
||||
isbn = vol_id.get('isbn')
|
||||
logger.info("g_id, isbn: %s %s", g_id, isbn)
|
||||
if isbn is not None:
|
||||
# check to see whether the isbn is actually already in the db but attached to another Edition
|
||||
existing_isbn_ids = models.Identifier.objects.filter(type='isbn', value=isbn)
|
||||
if len(existing_isbn_ids):
|
||||
# don't try to merge editions right now, just note the need to merge
|
||||
ed2 = existing_isbn_ids[0].edition
|
||||
editions_to_merge.append((ed.id, g_id, isbn, ed2.id))
|
||||
else:
|
||||
new_id = models.Identifier(type='isbn', value=isbn, edition=ed, work=ed.work)
|
||||
new_id.save()
|
||||
new_isbns.append((ed.id, g_id, isbn))
|
||||
else:
|
||||
no_isbn_found.append((ed.id, g_id, None))
|
||||
except Exception, e:
|
||||
logger.exception("add_missing_isbn_to_editions for edition.id %s: %s", ed.id, e)
|
||||
exceptions.append((ed.id, g_id, None, e))
|
||||
|
||||
logger.info("Number of editions with Google Books IDs but not ISBNs (after): %d",
|
||||
models.Edition.objects.filter(identifiers__type='goog').exclude(identifiers__type='isbn').count())
|
||||
|
||||
ok = None
|
||||
|
||||
if confirm:
|
||||
ok = True
|
||||
for (ed_id, g_id, isbn) in new_isbns:
|
||||
if models.Edition.objects.get(id=ed_id).identifiers.get(type='isbn').value != isbn:
|
||||
ok = False
|
||||
break
|
||||
|
||||
return {
|
||||
'new_isbns': new_isbns,
|
||||
'no_isbn_found': no_isbn_found,
|
||||
'editions_to_merge': editions_to_merge,
|
||||
'exceptions': exceptions,
|
||||
'google_id_not_found': google_id_not_found,
|
||||
'confirm': ok
|
||||
}
|
||||
|
||||
|
||||
class LookupFailure(Exception):
|
||||
pass
|
||||
|
||||
IDTABLE = [('librarything','ltwk'),('goodreads','gdrd'),('openlibrary','olwk'),('gutenberg','gtbg'),('isbn','isbn'),('oclc','oclc'),('edition_id','edid') ]
|
||||
IDTABLE = [('librarything', 'ltwk'), ('goodreads', 'gdrd'), ('openlibrary', 'olwk'),
|
||||
('gutenberg', 'gtbg'), ('isbn', 'isbn'), ('oclc', 'oclc'),
|
||||
('edition_id', 'edid'), ('googlebooks', 'goog'), ('doi', 'doi'),
|
||||
]
|
||||
|
||||
def unreverse(name):
|
||||
if not ',' in name:
|
||||
return name
|
||||
(last, rest) = name.split(',', 1)
|
||||
if not ',' in rest:
|
||||
return '%s %s' % (rest.strip(),last.strip())
|
||||
return '%s %s' % (rest.strip(), last.strip())
|
||||
(first, rest) = rest.split(',', 1)
|
||||
return '%s %s, %s' % (first.strip(),last.strip(),rest.strip())
|
||||
|
||||
return '%s %s, %s' % (first.strip(), last.strip(), rest.strip())
|
||||
|
||||
|
||||
def load_from_yaml(yaml_url, test_mode=False):
|
||||
"""
|
||||
This really should be called 'load_from_github_yaml'
|
||||
|
||||
if mock_ebook is True, don't construct list of ebooks from a release -- rather use an epub
|
||||
"""
|
||||
all_metadata = Pandata(yaml_url)
|
||||
loader = GithubLoader(yaml_url)
|
||||
for metadata in all_metadata.get_edition_list():
|
||||
edition = loader.load_from_pandata(metadata)
|
||||
loader.load_ebooks(metadata, edition, test_mode)
|
||||
return edition.work.id if edition else None
|
||||
|
||||
def edition_for_ident(id_type, id_value):
|
||||
print 'returning edition for {}: {}'.format(id_type, id_value)
|
||||
for ident in models.Identifier.objects.filter(type=id_type, value=id_value):
|
||||
return ident.edition if ident.edition else ident.work.editions[0]
|
||||
|
||||
def edition_for_etype(etype, metadata, default=None):
|
||||
'''
|
||||
assumes the metadata contains the isbn_etype attributes, and that the editions have been created.
|
||||
etype is 'epub', 'pdf', etc.
|
||||
'''
|
||||
isbn = metadata.identifiers.get('isbn_{}'.format(etype), None)
|
||||
if not isbn:
|
||||
isbn = metadata.identifiers.get('isbn_electronic', None)
|
||||
if isbn:
|
||||
return edition_for_ident('isbn', isbn)
|
||||
else:
|
||||
if default:
|
||||
return default
|
||||
# just return some edition
|
||||
for key in metadata.identifiers.keys():
|
||||
return edition_for_ident(key, metadata.identifiers[key])
|
||||
for key in metadata.edition_identifiers.keys():
|
||||
return edition_for_ident(key, metadata.identifiers[key])
|
||||
|
||||
MATCH_LICENSE = re.compile(r'creativecommons.org/licenses/([^/]+)/')
|
||||
|
||||
def load_ebookfile(url, etype):
|
||||
'''
|
||||
return a ContentFile if a new ebook has been loaded
|
||||
'''
|
||||
ebfs = models.EbookFile.objects.filter(source=url)
|
||||
if ebfs:
|
||||
return None
|
||||
try:
|
||||
r = requests.get(url)
|
||||
contentfile = ContentFile(r.content)
|
||||
test_file(contentfile, etype)
|
||||
return contentfile
|
||||
except IOError, e:
|
||||
logger.error(u'could not open {}'.format(url))
|
||||
except ValidationError, e:
|
||||
logger.error(u'downloaded {} was not a valid {}'.format(url, etype))
|
||||
|
||||
class BasePandataLoader(object):
|
||||
def __init__(self, url):
|
||||
self.base_url = url
|
||||
|
||||
def load_from_pandata(self, metadata, work=None):
|
||||
''' metadata is a Pandata object'''
|
||||
|
||||
#find an work to associate
|
||||
work = edition = None
|
||||
edition = None
|
||||
has_ed_id = False
|
||||
if metadata.url:
|
||||
new_ids = [('http','http', metadata.url)]
|
||||
new_ids = [('http', 'http', metadata.url)]
|
||||
else:
|
||||
new_ids = []
|
||||
for (identifier, id_code) in IDTABLE:
|
||||
# note that the work chosen is the last associated
|
||||
value = metadata.edition_identifiers.get(identifier,None)
|
||||
value = metadata.edition_identifiers.get(identifier, None)
|
||||
value = identifier_cleaner(id_code)(value)
|
||||
if not value:
|
||||
value = metadata.identifiers.get(identifier,None)
|
||||
value = metadata.identifiers.get(identifier, None)
|
||||
if value:
|
||||
if id_code not in WORK_IDENTIFIERS:
|
||||
has_ed_id = True
|
||||
value = value[0] if isinstance(value, list) else value
|
||||
try:
|
||||
id = models.Identifier.objects.get(type=id_code, value=value)
|
||||
|
@ -856,29 +848,32 @@ def load_from_yaml(yaml_url, test_mode=False):
|
|||
if id.edition and not edition:
|
||||
edition = id.edition
|
||||
except models.Identifier.DoesNotExist:
|
||||
new_ids.append((identifier, id_code, value))
|
||||
|
||||
|
||||
if id_code != 'edid' or not has_ed_id: #last in loop
|
||||
# only need to create edid if there is no edition id for the edition
|
||||
new_ids.append((identifier, id_code, value))
|
||||
|
||||
|
||||
if not work:
|
||||
work = models.Work.objects.create(title=metadata.title, language=metadata.language)
|
||||
if not edition:
|
||||
edition = models.Edition.objects.create(title=metadata.title, work=work)
|
||||
for (identifier, id_code, value) in new_ids:
|
||||
models.Identifier.set(type=id_code, value=value, edition=edition if id_code in ('isbn', 'oclc','edid') else None, work=work)
|
||||
models.Identifier.set(type=id_code, value=value, edition=edition if id_code not in WORK_IDENTIFIERS else None, work=work)
|
||||
if metadata.publisher: #always believe yaml
|
||||
edition.set_publisher(metadata.publisher)
|
||||
if metadata.publication_date: #always believe yaml
|
||||
edition.publication_date = metadata.publication_date
|
||||
if metadata.description and len(metadata.description)>len(work.description): #be careful about overwriting the work description
|
||||
if metadata.description and len(metadata.description) > len(work.description):
|
||||
#be careful about overwriting the work description
|
||||
work.description = metadata.description
|
||||
if metadata.creator: #always believe yaml
|
||||
edition.authors.clear()
|
||||
for key in metadata.creator.keys():
|
||||
creators=metadata.creator[key]
|
||||
rel_code=inverse_marc_rels.get(key,'aut')
|
||||
creators = creators if isinstance(creators,list) else [creators]
|
||||
creators = metadata.creator[key]
|
||||
rel_code = inverse_marc_rels.get(key, 'aut')
|
||||
creators = creators if isinstance(creators, list) else [creators]
|
||||
for creator in creators:
|
||||
edition.add_author(unreverse(creator.get('agent_name','')),relation=rel_code)
|
||||
edition.add_author(unreverse(creator.get('agent_name', '')), relation=rel_code)
|
||||
for yaml_subject in metadata.subjects: #always add yaml subjects (don't clear)
|
||||
if isinstance(yaml_subject, tuple):
|
||||
(authority, heading) = yaml_subject
|
||||
|
@ -894,21 +889,63 @@ def load_from_yaml(yaml_url, test_mode=False):
|
|||
# the default edition uses the first cover in covers.
|
||||
for cover in metadata.covers:
|
||||
if cover.get('image_path', False):
|
||||
edition.cover_image=urljoin(yaml_url,cover['image_path'])
|
||||
edition.cover_image = urljoin(self.base_url, cover['image_path'])
|
||||
break
|
||||
elif cover.get('image_url', False):
|
||||
edition.cover_image = cover['image_url']
|
||||
break
|
||||
work.save()
|
||||
edition.save()
|
||||
return edition
|
||||
|
||||
def load_ebooks(self, metadata, edition, test_mode=False, user=None):
|
||||
default_edition = edition
|
||||
for key in ['epub', 'pdf', 'mobi']:
|
||||
url = metadata.metadata.get('download_url_{}'.format(key), None)
|
||||
if url:
|
||||
edition = edition_for_etype(key, metadata, default=default_edition)
|
||||
if edition:
|
||||
contentfile = load_ebookfile(url, key)
|
||||
if contentfile:
|
||||
contentfile_name = '/loaded/ebook_{}.{}'.format(edition.id, key)
|
||||
path = default_storage.save(contentfile_name, contentfile)
|
||||
lic = MATCH_LICENSE.search(metadata.rights_url)
|
||||
license = 'CC {}'.format(lic.group(1).upper()) if lic else ''
|
||||
ebf = models.EbookFile.objects.create(
|
||||
format=key,
|
||||
edition=edition,
|
||||
source=url,
|
||||
)
|
||||
ebf.file.save(contentfile_name, contentfile)
|
||||
ebf.file.close()
|
||||
ebook = models.Ebook.objects.create(
|
||||
url=ebf.file.url,
|
||||
provider='Unglue.it',
|
||||
rights=license,
|
||||
format=key,
|
||||
edition=edition,
|
||||
filesize=contentfile.size,
|
||||
active=False,
|
||||
user=user,
|
||||
)
|
||||
ebf.ebook = ebook
|
||||
ebf.save()
|
||||
|
||||
|
||||
class GithubLoader(BasePandataLoader):
|
||||
def load_ebooks(self, metadata, edition, test_mode=False):
|
||||
# create Ebook for any ebook in the corresponding GitHub release
|
||||
# assuming yaml_url of form (from GitHub, though not necessarily GITenberg)
|
||||
# https://github.com/GITenberg/Adventures-of-Huckleberry-Finn_76/raw/master/metadata.yaml
|
||||
|
||||
url_path = urlparse(yaml_url).path.split("/")
|
||||
url_path = urlparse(self.base_url).path.split("/")
|
||||
(repo_owner, repo_name) = (url_path[1], url_path[2])
|
||||
repo_tag = metadata._version
|
||||
# allow for there not to be a token in the settings
|
||||
try:
|
||||
token = settings.GITHUB_PUBLIC_TOKEN
|
||||
except:
|
||||
token = None
|
||||
token = None
|
||||
|
||||
if metadata._version and not metadata._version.startswith('0.0.'):
|
||||
# use GitHub API to compute the ebooks in release until we're in test mode
|
||||
|
@ -919,19 +956,22 @@ def load_from_yaml(yaml_url, test_mode=False):
|
|||
ebooks_in_release = ebooks_in_github_release(repo_owner, repo_name, repo_tag, token=token)
|
||||
|
||||
for (ebook_format, ebook_name) in ebooks_in_release:
|
||||
(book_name_prefix, _ ) = re.search(r'(.*)\.([^\.]*)$', ebook_name).groups()
|
||||
(ebook, created)= models.Ebook.objects.get_or_create(
|
||||
url=git_download_from_yaml_url(yaml_url,metadata._version,edition_name=book_name_prefix,
|
||||
format_= ebook_format),
|
||||
(book_name_prefix, _ ) = re.search(r'(.*)\.([^\.]*)$', ebook_name).groups()
|
||||
(ebook, created) = models.Ebook.objects.get_or_create(
|
||||
url=git_download_from_yaml_url(
|
||||
self.base_url,
|
||||
metadata._version,
|
||||
edition_name=book_name_prefix,
|
||||
format_= ebook_format
|
||||
),
|
||||
provider='Github',
|
||||
rights = cc.match_license(metadata.rights),
|
||||
format = ebook_format,
|
||||
edition = edition,
|
||||
rights=cc.match_license(metadata.rights),
|
||||
format=ebook_format,
|
||||
edition=edition,
|
||||
)
|
||||
ebook.set_version(metadata._version)
|
||||
|
||||
return work.id
|
||||
|
||||
|
||||
def git_download_from_yaml_url(yaml_url, version, edition_name='book', format_='epub'):
|
||||
# go from https://github.com/GITenberg/Adventures-of-Huckleberry-Finn_76/raw/master/metadata.yaml
|
||||
# to https://github.com/GITenberg/Adventures-of-Huckleberry-Finn_76/releases/download/v0.0.3/Adventures-of-Huckleberry-Finn.epub
|
||||
|
@ -949,14 +989,13 @@ def release_from_tag(repo, tag_name):
|
|||
:param str tag_name: (required) name of tag
|
||||
:returns: :class:`Release <github3.repos.release.Release>`
|
||||
"""
|
||||
# release_from_tag adapted from
|
||||
# release_from_tag adapted from
|
||||
# https://github.com/sigmavirus24/github3.py/blob/38de787e465bffc63da73d23dc51f50d86dc903d/github3/repos/repo.py#L1781-L1793
|
||||
|
||||
url = repo._build_url('releases', 'tags', tag_name,
|
||||
base_url=repo._api)
|
||||
json = repo._json(repo._get(url), 200)
|
||||
return Release(json, repo) if json else None
|
||||
|
||||
json_obj = repo._json(repo._get(url), 200)
|
||||
return Release(json_obj, repo) if json_obj else None
|
||||
|
||||
def ebooks_in_github_release(repo_owner, repo_name, tag, token=None):
|
||||
"""
|
||||
|
@ -966,7 +1005,7 @@ def ebooks_in_github_release(repo_owner, repo_name, tag, token=None):
|
|||
"""
|
||||
|
||||
# map mimetype to file extension
|
||||
EBOOK_FORMATS = dict([(v,k) for (k,v) in settings.CONTENT_TYPES.items()])
|
||||
EBOOK_FORMATS = dict([(v, k) for (k, v) in settings.CONTENT_TYPES.items()])
|
||||
|
||||
if token is not None:
|
||||
gh = login(token=token)
|
||||
|
@ -980,3 +1019,18 @@ def ebooks_in_github_release(repo_owner, repo_name, tag, token=None):
|
|||
return [(EBOOK_FORMATS.get(asset.content_type), asset.name)
|
||||
for asset in release.iter_assets()
|
||||
if EBOOK_FORMATS.get(asset.content_type) is not None]
|
||||
|
||||
def add_by_webpage(url, work=None, user=None):
|
||||
edition = None
|
||||
scraper = BaseScraper(url)
|
||||
loader = BasePandataLoader(url)
|
||||
pandata = Pandata()
|
||||
pandata.metadata = scraper.metadata
|
||||
for metadata in pandata.get_edition_list():
|
||||
edition = loader.load_from_pandata(metadata, work)
|
||||
work = edition.work
|
||||
loader.load_ebooks(pandata, edition, user=user)
|
||||
return edition if edition else None
|
||||
|
||||
|
||||
|
||||
|
|
16
core/cc.py
16
core/cc.py
|
@ -5,13 +5,13 @@
|
|||
## need to add versioned CC entries
|
||||
|
||||
INFO_CC = (
|
||||
('CC BY-NC-ND', 'by-nc-nd', 'Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported (CC BY-NC-ND 3.0)', 'http://creativecommons.org/licenses/by-nc-nd/3.0/', 'Creative Commons Attribution-NonCommercial-NoDerivs'),
|
||||
('CC BY-NC-SA', 'by-nc-sa', 'Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0)', 'http://creativecommons.org/licenses/by-nc-sa/3.0/', 'Creative Commons Attribution-NonCommercial-ShareAlike'),
|
||||
('CC BY-NC', 'by-nc', 'Creative Commons Attribution-NonCommercial 3.0 Unported (CC BY-NC 3.0)', 'http://creativecommons.org/licenses/by-nc/3.0/', 'Creative Commons Attribution-NonCommercial'),
|
||||
('CC BY-ND', 'by-nd', 'Creative Commons Attribution-NoDerivs 3.0 Unported (CC BY-ND 3.0)', 'http://creativecommons.org/licenses/by-nd/3.0/','Creative Commons Attribution-NoDerivs'),
|
||||
('CC BY-SA', 'by-sa', 'Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)', 'http://creativecommons.org/licenses/by-sa/3.0/', 'Creative Commons Attribution-ShareAlike'),
|
||||
('CC BY', 'by', 'Creative Commons Attribution 3.0 Unported (CC BY 3.0)', 'http://creativecommons.org/licenses/by/3.0/', 'Creative Commons Attribution'),
|
||||
('CC0', 'cc0', 'No Rights Reserved (CC0)', 'http://creativecommons.org/about/cc0', 'No Rights Reserved (CC0)'),
|
||||
('CC BY-NC-ND', 'by-nc-nd', 'Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported (CC BY-NC-ND 3.0)', 'https://creativecommons.org/licenses/by-nc-nd/3.0/', 'Creative Commons Attribution-NonCommercial-NoDerivs'),
|
||||
('CC BY-NC-SA', 'by-nc-sa', 'Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0)', 'https://creativecommons.org/licenses/by-nc-sa/3.0/', 'Creative Commons Attribution-NonCommercial-ShareAlike'),
|
||||
('CC BY-NC', 'by-nc', 'Creative Commons Attribution-NonCommercial 3.0 Unported (CC BY-NC 3.0)', 'https://creativecommons.org/licenses/by-nc/3.0/', 'Creative Commons Attribution-NonCommercial'),
|
||||
('CC BY-ND', 'by-nd', 'Creative Commons Attribution-NoDerivs 3.0 Unported (CC BY-ND 3.0)', 'https://creativecommons.org/licenses/by-nd/3.0/','Creative Commons Attribution-NoDerivs'),
|
||||
('CC BY-SA', 'by-sa', 'Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)', 'https://creativecommons.org/licenses/by-sa/3.0/', 'Creative Commons Attribution-ShareAlike'),
|
||||
('CC BY', 'by', 'Creative Commons Attribution 3.0 Unported (CC BY 3.0)', 'https://creativecommons.org/licenses/by/3.0/', 'Creative Commons Attribution'),
|
||||
('CC0', 'cc0', 'No Rights Reserved (CC0)', 'https://creativecommons.org/about/cc0', 'No Rights Reserved (CC0)'),
|
||||
)
|
||||
INFO_FREE = INFO_CC + (
|
||||
('GFDL', 'gdfl', 'GNU Free Documentation License', 'http://www.gnu.org/licenses/fdl-1.3-standalone.html', 'GNU Free Documentation License'),
|
||||
|
@ -19,7 +19,7 @@ INFO_FREE = INFO_CC + (
|
|||
('OSI', 'opensource', 'OSI Approved License', 'https://opensource.org/licenses', 'OSI Approved License'),
|
||||
)
|
||||
INFO_PD = (
|
||||
('PD-US', 'pd-us', 'Public Domain, US', 'http://creativecommons.org/about/pdm', 'Public Domain, US'),
|
||||
('PD-US', 'pd-us', 'Public Domain, US', 'https://creativecommons.org/about/pdm', 'Public Domain, US'),
|
||||
)
|
||||
INFO_ALL = INFO_FREE + INFO_PD
|
||||
# CCHOICES, CCGRANTS, and FORMATS are all used in places that expect tuples
|
||||
|
|
|
@ -58,7 +58,7 @@ def safe_strip(a_string):
|
|||
|
||||
class GoodreadsClient(object):
|
||||
|
||||
url = 'http://www.goodreads.com'
|
||||
url = 'https://www.goodreads.com'
|
||||
request_token_url = urljoin(url,'oauth/request_token')
|
||||
authorize_url = urljoin(url, '/oauth/authorize')
|
||||
access_token_url = urljoin(url,'/oauth/access_token')
|
||||
|
|
60
core/isbn.py
60
core/isbn.py
|
@ -1,6 +1,7 @@
|
|||
# encoding: utf-8
|
||||
## {{{ http://code.activestate.com/recipes/498104/ (r1)
|
||||
|
||||
## also http://stackoverflow.com/questions/4047511/checking-if-an-isbn-number-is-correct
|
||||
## also https://stackoverflow.com/questions/4047511/checking-if-an-isbn-number-is-correct
|
||||
|
||||
import re
|
||||
|
||||
|
@ -35,7 +36,7 @@ def _convert_10_to_13(isbn):
|
|||
|
||||
def convert_10_to_13(isbn):
|
||||
try:
|
||||
isbn=ISBN(isbn)
|
||||
isbn = ISBN(isbn)
|
||||
isbn.validate()
|
||||
if isbn.valid:
|
||||
return isbn.to_string()
|
||||
|
@ -44,20 +45,24 @@ def convert_10_to_13(isbn):
|
|||
except:
|
||||
return None
|
||||
|
||||
ISBN_REGEX = re.compile(r'^(\d{9}|\d{12})(\d|X)$')
|
||||
DASH_REGEX = re.compile(r'[ \-–—]+')
|
||||
def strip(s):
|
||||
"""Strips away any - or spaces. If the remaining string is of length 10 or 13 with digits only in anything but the last
|
||||
check digit (which may be X), then return '' -- otherwise return the remaining string"""
|
||||
"""Strips away any - or spaces. If the remaining string is of length 10 or 13
|
||||
with digits only in anything but the last
|
||||
check digit (which may be X), then return '' -- otherwise return the remaining string
|
||||
"""
|
||||
try:
|
||||
s = s.replace("-", "").replace(" ", "").upper()
|
||||
match = re.search(r'^(\d{9}|\d{12})(\d|X)$', s)
|
||||
s = DASH_REGEX.sub('', s).upper()
|
||||
match = ISBN_REGEX.search(s)
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
if not match:
|
||||
return None
|
||||
else:
|
||||
return s
|
||||
|
||||
|
||||
def _convert_13_to_10(isbn):
|
||||
assert len(isbn) == 13
|
||||
# only ISBN-13 starting with 978 can have a valid ISBN 10 partner
|
||||
|
@ -66,7 +71,7 @@ def _convert_13_to_10(isbn):
|
|||
|
||||
def convert_13_to_10(isbn):
|
||||
try:
|
||||
isbn=ISBN(isbn)
|
||||
isbn = ISBN(isbn)
|
||||
isbn.validate()
|
||||
if isbn.valid:
|
||||
return isbn.to_string(type='10')
|
||||
|
@ -74,7 +79,7 @@ def convert_13_to_10(isbn):
|
|||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
class ISBNException(Exception):
|
||||
pass
|
||||
|
||||
|
@ -82,10 +87,10 @@ class ISBN(object):
|
|||
def __init__(self, input_isbn):
|
||||
self.input_isbn = input_isbn
|
||||
self.error = None
|
||||
|
||||
|
||||
stripped_isbn = strip(input_isbn)
|
||||
|
||||
if stripped_isbn is None or len(stripped_isbn) not in (10,13):
|
||||
|
||||
if stripped_isbn is None or len(stripped_isbn) not in (10, 13):
|
||||
self.error = "input_isbn does not seem to be a valid ISBN"
|
||||
self.__type = None
|
||||
self.__valid = False
|
||||
|
@ -99,17 +104,18 @@ class ISBN(object):
|
|||
self.__isbn10 = stripped_isbn
|
||||
self.__valid_10 = stripped_isbn[0:9] + check_digit_10(stripped_isbn[0:9])
|
||||
self.__valid = (self.__isbn10 == self.__valid_10)
|
||||
|
||||
|
||||
# this is the corresponding ISBN 13 based on the assumption of a valid ISBN 10
|
||||
self.__isbn13 = _convert_10_to_13(stripped_isbn)
|
||||
self.__valid_13 = self.__isbn13
|
||||
self.__valid_13 = self.__isbn13
|
||||
|
||||
elif len(stripped_isbn) == 13:
|
||||
# Assume ISBN 13 all have to begin with 978 or 979 and only 978 ISBNs can possibly have ISBN-10 counterpart
|
||||
|
||||
# Assume ISBN 13 all have to begin with 978 or 979 and only 978 ISBNs
|
||||
# can possibly have ISBN-10 counterpart
|
||||
|
||||
self.__type = '13'
|
||||
self.__isbn13 = stripped_isbn
|
||||
|
||||
|
||||
if stripped_isbn[0:3] not in ['978','979']:
|
||||
self.error = "ISBN 13 must begin with 978 or 979 not %s " % (stripped_isbn[0:3])
|
||||
self.__valid = False
|
||||
|
@ -117,7 +123,7 @@ class ISBN(object):
|
|||
else:
|
||||
self.__valid_13 = stripped_isbn[0:12] + check_digit_13(stripped_isbn[0:12])
|
||||
self.__valid = (self.__isbn13 == self.__valid_13)
|
||||
|
||||
|
||||
# now check to see whether the isbn starts with 978 -- only then convert to ISBN -10
|
||||
if self.__isbn13[0:3] == '978':
|
||||
self.__isbn10 = _convert_13_to_10(stripped_isbn)
|
||||
|
@ -158,11 +164,11 @@ class ISBN(object):
|
|||
s = self.__isbn13
|
||||
return "%s-%s-%s-%s-%s" % (s[0:3], s[3], s[4:7], s[7:12], s[12])
|
||||
else:
|
||||
return self.__isbn13
|
||||
return self.__isbn13
|
||||
def __unicode__(self):
|
||||
return unicode(self.to_string(type=self.type,hyphenate=False))
|
||||
return unicode(self.to_string(type=self.type, hyphenate=False))
|
||||
def __str__(self):
|
||||
s = self.to_string(type=self.type,hyphenate=False)
|
||||
s = self.to_string(type=self.type, hyphenate=False)
|
||||
if s is not None:
|
||||
return s
|
||||
else:
|
||||
|
@ -177,14 +183,16 @@ class ISBN(object):
|
|||
else:
|
||||
try:
|
||||
other_isbn = ISBN(other)
|
||||
if (self.valid and other_isbn.valid) and (self.to_string('13') == other_isbn.to_string('13')):
|
||||
if ((self.valid and other_isbn.valid) and
|
||||
(self.to_string('13') == other_isbn.to_string('13'))
|
||||
):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self.__eq__(other))
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ class LibraryThing(object):
|
|||
This class retrieves and parses the CSV representation of a LibraryThing user's library.
|
||||
"""
|
||||
url = "https://www.librarything.com"
|
||||
csv_file_url = "http://www.librarything.com/export-csv"
|
||||
csv_file_url = "https://www.librarything.com/export-csv"
|
||||
|
||||
def __init__(self, username=None, password=None):
|
||||
self.username = username
|
||||
|
@ -160,7 +160,7 @@ class LibraryThing(object):
|
|||
cookies = r.cookies
|
||||
|
||||
while next_page:
|
||||
url = "http://www.librarything.com/catalog_bottom.php?view=%s&viewstyle=%d&collection=%d&offset=%d" % (self.username,
|
||||
url = "https://www.librarything.com/catalog_bottom.php?view=%s&viewstyle=%d&collection=%d&offset=%d" % (self.username,
|
||||
view_style, COLLECTION, offset)
|
||||
logger.info("url: %s", url)
|
||||
if cookies is None:
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
import re
|
||||
import logging
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
#from gitenberg.metadata.pandata import Pandata
|
||||
from django.conf import settings
|
||||
from urlparse import urljoin
|
||||
|
||||
from regluit.core import models
|
||||
from regluit.core.validation import identifier_cleaner
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CONTAINS_COVER = re.compile('cover')
|
||||
CONTAINS_CC = re.compile('creativecommons.org')
|
||||
|
||||
class BaseScraper(object):
|
||||
def __init__(self, url):
|
||||
self.metadata = {}
|
||||
self.identifiers = {'http': url}
|
||||
self.doc = None
|
||||
self.base = url
|
||||
try:
|
||||
response = requests.get(url, headers={"User-Agent": settings.USER_AGENT})
|
||||
if response.status_code == 200:
|
||||
self.doc = BeautifulSoup(response.content, 'lxml')
|
||||
self.get_title()
|
||||
self.get_language()
|
||||
self.get_description()
|
||||
self.get_identifiers()
|
||||
self.get_keywords()
|
||||
self.get_publisher()
|
||||
self.get_pubdate()
|
||||
self.get_authors()
|
||||
self.get_cover()
|
||||
self.get_downloads()
|
||||
self.get_license()
|
||||
if not self.metadata.get('title', None):
|
||||
self.set('title', '!!! missing title !!!')
|
||||
if not self.metadata.get('language', None):
|
||||
self.set('language', 'en')
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(e)
|
||||
self.metadata = None
|
||||
self.metadata['identifiers'] = self.identifiers
|
||||
|
||||
def set(self, name, value):
|
||||
self.metadata[name] = value
|
||||
|
||||
def fetch_one_el_content(self, el_name):
|
||||
data_el = self.doc.find(el_name)
|
||||
value = ''
|
||||
if data_el:
|
||||
value = data_el.text
|
||||
return value
|
||||
|
||||
def check_metas(self, meta_list, **attrs):
|
||||
value = ''
|
||||
list_mode = attrs.pop('list_mode', 'longest')
|
||||
for meta_name in meta_list:
|
||||
attrs['name'] = meta_name
|
||||
|
||||
metas = self.doc.find_all('meta', attrs=attrs)
|
||||
for meta in metas:
|
||||
el_value = meta.get('content', '').strip()
|
||||
if list_mode == 'longest':
|
||||
if len(el_value) > len (value):
|
||||
value = el_value
|
||||
elif list_mode == 'list':
|
||||
if value == '':
|
||||
value = [el_value]
|
||||
else:
|
||||
value.append(el_value)
|
||||
if value:
|
||||
return value
|
||||
return value
|
||||
|
||||
def get_title(self):
|
||||
value = self.check_metas(['DC.Title','dc.title', 'citation_title', 'title'])
|
||||
if not value:
|
||||
value = self.fetch_one_el_content('title')
|
||||
self.set('title', value)
|
||||
|
||||
def get_language(self):
|
||||
value = self.check_metas(['DC.Language','dc.language','language'])
|
||||
self.set('language', value)
|
||||
|
||||
def get_description(self):
|
||||
value = self.check_metas(['DC.Description','dc.description','description'])
|
||||
self.set('description', value)
|
||||
|
||||
def get_identifiers(self):
|
||||
value = self.check_metas(['DC.Identifier.URI'])
|
||||
value = identifier_cleaner('http')(value)
|
||||
if value:
|
||||
self.identifiers['http'] = value
|
||||
value = self.check_metas(['DC.Identifier.DOI', 'citation_doi'])
|
||||
value = identifier_cleaner('doi')(value)
|
||||
if value:
|
||||
self.identifiers['doi'] = value
|
||||
isbns = {}
|
||||
label_map = {'epub': 'EPUB', 'mobi': 'Mobi', 'paper':
|
||||
'Paperback', 'pdf': 'PDF', 'hard':'Hardback'}
|
||||
for key in label_map.keys():
|
||||
isbn_key = 'isbn_{}'.format(key)
|
||||
value = self.check_metas(['citation_isbn'], type=label_map[key])
|
||||
value = identifier_cleaner('isbn')(value)
|
||||
if value:
|
||||
isbns[isbn_key] = value
|
||||
|
||||
ed_list = []
|
||||
if len(isbns):
|
||||
#need to create edition list
|
||||
for key in isbns.keys():
|
||||
isbn_type = key.split('_')[-1]
|
||||
ed_list.append({
|
||||
'edition': isbn_type,
|
||||
'edition_identifiers': {'isbn': isbns[key]}
|
||||
})
|
||||
else:
|
||||
value = self.check_metas(['citation_isbn'], list_mode='list')
|
||||
if len(value):
|
||||
for isbn in value:
|
||||
isbn = identifier_cleaner('isbn')(isbn)
|
||||
if isbn:
|
||||
ed_list.append({
|
||||
'edition': isbn,
|
||||
'edition_identifiers': {'isbn': isbn}
|
||||
})
|
||||
if len(ed_list):
|
||||
self.set('edition_list', ed_list)
|
||||
|
||||
def get_keywords(self):
|
||||
value = self.check_metas(['keywords']).strip(',;')
|
||||
if value:
|
||||
self.set('subjects', re.split(' *[;,] *', value))
|
||||
|
||||
def get_publisher(self):
|
||||
value = self.check_metas(['citation_publisher', 'DC.Source'])
|
||||
if value:
|
||||
self.set('publisher', value)
|
||||
|
||||
def get_pubdate(self):
|
||||
value = self.check_metas(['citation_publication_date', 'DC.Date.issued'])
|
||||
if value:
|
||||
self.set('publication_date', value)
|
||||
|
||||
def get_authors(self):
|
||||
value_list = self.check_metas(['DC.Creator.PersonalName', 'citation_author',], list_mode='list')
|
||||
if not value_list:
|
||||
return
|
||||
if len(value_list) == 1:
|
||||
creator = {'author': {'agent_name': value_list[0]}}
|
||||
else:
|
||||
creator_list = []
|
||||
for auth in value_list:
|
||||
creator_list.append({'agent_name': auth})
|
||||
creator = {'authors': creator_list }
|
||||
|
||||
self.set('creator', creator)
|
||||
|
||||
def get_cover(self):
|
||||
block = self.doc.find(class_=CONTAINS_COVER)
|
||||
block = block if block else self.doc
|
||||
img = block.find_all('img', src=CONTAINS_COVER)
|
||||
if img:
|
||||
cover_uri = img[0].get('src', None)
|
||||
if cover_uri:
|
||||
self.set('covers', [{'image_url': urljoin(self.base, cover_uri)}])
|
||||
|
||||
def get_downloads(self):
|
||||
for dl_type in ['epub', 'mobi', 'pdf']:
|
||||
dl_meta = 'citation_{}_url'.format(dl_type)
|
||||
value = self.check_metas([dl_meta])
|
||||
if value:
|
||||
self.set('download_url_{}'.format(dl_type), value)
|
||||
|
||||
def get_license(self):
|
||||
'''only looks for cc licenses'''
|
||||
links = self.doc.find_all(href=CONTAINS_CC)
|
||||
for link in links:
|
||||
self.set('rights_url', link['href'])
|
|
@ -23,7 +23,7 @@ def utf8_general_ci_norm(s):
|
|||
Normalize a la MySQL utf8_general_ci collation
|
||||
(As of 2016.05.24, we're using the utf8_general_ci collation for author names)
|
||||
|
||||
http://stackoverflow.com/questions/1036454/what-are-the-diffrences-between-utf8-general-ci-and-utf8-unicode-ci/1036459#1036459
|
||||
https://stackoverflow.com/questions/1036454/what-are-the-diffrences-between-utf8-general-ci-and-utf8-unicode-ci/1036459#1036459
|
||||
|
||||
* converts to Unicode normalization form D for canonical decomposition
|
||||
* removes any combining characters
|
||||
|
@ -330,4 +330,25 @@ def loaded_book_ok(book, work, edition):
|
|||
bisacsh = bisacsh.parent
|
||||
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
ID_URLPATTERNS = {
|
||||
'goog': re.compile(r'[\./]google\.com/books\?.*id=([a-zA-Z0-9\-_]{12})'),
|
||||
'olwk': re.compile(r'[\./]openlibrary\.org(/works/OL\d{1,8}W)'),
|
||||
'gdrd': re.compile(r'[\./]goodreads\.com/book/show/(\d{1,8})'),
|
||||
'ltwk': re.compile(r'[\./]librarything\.com/work/(\d{1,8})'),
|
||||
'oclc': re.compile(r'\.worldcat\.org/.*oclc/(\d{8,12})'),
|
||||
'doi': re.compile(r'[\./]doi\.org/(10\.\d+/\S+)'),
|
||||
'gtbg': re.compile(r'[\./]gutenberg\.org/ebooks/(\d{1,6})'),
|
||||
'glue': re.compile(r'[\./]unglue\.it/work/(\d{1,7})'),
|
||||
}
|
||||
|
||||
def ids_from_urls(url):
|
||||
ids = {}
|
||||
for ident in ID_URLPATTERNS.keys():
|
||||
id_match = ID_URLPATTERNS[ident].search(url)
|
||||
if id_match:
|
||||
ids[ident] = id_match.group(1)
|
||||
return ids
|
||||
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand
|
|||
|
||||
from regluit.core.models import Subject
|
||||
|
||||
#http://stackoverflow.com/questions/8733233/filtering-out-certain-bytes-in-python
|
||||
#https://stackoverflow.com/questions/8733233/filtering-out-certain-bytes-in-python
|
||||
|
||||
def valid_xml_char_ordinal(c):
|
||||
codepoint = ord(c)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="launch_top" id="degruyter_countdown" style="font-size:20px;text-align:center;width:50%"></div>
|
||||
|
||||
<h4>Help us unglue this book!</h4>
|
||||
<p>De Gruyter has agreed to run an ungluing campaign for this book, if it can get enough support from ungluers like you. The target price will be $2100, after which the book will be free for everyone on earth to read, copy, and share, forever (under a Creative Commons <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/">BY-NC-ND</a> license).</p>
|
||||
<p>De Gruyter has agreed to run an ungluing campaign for this book, if it can get enough support from ungluers like you. The target price will be $2100, after which the book will be free for everyone on earth to read, copy, and share, forever (under a Creative Commons <a href="https://creativecommons.org/licenses/by-nc-nd/3.0/">BY-NC-ND</a> license).</p>
|
||||
|
||||
<p>They'll launch a campaign when 50 ungluers have wished for this book. Right now <span id="wisher_data"></span>. </p>
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0008_auto_20161109_1448'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ebookfile',
|
||||
name='source',
|
||||
field=models.URLField(null=True, blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='campaign',
|
||||
name='status',
|
||||
field=models.CharField(default=b'INITIALIZED', max_length=15, null=True, db_index=True, choices=[(b'INITIALIZED', b'INITIALIZED'), (b'ACTIVE', b'ACTIVE'), (b'SUSPENDED', b'SUSPENDED'), (b'WITHDRAWN', b'WITHDRAWN'), (b'SUCCESSFUL', b'SUCCESSFUL'), (b'UNSUCCESSFUL', b'UNSUCCESSFUL')]),
|
||||
),
|
||||
]
|
|
@ -477,7 +477,7 @@ class Campaign(models.Model):
|
|||
# copy custom premiums
|
||||
new_premiums = self.premiums.filter(type='CU')
|
||||
|
||||
# setting pk to None will insert new copy http://stackoverflow.com/a/4736172/7782
|
||||
# setting pk to None will insert new copy https://stackoverflow.com/a/4736172/7782
|
||||
self.pk = None
|
||||
self.status = 'INITIALIZED'
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ from regluit.core.parameters import (
|
|||
AGE_LEVEL_CHOICES,
|
||||
BORROWED,
|
||||
BUY2UNGLUE,
|
||||
ID_CHOICES_MAP,
|
||||
INDIVIDUAL,
|
||||
LIBRARY,
|
||||
OFFER_CHOICES,
|
||||
|
@ -41,9 +42,10 @@ from regluit.core.parameters import (
|
|||
TEXT_RELATION_CHOICES,
|
||||
THANKED,
|
||||
THANKS,
|
||||
WORK_IDENTIFIERS,
|
||||
)
|
||||
|
||||
# fix truncated file problems per http://stackoverflow.com/questions/12984426/python-pil-ioerror-image-file-truncated-with-big-images
|
||||
# fix truncated file problems per https://stackoverflow.com/questions/12984426/python-pil-ioerror-image-file-truncated-with-big-images
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -57,7 +59,6 @@ def id_for(obj, type):
|
|||
except IndexError:
|
||||
return ''
|
||||
|
||||
|
||||
class Identifier(models.Model):
|
||||
# olib, ltwk, goog, gdrd, thng, isbn, oclc, olwk, doab, gtbg, glue, doi
|
||||
type = models.CharField(max_length=4, null=False)
|
||||
|
@ -72,7 +73,13 @@ class Identifier(models.Model):
|
|||
def set(type=None, value=None, edition=None, work=None):
|
||||
# if there's already an id of this type for this work and edition, change it
|
||||
# if not, create it. if the id exists and points to something else, change it.
|
||||
identifier = Identifier.get_or_add(type=type, value=value, edition=edition, work=work)
|
||||
try:
|
||||
identifier = Identifier.objects.filter(type=type, value=value)[0]
|
||||
except IndexError:
|
||||
if type in WORK_IDENTIFIERS:
|
||||
identifier = Identifier.objects.create(type=type, value=value, work=work)
|
||||
else:
|
||||
identifier = Identifier.objects.create(type=type, value=value, work=work, edition=edition)
|
||||
if identifier.work.id != work.id:
|
||||
identifier.work = work
|
||||
identifier.save()
|
||||
|
@ -97,6 +104,9 @@ class Identifier(models.Model):
|
|||
|
||||
def __unicode__(self):
|
||||
return u'{0}:{1}'.format(self.type, self.value)
|
||||
|
||||
def label(self):
|
||||
return ID_CHOICES_MAP.get(self.type, self.type)
|
||||
|
||||
class Work(models.Model):
|
||||
created = models.DateTimeField(auto_now_add=True, db_index=True,)
|
||||
|
@ -152,7 +162,7 @@ class Work(models.Model):
|
|||
@property
|
||||
def googlebooks_url(self):
|
||||
if self.googlebooks_id:
|
||||
return "http://books.google.com/books?id=%s" % self.googlebooks_id
|
||||
return "https://books.google.com/books?id=%s" % self.googlebooks_id
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
@ -168,7 +178,7 @@ class Work(models.Model):
|
|||
|
||||
@property
|
||||
def goodreads_url(self):
|
||||
return "http://www.goodreads.com/book/show/%s" % self.goodreads_id
|
||||
return "https://www.goodreads.com/book/show/%s" % self.goodreads_id
|
||||
|
||||
@property
|
||||
def librarything_id(self):
|
||||
|
@ -176,7 +186,7 @@ class Work(models.Model):
|
|||
|
||||
@property
|
||||
def librarything_url(self):
|
||||
return "http://www.librarything.com/work/%s" % self.librarything_id
|
||||
return "https://www.librarything.com/work/%s" % self.librarything_id
|
||||
|
||||
@property
|
||||
def openlibrary_id(self):
|
||||
|
@ -184,7 +194,7 @@ class Work(models.Model):
|
|||
|
||||
@property
|
||||
def openlibrary_url(self):
|
||||
return "http://openlibrary.org" + self.openlibrary_id
|
||||
return "https://openlibrary.org" + self.openlibrary_id
|
||||
|
||||
def cover_filetype(self):
|
||||
if self.uses_google_cover():
|
||||
|
@ -202,6 +212,9 @@ class Work(models.Model):
|
|||
else:
|
||||
return 'image'
|
||||
|
||||
def work_ids(self):
|
||||
return self.identifiers.filter(edition__isnull=True)
|
||||
|
||||
def uses_google_cover(self):
|
||||
if self.preferred_edition and self.preferred_edition.cover_image:
|
||||
return False
|
||||
|
@ -1003,6 +1016,7 @@ class EbookFile(models.Model):
|
|||
created = models.DateTimeField(auto_now_add=True)
|
||||
asking = models.BooleanField(default=False)
|
||||
ebook = models.ForeignKey('Ebook', related_name='ebook_files', null=True)
|
||||
source = models.URLField(null=True, blank=True)
|
||||
version = None
|
||||
def check_file(self):
|
||||
if self.format == 'epub':
|
||||
|
@ -1084,7 +1098,12 @@ class Ebook(models.Model):
|
|||
if self.save:
|
||||
self.filesize = self.filesize if self.filesize < 2147483647 else 2147483647 # largest safe positive integer
|
||||
self.save()
|
||||
ebf = EbookFile.objects.create(edition=self.edition, ebook=self, format=self.format)
|
||||
ebf = EbookFile.objects.create(
|
||||
edition=self.edition,
|
||||
ebook=self,
|
||||
format=self.format,
|
||||
source=self.url
|
||||
)
|
||||
ebf.file.save(path_for_file(ebf, None), ContentFile(r.read()))
|
||||
ebf.file.close()
|
||||
ebf.save()
|
||||
|
|
|
@ -15,6 +15,7 @@ AGE_LEVEL_CHOICES = (
|
|||
('15-18', 'Teen - Grade 10-12, Age 15-18'),
|
||||
('18-', 'Adult/Advanced Reader')
|
||||
)
|
||||
|
||||
TEXT_RELATION_CHOICES = (
|
||||
('translation', 'translation'),
|
||||
('revision', 'revision'),
|
||||
|
@ -22,6 +23,28 @@ TEXT_RELATION_CHOICES = (
|
|||
('compilation', 'compilation')
|
||||
)
|
||||
|
||||
ID_CHOICES = (
|
||||
('http', 'Web Address'),
|
||||
('isbn', 'ISBN'),
|
||||
('doab', 'DOABooks ID'),
|
||||
('gtbg', 'Project Gutenberg Number'),
|
||||
('doi', 'Digital Object Identifier'),
|
||||
('oclc', 'OCLC Number'),
|
||||
('goog', 'Google Books ID'),
|
||||
('gdrd', 'Goodreads ID'),
|
||||
('thng', 'Library Thing ID'),
|
||||
('olwk', 'Open Library Work ID'),
|
||||
('ltwk', 'Library Thing Work ID'),
|
||||
)
|
||||
|
||||
OTHER_ID_CHOICES = (
|
||||
('glue', 'Unglue.it ID'),
|
||||
('edid', 'pragmatic edition ID'),
|
||||
)
|
||||
|
||||
WORK_IDENTIFIERS = ('doi','olwk','glue','ltwk')
|
||||
|
||||
ID_CHOICES_MAP = dict(ID_CHOICES)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ logger = logging.getLogger(__name__)
|
|||
# create Wishlist and UserProfile to associate with User
|
||||
def create_user_objects(sender, created, instance, **kwargs):
|
||||
# use get_model to avoid circular import problem with models
|
||||
# don't create Wishlist or UserProfile if we are loading fixtures http://stackoverflow.com/a/3500009/7782
|
||||
# don't create Wishlist or UserProfile if we are loading fixtures https://stackoverflow.com/a/3500009/7782
|
||||
if not kwargs.get('raw', False):
|
||||
try:
|
||||
Wishlist = apps.get_model('core', 'Wishlist')
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# encoding: utf-8
|
||||
"""
|
||||
external library imports
|
||||
"""
|
||||
|
@ -201,7 +202,7 @@ class BookLoaderTests(TestCase):
|
|||
def test_thingisbn_mock(self):
|
||||
with requests_mock.Mocker(real_http=True) as m:
|
||||
with open(os.path.join(TESTDIR, '9780441569595.xml')) as lt:
|
||||
m.get('http://www.librarything.com/api/thingISBN/0441007465', content=lt.read())
|
||||
m.get('https://www.librarything.com/api/thingISBN/0441007465', content=lt.read())
|
||||
self.test_thingisbn(mocking=True)
|
||||
|
||||
def test_thingisbn(self, mocking=False):
|
||||
|
@ -222,7 +223,7 @@ class BookLoaderTests(TestCase):
|
|||
# ask for related editions to be added using the work we just created
|
||||
with requests_mock.Mocker(real_http=True) as m:
|
||||
with open(os.path.join(TESTDIR, '9780441569595.xml')) as lt:
|
||||
m.get('http://www.librarything.com/api/thingISBN/0441007465', content=lt.read())
|
||||
m.get('https://www.librarything.com/api/thingISBN/0441007465', content=lt.read())
|
||||
bookloader.add_related('0441007465') # should join the editions
|
||||
self.assertTrue(models.Edition.objects.count() >= edbefore)
|
||||
self.assertTrue(models.Work.objects.filter(language=lang).count() < langbefore)
|
||||
|
@ -243,7 +244,7 @@ class BookLoaderTests(TestCase):
|
|||
edition = bookloader.add_by_isbn('9780606301121') # A People's History Of The United States
|
||||
with requests_mock.Mocker(real_http=True) as m:
|
||||
with open(os.path.join(TESTDIR, '9780061989834.xml')) as lt:
|
||||
m.get('http://www.librarything.com/api/thingISBN/9780606301121', content=lt.read())
|
||||
m.get('https://www.librarything.com/api/thingISBN/9780606301121', content=lt.read())
|
||||
edition = tasks.populate_edition.run(edition.isbn_13)
|
||||
self.assertTrue(edition.work.editions.all().count() > 10)
|
||||
self.assertTrue(edition.work.subjects.all().count() > 8)
|
||||
|
@ -444,7 +445,7 @@ class BookLoaderTests(TestCase):
|
|||
self.assertEqual(w.first_epub_url(), ebook_epub.url)
|
||||
self.assertEqual(w.first_pdf_url(), ebook_pdf.url)
|
||||
|
||||
ebook_pdf.url='http://en.wikisource.org/wiki/Frankenstein'
|
||||
ebook_pdf.url='https://en.wikisource.org/wiki/Frankenstein'
|
||||
self.assertEqual(ebook_pdf.set_provider(), 'Wikisource')
|
||||
|
||||
self.user.wishlist.add_work(w, 'test')
|
||||
|
@ -492,12 +493,12 @@ class BookLoaderTests(TestCase):
|
|||
title = "Moby Dick"
|
||||
ol_work_id = "/works/OL102749W"
|
||||
gutenberg_etext_id = 2701
|
||||
epub_url = "http://www.gutenberg.org/cache/epub/2701/pg2701.epub"
|
||||
license = 'http://www.gutenberg.org/license'
|
||||
epub_url = "https://www.gutenberg.org/cache/epub/2701/pg2701.epub"
|
||||
license = 'https://www.gutenberg.org/license'
|
||||
lang = 'en'
|
||||
format = 'epub'
|
||||
publication_date = datetime(2001,7,1)
|
||||
seed_isbn = '9780142000083' # http://www.amazon.com/Moby-Dick-Whale-Penguin-Classics-Deluxe/dp/0142000086
|
||||
seed_isbn = '9780142000083' # https://www.amazon.com/Moby-Dick-Whale-Penguin-Classics-Deluxe/dp/0142000086
|
||||
|
||||
ebook = bookloader.load_gutenberg_edition(title, gutenberg_etext_id, ol_work_id, seed_isbn, epub_url, format, license, lang, publication_date)
|
||||
self.assertEqual(ebook.url, epub_url)
|
||||
|
@ -575,7 +576,7 @@ class CampaignTests(TestCase):
|
|||
|
||||
def test_required_fields(self):
|
||||
# a campaign must have a target, deadline and a work
|
||||
# see http://stackoverflow.com/questions/21458387/transactionmanagementerror-you-cant-execute-queries-until-the-end-of-the-atom
|
||||
# see https://stackoverflow.com/questions/21458387/transactionmanagementerror-you-cant-execute-queries-until-the-end-of-the-atom
|
||||
with transaction.atomic():
|
||||
c = Campaign()
|
||||
self.assertRaises(IntegrityError, c.save)
|
||||
|
@ -591,7 +592,7 @@ class CampaignTests(TestCase):
|
|||
c = Campaign(target=D('1000.00'), deadline=datetime(2013, 1, 1), work=w)
|
||||
c.license = 'CC BY-NC'
|
||||
c.save()
|
||||
self.assertEqual(c.license_url, 'http://creativecommons.org/licenses/by-nc/3.0/')
|
||||
self.assertEqual(c.license_url, 'https://creativecommons.org/licenses/by-nc/3.0/')
|
||||
self.assertEqual(c.license_badge, '/static/images/ccbync.png')
|
||||
|
||||
def test_campaign_status(self):
|
||||
|
@ -769,7 +770,7 @@ class GoodreadsTest(TestCase):
|
|||
gc = goodreads.GoodreadsClient(key=settings.GOODREADS_API_KEY, secret=settings.GOODREADS_API_SECRET)
|
||||
reviews = gc.review_list_unauth(user_id=gr_uid, shelf='read')
|
||||
# test to see whether there is a book field in each of the review
|
||||
# url for test is http://www.goodreads.com/review/list.xml?id=767708&shelf=read&page=1&per_page=20&order=a&v=2&key=[key]
|
||||
# url for test is https://www.goodreads.com/review/list.xml?id=767708&shelf=read&page=1&per_page=20&order=a&v=2&key=[key]
|
||||
self.assertTrue(all([r.has_key("book") for r in reviews]))
|
||||
|
||||
class LibraryThingTest(TestCase):
|
||||
|
@ -790,6 +791,7 @@ class ISBNTest(TestCase):
|
|||
milosz_10 = '006019667X'
|
||||
milosz_13 = '9780060196677'
|
||||
python_10 = '0-672-32978-6'
|
||||
funky = '0–672—329 78-6' # endash, mdash, space
|
||||
python_10_wrong = '0-672-32978-7'
|
||||
python_13 = '978-0-672-32978-4'
|
||||
|
||||
|
@ -798,8 +800,11 @@ class ISBNTest(TestCase):
|
|||
# return None for invalid characters
|
||||
self.assertEqual(None, isbn.ISBN("978-0-M72-32978-X").to_string('13'))
|
||||
self.assertEqual(isbn.ISBN("978-0-M72-32978-X").valid, False)
|
||||
self.assertEqual(None, bookloader.valid_isbn("978-0-M72-32978-X"))
|
||||
# check that only ISBN 13 starting with 978 or 979 are accepted
|
||||
self.assertEqual(None, isbn.ISBN("111-0-M72-32978-X").to_string())
|
||||
self.assertEqual(None, bookloader.valid_isbn("111-0-M72-32978-X"))
|
||||
self.assertEqual(isbn_python_13.to_string(), bookloader.valid_isbn(funky))
|
||||
|
||||
# right type?
|
||||
self.assertEqual(isbn_python_10.type, '10')
|
||||
|
@ -807,12 +812,16 @@ class ISBNTest(TestCase):
|
|||
# valid?
|
||||
self.assertEqual(isbn_python_10.valid, True)
|
||||
self.assertEqual(isbn.ISBN(python_10_wrong).valid, False)
|
||||
self.assertEqual(13, len(bookloader.valid_isbn(python_10)))
|
||||
self.assertEqual(isbn_python_13.to_string(), bookloader.valid_isbn(python_10_wrong))
|
||||
|
||||
# do conversion -- first the outside methods
|
||||
self.assertEqual(isbn.convert_10_to_13(isbn.strip(python_10)),isbn.strip(python_13))
|
||||
self.assertEqual(isbn.convert_13_to_10(isbn.strip(python_13)),isbn.strip(python_10))
|
||||
self.assertEqual(isbn.convert_10_to_13(isbn.strip(python_10)),isbn.strip(python_13))
|
||||
self.assertEqual(isbn.convert_13_to_10('xxxxxxxxxxxxx'),None)
|
||||
self.assertEqual(isbn.convert_10_to_13('xxxxxxxxxx'),None)
|
||||
self.assertEqual(None, bookloader.valid_isbn('xxxxxxxxxxxxx'))
|
||||
self.assertEqual(None, bookloader.valid_isbn('xxxxxxxxxx'))
|
||||
|
||||
# check formatting
|
||||
self.assertEqual(isbn.ISBN(python_13).to_string(type='13'), '9780672329784')
|
||||
|
@ -897,12 +906,12 @@ class DownloadPageTest(TestCase):
|
|||
e2.save()
|
||||
|
||||
eb1 = models.Ebook()
|
||||
eb1.url = "http://example.org"
|
||||
eb1.url = "https://example.org"
|
||||
eb1.edition = e1
|
||||
eb1.format = 'epub'
|
||||
|
||||
eb2 = models.Ebook()
|
||||
eb2.url = "http://example2.com"
|
||||
eb2.url = "https://example2.com"
|
||||
eb2.edition = e2
|
||||
eb2.format = 'mobi'
|
||||
|
||||
|
@ -1203,7 +1212,7 @@ class OnixLoaderTests(TestCase):
|
|||
'Subtitle': u'',
|
||||
'TableOfContents': u'',
|
||||
'Title': u'Immersion into Noise',
|
||||
'URL': u'http://dx.doi.org/10.3998/ohp.9618970.0001.001',
|
||||
'URL': u'https://doi.org/10.3998/ohp.9618970.0001.001',
|
||||
'eISBN': u'N/A',
|
||||
'eListPrice': u'N/A',
|
||||
'ePublicationDate': u'',
|
||||
|
@ -1218,7 +1227,7 @@ class OnixLoaderTests(TestCase):
|
|||
'keywords': u'Greece; Greek History; Lord Byron; War of Independence; Philhellenes; war; history; Romanticism',
|
||||
'Publication type': u'Monograph', 'GBP price epub': u'5.95', 'publication month': u'11', 'no of tables': u'',
|
||||
'GBP price paperback': u'15.95', 'AUD price epub': u'9.95', 'ISBN 4 with dashes': u'978-1-906924-02-7-epub',
|
||||
'DOI prefix': u'10.11647', 'License URL (human-readable summary)': u'http://creativecommons.org/licenses/by-nc-nd/2.0/',
|
||||
'DOI prefix': u'10.11647', 'License URL (human-readable summary)': u'https://creativecommons.org/licenses/by-nc-nd/2.0/',
|
||||
'Contributor 5 surname': u'', 'Contributor 1 first name': u'William', 'Contributor 6 first name': u'',
|
||||
'ONIX Role Code (List 17)6': u'', 'ONIX Role Code (List 17)5': u'', 'ONIX Role Code (List 17)4': u'',
|
||||
'ONIX Role Code (List 17)3': u'', 'ONIX Role Code (List 17)2': u'A24', 'ONIX Role Code (List 17)1': u'A01',
|
||||
|
@ -1226,7 +1235,7 @@ class OnixLoaderTests(TestCase):
|
|||
'ISBN 3 with dashes': u'978-1-906924-02-7', 'Countries excluded': u'None', 'first edition publication date': u'39753',
|
||||
'Original Language': u'English', 'ISBN 1 with dashes': u'978-1-906924-00-3', 'Contributor 4 first name': u'',
|
||||
'ISBN 5 with dashes': u'978-1-906924-02-7-mobi', 'Contributor 2 surname': u'Beaton',
|
||||
'License URL (our copyright tab)': u'http://www.openbookpublishers.com/isbn/9781906924003#copyright',
|
||||
'License URL (our copyright tab)': u'https://www.openbookpublishers.com/isbn/9781906924003#copyright',
|
||||
'BISAC subject code 1': u'HIS042000', 'BISAC subject code 3': u'HIS037060',
|
||||
'BISAC subject code 2': u'HIS054000', 'BISAC subject code 5': u'',
|
||||
'BISAC subject code 4': u'', 'Status': u'Active', 'Geographic rights': u'Worldwide',
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
# encoding: utf-8
|
||||
'''
|
||||
methods to validate and clean identifiers
|
||||
'''
|
||||
import re
|
||||
|
||||
from PyPDF2 import PdfFileReader
|
||||
|
||||
from django.forms import ValidationError
|
||||
from regluit.pyepub import EPUB
|
||||
from regluit.mobi import Mobi
|
||||
from .isbn import ISBN
|
||||
|
||||
ID_VALIDATION = {
|
||||
'http': (re.compile(r"(https?|ftp)://(-\.)?([^\s/?\.#]+\.?)+(/[^\s]*)?$",
|
||||
flags=re.IGNORECASE|re.S ),
|
||||
"The Web Address must be a valid http(s) URL."),
|
||||
'isbn': (r'^([\dxX\-–— ]+|delete)$',
|
||||
"The ISBN must be a valid ISBN-13."),
|
||||
'doab': (r'^(\d{1,6}|delete)$',
|
||||
"The value must be 1-6 digits."),
|
||||
'gtbg': (r'^(\d{1,6}|delete)$',
|
||||
"The Gutenberg number must be 1-6 digits."),
|
||||
'doi': (r'^(https?://dx\.doi\.org/|https?://doi\.org/)?(10\.\d+/\S+|delete)$',
|
||||
"The DOI value must be a valid DOI."),
|
||||
'oclc': (r'^(\d{8,12}|delete)$',
|
||||
"The OCLCnum must be 8 or more digits."),
|
||||
'goog': (r'^([a-zA-Z0-9\-_]{12}|delete)$',
|
||||
"The Google id must be 12 alphanumeric characters, dash or underscore."),
|
||||
'gdrd': (r'^(\d{1,8}|delete)$',
|
||||
"The Goodreads ID must be 1-8 digits."),
|
||||
'thng': (r'(^\d{1,8}|delete)$',
|
||||
"The LibraryThing ID must be 1-8 digits."),
|
||||
'olwk': (r'^(/works/\)?OLd{1,8}W|delete)$',
|
||||
"The Open Library Work ID looks like 'OL####W'."),
|
||||
'glue': (r'^(\d{1,6}|delete)$',
|
||||
"The Unglue.it ID must be 1-6 digits."),
|
||||
'ltwk': (r'^(\d{1,8}|delete)$',
|
||||
"The LibraryThing work ID must be 1-8 digits."),
|
||||
}
|
||||
|
||||
def isbn_cleaner(value):
|
||||
if value == 'delete':
|
||||
return value
|
||||
if not value:
|
||||
raise forms.ValidationError('no identifier value found')
|
||||
elif value == 'delete':
|
||||
return value
|
||||
isbn=ISBN(value)
|
||||
if isbn.error:
|
||||
raise forms.ValidationError(isbn.error)
|
||||
isbn.validate()
|
||||
return isbn.to_string()
|
||||
|
||||
def olwk_cleaner(value):
|
||||
if not value == 'delete' and value.startswith('/works/'):
|
||||
value = '/works/{}'.format(value)
|
||||
return value
|
||||
|
||||
doi_match = re.compile( r'10\.\d+/\S+')
|
||||
|
||||
def doi_cleaner(value):
|
||||
if not value == 'delete' and not value.startswith('10.'):
|
||||
return doi_match.match(value).group(0)
|
||||
return value
|
||||
|
||||
ID_MORE_VALIDATION = {
|
||||
'isbn': isbn_cleaner,
|
||||
'olwk': olwk_cleaner,
|
||||
'olwk': doi_cleaner,
|
||||
}
|
||||
|
||||
def identifier_cleaner(id_type):
|
||||
if ID_VALIDATION.has_key(id_type):
|
||||
(regex, err_msg) = ID_VALIDATION[id_type]
|
||||
extra = ID_MORE_VALIDATION.get(id_type, None)
|
||||
if isinstance(regex, (str, unicode)):
|
||||
regex = re.compile(regex)
|
||||
def cleaner(value):
|
||||
if not value:
|
||||
return None
|
||||
if regex.match(value):
|
||||
if extra:
|
||||
value = extra(value)
|
||||
return value
|
||||
else:
|
||||
raise ValidationError(err_msg)
|
||||
return cleaner
|
||||
return lambda value: value
|
||||
|
||||
def test_file(the_file, fformat):
|
||||
if the_file and the_file.name:
|
||||
if fformat == 'epub':
|
||||
try:
|
||||
book = EPUB(the_file.file)
|
||||
except Exception as e:
|
||||
raise forms.ValidationError(_('Are you sure this is an EPUB file?: %s' % e) )
|
||||
elif fformat == 'mobi':
|
||||
try:
|
||||
book = Mobi(the_file.file)
|
||||
book.parse()
|
||||
except Exception as e:
|
||||
raise forms.ValidationError(_('Are you sure this is a MOBI file?: %s' % e) )
|
||||
elif fformat == 'pdf':
|
||||
try:
|
||||
doc = PdfFileReader(the_file.file)
|
||||
except Exception, e:
|
||||
raise forms.ValidationError(_('%s is not a valid PDF file' % the_file.name) )
|
||||
return True
|
||||
|
|
@ -73,7 +73,7 @@ export ANSIBLE_VAULT_PASSWORD_FILE=[path]
|
|||
```
|
||||
|
||||
To use `git diff` with these encrypted files, see the
|
||||
[.gitattributes](https://github.com/Gluejar/regluit/blob/open_source/.gitattributes) has been added to allow for using `git diff` with `ansible-vault` files: [git - How to diff ansible vault changes? - Stack Overflow](http://stackoverflow.com/questions/29937195/how-to-diff-ansible-vault-changes/39511274#39511274). One highlight from the tips, run:
|
||||
[.gitattributes](https://github.com/Gluejar/regluit/blob/open_source/.gitattributes) has been added to allow for using `git diff` with `ansible-vault` files: [git - How to diff ansible vault changes? - Stack Overflow](https://stackoverflow.com/questions/29937195/how-to-diff-ansible-vault-changes/39511274#39511274). One highlight from the tips, run:
|
||||
|
||||
|
||||
```
|
||||
|
|
|
@ -11,7 +11,6 @@ from decimal import Decimal as D
|
|||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.conf.global_settings import LANGUAGES
|
||||
from django.contrib.auth.models import User
|
||||
from django.forms.widgets import RadioSelect
|
||||
from django.forms.extras.widgets import SelectDateWidget
|
||||
|
@ -26,8 +25,6 @@ from selectable.forms import (
|
|||
AutoCompleteSelectField
|
||||
)
|
||||
|
||||
from PyPDF2 import PdfFileReader
|
||||
|
||||
#regluit imports
|
||||
|
||||
from regluit.core.models import (
|
||||
|
@ -55,36 +52,21 @@ from regluit.core.parameters import (
|
|||
REWARDS,
|
||||
BUY2UNGLUE,
|
||||
THANKS,
|
||||
AGE_LEVEL_CHOICES,
|
||||
TEXT_RELATION_CHOICES,
|
||||
)
|
||||
from regluit.core.lookups import (
|
||||
OwnerLookup,
|
||||
WorkLookup,
|
||||
PublisherNameLookup,
|
||||
SubjectLookup,
|
||||
EditionNoteLookup,
|
||||
)
|
||||
from regluit.core.validation import test_file
|
||||
from regluit.utils.localdatetime import now
|
||||
from regluit.utils.fields import ISBNField
|
||||
from regluit.utils.text import sanitize_line, remove_badxml
|
||||
from regluit.mobi import Mobi
|
||||
from regluit.pyepub import EPUB
|
||||
from regluit.bisac.models import BisacHeading
|
||||
|
||||
|
||||
from .bibforms import EditionForm, IdentifierForm
|
||||
from questionnaire.models import Questionnaire
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
nulls = [False, 'delete', '']
|
||||
CREATOR_RELATIONS = (
|
||||
('aut', 'Author'),
|
||||
('edt', 'Editor'),
|
||||
('trl', 'Translator'),
|
||||
('ill', 'Illustrator'),
|
||||
('dsr', 'Designer'),
|
||||
('aui', 'Author of Introduction'),
|
||||
)
|
||||
|
||||
bisac_headings = BisacHeading.objects.all()
|
||||
|
||||
class SurveyForm(forms.Form):
|
||||
label = forms.CharField(max_length=64, required=True)
|
||||
|
@ -110,188 +92,10 @@ class SurveyForm(forms.Form):
|
|||
self.work = None
|
||||
raise forms.ValidationError( 'That ISBN is not in our database')
|
||||
|
||||
|
||||
class EditionForm(forms.ModelForm):
|
||||
add_author = forms.CharField(max_length=500, required=False)
|
||||
add_author_relation = forms.ChoiceField(choices=CREATOR_RELATIONS, initial=('aut', 'Author'))
|
||||
add_subject = AutoCompleteSelectField(
|
||||
SubjectLookup,
|
||||
widget=AutoCompleteSelectWidget(SubjectLookup, allow_new=True),
|
||||
label='Keyword',
|
||||
required=False,
|
||||
)
|
||||
add_related_work = AutoCompleteSelectField(
|
||||
WorkLookup,
|
||||
widget=AutoCompleteSelectWidget(WorkLookup, allow_new=False, attrs={'size': 40}),
|
||||
label='Related Work',
|
||||
required=False,
|
||||
)
|
||||
add_work_relation = forms.ChoiceField(
|
||||
choices=TEXT_RELATION_CHOICES,
|
||||
initial=('translation', 'translation'),
|
||||
required=False,
|
||||
)
|
||||
|
||||
bisac = forms.ModelChoiceField( bisac_headings, required=False )
|
||||
|
||||
publisher_name = AutoCompleteSelectField(
|
||||
PublisherNameLookup,
|
||||
label='Publisher Name',
|
||||
widget=AutoCompleteSelectWidget(PublisherNameLookup,allow_new=True),
|
||||
required=False,
|
||||
allow_new=True,
|
||||
)
|
||||
|
||||
isbn = ISBNField(
|
||||
label=_("ISBN"),
|
||||
max_length=17,
|
||||
required = False,
|
||||
help_text = _("13 digits, no dash."),
|
||||
error_messages = {
|
||||
'invalid': _("This must be a valid ISBN-13."),
|
||||
}
|
||||
)
|
||||
goog = forms.RegexField(
|
||||
label=_("Google Books ID"),
|
||||
max_length=12,
|
||||
regex=r'^([a-zA-Z0-9\-_]{12}|delete)$',
|
||||
required = False,
|
||||
help_text = _("12 alphanumeric characters, dash or underscore, case sensitive."),
|
||||
error_messages = {
|
||||
'invalid': _("This value must be 12 alphanumeric characters, dash or underscore."),
|
||||
}
|
||||
)
|
||||
gdrd = forms.RegexField(
|
||||
label=_("GoodReads ID"),
|
||||
max_length=8,
|
||||
regex=r'^(\d+|delete)$',
|
||||
required = False,
|
||||
help_text = _("1-8 digits."),
|
||||
error_messages = {
|
||||
'invalid': _("This value must be 1-8 digits."),
|
||||
}
|
||||
)
|
||||
thng = forms.RegexField(
|
||||
label=_("LibraryThing ID"),
|
||||
max_length=8,
|
||||
regex=r'(^\d+|delete)$',
|
||||
required = False,
|
||||
help_text = _("1-8 digits."),
|
||||
error_messages = {
|
||||
'invalid': _("This value must be 1-8 digits."),
|
||||
}
|
||||
)
|
||||
oclc = forms.RegexField(
|
||||
label=_("OCLCnum"),
|
||||
regex=r'^(\d\d\d\d\d\d\d\d\d*|delete)$',
|
||||
required = False,
|
||||
help_text = _("8 or more digits."),
|
||||
error_messages = {
|
||||
'invalid': _("This value must be 8 or more digits."),
|
||||
}
|
||||
)
|
||||
http = forms.RegexField(
|
||||
label=_("HTTP URL"),
|
||||
# https://mathiasbynens.be/demo/url-regex
|
||||
regex=re.compile(r"(https?|ftp)://(-\.)?([^\s/?\.#]+\.?)+(/[^\s]*)?$",
|
||||
flags=re.IGNORECASE|re.S ),
|
||||
required = False,
|
||||
help_text = _("no spaces of funny stuff."),
|
||||
error_messages = {
|
||||
'invalid': _("This value must be a valid http(s) URL."),
|
||||
}
|
||||
)
|
||||
doi = forms.RegexField(
|
||||
label=_("DOI"),
|
||||
regex=r'^(https?://dx\.doi\.org/|https?://doi\.org/)?(10\.\d+/\S+|delete)$',
|
||||
required = False,
|
||||
help_text = _("starts with '10.' or 'https://doi.org'"),
|
||||
error_messages = {
|
||||
'invalid': _("This value must be a valid DOI."),
|
||||
}
|
||||
)
|
||||
language = forms.ChoiceField(choices=LANGUAGES)
|
||||
age_level = forms.ChoiceField(choices=AGE_LEVEL_CHOICES, required=False)
|
||||
description = forms.CharField( required=False, widget=CKEditorWidget())
|
||||
coverfile = forms.ImageField(required=False)
|
||||
note = AutoCompleteSelectField(
|
||||
EditionNoteLookup,
|
||||
widget=AutoCompleteSelectWidget(EditionNoteLookup, allow_new=True),
|
||||
label='Edition Note',
|
||||
required=False,
|
||||
allow_new=True,
|
||||
)
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(EditionForm, self).__init__(*args, **kwargs)
|
||||
self.relators = []
|
||||
if self.instance:
|
||||
for relator in self.instance.relators.all():
|
||||
select = forms.Select(choices=CREATOR_RELATIONS).render('change_relator_%s' % relator.id , relator.relation.code )
|
||||
self.relators.append({'relator':relator, 'select':select})
|
||||
|
||||
def clean_doi(self):
|
||||
doi = self.cleaned_data["doi"]
|
||||
if doi and doi.startswith("http"):
|
||||
return doi.split('/',3)[3]
|
||||
return doi
|
||||
|
||||
def clean_title(self):
|
||||
return sanitize_line(self.cleaned_data["title"])
|
||||
|
||||
def clean_add_author(self):
|
||||
return sanitize_line(self.cleaned_data["add_author"])
|
||||
|
||||
def clean_description(self):
|
||||
return remove_badxml(self.cleaned_data["description"])
|
||||
|
||||
def clean(self):
|
||||
has_isbn = self.cleaned_data.get("isbn", False) not in nulls
|
||||
has_oclc = self.cleaned_data.get("oclc", False) not in nulls
|
||||
has_goog = self.cleaned_data.get("goog", False) not in nulls
|
||||
has_http = self.cleaned_data.get("http", False) not in nulls
|
||||
has_doi = self.cleaned_data.get("doi", False) not in nulls
|
||||
try:
|
||||
has_id = self.instance.work.identifiers.all().count() > 0
|
||||
except AttributeError:
|
||||
has_id = False
|
||||
if not has_id and not has_isbn and not has_oclc and not has_goog and not has_http and not has_doi:
|
||||
raise forms.ValidationError(_("There must be either an ISBN, a DOI, a URL or an OCLC number."))
|
||||
return self.cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = Edition
|
||||
exclude = ('created', 'work')
|
||||
widgets = {
|
||||
'title': forms.TextInput(attrs={'size': 40}),
|
||||
'add_author': forms.TextInput(attrs={'size': 30}),
|
||||
'add_subject': forms.TextInput(attrs={'size': 30}),
|
||||
'unglued': forms.CheckboxInput(),
|
||||
'cover_image': forms.TextInput(attrs={'size': 60}),
|
||||
}
|
||||
|
||||
def test_file(the_file):
|
||||
if the_file and the_file.name:
|
||||
if format == 'epub':
|
||||
try:
|
||||
book = EPUB(the_file.file)
|
||||
except Exception as e:
|
||||
raise forms.ValidationError(_('Are you sure this is an EPUB file?: %s' % e) )
|
||||
elif format == 'mobi':
|
||||
try:
|
||||
book = Mobi(the_file.file)
|
||||
book.parse()
|
||||
except Exception as e:
|
||||
raise forms.ValidationError(_('Are you sure this is a MOBI file?: %s' % e) )
|
||||
elif format == 'pdf':
|
||||
try:
|
||||
doc = PdfFileReader(the_file.file)
|
||||
except Exception, e:
|
||||
raise forms.ValidationError(_('%s is not a valid PDF file' % the_file.name) )
|
||||
|
||||
class EbookFileForm(forms.ModelForm):
|
||||
file = forms.FileField(max_length=16777216)
|
||||
file = forms.FileField(max_length=16777216)
|
||||
version_label = forms.CharField(max_length=512, required=False)
|
||||
new_version_label = forms.CharField(required=False)
|
||||
new_version_label = forms.CharField(required=False)
|
||||
|
||||
def __init__(self, campaign_type=BUY2UNGLUE, *args, **kwargs):
|
||||
super(EbookFileForm, self).__init__(*args, **kwargs)
|
||||
|
@ -315,22 +119,22 @@ class EbookFileForm(forms.ModelForm):
|
|||
return self.cleaned_data.get('format','')
|
||||
|
||||
def clean(self):
|
||||
format = self.cleaned_data['format']
|
||||
fformat = self.cleaned_data['format']
|
||||
the_file = self.cleaned_data.get('file', None)
|
||||
test_file(the_file)
|
||||
test_file(the_file, fformat)
|
||||
return self.cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = EbookFile
|
||||
widgets = { 'edition': forms.HiddenInput}
|
||||
exclude = { 'created', 'asking', 'ebook' }
|
||||
|
||||
fields = ('file', 'format', 'edition')
|
||||
|
||||
class EbookForm(forms.ModelForm):
|
||||
file = forms.FileField(max_length=16777216, required=False)
|
||||
file = forms.FileField(max_length=16777216, required=False)
|
||||
url = forms.CharField(required=False, widget=forms.TextInput(attrs={'size' : 60},))
|
||||
version_label = forms.CharField(required=False)
|
||||
new_version_label = forms.CharField(required=False)
|
||||
|
||||
version_label = forms.CharField(required=False)
|
||||
new_version_label = forms.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Ebook
|
||||
#exclude = ('created', 'download_count', 'active', 'filesize', 'version_iter')
|
||||
|
@ -343,7 +147,7 @@ class EbookForm(forms.ModelForm):
|
|||
def clean_version_label(self):
|
||||
new_label = self.data.get('new_version_label','')
|
||||
return new_label if new_label else self.cleaned_data['version_label']
|
||||
|
||||
|
||||
def clean_provider(self):
|
||||
url = self.cleaned_data['url']
|
||||
new_provider = Ebook.infer_provider(url)
|
|
@ -0,0 +1,175 @@
|
|||
# encoding: utf-8
|
||||
'''
|
||||
forms for bib models
|
||||
'''
|
||||
import re
|
||||
|
||||
from ckeditor.widgets import CKEditorWidget
|
||||
|
||||
from django import forms
|
||||
from django.conf.global_settings import LANGUAGES
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from selectable.forms import (
|
||||
AutoCompleteSelectWidget,
|
||||
AutoCompleteSelectField
|
||||
)
|
||||
from regluit.core.lookups import (
|
||||
WorkLookup,
|
||||
PublisherNameLookup,
|
||||
SubjectLookup,
|
||||
EditionNoteLookup,
|
||||
)
|
||||
from regluit.bisac.models import BisacHeading
|
||||
from regluit.core.models import Edition, Identifier
|
||||
from regluit.core.parameters import (
|
||||
AGE_LEVEL_CHOICES,
|
||||
ID_CHOICES,
|
||||
TEXT_RELATION_CHOICES,
|
||||
)
|
||||
from regluit.core.validation import identifier_cleaner
|
||||
from regluit.utils.text import sanitize_line, remove_badxml
|
||||
|
||||
CREATOR_RELATIONS = (
|
||||
('aut', 'Author'),
|
||||
('edt', 'Editor'),
|
||||
('trl', 'Translator'),
|
||||
('ill', 'Illustrator'),
|
||||
('dsr', 'Designer'),
|
||||
('aui', 'Author of Introduction'),
|
||||
)
|
||||
|
||||
NULLS = [False, 'delete', '']
|
||||
|
||||
bisac_headings = BisacHeading.objects.all()
|
||||
|
||||
class IdentifierForm(forms.ModelForm):
|
||||
id_type = forms.ChoiceField(label='Identifier Type', choices=ID_CHOICES)
|
||||
id_value = forms.CharField(
|
||||
label='Identifier Value',
|
||||
widget=forms.TextInput(attrs={'size': 60}),
|
||||
required=False,
|
||||
)
|
||||
make_new = forms.BooleanField(
|
||||
label='There\'s no existing Identifier, so please use an Unglue.it ID',
|
||||
required=False,
|
||||
)
|
||||
identifier = None
|
||||
|
||||
def clean(self):
|
||||
id_type = self.cleaned_data['id_type']
|
||||
id_value = self.cleaned_data.get('id_value', '').strip()
|
||||
make_new = self.cleaned_data.get('make_new', False)
|
||||
if not make_new:
|
||||
self.cleaned_data['value'] = identifier_cleaner(id_type)(id_value)
|
||||
return self.cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = Identifier
|
||||
fields = ('id_type', 'id_value')
|
||||
widgets = {
|
||||
'id_value': forms.TextInput(attrs={'size': 40}),
|
||||
}
|
||||
|
||||
class EditionForm(forms.ModelForm):
|
||||
'''
|
||||
form for bibliographic data (both editions and works
|
||||
'''
|
||||
add_author = forms.CharField(max_length=500, required=False)
|
||||
add_author_relation = forms.ChoiceField(choices=CREATOR_RELATIONS, initial=('aut', 'Author'))
|
||||
add_subject = AutoCompleteSelectField(
|
||||
SubjectLookup,
|
||||
widget=AutoCompleteSelectWidget(SubjectLookup, allow_new=True),
|
||||
label='Keyword',
|
||||
required=False,
|
||||
)
|
||||
add_related_work = AutoCompleteSelectField(
|
||||
WorkLookup,
|
||||
widget=AutoCompleteSelectWidget(WorkLookup, allow_new=False, attrs={'size': 40}),
|
||||
label='Related Work',
|
||||
required=False,
|
||||
)
|
||||
add_work_relation = forms.ChoiceField(
|
||||
choices=TEXT_RELATION_CHOICES,
|
||||
initial=('translation', 'translation'),
|
||||
required=False,
|
||||
)
|
||||
|
||||
bisac = forms.ModelChoiceField( bisac_headings, required=False )
|
||||
|
||||
publisher_name = AutoCompleteSelectField(
|
||||
PublisherNameLookup,
|
||||
label='Publisher Name',
|
||||
widget=AutoCompleteSelectWidget(PublisherNameLookup,allow_new=True),
|
||||
required=False,
|
||||
allow_new=True,
|
||||
)
|
||||
|
||||
id_type = forms.ChoiceField(label='Identifier Type', choices=ID_CHOICES)
|
||||
id_value = forms.CharField(
|
||||
label='Identifier Value',
|
||||
widget=forms.TextInput(attrs={'size': 60}),
|
||||
required=False,
|
||||
)
|
||||
language = forms.ChoiceField(choices=LANGUAGES)
|
||||
age_level = forms.ChoiceField(choices=AGE_LEVEL_CHOICES, required=False)
|
||||
description = forms.CharField( required=False, widget=CKEditorWidget())
|
||||
coverfile = forms.ImageField(required=False)
|
||||
note = AutoCompleteSelectField(
|
||||
EditionNoteLookup,
|
||||
widget=AutoCompleteSelectWidget(EditionNoteLookup, allow_new=True),
|
||||
label='Edition Note',
|
||||
required=False,
|
||||
allow_new=True,
|
||||
)
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(EditionForm, self).__init__(*args, **kwargs)
|
||||
self.relators = []
|
||||
if self.instance:
|
||||
for relator in self.instance.relators.all():
|
||||
select = forms.Select(choices=CREATOR_RELATIONS).render(
|
||||
'change_relator_%s' % relator.id,
|
||||
relator.relation.code
|
||||
)
|
||||
self.relators.append({'relator':relator, 'select':select})
|
||||
|
||||
def clean_doi(self):
|
||||
doi = self.cleaned_data["doi"]
|
||||
if doi and doi.startswith("http"):
|
||||
return doi.split('/', 3)[3]
|
||||
return doi
|
||||
|
||||
def clean_title(self):
|
||||
return sanitize_line(self.cleaned_data["title"])
|
||||
|
||||
def clean_add_author(self):
|
||||
return sanitize_line(self.cleaned_data["add_author"])
|
||||
|
||||
def clean_description(self):
|
||||
return remove_badxml(self.cleaned_data["description"])
|
||||
|
||||
def clean(self):
|
||||
id_type = self.cleaned_data['id_type']
|
||||
id_value = self.cleaned_data.get('id_value','').strip()
|
||||
if id_value:
|
||||
identifier = Identifier.objects.filter(type=id_type, value=id_value)
|
||||
if identifier:
|
||||
err_msg = "{} is a duplicate for work #{}.".format(identifier[0], identifier[0].work.id)
|
||||
self.add_error('id_value', forms.ValidationError(err_msg))
|
||||
try:
|
||||
self.cleaned_data['value'] = identifier_cleaner(id_type)(id_value)
|
||||
except forms.ValidationError, ve:
|
||||
self.add_error('id_value', forms.ValidationError('{}: {}'.format(ve.message, id_value)))
|
||||
return self.cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = Edition
|
||||
exclude = ('created', 'work')
|
||||
widgets = {
|
||||
'title': forms.TextInput(attrs={'size': 40}),
|
||||
'add_author': forms.TextInput(attrs={'size': 30}),
|
||||
'add_subject': forms.TextInput(attrs={'size': 30}),
|
||||
'unglued': forms.CheckboxInput(),
|
||||
'cover_image': forms.TextInput(attrs={'size': 60}),
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
<h1>Unglue.it is currently undergoing maintenance</h1>
|
||||
|
||||
<p>
|
||||
While you wait, why not like us on <a href="http://facebook.com/unglueit">Facebook</a>, follow us on <a href="http://twitter.com/unglueit">Twitter</a>, or subscribe to our <a href="https://blog.unglue.it">blog</a>? We'll keep you up to date there with our progress fixing things.
|
||||
While you wait, why not like us on <a href="https://facebook.com/unglueit">Facebook</a>, follow us on <a href="https://twitter.com/unglueit">Twitter</a>, or subscribe to our <a href="https://blog.unglue.it">blog</a>? We'll keep you up to date there with our progress fixing things.
|
||||
</p>
|
||||
|
||||
<p>You can also help us by <a href="{% url 'feedback' %}">sending us feedback</a>.</p>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{% block title %} Everything You Always Wanted to Know {% endblock %}
|
||||
{% block doccontent %}
|
||||
<h2>About</h2>
|
||||
<p><a href="https://unglue.it">Unglue.it</a> is a service provided by <a href="http://ebookfoundation.org">The Free Ebook Foundation</a> It's a place for individuals and institutions to join together to liberate specific ebooks and other types of digital content by paying authors and publishers to relicense their works under <a href="http://creativecommons.org">Creative Commons</a> licenses.</p>
|
||||
<p><a href="https://unglue.it">Unglue.it</a> is a service provided by <a href="https://ebookfoundation.org">The Free Ebook Foundation</a> It's a place for individuals and institutions to join together to liberate specific ebooks and other types of digital content by paying authors and publishers to relicense their works under <a href="https://creativecommons.org">Creative Commons</a> licenses.</p>
|
||||
|
||||
<p>What does this mean?</p>
|
||||
<ul>
|
||||
|
@ -28,7 +28,7 @@
|
|||
|
||||
<div class="outer">
|
||||
<div><img src="/static/images/headshots/raymond.jpg" class="mediaborder" alt="raymond" /></div>
|
||||
<div class="text"><b>Raymond Yee</b> is a data architect, author, consultant, and teacher. He is author of the leading book on web mashups, <a href="http://blog.mashupguide.net/2008/02/29/the-book-is-available-now/">Pro Web 2.0 Mashups: Remixing Data and Web Services</a> (published by Apress and licensed under a Creative Commons license), and has numerous blogs at his <a href="http://raymondyee.net/">personal site</a>. At the UC Berkeley School of Information, he taught Mixing and Remixing Information, a course on using APIs to create mashups. An open data and open government afficionado, he recently co-wrote three influential reports on how the US government can improve its efforts to make data and services available through APIs. Raymond served as the Integration Advisor for the Zotero Project (a widely used open source research tool) and managed the <a href="http://www.archive.org/details/zoterocommons">Zotero Commons</a>, a collaboration between George Mason University and the Internet Archive. Raymond has been an invited speaker about web technology at the Library of Congress, Fashion Institute of Technology, the O'Reilly Emerging Technology Conference, American Library Association, the Open Education conference, Code4lib, Educause, and NISO. While earning a Ph.D. in biophysics, he taught computer science, philosophy, and personal development to middle and high school students in the Academic Talent Development Program on the Berkeley campus. Raymond is an erstwhile tubaist, admirer of J. S. Bach, and son of industrious Chinese-Canadian restaurateurs.<br /><br />
|
||||
<div class="text"><b>Raymond Yee</b> is a data architect, author, consultant, and teacher. He is author of the leading book on web mashups, <a href="http://blog.mashupguide.net/2008/02/29/the-book-is-available-now/">Pro Web 2.0 Mashups: Remixing Data and Web Services</a> (published by Apress and licensed under a Creative Commons license), and has numerous blogs at his <a href="http://raymondyee.net/">personal site</a>. At the UC Berkeley School of Information, he taught Mixing and Remixing Information, a course on using APIs to create mashups. An open data and open government afficionado, he recently co-wrote three influential reports on how the US government can improve its efforts to make data and services available through APIs. Raymond served as the Integration Advisor for the Zotero Project (a widely used open source research tool) and managed the <a href="https://www.archive.org/details/zoterocommons">Zotero Commons</a>, a collaboration between George Mason University and the Internet Archive. Raymond has been an invited speaker about web technology at the Library of Congress, Fashion Institute of Technology, the O'Reilly Emerging Technology Conference, American Library Association, the Open Education conference, Code4lib, Educause, and NISO. While earning a Ph.D. in biophysics, he taught computer science, philosophy, and personal development to middle and high school students in the Academic Talent Development Program on the Berkeley campus. Raymond is an erstwhile tubaist, admirer of J. S. Bach, and son of industrious Chinese-Canadian restaurateurs.<br /><br />
|
||||
|
||||
Recently, Raymond has been teaching a course at UC Berkeley's School of Information science on working with Open Data; with any luck, the textbook he's working on will be a subject of a future ungluing campaign!</div>
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<b>What if you could</b> give a book to everyone on earth? Get an ebook and read it on any device, in any format, forever? Give an ebook to your library, for them to share? Own DRM-free ebooks, legally? Read free ebooks, and know their creators had been fairly paid?
|
||||
</p>
|
||||
<p>
|
||||
At Unglue.it, you can reward creators that have set their books free, via <a href="http://creativecommons.org">Creative Commons</a> licenses. You can buy books that WANT to be free, but need funding to get there. You can pledge toward creating free ebooks from print books.
|
||||
At Unglue.it, you can reward creators that have set their books free, via <a href="https://creativecommons.org">Creative Commons</a> licenses. You can buy books that WANT to be free, but need funding to get there. You can pledge toward creating free ebooks from print books.
|
||||
</p>
|
||||
<p>
|
||||
Traditionally published books are stuck: legal restrictions keep you from being able to enjoy and share them. Unglue.it gets them unstuck. Authors and publishers decide what amount lets them freely share their books with the world while still making a living. We raise that fee here through crowdfunding: people like you chipping in. When campaigns succeed, authors and publishers get paid, and you get a free ebook.
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
<p>Ungluing campaigns offer ways to earn income while at the same time making your books free. They're a tool for you to reach out to, discover, interact with, and increase your audience while raising the profile of your work. Your unglued book, in turn, becomes an ambassador for the rest of your work.</p>
|
||||
|
||||
<p>Rest assured that, when you unglue a book, you retain the copyright. You can choose whichever <a href="http://creativecommons.org/licenses/">Creative Commons license</a> serves your interests best, whether that's inviting your fans to remix your work or sharing it widely while reserving certain rights. If a book is unglued via a pledge campaign, you should set the price to zero in the Smashwords admin so as to honor the intentions of your supporters. We encourage you to develop enhanced digital editions to sell alongside the Creative Commons Licensed edition and print versions for sale. </p>
|
||||
<p>Rest assured that, when you unglue a book, you retain the copyright. You can choose whichever <a href="https://creativecommons.org/licenses/">Creative Commons license</a> serves your interests best, whether that's inviting your fans to remix your work or sharing it widely while reserving certain rights. If a book is unglued via a pledge campaign, you should set the price to zero in the Smashwords admin so as to honor the intentions of your supporters. We encourage you to develop enhanced digital editions to sell alongside the Creative Commons Licensed edition and print versions for sale. </p>
|
||||
|
||||
<h2>The Fine Print</h2>
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
Of course, <span class="ungluer"></span> can't do that, because books are stuck. Copyright and ebook licensing often make it illegal for individuals, and even libraries, to share digital books. DRM makes sharing hard. At Unglue.it, we respect copyright and pay book creators, while enabling book lovers to share their favorite books with everyone.
|
||||
</p>
|
||||
<p>
|
||||
In short, we get books unstuck. Authors and publishers decide what amount lets them freely share their books with the world while still making a living. We raise that fee here through crowdfunding: people like you and <span class="ungluer"></span> chipping in. When campaigns succeed, authors and publishers get paid, and they issue a free electronic edition under a <a href="http://creativecommons.org">Creative Commons</a> license. It's like public radio: once donors have covered the station's costs, its programs are free to all.
|
||||
In short, we get books unstuck. Authors and publishers decide what amount lets them freely share their books with the world while still making a living. We raise that fee here through crowdfunding: people like you and <span class="ungluer"></span> chipping in. When campaigns succeed, authors and publishers get paid, and they issue a free electronic edition under a <a href="https://creativecommons.org">Creative Commons</a> license. It's like public radio: once donors have covered the station's costs, its programs are free to all.
|
||||
</p>
|
||||
<p>
|
||||
What if your favorite book could be free to the world?
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
We all do, but we can't, because books are stuck. Copyright and ebook licensing often make it illegal for individuals, and even libraries, to share digital books. DRM makes sharing hard. At Unglue.it, we respect copyright and pay book creators, while enabling book lovers to share their favorite books with everyone.
|
||||
</p>
|
||||
<p>
|
||||
In short, we get books unstuck. Authors and publishers decide what amount lets them freely share their books with the world while still making a living. We raise that fee here through crowdfunding: people like you and <span class="ungluer"></span> chipping in. When campaigns succeed, book creators get paid, and they issue a free electronic edition under a <a href="http://creativecommons.org">Creative Commons</a> license. It's like public radio: once donors have covered the station's costs, its programs are free to all.
|
||||
In short, we get books unstuck. Authors and publishers decide what amount lets them freely share their books with the world while still making a living. We raise that fee here through crowdfunding: people like you and <span class="ungluer"></span> chipping in. When campaigns succeed, book creators get paid, and they issue a free electronic edition under a <a href="https://creativecommons.org">Creative Commons</a> license. It's like public radio: once donors have covered the station's costs, its programs are free to all.
|
||||
</p>
|
||||
<p>
|
||||
<span class="ungluer"></span> hasn't nominated any favorite books yet, but you can. What if your favorite book could be free to the world?
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<div id="user-block1">
|
||||
<div id="block-intro-text"><br /><span class="special-user-name">Ready to Read</span></div>
|
||||
</div>
|
||||
<div class="user-block24"><p style="font-size: larger;"><span class="user-short-info">These {% if pub_lang %}{{ pub_lang|ez_lang_name }} language {% endif %}{% if cc.is_cc %}<a href="http://creativecommons.org/">Creative Commons</a>{% endif %} {% if license %}<a href="{{cc.url}}">{{ license }}</a>{% endif %} {%if cc.is_pd %}ebooks are ready to read - they belong to all of us!{% else %}licensed ebooks are ready to read - the people who created them want you to read and share them.{% endif %}</span></p>
|
||||
<div class="user-block24"><p style="font-size: larger;"><span class="user-short-info">These {% if pub_lang %}{{ pub_lang|ez_lang_name }} language {% endif %}{% if cc.is_cc %}<a href="https://creativecommons.org/">Creative Commons</a>{% endif %} {% if license %}<a href="{{cc.url}}">{{ license }}</a>{% endif %} {%if cc.is_pd %}ebooks are ready to read - they belong to all of us!{% else %}licensed ebooks are ready to read - the people who created them want you to read and share them.{% endif %}</span></p>
|
||||
{% if license %}<p style="font-size: smaller;"><a href="{{cc.url}}"><img src="{{cc.badge}}" alt="badge for {{license}}" style="float:left;padding:0.5em" /></a> {{cc.description}}
|
||||
</p>{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -233,7 +233,7 @@ $j(document).ready(function() {
|
|||
</p>
|
||||
<p class="ebook_download logo"><img src="/static/images/ibooks_logo.jpg" alt="iBooks Logo" />iBooks</p>
|
||||
<ul>
|
||||
<li><a href="http://itunes.apple.com/us/app/ibooks/id364709193?mt=8">Download the free iBooks app</a> from the App Store.</li>
|
||||
<li><a href="https://itunes.apple.com/us/app/ibooks/id364709193?mt=8">Download the free iBooks app</a> from the App Store.</li>
|
||||
<li>Download the <a href="{{ formats.epub }}">epub file</a>.</li>
|
||||
<li>You will be given the option of opening the file in iBooks.</li>
|
||||
</ul>
|
||||
|
@ -256,7 +256,7 @@ $j(document).ready(function() {
|
|||
</p>
|
||||
<p class="ebook_download logo"><img src="/static/images/ibooks_logo.jpg" alt="iBooks Logo" />iBooks</p>
|
||||
<ul>
|
||||
<li><a href="http://itunes.apple.com/us/app/ibooks/id364709193?mt=8">Download the free iBooks app</a> from the App Store.</li>
|
||||
<li><a href="https://itunes.apple.com/us/app/ibooks/id364709193?mt=8">Download the free iBooks app</a> from the App Store.</li>
|
||||
<li>Download the <a href="{{ formats.pdf }}">pdf file</a>.</li>
|
||||
<li>You will be given the option of opening the file in iBooks.</li>
|
||||
</ul>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
{% if work.ebooks_all %}
|
||||
<h3>Manage eBooks</h3>
|
||||
<ul>
|
||||
{% for ebook in work.ebooks_all %}
|
||||
<li>
|
||||
<a href="{{ ebook.url }}">{{ ebook.format }}</a>, created {{ ebook.created }}{% if ebook.user %},
|
||||
by <a href="{% url 'supporter' ebook.user.id %}">{{ ebook.user }}</a>{% endif %}.
|
||||
{% if ebook.filesize %}{{ ebook.filesize }}{% else %}??{% endif %}B
|
||||
{% if ebook.version_label %}{{ ebook.version }}{% endif %}
|
||||
{% if ebook.active %}<input type="submit" name="deactivate_ebook_{{ ebook.id }}" value="deactivate" class="deletebutton" title="deactivate ebook" />{% else %}<input type="submit" name="activate_ebook_{{ ebook.id }}" value="activate" class="deletebutton" title="activate ebook" />{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<br />
|
||||
<br />
|
||||
<input type="submit" name="activate_all_ebooks" value="activate all ebooks" class="deletebutton" title="activate all ebooks" />
|
||||
<input type="submit" name="deactivate_all_ebooks" value="deactivate all ebooks" class="deletebutton" title="deactivate all ebooks" />
|
||||
{% endif %}
|
|
@ -7,7 +7,7 @@
|
|||
<ul class="terms">
|
||||
<li>a list of ISBNs of other editions of the work</li>
|
||||
<li> <i>For Pledge Campaigns only</i><ol>
|
||||
<li>the Creative Commons license selected for the campaign, formatted in accordance with best practices at <a href="http://wiki.creativecommons.org/Marking/Creators">http://wiki.creativecommons.org/Marking/Creators</a>, and a statement that for the purposes of the license, "Non-Commercial" use shall include, but not be limited to, the distribution of the Work by a commercial entity without charge.</li>
|
||||
<li>the Creative Commons license selected for the campaign, formatted in accordance with best practices at <a href="https://wiki.creativecommons.org/Marking/Creators">https://wiki.creativecommons.org/Marking/Creators</a>, and a statement that for the purposes of the license, "Non-Commercial" use shall include, but not be limited to, the distribution of the Work by a commercial entity without charge.</li>
|
||||
<li>an acknowledgement of Ungluers of the work, formatted in accordance with the premium descriptions on the Campaign page.</li>
|
||||
<li>The Unglue.it logo</li>
|
||||
<li>the text “CC edition release enabled by Unglue.it users” (including the hyperlink to the site)</li>
|
||||
|
|
|
@ -0,0 +1,249 @@
|
|||
{% extends 'basedocumentation.html' %}
|
||||
|
||||
|
||||
{% block extra_extra_head %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" href="/static/css/ui-lightness/jquery-ui-1.8.16.custom.css" type="text/css" media="screen">
|
||||
<link href="/static/css/ui.fancytree.min.css" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
ul.fancytree-container {
|
||||
width: 100%;
|
||||
height: 10em;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
</style>
|
||||
{{ form.media.css }}
|
||||
<script type="text/javascript" src="{{ jquery_ui_home }}" ></script>
|
||||
<script src="/static/js/jquery.fancytree-all.min.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
$j(function(){
|
||||
// Initialize Fancytree
|
||||
$j("#tree").fancytree({
|
||||
extensions: ["glyph"],
|
||||
checkbox: true,
|
||||
selectMode: 1,
|
||||
glyph: {
|
||||
map: {
|
||||
doc: "fa fa-file-o",
|
||||
docOpen: "fa fa-file-o",
|
||||
checkbox: "fa fa-square-o",
|
||||
checkboxSelected: "fa fa-check-square-o",
|
||||
checkboxUnknown: "fa fa-square",
|
||||
dragHelper: "fa arrow-right",
|
||||
dropMarker: "fa long-arrow-right",
|
||||
error: "fa fa-warning",
|
||||
expanderClosed: "fa fa-caret-right",
|
||||
expanderLazy: "fa fa-angle-right",
|
||||
expanderOpen: "fa fa-caret-down",
|
||||
folder: "fa fa-folder-o",
|
||||
folderOpen: "fa fa-folder-open-o",
|
||||
loading: "fa fa-spinner fa-pulse"
|
||||
}
|
||||
},
|
||||
source: { url: "/bisac/tree", cache: true }
|
||||
});
|
||||
$j("#editform").submit(function() {
|
||||
// Render hidden <input> elements for active and selected nodes
|
||||
$j("#tree").fancytree("getTree").generateFormElements(selected="bisac");
|
||||
//alert("POST data:\n" + $j.param($j(this).serializeArray()));
|
||||
return true;
|
||||
});
|
||||
|
||||
$j().ready(function(){
|
||||
var contentblock = $j('#content-block');
|
||||
contentblock.on("click", "span.deletebutton", function () {
|
||||
var kw = $j(this).attr('data');
|
||||
var li = $j(this).parent();
|
||||
|
||||
// perform action
|
||||
{% if edition.work %}
|
||||
jQuery.post('{% url 'kw_edit' edition.work.id %}', {'remove_kw': kw, 'csrfmiddlewaretoken': '{{ csrf_token }}' }, function(data) {
|
||||
li.html('kw removed');
|
||||
});
|
||||
{% else %}
|
||||
li.html('kw removed');
|
||||
{% endif %}
|
||||
});
|
||||
// this is the id of the submit button
|
||||
|
||||
$j('#add_subject_submit').click(function(event) {
|
||||
data= $j('#id_add_subject_0').attr('value')
|
||||
if (data == 'xxbadform'){
|
||||
alert("bad keyword");
|
||||
} else {
|
||||
$j('#kw_list').append('<li>' + data + '<input type="hidden" name="new_subject" value="'+data +'" /><span class="deletebutton" data="' + data +'">x</span></li>')
|
||||
}; // data will be the added kw.
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{{ form.media.js }}
|
||||
{% endblock %}
|
||||
|
||||
{% block doccontent %}
|
||||
{% if admin %}
|
||||
{% if edition.pk %}
|
||||
<h2>Edit Edition for <a href="{% url 'work' edition.work.id %}">{{ edition.work.title }}</a></h2>
|
||||
{% else %}
|
||||
<h2>Create New Edition</h2>
|
||||
{% endif %}
|
||||
|
||||
<p>Title is required; the rest is optional, though a cover image is strongly recommended.</p>
|
||||
{% if alert %}
|
||||
<ul class='errorlist'>
|
||||
<li>{{ alert }}</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<form id="editform" enctype="multipart/form-data" method="POST" action="#">
|
||||
{% csrf_token %}
|
||||
{{ form.work }}
|
||||
{{ form.non_field_errors }}
|
||||
<!--{{ form.errors }}-->
|
||||
<div>
|
||||
<p><b>Title</b>: {{ form.title.errors }}{{ form.title }}</p>
|
||||
<p><b>Publisher Name</b> : {{ form.publisher_name.errors }}{{ form.publisher_name }}<br />(If you change this, click another form element before submitting)</p>
|
||||
|
||||
<p>
|
||||
<b>Authors</b>:
|
||||
{% if edition.pk and edition.relators or edition.new_authors %}
|
||||
<ul>
|
||||
{% for relator in form.relators %}
|
||||
<li>{{ relator.relator.name }} {{ relator.select }} <input type="submit" name="delete_author_{{ relator.relator.author.id }}" value="x" class="deletebutton" title="delete author"></li>
|
||||
{% endfor %}
|
||||
{% for author in edition.new_authors %}
|
||||
<li>{{ author.0 }}<input type="hidden" name="new_author" value="{{ author.0 }}" /> ({{ author.1 }})<input type="hidden" name="new_author_relation" value="{{ author.1 }}" /></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
(None listed)
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<p><b>Add a Creator</b> (<I>Firstname Lastname</I>): {{ form.add_author.errors }}{{ form.add_author }}{{ form.add_author_relation.errors }}{{ form.add_author_relation }}
|
||||
<input type="submit" name="add_author_submit" value="Add Author" id="submit_author"></p>
|
||||
|
||||
<p><b>Language</b>: {{ form.language.errors }}{{ form.language }}</p>
|
||||
{% if edition.pk %}
|
||||
<p><b>Add a Related Work</b>: {{ form.add_work_relation.errors }}{{ form.add_work_relation }} of {{ form.add_related_work.errors }}{{ form.add_related_work }}</p>
|
||||
<ul>{% for work_rel in edition.work.works_related_to.all %}
|
||||
<li>
|
||||
This work is a {{ work_rel.relation }} of <a href="{% url 'work' work_rel.from_work.id %}">{{ work_rel.from_work }}</a>.
|
||||
<input type="submit" name="delete_work_rel_{{ work_rel.id }}" value="x" class="deletebutton" title="delete work relation">
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% for work_rel in edition.work.works_related_from.all %}
|
||||
<li>
|
||||
<a href="{% url 'work' work_rel.to_work.id %}">{{ work_rel.to_work }}</a> is a {{ work_rel.relation }} of this work.
|
||||
<input type="submit" name="delete_work_rel_{{ work_rel.id }}" value="x" class="deletebutton" title="delete work relation">
|
||||
</li>
|
||||
{% endfor %}</ul>
|
||||
|
||||
|
||||
{% endif %}
|
||||
<p><b>Age Level</b>: {{ form.age_level.errors }}{{ form.age_level }}</p>
|
||||
<p><b>Edition Note</b>: {{ form.note.errors }}{{ form.note }}</p>
|
||||
<h4>Identifiers </h4>
|
||||
{% if edition.work.work_ids %}
|
||||
<p><b>For the Work:</b></p>
|
||||
<ul class="bullets">
|
||||
{% for ident in edition.work.work_ids %}
|
||||
<li><b>{{ ident.label }}</b>: {{ ident.value }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if edition.identifiers.all %}
|
||||
<p><b>For the Edition:</b></p>
|
||||
<ul class="bullets">
|
||||
{% for ident in edition.identifiers.all %}
|
||||
<li><b>{{ ident.label }}</b>: {{ ident.value }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<p> Add/Change an Identifier (Enter 'delete' to remove it). </p>
|
||||
{{ form.id_value.errors }}
|
||||
{{ identform.as_p }}
|
||||
<p><b>Description</b>: <br />
|
||||
{{ form.description.errors }}{{ form.description }}<br />
|
||||
(<i>{% if work.last_campaign %}
|
||||
{% ifequal work.last_campaign.type 3 %}
|
||||
This will appear in the Description tab on the book page.
|
||||
{% else %}
|
||||
The campaign pitch will override this description.
|
||||
{% endifequal %}
|
||||
{% else %}
|
||||
This will appear in the Description tab on the book page.
|
||||
{% endif %}
|
||||
</i>)</p>
|
||||
<p><b>Publication Date</b> (<I>four-digit year</I>): {{ form.publication_date.errors }}{{ form.publication_date }}</p>
|
||||
<p><b>Subjects</b>:
|
||||
<ul id="kw_list">
|
||||
{% if edition.work.pk and edition.work.subjects %}
|
||||
{% for subject in edition.work.subjects.all %}
|
||||
<li>{{ subject.name }}
|
||||
{% if subject.authority %}
|
||||
({{subject.authority}})
|
||||
{% endif %}
|
||||
<span class="deletebutton" data="{{ subject.name }}">x</span></li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% for new_subject in edition.new_subjects %}
|
||||
<li>{{ new_subject }}<input type="hidden" name="new_subject" value="{{ new_subject }}" /></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<b>Add a Subject</b>: {{ form.add_subject.errors }}{{ form.add_subject }}
|
||||
<a class="fakeinput" id="add_subject_submit" style="font-size: smaller;" >Add Subject</a></p>
|
||||
<p id="tree" name="is_bisac"><b>Add BISAC Subject</b>:
|
||||
|
||||
</p>
|
||||
|
||||
<p><b>Cover Image</b>: <br />
|
||||
{% if edition.cover_image %}
|
||||
<img src="{{edition.cover_image}}" /><br />
|
||||
{% else %}
|
||||
[ no cover specified for this edition ]<br />
|
||||
{% endif %}
|
||||
{{ form.cover_image.errors }}{{ form.cover_image }}{{ form.cover_image.help_text }}
|
||||
(<i>Enter a URL for an image, at least 300 px wide. The image will be scaled to the proportions of a 6x9 cover. </i>)<br />
|
||||
OR...<br />
|
||||
|
||||
{{ form.coverfile.errors }}{{ form.coverfile }}{{ form.coverfile.help_text }}
|
||||
(<i>upload a cover image file (we'll automatically size if for you). </i>)<br />
|
||||
</p>
|
||||
</div>
|
||||
<input type="submit" name="create_new_edition" style="font-size: larger;" value="{% if edition.pk %}Save Edits{% else %}Create Edition{% endif %}" id="submit">
|
||||
{% with edition.work as work %}
|
||||
{% include 'ebook_list.html' %}
|
||||
{% endwith %}
|
||||
</form>
|
||||
|
||||
{% if edition.work %}
|
||||
|
||||
<h2>More Edition Management</h2>
|
||||
|
||||
<div><a href="{% url 'merge' edition.work.id %}">Merge other works into this one</a></div>
|
||||
<div><a href="{% url 'work_editions' edition.work.id %}">Remove editions from this work</a></div>
|
||||
{% if edition.id %}
|
||||
<div><a href="{% url 'manage_ebooks' edition.id %}">Add ebooks for this edition</a></div>
|
||||
{% endif %}
|
||||
{% if request.user.is_staff %}
|
||||
<div><a href="{% url 'feature' edition.work.id %}">Feature this work today</a></div>
|
||||
{% endif %}
|
||||
<br />
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
{% if edition.work %}
|
||||
{% include 'edition_display.html' %}
|
||||
{% else %}
|
||||
Sorry, there's no work specified.
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
ISBN: <span itemprop="isbn">{{ edition.isbn_13 }}</span><br />
|
||||
{% endif %}
|
||||
{% if edition.oclc %}
|
||||
OCLC: <a href="http://www.worldcat.org/oclc/{{ edition.oclc }}">{{ edition.oclc }}</a><br />
|
||||
OCLC: <a href="https://www.worldcat.org/oclc/{{ edition.oclc }}">{{ edition.oclc }}</a><br />
|
||||
{% endif %}
|
||||
{% if edition.doi %}
|
||||
DOI: <a href="https://doi.org/{{ edition.doi }}">{{ edition.doi }}</a><br />
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<dl>
|
||||
<dt>What is Unglue.it?</dt>
|
||||
|
||||
<dd><a href="/">Unglue.it</a> is a place for individuals and institutions to join together to make ebooks free to the world. We work together to support authors, publishers, or other rights holders who want their ebooks to be free. We support <a href="http://creativecommons.org">Creative Commons</a> licensing as an enabling tool to "unglue" the ebooks. </dd>
|
||||
<dd><a href="/">Unglue.it</a> is a place for individuals and institutions to join together to make ebooks free to the world. We work together to support authors, publishers, or other rights holders who want their ebooks to be free. We support <a href="https://creativecommons.org">Creative Commons</a> licensing as an enabling tool to "unglue" the ebooks. </dd>
|
||||
|
||||
<dt>What are Ungluing Campaigns?</dt>
|
||||
|
||||
|
@ -65,11 +65,11 @@ You may get premiums as part of a Pledge Campaign, depending on the amount you p
|
|||
|
||||
<dt>Does Unglue.it own the copyright of unglued books?</dt>
|
||||
|
||||
<dd>No. When you unglue a book, the copyright stays with its current owner. Unglue.it does not buy copyrights. A <a href="http://creativecommons.org">Creative Commons</a> license is a licensing agreement. As with other licensing agreements, it grants certain rights to others but does not affect the status of the copyright.<br /><br />
|
||||
<dd>No. When you unglue a book, the copyright stays with its current owner. Unglue.it does not buy copyrights. A <a href="https://creativecommons.org">Creative Commons</a> license is a licensing agreement. As with other licensing agreements, it grants certain rights to others but does not affect the status of the copyright.<br /><br />
|
||||
|
||||
Just like many traditional publishing transactions, ungluing is about licensing rights. We use <a href="http://creativecommons.org">Creative Commons</a> licenses which allow creators to choose the ways in which their works can be copied, shared and re-used.<br /><br />
|
||||
Just like many traditional publishing transactions, ungluing is about licensing rights. We use <a href="https://creativecommons.org">Creative Commons</a> licenses which allow creators to choose the ways in which their works can be copied, shared and re-used.<br /><br />
|
||||
|
||||
If you are a copyright holder, you retain your copyright when you unglue a book. Creative Commons licenses are non-exclusive, so you also retain the right to enter into separate licensing agreements. You can read more about these licenses at the <a href="http://wiki.creativecommons.org/Frequently_Asked_Questions">Creative Commons FAQ</a>.</dd>
|
||||
If you are a copyright holder, you retain your copyright when you unglue a book. Creative Commons licenses are non-exclusive, so you also retain the right to enter into separate licensing agreements. You can read more about these licenses at the <a href="https://wiki.creativecommons.org/Frequently_Asked_Questions">Creative Commons FAQ</a>.</dd>
|
||||
|
||||
<dt>If I'm a rights holder and my book is unglued, does that mean I can never make money from it again?</dt>
|
||||
|
||||
|
@ -271,7 +271,7 @@ Support the books you've loved and make them free to everyone.
|
|||
<dl>
|
||||
<dt>So what is an unglued ebook?</dt>
|
||||
|
||||
<dd>An unglued ebook is a book that's added a <a href="http://creativecommons.org">Creative Commons</a> license, after payments to the author, publisher, or other rights holder.<br /><br />
|
||||
<dd>An unglued ebook is a book that's added a <a href="https://creativecommons.org">Creative Commons</a> license, after payments to the author, publisher, or other rights holder.<br /><br />
|
||||
|
||||
What does this mean for you? If you're a book lover, you can read unglued ebooks for free, on the device of your choice, in the format of your choice, and share them with all your friends. If you're a library, you can lend them to your patrons with no checkout limits or simultaneous user restrictions, and preserve them however you think best. If you're a rights holder, you get a payments in lieu of further royalties, while retaining copyright and all interests in your work.
|
||||
</dd>
|
||||
|
@ -314,7 +314,7 @@ Under all CC licenses (except CC0), you <b>cannot</b>: remove the author's name
|
|||
|
||||
<dt>What does non-commercial mean under Creative Commons?</dt>
|
||||
|
||||
<dd>Creative Commons doesn't define "commercial" thoroughly (though it's worth reading what <a href="http://wiki.creativecommons.org/FAQ#Does_my_use_violate_the_NonCommercial_clause_of_the_licenses.3F">they have to say on the topic</a>). So as part of the Unglue.it process, ungluing rights holders agree that "For purposes of interpreting the CC License, Rights Holder agrees that 'non-commercial' use shall include, without limitation, distribution by a commercial entity without charge for access to the Work."<br /><br />
|
||||
<dd>Creative Commons doesn't define "commercial" thoroughly (though it's worth reading what <a href="https://wiki.creativecommons.org/FAQ#Does_my_use_violate_the_NonCommercial_clause_of_the_licenses.3F">they have to say on the topic</a>). So as part of the Unglue.it process, ungluing rights holders agree that "For purposes of interpreting the CC License, Rights Holder agrees that 'non-commercial' use shall include, without limitation, distribution by a commercial entity without charge for access to the Work."<br /><br />
|
||||
|
||||
Unglue.it can't provide legal advice about how to interpret Creative Commons licenses, and we encourage you to read about the licenses yourself and, if necessary, seek qualified legal advice.</dd>
|
||||
|
||||
|
@ -344,7 +344,7 @@ Unglue.it also provides links to download previously unglued books, creative com
|
|||
<dl>
|
||||
<dt>Why does an unglued book have to be in copyright?</dt>
|
||||
|
||||
<dd>Because books out of copyright are already free for you to copy, remix, and share! If a book is in the <a href="http://en.wikipedia.org/wiki/Public_domain">public domain</a>, it doesn't need to be unglued.</dd>
|
||||
<dd>Because books out of copyright are already free for you to copy, remix, and share! If a book is in the <a href="https://en.wikipedia.org/wiki/Public_domain">public domain</a>, it doesn't need to be unglued.</dd>
|
||||
|
||||
<dt>How can I tell if a book is in copyright or not?</dt>
|
||||
|
||||
|
@ -368,7 +368,7 @@ Unglue.it signs agreements assuring the copyright status of every work we unglue
|
|||
<dd>To start a campaign, you need to be a verified rights holder who has signed a Platform Services Agreement with us. </dd>
|
||||
<dt>What do I need to do to become an authorized Rights Holder on Unglue.it?</dt>
|
||||
|
||||
<dd>Contact our Rights Holder Relations team, <a href="mailto:rights@gluejar.com">rights@gluejar.com</a>, to discuss signing our <a href="https://www.docracy.com/1mud3ve9w8/unglue-it-non-exclusive-platform-services-agreement">Platform Services Agreement (PSA)</a>, setting a revenue goal, and running a campaign on Unglue.it to release your book under a <a href="http://creativecommons.org">Creative Commons</a> license, or to raise money for a book you've already released under such a license.</dd>
|
||||
<dd>Contact our Rights Holder Relations team, <a href="mailto:rights@gluejar.com">rights@gluejar.com</a>, to discuss signing our <a href="https://www.docracy.com/1mud3ve9w8/unglue-it-non-exclusive-platform-services-agreement">Platform Services Agreement (PSA)</a>, setting a revenue goal, and running a campaign on Unglue.it to release your book under a <a href="https://creativecommons.org">Creative Commons</a> license, or to raise money for a book you've already released under such a license.</dd>
|
||||
|
||||
<dt>I haven't signed an agreement, but my books are listed on Unglue.it. What's going on?</dt>
|
||||
<dd>If your book is listed on Unglue.it, it's because an unglue.it user has indicated some interested in the book. Perhaps your book has been added to a user's favorites list. Users can also use the site to post links to Creative Commons licensed or public domain books. Unglue.it works with non-profit sites such as Internet Archive to improve their coverage and preservation of Creative Commons licensed books.
|
||||
|
@ -485,7 +485,7 @@ If you want to find an interesting campaign and don't have a specific book in mi
|
|||
<p>This assumes that you have a (free) Google account.</p>
|
||||
|
||||
<ul class="bullets">
|
||||
<li>Upload your PDF to <a href="http://drive.google.com">Google Drive</a>.</li>
|
||||
<li>Upload your PDF to <a href="https://drive.google.com">Google Drive</a>.</li>
|
||||
<li>Click on the Share button.</li>
|
||||
<li>Change the visibility to public on the web and click Save, then Done.</li>
|
||||
<li>In the File menu, click the Embed this PDF option.</li>
|
||||
|
@ -646,7 +646,7 @@ If you're concerned a campaign may not reach its goal you have several options.
|
|||
|
||||
<dt>If I am an author do I have to talk to my publisher or agent first?</dt>
|
||||
|
||||
<dd>It is your responsibility to get advice on the current status of any contracts you may have ever had for the right to publish your work, whether or not a book is in print now. <a href="http://creativecommons.org">Creative Commons</a> licenses are media neutral and worldwide (English). You may need waivers from other parties who have exclusive licenses for this book.</dd>
|
||||
<dd>It is your responsibility to get advice on the current status of any contracts you may have ever had for the right to publish your work, whether or not a book is in print now. <a href="https://creativecommons.org">Creative Commons</a> licenses are media neutral and worldwide (English). You may need waivers from other parties who have exclusive licenses for this book.</dd>
|
||||
|
||||
<dt>If I am a publisher, but I do not have an ebook royalty in my contract, can I sign your Platform Services Agreement?</dt>
|
||||
|
||||
|
@ -666,7 +666,7 @@ If you're concerned a campaign may not reach its goal you have several options.
|
|||
|
||||
<dt>Can an unglued ebook be issued only in the US?</dt>
|
||||
|
||||
<dd>No. An unglued ebook is released to the world. It uses a <a href="http://creativecommons.org">Creative Commons</a> license which is territory-neutral. That is, the unglued ebook can be read by anyone, anywhere in the world, subject always to the limitations of the license.</dd>
|
||||
<dd>No. An unglued ebook is released to the world. It uses a <a href="https://creativecommons.org">Creative Commons</a> license which is territory-neutral. That is, the unglued ebook can be read by anyone, anywhere in the world, subject always to the limitations of the license.</dd>
|
||||
{% endif %}
|
||||
|
||||
</dl>
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
<form id="clear_goodreads_id" method="post" action="{% url 'goodreads_flush_assoc' %}">
|
||||
<p><input type="submit" value="Unlink your Goodreads account from Unglue.it" /></p>
|
||||
</form>
|
||||
<p>You can also revoke access for Unglue.it at <a href="http://www.goodreads.com/user/edit?tab=apps">Goodreads user apps panel</a>.</p>
|
||||
<p>You can also revoke access for Unglue.it at <a href="https://www.goodreads.com/user/edit?tab=apps">Goodreads user apps panel</a>.</p>
|
||||
|
||||
<hr />
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ The library license gives download access to one library member at a time for 14
|
|||
</ol>
|
||||
</dd>
|
||||
<dt>Stay in touch.</dt>
|
||||
<dd>You can follow us on Twitter (<a href="http://twitter.com/unglueit">@unglueit</a>), <a href="http://facebook/com/unglueit">Facebook</a>, and our <a href="https://blog.unglue.it">blog</a>, and <a href="http://eepurl.com/fKLfI">subscribe to our newsletter</a> (1-2 emails per month).</dd>
|
||||
<dd>You can follow us on Twitter (<a href="https://twitter.com/unglueit">@unglueit</a>), <a href="https://facebook/com/unglueit">Facebook</a>, and our <a href="https://blog.unglue.it">blog</a>, and <a href="http://eepurl.com/fKLfI">subscribe to our newsletter</a> (1-2 emails per month).</dd>
|
||||
<dt>Spread the word.</dt>
|
||||
<dd>There are social media sharing links on most pages on the site. There are some right here!
|
||||
<div id="widgetcode">Copy/paste this into your site:<br /><textarea rows="7" cols="22"><iframe src="https://{{request.META.HTTP_HOST}}/api/widget/{{work.first_isbn_13}}/" width="152" height="325" frameborder="0"></iframe></textarea></div>
|
||||
|
@ -56,7 +56,7 @@ The library license gives download access to one library member at a time for 14
|
|||
</ul>
|
||||
<div class="clearfix"></div></dd>
|
||||
<dt>Educate yourself and your patrons about ebook issues and Creative Commons licenses.</dt>
|
||||
<dd>Checkout limits, publishers who won't sell ebooks to libraries, DRM, companies tracking readers' behavior, library prices far in excess of consumer costs, incompatible technologies and formats, cataloging silos...you know why it's hard for you to deliver a seamless ereading experience to your patrons. Make sure they know, too. And make sure everyone knows how solutions, like <a href="http://creativecommons.org">Creative Commons</a> licenses, can help libraries and readers while respecting copyright and protecting creators' rights.</dd>
|
||||
<dd>Checkout limits, publishers who won't sell ebooks to libraries, DRM, companies tracking readers' behavior, library prices far in excess of consumer costs, incompatible technologies and formats, cataloging silos...you know why it's hard for you to deliver a seamless ereading experience to your patrons. Make sure they know, too. And make sure everyone knows how solutions, like <a href="https://creativecommons.org">Creative Commons</a> licenses, can help libraries and readers while respecting copyright and protecting creators' rights.</dd>
|
||||
<dt>Support <a href="{% url 'campaign_list' 'ending' %}">our active campaigns</a>.</dt>
|
||||
<dd>Ultimately ebooks can't be unglued unless authors and publishers are paid for their work. Many of our staunchest supporters are librarians. There are also several libraries which have supported campaigns, including Leddy Library (University of Windsor, Ontario); the University of Alberta library ; and the Z. Smith Reynolds library (Wake Forest University).</dd>
|
||||
<dt>Give feedback and ask questions.</dt>
|
||||
|
|
|
@ -77,7 +77,7 @@ function highlightTarget(targetdiv) {
|
|||
</a>
|
||||
{% endif %}
|
||||
{% if library.user.profile.facebook_id %}
|
||||
<a href="http://www.facebook.com/profile.php?id={{library.user.profile.facebook_id}}" class="nounderline">
|
||||
<a href="https://www.facebook.com/profile.php?id={{library.user.profile.facebook_id}}" class="nounderline">
|
||||
<img src="/static/images/supporter_icons/facebook_square.png" alt="{{ supporter }}'s Facebook" title="{{ supporter }}'s Facebook" />
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -92,7 +92,7 @@ function highlightTarget(targetdiv) {
|
|||
</a>
|
||||
{% endif %}
|
||||
{% if library.user.profile.librarything_id %}
|
||||
<a href="http://www.librarything.com/profile/{{ library.user.profile.librarything_id }}" class="nounderline">
|
||||
<a href="https://www.librarything.com/profile/{{ library.user.profile.librarything_id }}" class="nounderline">
|
||||
<img src="/static/images/supporter_icons/librarything_square.png" alt="{{ supporter }}'s profile on LibraryThing" title="{{ supporter }}'s page on LibraryThing" />
|
||||
</a>
|
||||
{% endif %}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
{% if books %}
|
||||
<ul>
|
||||
{% for book in books|slice:":50" %}
|
||||
<li>{% if lt_api_key %}<img src="http://covers.librarything.com/devkey/{{lt_api_key}}/small/isbn/{{book.isbn}}" />{% endif %}<img src="http://covers.openlibrary.org/b/isbn/{{book.isbn}}-S.jpg" />{{book.title.title}} | ISBN:{{book.isbn}} | Work ID:{{book.work_id}} | Book ID:{{book.book_id}}</li>
|
||||
<li>{% if lt_api_key %}<img src="https://covers.librarything.com/devkey/{{lt_api_key}}/small/isbn/{{book.isbn}}" />{% endif %}<img src="https://covers.openlibrary.org/b/isbn/{{book.isbn}}-S.jpg" />{{book.title.title}} | ISBN:{{book.isbn}} | Work ID:{{book.work_id}} | Book ID:{{book.book_id}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
|
|
@ -181,10 +181,10 @@ Please fix the following before launching your campaign:
|
|||
{% ifnotequal campaign_status 'ACTIVE' %}
|
||||
{% ifnotequal campaign.type 3 %}
|
||||
<h3>License being offered</h3>
|
||||
<p> This is the license you are offering to use once the campaign succeeds. For more information on the licenses you can use, see <a href="http://creativecommons.org/licenses">Creative Commons: About the Licenses</a>. Once your campaign is active, you'll be able to switch to a less restrictive license, but not a more restrictive one. We encourage you to pick the least restrictive license you are comfortable with, as this will increase the ways people can use your unglued ebook and motivate more people to donate.</p>
|
||||
<p> This is the license you are offering to use once the campaign succeeds. For more information on the licenses you can use, see <a href="https://creativecommons.org/licenses">Creative Commons: About the Licenses</a>. Once your campaign is active, you'll be able to switch to a less restrictive license, but not a more restrictive one. We encourage you to pick the least restrictive license you are comfortable with, as this will increase the ways people can use your unglued ebook and motivate more people to donate.</p>
|
||||
{% else %}
|
||||
<h3>License Being Used</h3>
|
||||
<p> This is the license you are using. For more information on the licenses you can use, see <a href="http://creativecommons.org/licenses">Creative Commons: About the Licenses</a>.
|
||||
<p> This is the license you are using. For more information on the licenses you can use, see <a href="https://creativecommons.org/licenses">Creative Commons: About the Licenses</a>.
|
||||
Once your campaign is active, you'll be able to switch to a less restrictive license, but not a more restrictive one.
|
||||
We encourage you to pick the least restrictive license you are comfortable with, as this will increase the ways people can use your unglued ebook and motivate more people to participate.
|
||||
</p>
|
||||
|
@ -241,7 +241,7 @@ Please fix the following before launching your campaign:
|
|||
{% else %}
|
||||
<p>You are offering your ebook under a <b><a href="{{campaign.license_url }}">{{ campaign.license }}</a></b> license.</p>
|
||||
{% endifnotequal %}
|
||||
<p>Since your campaign is active, you may only change the license to remove restrictions. For more information on the licenses you can use, see <a href="http://creativecommons.org/licenses">Creative Commons: About the Licenses</a>.</p>
|
||||
<p>Since your campaign is active, you may only change the license to remove restrictions. For more information on the licenses you can use, see <a href="https://creativecommons.org/licenses">Creative Commons: About the Licenses</a>.</p>
|
||||
<div>
|
||||
{{ form.license.errors }}<span>{{ form.license }}</span>
|
||||
</div>
|
||||
|
|
|
@ -42,7 +42,7 @@ onload = function(){
|
|||
ISBN: <span itemprop="isbn">{{ edition.isbn_13 }}</span><br />
|
||||
{% endif %}
|
||||
{% if edition.oclc %}
|
||||
OCLC: <a href="http://www.worldcat.org/oclc/{{ edition.oclc }}">{{ edition.oclc }}</a><br />
|
||||
OCLC: <a href="https://www.worldcat.org/oclc/{{ edition.oclc }}">{{ edition.oclc }}</a><br />
|
||||
{% endif %}
|
||||
|
||||
{% include 'edition_upload.html' %}
|
||||
|
|
|
@ -4,227 +4,31 @@
|
|||
{% block extra_extra_head %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" href="/static/css/ui-lightness/jquery-ui-1.8.16.custom.css" type="text/css" media="screen">
|
||||
<link href="/static/css/ui.fancytree.min.css" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
ul.fancytree-container {
|
||||
width: 100%;
|
||||
height: 10em;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
</style>
|
||||
{{ form.media.css }}
|
||||
<script type="text/javascript" src="{{ jquery_ui_home }}" ></script>
|
||||
<script src="/static/js/jquery.fancytree-all.min.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
$j(function(){
|
||||
// Initialize Fancytree
|
||||
$j("#tree").fancytree({
|
||||
extensions: ["glyph"],
|
||||
checkbox: true,
|
||||
selectMode: 1,
|
||||
glyph: {
|
||||
map: {
|
||||
doc: "fa fa-file-o",
|
||||
docOpen: "fa fa-file-o",
|
||||
checkbox: "fa fa-square-o",
|
||||
checkboxSelected: "fa fa-check-square-o",
|
||||
checkboxUnknown: "fa fa-square",
|
||||
dragHelper: "fa arrow-right",
|
||||
dropMarker: "fa long-arrow-right",
|
||||
error: "fa fa-warning",
|
||||
expanderClosed: "fa fa-caret-right",
|
||||
expanderLazy: "fa fa-angle-right",
|
||||
expanderOpen: "fa fa-caret-down",
|
||||
folder: "fa fa-folder-o",
|
||||
folderOpen: "fa fa-folder-open-o",
|
||||
loading: "fa fa-spinner fa-pulse"
|
||||
}
|
||||
},
|
||||
source: { url: "/bisac/tree", cache: true }
|
||||
});
|
||||
$j("#editform").submit(function() {
|
||||
// Render hidden <input> elements for active and selected nodes
|
||||
$j("#tree").fancytree("getTree").generateFormElements(selected="bisac");
|
||||
//alert("POST data:\n" + $j.param($j(this).serializeArray()));
|
||||
return true;
|
||||
});
|
||||
|
||||
$j().ready(function(){
|
||||
var contentblock = $j('#content-block');
|
||||
contentblock.on("click", "span.deletebutton", function () {
|
||||
var kw = $j(this).attr('data');
|
||||
var li = $j(this).parent();
|
||||
|
||||
// perform action
|
||||
{% if edition.work %}
|
||||
jQuery.post('{% url 'kw_edit' edition.work.id %}', {'remove_kw': kw, 'csrfmiddlewaretoken': '{{ csrf_token }}' }, function(data) {
|
||||
li.html('kw removed');
|
||||
});
|
||||
{% else %}
|
||||
li.html('kw removed');
|
||||
{% endif %}
|
||||
});
|
||||
// this is the id of the submit button
|
||||
|
||||
$j('#add_subject_submit').click(function(event) {
|
||||
data= $j('#id_add_subject_0').attr('value')
|
||||
if (data == 'xxbadform'){
|
||||
alert("bad keyword");
|
||||
} else {
|
||||
$j('#kw_list').append('<li>' + data + '<input type="hidden" name="new_subject" value="'+data +'" /><span class="deletebutton" data="' + data +'">x</span></li>')
|
||||
}; // data will be the added kw.
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{{ form.media.js }}
|
||||
{% endblock %}
|
||||
|
||||
{% block doccontent %}
|
||||
{% if admin %}
|
||||
{% if edition.pk %}
|
||||
<h2>Edit Edition for <a href="{% url 'work' edition.work.id %}">{{ edition.work.title }}</a></h2>
|
||||
{% else %}
|
||||
<h2>Create New Edition</h2>
|
||||
{% endif %}
|
||||
|
||||
<p>Title and ISBN 13 (or DOI, OCLCNum or URL) are required; the rest is optional, though a cover image is strongly recommended.</p>
|
||||
|
||||
<h2>Create New Edition</h2>
|
||||
<p>Step 1 - <b>Identifier</b></p>
|
||||
|
||||
|
||||
<p>To add a work and edition to Unglue.it, we need to start with an identifier. We'll see if we can find some metadata based on the identifier you give us. If you give us an ISBN, we'll often be able to find some. If there's no ISBN, give us a URL (a web address) and we'll check the page for bibliographic info. </p>
|
||||
<form id="editform" enctype="multipart/form-data" method="POST" action="#">
|
||||
{% csrf_token %}
|
||||
{{ form.work }}
|
||||
{{ form.non_field_errors }}
|
||||
<!--{{ form.errors }}-->
|
||||
<div>
|
||||
<p><b>Title</b>: {{ form.title.errors }}{{ form.title }}</p>
|
||||
<p><b>Publisher Name</b> : {{ form.publisher_name.errors }}{{ form.publisher_name }}<br />(If you change this, click another form element before submitting)</p>
|
||||
|
||||
<p>
|
||||
<b>Authors</b>:
|
||||
{% if edition.pk and edition.relators or edition.new_authors %}
|
||||
<ul>
|
||||
{% for relator in form.relators %}
|
||||
<li>{{ relator.relator.name }} {{ relator.select }} <input type="submit" name="delete_author_{{ relator.relator.author.id }}" value="x" class="deletebutton" title="delete author"></li>
|
||||
{% endfor %}
|
||||
{% for author in edition.new_authors %}
|
||||
<li>{{ author.0 }}<input type="hidden" name="new_author" value="{{ author.0 }}" /> ({{ author.1 }})<input type="hidden" name="new_author_relation" value="{{ author.1 }}" /></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
(None listed)
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<p><b>Add a Creator</b> (<I>Firstname Lastname</I>): {{ form.add_author.errors }}{{ form.add_author }}{{ form.add_author_relation.errors }}{{ form.add_author_relation }}
|
||||
<input type="submit" name="add_author_submit" value="Add Author" id="submit_author"></p>
|
||||
|
||||
<p><b>Language</b>: {{ form.language.errors }}{{ form.language }}</p>
|
||||
{% if edition.pk %}
|
||||
<p><b>Add a Related Work</b>: {{ form.add_work_relation.errors }}{{ form.add_work_relation }} of {{ form.add_related_work.errors }}{{ form.add_related_work }}</p>
|
||||
<ul>{% for work_rel in edition.work.works_related_to.all %}
|
||||
<li>
|
||||
This work is a {{ work_rel.relation }} of <a href="{% url 'work' work_rel.from_work.id %}">{{ work_rel.from_work }}</a>.
|
||||
<input type="submit" name="delete_work_rel_{{ work_rel.id }}" value="x" class="deletebutton" title="delete work relation">
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% for work_rel in edition.work.works_related_from.all %}
|
||||
<li>
|
||||
<a href="{% url 'work' work_rel.to_work.id %}">{{ work_rel.to_work }}</a> is a {{ work_rel.relation }} of this work.
|
||||
<input type="submit" name="delete_work_rel_{{ work_rel.id }}" value="x" class="deletebutton" title="delete work relation">
|
||||
</li>
|
||||
{% endfor %}</ul>
|
||||
|
||||
|
||||
{% endif %}
|
||||
<p><b>Age Level</b>: {{ form.age_level.errors }}{{ form.age_level }}</p>
|
||||
<p><b>Edition Note</b>: {{ form.note.errors }}{{ form.note }}</p>
|
||||
<h4> Identifiers </h4>
|
||||
{% if id_msg %} <span class="errorlist">{{ id_msg }} </span>{% endif %}
|
||||
<p> Enter 'delete' to remove the identifier. </p>
|
||||
<p><b>ISBN-13</b>: {{ form.isbn.errors }}{{ form.isbn }}</p>
|
||||
<p><b>OCLC Number</b>: {{ form.oclc.errors }}{{ form.oclc }}</p>
|
||||
<p><b>Google Books ID</b>: {{ form.goog.errors }}{{ form.goog }}</p>
|
||||
<p><b>GoodReads ID</b>: {{ form.gdrd.errors }}{{ form.gdrd }}</p>
|
||||
<p><b>LibraryThing ID</b>: {{ form.thng.errors }}{{ form.thng }}</p>
|
||||
<p><b>DOI</b>: {{ form.doi.errors }}{{ form.doi }}</p>
|
||||
<p><b>HTTP(S) ID</b>: {{ form.http.errors }}{{ form.http }}</p>
|
||||
<p><b>Description</b>: <br />
|
||||
{{ form.description.errors }}{{ form.description }}<br />
|
||||
(<i>{% if work.last_campaign %}
|
||||
{% ifequal work.last_campaign.type 3 %}
|
||||
This will appear in the Description tab on the book page.
|
||||
{% else %}
|
||||
The campaign pitch will override this description.
|
||||
{% endifequal %}
|
||||
{% else %}
|
||||
This will appear in the Description tab on the book page.
|
||||
{% endif %}
|
||||
</i>)</p>
|
||||
<p><b>Publication Date</b> (<I>four-digit year</I>): {{ form.publication_date.errors }}{{ form.publication_date }}</p>
|
||||
<p><b>Subjects</b>:
|
||||
<ul id="kw_list">
|
||||
{% if edition.work.pk and edition.work.subjects %}
|
||||
{% for subject in edition.work.subjects.all %}
|
||||
<li>{{ subject.name }}
|
||||
{% if subject.authority %}
|
||||
({{subject.authority}})
|
||||
{% endif %}
|
||||
<span class="deletebutton" data="{{ subject.name }}">x</span></li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% for new_subject in edition.new_subjects %}
|
||||
<li>{{ new_subject }}<input type="hidden" name="new_subject" value="{{ new_subject }}" /></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<b>Add a Subject</b>: {{ form.add_subject.errors }}{{ form.add_subject }}
|
||||
<a class="fakeinput" id="add_subject_submit" style="font-size: smaller;" >Add Subject</a></p>
|
||||
<p id="tree" name="is_bisac"><b>Add BISAC Subject</b>:
|
||||
|
||||
</p>
|
||||
|
||||
<p><b>Cover Image</b>: <br />
|
||||
{% if edition.cover_image %}
|
||||
<img src="{{edition.cover_image}}" /><br />
|
||||
{% else %}
|
||||
[ no cover specified for this edition ]<br />
|
||||
{% endif %}
|
||||
{{ form.cover_image.errors }}{{ form.cover_image }}{{ form.cover_image.help_text }}
|
||||
(<i>Enter a URL for an image, at least 300 px wide. The image will be scaled to the proportions of a 6x9 cover. </i>)<br />
|
||||
OR...<br />
|
||||
|
||||
{{ form.coverfile.errors }}{{ form.coverfile }}{{ form.coverfile.help_text }}
|
||||
(<i>upload a cover image file (we'll automatically size if for you). </i>)<br />
|
||||
</p>
|
||||
</div>
|
||||
<input type="submit" name="create_new_edition" style="font-size: larger;" value="{% if edition.pk %}Save Edits{% else %}Create Edition{% endif %}" id="submit">
|
||||
{{ form.as_p }}
|
||||
<!--{{ form.errors.as_data }}-->
|
||||
|
||||
<p>
|
||||
<input type="submit" name="new_edition_identifier" style="font-size: larger;" value="Start Edition" id="submit">
|
||||
</p>
|
||||
</form>
|
||||
{% if edition.work %}
|
||||
|
||||
<h2>More Edition Management</h2>
|
||||
|
||||
<div><a href="{% url 'merge' edition.work.id %}">Merge other works into this one</a></div>
|
||||
<div><a href="{% url 'work_editions' edition.work.id %}">Remove editions from this work</a></div>
|
||||
{% if edition.id %}
|
||||
<div><a href="{% url 'manage_ebooks' edition.id %}">Add ebooks for this edition</a></div>
|
||||
{% endif %}
|
||||
{% if request.user.is_staff %}
|
||||
<div><a href="{% url 'feature' edition.work.id %}">Feature this work today</a></div>
|
||||
{% endif %}
|
||||
<br />
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
{% if edition.work %}
|
||||
{% include 'edition_display.html' %}
|
||||
{% else %}
|
||||
Sorry, there's no work specified.
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Your donation of ${{transaction.max_amount|default:"0"}} to the Free Ebook Foundation will help us make free ebooks of all types more accessible for those that need them. The Free Ebook Foundation is a US 501(c)3 non-profit organization. Our tax ID number is 61-1767266. Your gift is tax deductible to the full extent provided by the law.
|
||||
|
||||
For more information about the Free Ebook Foundation, visit http://ebookfoundation.org/
|
||||
For more information about the Free Ebook Foundation, visit https://ebookfoundation.org/
|
||||
|
||||
Thank you again for your generous support.
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
{% block comments_textual %}
|
||||
<p>Your donation of ${{transaction.max_amount|default:"0"}} to the Free Ebook Foundation will help us make free ebooks of all types more accessible for those that need them. The Free Ebook Foundation is a US 501(c)3 non-profit organization. Our tax ID number is 61-1767266. Your gift is tax deductible to the full extent provided by the law.<p>
|
||||
<p>For more information about the Free Ebook Foundation, visit <a href="http://ebookfoundation.org/ ">our website</a>.
|
||||
<p>For more information about the Free Ebook Foundation, visit <a href="https://ebookfoundation.org/ ">our website</a>.
|
||||
</p>
|
||||
<p>Thank you again for your support.
|
||||
</p>
|
||||
|
|
|
@ -2,7 +2,7 @@ Your Platform Services Agreement has been accepted and you're now an official Un
|
|||
|
||||
Here's what to do next. Find your book(s) on Unglue.it. On the More... tab of the book page, you'll now see an option to claim the book. Do this. We'll follow up. Once we've approved your claim, you'll be able to run campaigns for the book.
|
||||
|
||||
If your book isn't listed in Google Books (which powers our search), you won't be able to find it at Unglue.it. That's okay. You can submit your books for inclusion in Google's search results: http://books.google.com/googlebooks/publishers.html . We can also create a custom page for you; just notify us.
|
||||
If your book isn't listed in Google Books (which powers our search), you won't be able to find it at Unglue.it. That's okay. You can submit your books for inclusion in Google's search results: https://books.google.com/googlebooks/publishers.html . We can also create a custom page for you; just notify us.
|
||||
|
||||
You can also start thinking ahead about what you'd like your campaigns to look like and how you'd like to publicize them. Some good things to brainstorm: your campaign pitch; any photos or video you can include; compelling premiums you might be able to offer; what you want your target to be and how long you think your campaign should last; and how to share your campaign with your social networks (online and off) and media contacts.
|
||||
|
||||
|
|
|
@ -12,11 +12,11 @@
|
|||
|
||||
{% block comments_textual %}
|
||||
{% ifequal campaign.type 1 %}
|
||||
<div>Congratulations! You wished for a campaign, and here it is. If ungluers like you pledge {{ campaign.target|intcomma }} by {{ campaign.deadline|date:"M d, Y" }}, <I>{{ campaign.work.title }}</i> will be released under a <a href="http://creativecommons.org">Creative Commons</a> license for all to enjoy.</div>
|
||||
<div>Congratulations! You wished for a campaign, and here it is. If ungluers like you pledge {{ campaign.target|intcomma }} by {{ campaign.deadline|date:"M d, Y" }}, <I>{{ campaign.work.title }}</i> will be released under a <a href="https://creativecommons.org">Creative Commons</a> license for all to enjoy.</div>
|
||||
<div>You can help! <a href="{% url 'pledge' campaign.work.id %}">Pledge</a> any amount, and use the sharing options on the <a href="{% url 'work' campaign.work.id %}">campaign page</a> to tell your friends.</a></div>
|
||||
{% endifequal %}
|
||||
{% ifequal campaign.type 2 %}
|
||||
<div>Great! You wished for a campaign, and here it is. Someday, the book will be released under a <a href="http://creativecommons.org">Creative Commons license</a> for everyone to enjoy. Every copy purchased brings that day {{ campaign.days_per_copy|floatformat }} days sooner.</div>
|
||||
<div>Great! You wished for a campaign, and here it is. Someday, the book will be released under a <a href="https://creativecommons.org">Creative Commons license</a> for everyone to enjoy. Every copy purchased brings that day {{ campaign.days_per_copy|floatformat }} days sooner.</div>
|
||||
<div>You can help! <a href="{% url 'purchase' campaign.work.id %}">Purchase</a> a copy, and use the sharing options on the <a href="{% url 'work' campaign.work.id %}">campaign page</a> to tell your friends.</a></div>
|
||||
{% endifequal %}
|
||||
{% ifequal campaign.type 3 %}
|
||||
|
|
|
@ -20,7 +20,7 @@ URL: {{ ebook.download_url }}
|
|||
{% if work.ebooks.0.rights == 'PD-US' %}
|
||||
A public domain ebook belongs to all of us. You can do anything you like with it.
|
||||
{% else %}
|
||||
The Creative Commons licensing terms for {{ work.title }} allow you to redistribute the files under the specified license terms. There's no DRM. For full details, see http://creativecommons.org/licenses/.
|
||||
The Creative Commons licensing terms for {{ work.title }} allow you to redistribute the files under the specified license terms. There's no DRM. For full details, see https://creativecommons.org/licenses/.
|
||||
{% endif %}
|
||||
|
||||
{% if work.last_campaign_status == 'SUCCESSFUL' %}
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<h2 class="thank-you">Thank you!</h2>
|
||||
{% if not campaign %}
|
||||
<p class="pledge_complete">You've just donated ${{ transaction.amount|floatformat:2|intcomma }} to the <a href="http://ebookfoundation.org">Free Ebook Foundation</a></p>
|
||||
<p class="pledge_complete">You've just donated ${{ transaction.amount|floatformat:2|intcomma }} to the <a href="https://ebookfoundation.org">Free Ebook Foundation</a></p>
|
||||
{% endif %}
|
||||
{% ifequal campaign.type 1 %}
|
||||
<p class="pledge_complete">You've just {% if modified %}modified your pledge for{% else %}pledged{% endif %} ${{ transaction.amount|floatformat:2|intcomma }} to <I><a href="{% url 'work' work.id %}">{{ work.title }}</a></I>. If it reaches its goal of ${{ campaign.target|intcomma }} by {{ campaign.deadline|date:"M d Y"}}, it will be unglued for all to enjoy.</p>
|
||||
|
@ -74,8 +74,8 @@
|
|||
{% else %}
|
||||
|
||||
<ul class="social menu pledge">
|
||||
<a href="https://www.facebook.com/sharer.php?u=http://ebookfoundation.org"><li class="facebook first"><span>Facebook</span></li></a>
|
||||
<a href="https://twitter.com/intent/tweet?url=http://ebookfoundation.org&text=I%20just%20supported%20The%20Free%20Ebook%20Foundation.%20Will%20you%20join%20me%3F"><li class="twitter"><span>Twitter</span></li></a>
|
||||
<a href="https://www.facebook.com/sharer.php?u=https://ebookfoundation.org"><li class="facebook first"><span>Facebook</span></li></a>
|
||||
<a href="https://twitter.com/intent/tweet?url=https://ebookfoundation.org&text=I%20just%20supported%20The%20Free%20Ebook%20Foundation.%20Will%20you%20join%20me%3F"><li class="twitter"><span>Twitter</span></li></a>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -60,10 +60,10 @@ When books have a clear, established legal license which promotes use, they can
|
|||
<dt>Where?</dt>
|
||||
<dd>The Free Ebook Foundation is a New Jersey non-for-profit corporation, but its employees and contractors live and work across North America. The best way to contact us is by email, <a href="mailto:press@gluejar.com">freeebookfoundation@gmail.com</a>.</dd>
|
||||
<dt>What's your technology?</dt>
|
||||
<dd>Unglue.it is built using <a href="http://python.org/">Python</a> and the <a href="https://www.djangoproject.com/">Django framework</a>. We use data from the <a href="http://code.google.com/apis/books/docs/v1/getting_started.html">Google Books</a>, <a href="http://openlibrary.org/developers/api">Open Library</a>, <a href="http://www.librarything.com/api">LibraryThing</a>, and <a href="http://www.goodreads.com/api">GoodReads</a> APIs; we appreciate that they've made these APIs available, and we're returning the favor with <a href="/api/help">our own API</a>. You're welcome to use it. We use <a href="http://lesscss.org/">Less</a> to organize our CSS. We process pledges with <a href="https://stripe.com/">Stripe</a>. We collaborate on our code at <a href="https://github.com/">GitHub</a> and deploy to the cloud with <a href="http://aws.amazon.com/ec2/">Amazon EC2</a>.</dd>
|
||||
<dd>Unglue.it is built using <a href="http://python.org/">Python</a> and the <a href="https://www.djangoproject.com/">Django framework</a>. We use data from the <a href="https://code.google.com/apis/books/docs/v1/getting_started.html">Google Books</a>, <a href="https://openlibrary.org/developers/api">Open Library</a>, <a href="https://www.librarything.com/api">LibraryThing</a>, and <a href="https://www.goodreads.com/api">GoodReads</a> APIs; we appreciate that they've made these APIs available, and we're returning the favor with <a href="/api/help">our own API</a>. You're welcome to use it. We use <a href="http://lesscss.org/">Less</a> to organize our CSS. We process pledges with <a href="https://stripe.com/">Stripe</a>. We collaborate on our code at <a href="https://github.com/">GitHub</a> and deploy to the cloud with <a href="https://aws.amazon.com/ec2/">Amazon EC2</a>.</dd>
|
||||
<dt>What licenses are supported? </dt>
|
||||
<dd>
|
||||
We support the <a href="http://creativecommons.org/licenses/">Creative Commons</a> licenses in all of our programs. Ebooks with these licenses are free and legal for everyone to read, copy, and share worldwide and requires that the author attribution.<br /><br />
|
||||
We support the <a href="https://creativecommons.org/licenses/">Creative Commons</a> licenses in all of our programs. Ebooks with these licenses are free and legal for everyone to read, copy, and share worldwide and requires that the author attribution.<br /><br />
|
||||
We support a additional Free Licenses in our Thanks-for-Ungluing program.
|
||||
</dd>
|
||||
|
||||
|
@ -110,7 +110,7 @@ We support a additional Free Licenses in our Thanks-for-Ungluing program.
|
|||
<div>
|
||||
<iframe width="480" height="274" src="//www.youtube-nocookie.com/embed/YNGPCR-iM24?rel=0" frameborder="0" allowfullscreen></iframe><br />
|
||||
<I>November 2011</I><br />
|
||||
Eric Hellman, "The Network is Overrated"; talk at <a href="http://bib.archive.org/">Books in Browsers</a> 2011.
|
||||
Eric Hellman, "The Network is Overrated"; talk at <a href="https://bib.archive.org/">Books in Browsers</a> 2011.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -70,11 +70,11 @@ We use <a href=" http://w3c.github.io/webappsec/specs/referrer-policy">referrer
|
|||
<ul class="bullets">
|
||||
|
||||
<li>
|
||||
We use <a href="http://www.google.com/analytics/">Google Analytics</a>. If you believe their <a href="http://www.google.com/analytics/terms/us.html">terms of service</a>, they can't share this data outside Google. But Google is fundamentally an advertising company, and it's very likely that Google knows exactly who you are.
|
||||
We use <a href="https://www.google.com/analytics/">Google Analytics</a>. If you believe their <a href="https://www.google.com/analytics/terms/us.html">terms of service</a>, they can't share this data outside Google. But Google is fundamentally an advertising company, and it's very likely that Google knows exactly who you are.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
We use cover images from <a href="http://books.google.com/">Google Books</a>. We don't really know if Google Books does much with the data they receive as a result. Google isn't learning anything they don't already know from analytics, but the <a href="http://books.google.com/intl/en/googlebooks/privacy.html">Google Books Privacy Policy</a> is interesting reading for privacy wonks.
|
||||
We use cover images from <a href="https://books.google.com/">Google Books</a>. We don't really know if Google Books does much with the data they receive as a result. Google isn't learning anything they don't already know from analytics, but the <a href="https://books.google.com/intl/en/googlebooks/privacy.html">Google Books Privacy Policy</a> is interesting reading for privacy wonks.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
|
@ -94,7 +94,7 @@ When you download an ebook via Unglue.it, it usually comes from a third party. W
|
|||
|
||||
<ul class="bullets">
|
||||
<li><a href="https://archive.org/about/terms.php">Internet Archive</a> (Excellent privacy!) </li>
|
||||
<li><a href="http://www.gutenberg.org/wiki/Gutenberg:Privacy_policy">Project Gutenberg</a> (insecure) </li>
|
||||
<li><a href="https://www.gutenberg.org/wiki/Gutenberg:Privacy_policy">Project Gutenberg</a> (insecure) </li>
|
||||
<li><a href="http://www.oapen.org/about">OAPEN</a> (no privacy policy, insecure) </li>
|
||||
<li><a href="http://www.hathitrust.org/privacy">Hathitrust</a> (insecure) </li>
|
||||
<li><a href="https://help.github.com/articles/github-privacy-policy/">Github</a> </li>
|
||||
|
@ -115,7 +115,7 @@ We have used a small number of third party services that do not set cookies to t
|
|||
|
||||
<ul class="bullets">
|
||||
<li>
|
||||
We've worked to pare this list down to one. <a href="http://aws.amazon.com/s3/">Amazon S3</a>. We disable logging for our S3 buckets, but we're not aware of any privacy commitment by Amazon Web Services regarding their logging practices for S3 (Simple Storage Service) separate from the Amazon privacy policies. But we can verify that S3 sets no tracking cookies.
|
||||
We've worked to pare this list down to one. <a href="https://aws.amazon.com/s3/">Amazon S3</a>. We disable logging for our S3 buckets, but we're not aware of any privacy commitment by Amazon Web Services regarding their logging practices for S3 (Simple Storage Service) separate from the Amazon privacy policies. But we can verify that S3 sets no tracking cookies.
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
|
|
@ -150,7 +150,7 @@ function highlightTarget(targetdiv) {
|
|||
</a>
|
||||
{% endif %}
|
||||
{% if supporter.profile.librarything_id %}
|
||||
<a href="http://www.librarything.com/profile/{{ supporter.profile.librarything_id }}" class="nounderline">
|
||||
<a href="https://www.librarything.com/profile/{{ supporter.profile.librarything_id }}" class="nounderline">
|
||||
<img src="/static/images/supporter_icons/librarything_square.png" alt="{{ supporter }}'s profile on LibraryThing" title="{{ supporter }}'s page on LibraryThing" />
|
||||
</a>
|
||||
{% endif %}
|
||||
|
|
|
@ -214,7 +214,7 @@
|
|||
<a id="find-google" href="{{ work.googlebooks_url }}"><img src="/static/images/supporter_icons/googlebooks_square.png" title="Find on Google Books" alt="Find on Google Books" /></a>
|
||||
{% endif %}
|
||||
{% if work.first_oclc %}
|
||||
<a rel="nofollow" id="find-oclc" href="http://www.worldcat.org/oclc/{{ work.first_oclc }}"><img src="/static/images/supporter_icons/worldcat_square.png" title="Find on Worldcat" alt="Find on Worldcat" /></a>
|
||||
<a rel="nofollow" id="find-oclc" href="https://www.worldcat.org/oclc/{{ work.first_oclc }}"><img src="/static/images/supporter_icons/worldcat_square.png" title="Find on Worldcat" alt="Find on Worldcat" /></a>
|
||||
{% endif %}
|
||||
<a rel="nofollow" class="find-openlibrary" href="{% url 'work_openlibrary' work_id %}"><img src="/static/images/supporter_icons/openlibrary_square.png" title="Find on OpenLibrary" alt="Find on OpenLibrary" /></a>
|
||||
<a rel="nofollow" class="find-goodreads" href="{% url 'work_goodreads' work_id %}"><img src="/static/images/supporter_icons/goodreads_square.png" title="Find on GoodReads" alt="Find on GoodReads" /></a>
|
||||
|
|
|
@ -215,9 +215,9 @@ class PledgingUiTests(TestCase):
|
|||
|
||||
# login and heck whether user logged in
|
||||
self.assertTrue(self.client.login(username=self.USERNAME, password=self.PASSWORD))
|
||||
# http://stackoverflow.com/a/6013115
|
||||
#self.assertEqual(self.client.session['_auth_user_id'], self.user.pk)
|
||||
|
||||
# https://stackoverflow.com/a/6013115
|
||||
#self.assertEqual(self.client.session['_auth_user_id'], self.user.pk)
|
||||
|
||||
user = auth.get_user(self.client)
|
||||
assert user.is_authenticated()
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ urlpatterns = [
|
|||
url(r"^rightsholders/campaign/(?P<id>\d+)/results/$", views.manage_campaign, {'action': 'results'}, name="campaign_results"),
|
||||
url(r"^rightsholders/campaign/(?P<id>\d+)/(?P<ebf>\d+)/makemobi/$", views.manage_campaign, {'action': 'makemobi'}, name="makemobi"),
|
||||
url(r"^rightsholders/campaign/(?P<id>\d+)/mademobi/$", views.manage_campaign, {'action': 'mademobi'}, name="mademobi"),
|
||||
url(r"^rightsholders/edition/(?P<work_id>\d*)/(?P<edition_id>\d*)$", views.new_edition, {'by': 'rh'}, name="rh_edition"),
|
||||
url(r"^rightsholders/edition/(?P<work_id>\d*)/(?P<edition_id>\d*)$", views.edit_edition, {'by': 'rh'}, name="rh_edition"),
|
||||
url(r"^rightsholders/edition/(?P<edition_id>\d*)/upload/$", views.edition_uploads, name="edition_uploads"),
|
||||
url(r"^rightsholders/claim/$", views.claim, name="claim"),
|
||||
url(r"^rightsholders/surveys/$", views.surveys, name="surveys"),
|
||||
|
@ -87,8 +87,8 @@ urlpatterns = [
|
|||
url(r"^work/(?P<work_id>\d+)/librarything/$", views.work_librarything, name="work_librarything"),
|
||||
url(r"^work/(?P<work_id>\d+)/goodreads/$", views.work_goodreads, name="work_goodreads"),
|
||||
url(r"^work/(?P<work_id>\d+)/openlibrary/$", views.work_openlibrary, name="work_openlibrary"),
|
||||
url(r"^new_edition/(?P<work_id>)(?P<edition_id>)$", views.new_edition, name="new_edition"),
|
||||
url(r"^new_edition/(?P<work_id>\d*)/(?P<edition_id>\d*)$", views.new_edition, name="new_edition"),
|
||||
url(r"^new_edition/(?P<work_id>)(?P<edition_id>)$", views.edit_edition, name="new_edition"),
|
||||
url(r"^new_edition/(?P<work_id>\d*)/(?P<edition_id>\d*)$", views.edit_edition, name="new_edition"),
|
||||
url(r"^manage_ebooks/(?P<edition_id>\d*)$", views.manage_ebooks, name="manage_ebooks"),
|
||||
url(r"^googlebooks/(?P<googlebooks_id>.+)/$", views.googlebooks, name="googlebooks"),
|
||||
url(r"^download_ebook/(?P<ebook_id>\w+)/$", views.download_ebook, name="download_ebook"),
|
||||
|
|
|
@ -91,7 +91,6 @@ from regluit.frontend.forms import (
|
|||
CustomPremiumForm,
|
||||
OfferForm,
|
||||
EditManagersForm,
|
||||
EditionForm,
|
||||
PledgeCancelForm,
|
||||
getTransferCreditForm,
|
||||
CCForm,
|
||||
|
@ -137,6 +136,9 @@ from regluit.marc.views import qs_marc_records
|
|||
from questionnaire.models import Landing, Questionnaire
|
||||
from questionnaire.views import export_summary as answer_summary, export_csv as export_answers
|
||||
|
||||
from .bibedit import edit_edition, user_can_edit_work, safe_get_work, get_edition
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def static_redirect_view(request, file_name, dir=""):
|
||||
|
@ -186,16 +188,6 @@ def next(request):
|
|||
else:
|
||||
return HttpResponseRedirect('/')
|
||||
|
||||
def safe_get_work(work_id):
|
||||
"""
|
||||
use this rather than querying the db directly for a work by id
|
||||
"""
|
||||
try:
|
||||
work = models.safe_get_work(work_id)
|
||||
except models.Work.DoesNotExist:
|
||||
raise Http404
|
||||
return work
|
||||
|
||||
def cover_width(work):
|
||||
if work.percent_of_goal() < 100:
|
||||
cover_width = 100 - work.percent_of_goal()
|
||||
|
@ -426,10 +418,7 @@ def edition_uploads(request, edition_id):
|
|||
context = {}
|
||||
if not request.user.is_authenticated() :
|
||||
return render(request, "admins_only.html")
|
||||
try:
|
||||
edition = models.Edition.objects.get(id = edition_id)
|
||||
except models.Edition.DoesNotExist:
|
||||
raise Http404
|
||||
edition = get_edition(edition_id)
|
||||
campaign_type = edition.work.last_campaign().type
|
||||
if not user_can_edit_work(request.user, edition.work):
|
||||
return render(request, "admins_only.html")
|
||||
|
@ -486,179 +475,6 @@ def edition_uploads(request, edition_id):
|
|||
})
|
||||
return render(request, 'edition_uploads.html', context)
|
||||
|
||||
def add_subject(subject_name, work, authority=''):
|
||||
try:
|
||||
subject = models.Subject.objects.get(name=subject_name)
|
||||
except models.Subject.DoesNotExist:
|
||||
subject = models.Subject.objects.create(name=subject_name, authority=authority)
|
||||
subject.works.add(work)
|
||||
|
||||
def user_can_edit_work(user, work):
|
||||
if user.is_staff :
|
||||
return True
|
||||
elif work and work.last_campaign():
|
||||
return user in work.last_campaign().managers.all()
|
||||
elif user.rights_holder.count() and (work == None or not work.last_campaign()): # allow rights holders to edit unless there is a campaign
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
def new_edition(request, work_id, edition_id, by=None):
|
||||
if not request.user.is_authenticated():
|
||||
return render(request, "admins_only.html")
|
||||
# if the work and edition are set, we save the edition and set the work
|
||||
language = 'en'
|
||||
age_level = ''
|
||||
description = ''
|
||||
title = ''
|
||||
if work_id:
|
||||
work = safe_get_work(work_id)
|
||||
language = work.language
|
||||
age_level = work.age_level
|
||||
description = work.description
|
||||
title = work.title
|
||||
else:
|
||||
work = None
|
||||
|
||||
alert = ''
|
||||
admin = user_can_edit_work(request.user, work)
|
||||
if edition_id:
|
||||
try:
|
||||
edition = models.Edition.objects.get(id = edition_id)
|
||||
except models.Edition.DoesNotExist:
|
||||
raise Http404
|
||||
if work:
|
||||
edition.work = work
|
||||
language = edition.work.language
|
||||
age_level = edition.work.age_level
|
||||
description = edition.work.description
|
||||
else:
|
||||
edition = models.Edition()
|
||||
if work:
|
||||
edition.work = work
|
||||
edition.publication_date = work.earliest_publication_date
|
||||
edition.new_authors = []
|
||||
for relator in work.relators():
|
||||
edition.new_authors.append((relator.author.name, relator.relation.code))
|
||||
|
||||
initial = {
|
||||
'language': language,
|
||||
'age_level': age_level,
|
||||
'publisher_name': edition.publisher_name,
|
||||
'isbn': edition.isbn_13,
|
||||
'oclc': edition.oclc,
|
||||
'description': description,
|
||||
'title': title,
|
||||
'goog': edition.googlebooks_id,
|
||||
'gdrd': edition.goodreads_id,
|
||||
'thng': edition.librarything_id,
|
||||
'http': edition.http_id,
|
||||
'doi': edition.id_for('doi'),
|
||||
}
|
||||
if request.method == 'POST':
|
||||
form = None
|
||||
edition.new_authors = zip(request.POST.getlist('new_author'), request.POST.getlist('new_author_relation'))
|
||||
edition.new_subjects = request.POST.getlist('new_subject')
|
||||
if edition.id and admin:
|
||||
for author in edition.authors.all():
|
||||
if request.POST.has_key('delete_author_%s' % author.id):
|
||||
edition.remove_author(author)
|
||||
form = EditionForm(instance=edition, data=request.POST, files=request.FILES)
|
||||
break
|
||||
work_rels = models.WorkRelation.objects.filter(Q(to_work=work) | Q(from_work=work))
|
||||
for work_rel in work_rels:
|
||||
if request.POST.has_key('delete_work_rel_%s' % work_rel.id):
|
||||
work_rel.delete()
|
||||
form = EditionForm(instance=edition, data=request.POST, files=request.FILES)
|
||||
break
|
||||
|
||||
if request.POST.has_key('add_author_submit') and admin:
|
||||
new_author_name = request.POST['add_author'].strip()
|
||||
new_author_relation = request.POST['add_author_relation']
|
||||
try:
|
||||
author = models.Author.objects.get(name=new_author_name)
|
||||
except models.Author.DoesNotExist:
|
||||
author = models.Author.objects.create(name=new_author_name)
|
||||
edition.new_authors.append((new_author_name, new_author_relation))
|
||||
form = EditionForm(instance=edition, data=request.POST, files=request.FILES)
|
||||
elif not form and admin:
|
||||
form = EditionForm(instance=edition, data=request.POST, files=request.FILES)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
if not work:
|
||||
work = models.Work(
|
||||
title=form.cleaned_data['title'],
|
||||
language=form.cleaned_data['language'],
|
||||
age_level=form.cleaned_data['age_level'],
|
||||
description=form.cleaned_data['description'],
|
||||
)
|
||||
work.save()
|
||||
edition.work = work
|
||||
edition.save()
|
||||
else:
|
||||
work.description = form.cleaned_data['description']
|
||||
work.title = form.cleaned_data['title']
|
||||
work.publication_range = None # will reset on next access
|
||||
work.language = form.cleaned_data['language']
|
||||
work.age_level = form.cleaned_data['age_level']
|
||||
work.save()
|
||||
|
||||
id_msg = ""
|
||||
for id_type in ('isbn', 'oclc', 'goog', 'thng', 'gdrd', 'http', 'doi'):
|
||||
id_val = form.cleaned_data[id_type]
|
||||
if id_val == 'delete':
|
||||
edition.identifiers.filter(type=id_type).delete()
|
||||
elif id_val:
|
||||
existing = models.Identifier.objects.filter(type=id_type, value=form.cleaned_data[id_type])
|
||||
if existing.count() and existing[0].edition != edition:
|
||||
return render(request, 'new_edition.html', {
|
||||
'form': form, 'edition': edition, 'admin': admin,
|
||||
'id_msg': "%s = %s already exists"%(id_type, id_val),
|
||||
})
|
||||
else:
|
||||
models.Identifier.set(type=id_type, value=id_val, edition=edition, work=work)
|
||||
for relator in edition.relators.all():
|
||||
if request.POST.has_key('change_relator_%s' % relator.id):
|
||||
new_relation = request.POST['change_relator_%s' % relator.id]
|
||||
relator.set(new_relation)
|
||||
related_work = form.cleaned_data['add_related_work']
|
||||
if related_work:
|
||||
models.WorkRelation.objects.get_or_create(
|
||||
to_work=work,
|
||||
from_work=related_work,
|
||||
relation=form.cleaned_data['add_work_relation'],
|
||||
)
|
||||
for (author_name, author_relation) in edition.new_authors:
|
||||
edition.add_author(author_name, author_relation)
|
||||
if form.cleaned_data.has_key('bisac'):
|
||||
bisacsh = form.cleaned_data['bisac']
|
||||
while bisacsh:
|
||||
add_subject(bisacsh.full_label, work, authority="bisacsh")
|
||||
bisacsh = bisacsh.parent
|
||||
for subject_name in edition.new_subjects:
|
||||
add_subject(subject_name, work)
|
||||
work_url = reverse('work', kwargs={'work_id': edition.work.id})
|
||||
cover_file = form.cleaned_data.get("coverfile", None)
|
||||
if cover_file:
|
||||
# save it
|
||||
cover_file_name = '/Users/%s/covers/%s/%s' % (request.user.username, edition.pk, cover_file.name)
|
||||
file = default_storage.open(cover_file_name, 'w')
|
||||
file.write(cover_file.read())
|
||||
file.close()
|
||||
#and put its url into cover_image
|
||||
edition.cover_image = default_storage.url(cover_file_name)
|
||||
edition.save()
|
||||
return HttpResponseRedirect(work_url)
|
||||
else:
|
||||
form = EditionForm(instance=edition, initial=initial)
|
||||
return render(request, 'new_edition.html', {
|
||||
'form': form, 'edition': edition, 'admin':admin, 'alert':alert,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def manage_ebooks(request, edition_id, by=None):
|
||||
if edition_id:
|
||||
|
@ -682,7 +498,7 @@ def manage_ebooks(request, edition_id, by=None):
|
|||
format=ebook_form.cleaned_data['format'],
|
||||
edition=edition,
|
||||
active=True,
|
||||
|
||||
|
||||
)
|
||||
ebook_form.instance.url = new_ebf.file.url
|
||||
ebook_form.instance.provider = "Unglue.it"
|
||||
|
@ -1435,8 +1251,8 @@ class FundView(FormView):
|
|||
else:
|
||||
data = {}
|
||||
kwargs['initial'] = data
|
||||
t_id=self.kwargs["t_id"]
|
||||
|
||||
t_id=self.kwargs["t_id"]
|
||||
|
||||
if self.transaction is None:
|
||||
self.transaction = get_object_or_404(Transaction, id=t_id)
|
||||
|
||||
|
@ -1480,7 +1296,7 @@ class FundView(FormView):
|
|||
# if there's an email address, put it in the receipt column, so far unused.
|
||||
self.transaction.receipt = form.cleaned_data.get("email", None)
|
||||
t, url = p.charge(self.transaction, return_url = return_url, token=stripe_token)
|
||||
|
||||
|
||||
elif self.transaction.campaign.type == THANKS and self.transaction.user == None:
|
||||
#anonymous user, just charge the card!
|
||||
if self.request.user.is_authenticated():
|
||||
|
@ -1637,7 +1453,7 @@ class FundCompleteView(TemplateView):
|
|||
|
||||
if self.transaction:
|
||||
if not self.transaction.campaign:
|
||||
return self.render_to_response(context)
|
||||
return self.render_to_response(context)
|
||||
if self.transaction.campaign.type == THANKS:
|
||||
return DownloadView.as_view()(request, work=self.transaction.campaign.work)
|
||||
|
||||
|
@ -1886,11 +1702,11 @@ def surveys_summary(request, qid, work_id):
|
|||
if not request.user.is_authenticated() :
|
||||
return HttpResponseRedirect(reverse('surveys'))
|
||||
return answer_summary(
|
||||
request,
|
||||
request,
|
||||
qid,
|
||||
answer_filter=works_user_can_admin_filter(request, work_id),
|
||||
)
|
||||
|
||||
|
||||
def new_survey(request, work_id):
|
||||
if not request.user.is_authenticated() :
|
||||
return HttpResponseRedirect(reverse('surveys'))
|
||||
|
@ -2761,11 +2577,11 @@ def work_librarything(request, work_id):
|
|||
url = work.librarything_url
|
||||
elif isbn:
|
||||
# TODO: do the redirect here and capture the work id?
|
||||
url = "http://www.librarything.com/isbn/%s" % isbn
|
||||
url = "https://www.librarything.com/isbn/%s" % isbn
|
||||
else:
|
||||
term = work.title + " " + work.author()
|
||||
q = urlencode({'searchtpe': 'work', 'term': term})
|
||||
url = "http://www.librarything.com/search.php?" + q
|
||||
url = "https://www.librarything.com/search.php?" + q
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
def work_openlibrary(request, work_id):
|
||||
|
@ -2777,20 +2593,20 @@ def work_openlibrary(request, work_id):
|
|||
url = work.openlibrary_url
|
||||
elif len(isbns) > 0:
|
||||
isbns = ",".join(isbns)
|
||||
u = 'http://openlibrary.org/api/books?bibkeys=%s&jscmd=data&format=json' % isbns
|
||||
u = 'https://openlibrary.org/api/books?bibkeys=%s&jscmd=data&format=json' % isbns
|
||||
try:
|
||||
j = json.loads(requests.get(u).content)
|
||||
# as long as there were some matches get the first one and route to it
|
||||
if len(j.keys()) > 0:
|
||||
first = j.keys()[0]
|
||||
url = "http://openlibrary.org" + j[first]['key']
|
||||
url = "https://openlibrary.org" + j[first]['key']
|
||||
except ValueError:
|
||||
# fail at openlibrary
|
||||
logger.warning("failed to get OpenLibrary json at %s" % u)
|
||||
# fall back to doing a search on openlibrary
|
||||
if not url:
|
||||
q = urlencode({'q': work.title + " " + work.author()})
|
||||
url = "http://openlibrary.org/search?" + q
|
||||
url = "https://openlibrary.org/search?" + q
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
def work_goodreads(request, work_id):
|
||||
|
@ -2799,10 +2615,10 @@ def work_goodreads(request, work_id):
|
|||
if work.goodreads_id:
|
||||
url = work.goodreads_url
|
||||
elif isbn:
|
||||
url = "http://www.goodreads.com/book/isbn/%s" % isbn
|
||||
url = "https://www.goodreads.com/book/isbn/%s" % isbn
|
||||
else:
|
||||
q = urlencode({'query': work.title + " " + work.author()})
|
||||
url = "http://www.goodreads.com/search?" + q
|
||||
url = "https://www.goodreads.com/search?" + q
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@login_required
|
||||
|
@ -3403,10 +3219,10 @@ def send_to_kindle(request, work_id, javascript='0'):
|
|||
|
||||
|
||||
"""
|
||||
Amazon SES has a 10 MB size limit (http://aws.amazon.com/ses/faqs/#49) in messages sent
|
||||
Amazon SES has a 10 MB size limit (https://aws.amazon.com/ses/faqs/#49) in messages sent
|
||||
to determine whether the file will meet this limit, we probably need to compare the
|
||||
size of the mime-encoded file to 10 MB. (and it's unclear exactly what the Amazon FAQ means precisely by
|
||||
MB either: http://en.wikipedia.org/wiki/Megabyte) http://www.velocityreviews.com/forums/t335208-how-to-get-size-of-email-attachment.html might help
|
||||
MB either: https://en.wikipedia.org/wiki/Megabyte) http://www.velocityreviews.com/forums/t335208-how-to-get-size-of-email-attachment.html might help
|
||||
|
||||
for the moment, we will hardwire a 749229 limit in filesize:
|
||||
* assume conservative size of megabyte, 1000000B
|
|
@ -0,0 +1,333 @@
|
|||
'''
|
||||
views to edit bibmodels
|
||||
'''
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import Q
|
||||
from django.http import (
|
||||
HttpResponseRedirect,
|
||||
Http404,
|
||||
)
|
||||
from django.shortcuts import render
|
||||
|
||||
from regluit.core import models
|
||||
|
||||
from regluit.core.bookloader import (
|
||||
add_by_googlebooks_id,
|
||||
add_by_isbn,
|
||||
add_by_oclc,
|
||||
add_by_webpage,
|
||||
)
|
||||
from regluit.core.parameters import WORK_IDENTIFIERS
|
||||
|
||||
from regluit.core.loaders.utils import ids_from_urls
|
||||
from regluit.frontend.forms import EditionForm, IdentifierForm
|
||||
|
||||
|
||||
def user_can_edit_work(user, work):
|
||||
'''
|
||||
Check if a user is allowed to edit the work
|
||||
'''
|
||||
if user.is_staff :
|
||||
return True
|
||||
elif work and work.last_campaign():
|
||||
return user in work.last_campaign().managers.all()
|
||||
elif user.rights_holder.count() and (work == None or not work.last_campaign()):
|
||||
# allow rights holders to edit unless there is a campaign
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def safe_get_work(work_id):
|
||||
"""
|
||||
use this rather than querying the db directly for a work by id
|
||||
"""
|
||||
try:
|
||||
work = models.safe_get_work(work_id)
|
||||
except models.Work.DoesNotExist:
|
||||
raise Http404
|
||||
return work
|
||||
|
||||
def add_subject(subject_name, work, authority=''):
|
||||
'''
|
||||
add a subject to a work
|
||||
'''
|
||||
try:
|
||||
subject = models.Subject.objects.get(name=subject_name)
|
||||
except models.Subject.DoesNotExist:
|
||||
subject = models.Subject.objects.create(name=subject_name, authority=authority)
|
||||
subject.works.add(work)
|
||||
|
||||
def get_edition(edition_id):
|
||||
'''
|
||||
get edition and 404 if not found
|
||||
'''
|
||||
try:
|
||||
return models.Edition.objects.get(id = edition_id)
|
||||
except models.Edition.DoesNotExist:
|
||||
raise Http404 (duplicate-code)
|
||||
|
||||
def get_edition_for_id(id_type, id_value, user=None):
|
||||
''' the identifier is assumed to not be in database '''
|
||||
identifiers = {id_type: id_value}
|
||||
if id_type == 'http':
|
||||
# check for urls implying other identifiers
|
||||
identifiers.update(ids_from_urls(id_value))
|
||||
for new_id_type in identifiers.keys():
|
||||
idents = models.Identifier.objects.filter(
|
||||
type=new_id_type,
|
||||
value=identifiers[new_id_type],
|
||||
)
|
||||
if idents:
|
||||
ident = idents[0]
|
||||
return ident.edition if ident.edition else ident.work.preferred_edition
|
||||
|
||||
#need to make a new edition
|
||||
if identifiers.has_key('goog'):
|
||||
edition = add_by_googlebooks_id(identifiers['goog'])
|
||||
if edition:
|
||||
return edition
|
||||
|
||||
if identifiers.has_key('isbn'):
|
||||
edition = add_by_isbn(identifiers['isbn'])
|
||||
if edition:
|
||||
return edition
|
||||
|
||||
if identifiers.has_key('oclc'):
|
||||
edition = add_by_oclc(identifiers['oclc'])
|
||||
if edition:
|
||||
return edition
|
||||
|
||||
if identifiers.has_key('glue'):
|
||||
try:
|
||||
work = models.safe_get_work(identifiers['glue'])
|
||||
return work.preferred_edition
|
||||
except:
|
||||
pass
|
||||
|
||||
if identifiers.has_key('http'):
|
||||
edition = add_by_webpage(identifiers['http'], user=user)
|
||||
return edition
|
||||
|
||||
|
||||
# return a dummy edition and identifier
|
||||
|
||||
title = '!!! missing title !!!'
|
||||
work = models.Work.objects.create(title=title)
|
||||
edition = models.Edition.objects.create(title='!!! missing title !!!', work=work)
|
||||
for key in identifiers.keys():
|
||||
if key == 'glue':
|
||||
id_value = work.id
|
||||
if key not in ('http', 'goog', 'oclc', 'isbn'):
|
||||
if key in WORK_IDENTIFIERS:
|
||||
edid = str(edition.id)
|
||||
models.Identifier.objects.create(type='edid', value=edid, work=work, edition=edition)
|
||||
models.Identifier.objects.create(type=key, value=id_value, work=work, edition=None)
|
||||
else:
|
||||
models.Identifier.objects.create(type=key, value=id_value, work=work, edition=edition)
|
||||
return edition
|
||||
|
||||
@login_required
|
||||
def new_edition(request, by=None):
|
||||
'''
|
||||
view for creating editions
|
||||
'''
|
||||
alert = ''
|
||||
if request.method == 'POST':
|
||||
form = IdentifierForm(data=request.POST)
|
||||
if form.is_valid():
|
||||
if form.cleaned_data.get('make_new', False):
|
||||
edition = get_edition_for_id('glue', 'new')
|
||||
else:
|
||||
id_type = form.cleaned_data['id_type']
|
||||
id_value = form.cleaned_data['id_value']
|
||||
identifiers = models.Identifier.objects.filter(type=id_type, value=id_value)
|
||||
if identifiers:
|
||||
# pre-existing identifier
|
||||
ident = identifiers[0]
|
||||
work = ident.work
|
||||
edition = ident.edition if ident.edition else work.preferred_edition
|
||||
else:
|
||||
edition = get_edition_for_id(id_type, id_value, user=request.user)
|
||||
|
||||
return HttpResponseRedirect(
|
||||
reverse('new_edition', kwargs={
|
||||
'work_id': edition.work.id,
|
||||
'edition_id': edition.id
|
||||
})
|
||||
)
|
||||
else:
|
||||
form = IdentifierForm()
|
||||
return render(request, 'new_edition.html', {'form': form, 'alert':alert})
|
||||
|
||||
@login_required
|
||||
def edit_edition(request, work_id, edition_id, by=None):
|
||||
'''
|
||||
view for editing editions
|
||||
'''
|
||||
if not work_id and not edition_id:
|
||||
return new_edition(request, by=by)
|
||||
# if the work and edition are set, we save the edition and set the work
|
||||
language = 'en'
|
||||
age_level = ''
|
||||
description = ''
|
||||
title = ''
|
||||
if work_id:
|
||||
work = safe_get_work(work_id)
|
||||
language = work.language
|
||||
age_level = work.age_level
|
||||
description = work.description
|
||||
title = work.title
|
||||
else:
|
||||
work = None
|
||||
|
||||
alert = ''
|
||||
admin = user_can_edit_work(request.user, work)
|
||||
if edition_id:
|
||||
edition = get_edition(edition_id)
|
||||
if work:
|
||||
edition.work = work
|
||||
language = edition.work.language
|
||||
age_level = edition.work.age_level
|
||||
description = edition.work.description
|
||||
else:
|
||||
edition = models.Edition()
|
||||
if work:
|
||||
edition.work = work
|
||||
edition.publication_date = work.earliest_publication_date
|
||||
edition.new_authors = []
|
||||
for relator in work.relators():
|
||||
edition.new_authors.append((relator.author.name, relator.relation.code))
|
||||
|
||||
initial = {
|
||||
'language': language,
|
||||
'age_level': age_level,
|
||||
'publisher_name': edition.publisher_name,
|
||||
'description': description,
|
||||
'title': title,
|
||||
}
|
||||
if request.method == 'POST':
|
||||
form = None
|
||||
edition.new_authors = zip(
|
||||
request.POST.getlist('new_author'),
|
||||
request.POST.getlist('new_author_relation')
|
||||
)
|
||||
edition.new_subjects = request.POST.getlist('new_subject')
|
||||
if edition.id and admin:
|
||||
for author in edition.authors.all():
|
||||
if request.POST.has_key('delete_author_%s' % author.id):
|
||||
edition.remove_author(author)
|
||||
form = EditionForm(instance=edition, data=request.POST, files=request.FILES)
|
||||
break
|
||||
work_rels = models.WorkRelation.objects.filter(Q(to_work=work) | Q(from_work=work))
|
||||
for work_rel in work_rels:
|
||||
if request.POST.has_key('delete_work_rel_%s' % work_rel.id):
|
||||
work_rel.delete()
|
||||
form = EditionForm(instance=edition, data=request.POST, files=request.FILES)
|
||||
break
|
||||
activate_all = request.POST.has_key('activate_all_ebooks')
|
||||
deactivate_all = request.POST.has_key('deactivate_all_ebooks')
|
||||
ebookchange = False
|
||||
for ebook in work.ebooks_all():
|
||||
if request.POST.has_key('activate_ebook_%s' % ebook.id) or activate_all:
|
||||
ebook.activate()
|
||||
ebookchange = True
|
||||
elif request.POST.has_key('deactivate_ebook_%s' % ebook.id) or deactivate_all:
|
||||
ebook.deactivate()
|
||||
ebookchange = True
|
||||
if ebookchange:
|
||||
form = EditionForm(instance=edition, data=request.POST, files=request.FILES)
|
||||
if request.POST.has_key('add_author_submit') and admin:
|
||||
new_author_name = request.POST['add_author'].strip()
|
||||
new_author_relation = request.POST['add_author_relation']
|
||||
try:
|
||||
author = models.Author.objects.get(name=new_author_name)
|
||||
except models.Author.DoesNotExist:
|
||||
author = models.Author.objects.create(name=new_author_name)
|
||||
edition.new_authors.append((new_author_name, new_author_relation))
|
||||
form = EditionForm(instance=edition, data=request.POST, files=request.FILES)
|
||||
elif not form and admin:
|
||||
form = EditionForm(instance=edition, data=request.POST, files=request.FILES)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
if not work:
|
||||
work = models.Work(
|
||||
title=form.cleaned_data['title'],
|
||||
language=form.cleaned_data['language'],
|
||||
age_level=form.cleaned_data['age_level'],
|
||||
description=form.cleaned_data['description'],
|
||||
)
|
||||
work.save()
|
||||
edition.work = work
|
||||
edition.save()
|
||||
else:
|
||||
work.description = form.cleaned_data['description']
|
||||
work.title = form.cleaned_data['title']
|
||||
work.publication_range = None # will reset on next access
|
||||
work.language = form.cleaned_data['language']
|
||||
work.age_level = form.cleaned_data['age_level']
|
||||
work.save()
|
||||
|
||||
id_type = form.cleaned_data['id_type']
|
||||
id_val = form.cleaned_data['id_value']
|
||||
if id_val == 'delete':
|
||||
if edition.identifiers.exclude(type=id_type):
|
||||
edition.identifiers.filter(type=id_type).delete()
|
||||
else:
|
||||
alert = ('Can\'t delete identifier - must have at least one left.')
|
||||
elif id_val:
|
||||
models.Identifier.set(
|
||||
type=id_type,
|
||||
value=id_val,
|
||||
edition=edition,
|
||||
work=work
|
||||
)
|
||||
for relator in edition.relators.all():
|
||||
if request.POST.has_key('change_relator_%s' % relator.id):
|
||||
new_relation = request.POST['change_relator_%s' % relator.id]
|
||||
relator.set(new_relation)
|
||||
related_work = form.cleaned_data['add_related_work']
|
||||
if related_work:
|
||||
models.WorkRelation.objects.get_or_create(
|
||||
to_work=work,
|
||||
from_work=related_work,
|
||||
relation=form.cleaned_data['add_work_relation'],
|
||||
)
|
||||
for (author_name, author_relation) in edition.new_authors:
|
||||
edition.add_author(author_name, author_relation)
|
||||
if form.cleaned_data.has_key('bisac'):
|
||||
bisacsh = form.cleaned_data['bisac']
|
||||
while bisacsh:
|
||||
add_subject(bisacsh.full_label, work, authority="bisacsh")
|
||||
bisacsh = bisacsh.parent
|
||||
for subject_name in edition.new_subjects:
|
||||
add_subject(subject_name, work)
|
||||
work_url = reverse('work', kwargs={'work_id': edition.work.id})
|
||||
cover_file = form.cleaned_data.get("coverfile", None)
|
||||
if cover_file:
|
||||
# save it
|
||||
cover_file_name = '/Users/%s/covers/%s/%s' % (
|
||||
request.user.username,
|
||||
edition.pk,
|
||||
cover_file.name
|
||||
)
|
||||
new_file = default_storage.open(cover_file_name, 'w')
|
||||
new_file.write(cover_file.read())
|
||||
new_file.close()
|
||||
#and put its url into cover_image
|
||||
edition.cover_image = default_storage.url(cover_file_name)
|
||||
edition.save()
|
||||
if not alert:
|
||||
return HttpResponseRedirect(work_url)
|
||||
else:
|
||||
form = EditionForm(instance=edition, initial=initial)
|
||||
|
||||
return render(request, 'edit_edition.html', {
|
||||
'form': form,
|
||||
'identform': IdentifierForm(),
|
||||
'edition': edition,
|
||||
'admin': admin,
|
||||
'alert': alert,
|
||||
})
|
||||
|
|
@ -83,7 +83,7 @@ def selective_social_user(backend, uid, user=None, *args, **kwargs):
|
|||
'is_new': user is None,
|
||||
'new_association': False}
|
||||
|
||||
# http://stackoverflow.com/a/19361220
|
||||
# https://stackoverflow.com/a/19361220
|
||||
# adapting https://github.com/omab/python-social-auth/blob/v0.2.10/social/apps/django_app/middleware.py#L25
|
||||
|
||||
class SocialAuthExceptionMiddlewareWithoutMessages(SocialAuthExceptionMiddleware):
|
||||
|
|
|
@ -235,7 +235,7 @@ class Block(models.Model):
|
|||
ordering = ['lower',]
|
||||
|
||||
|
||||
# from http://en.wikipedia.org/wiki/Luhn_algorithm#Implementation_of_standard_Mod_10
|
||||
# from https://en.wikipedia.org/wiki/Luhn_algorithm#Implementation_of_standard_Mod_10
|
||||
def luhn_checksum(card_number):
|
||||
def digits_of(n):
|
||||
return [int(d) for d in str(n)]
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
<br /><br />
|
||||
Lower: <code>24.0.0.1</code> Upper: <code>24.0.0.255</code>
|
||||
<br /><br />
|
||||
Don't use "loopback" addresses like 127.0.0.1, or the <a href="http://en.wikipedia.org/wiki/Private_network">private network blocks</a>: 10.0.0.0 - 10.255.255.255, 172.16.0.0 - 172.31.255.255, 192.168.0.0 - 192.168.255.255. </p>
|
||||
Don't use "loopback" addresses like 127.0.0.1, or the <a href="https://en.wikipedia.org/wiki/Private_network">private network blocks</a>: 10.0.0.0 - 10.255.255.255, 172.16.0.0 - 172.31.255.255, 192.168.0.0 - 192.168.255.255. </p>
|
|
@ -19,7 +19,7 @@ from django.contrib.auth.models import User
|
|||
# create Credit to associate with User
|
||||
def create_user_objects(sender, created, instance, **kwargs):
|
||||
# use get_model to avoid circular import problem with models
|
||||
# don't create Credit if we are loading fixtures http://stackoverflow.com/a/3500009/7782
|
||||
# don't create Credit if we are loading fixtures https://stackoverflow.com/a/3500009/7782
|
||||
if not kwargs.get('raw', False):
|
||||
try:
|
||||
Credit = apps.get_model('payment', 'Credit')
|
||||
|
|
|
@ -57,7 +57,7 @@ STRIPE_EVENT_TYPES = ['account.updated', 'account.application.deauthorized', 'ba
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# http://stackoverflow.com/questions/2348317/how-to-write-a-pager-for-python-iterators/2350904#2350904
|
||||
# https://stackoverflow.com/questions/2348317/how-to-write-a-pager-for-python-iterators/2350904#2350904
|
||||
def grouper(iterable, page_size):
|
||||
page= []
|
||||
for item in iterable:
|
||||
|
@ -155,7 +155,7 @@ def filter_none(d):
|
|||
|
||||
# if you create a Customer object, then you'll be able to charge multiple times. You can create a customer with a token.
|
||||
|
||||
# http://en.wikipedia.org/wiki/Luhn_algorithm#Implementation_of_standard_Mod_10
|
||||
# https://en.wikipedia.org/wiki/Luhn_algorithm#Implementation_of_standard_Mod_10
|
||||
|
||||
def luhn_checksum(card_number):
|
||||
def digits_of(n):
|
||||
|
|
|
@ -46,7 +46,8 @@ feedparser==5.1.2
|
|||
fef-questionnaire==4.0.0
|
||||
freebase==1.0.8
|
||||
#gitenberg.metadata==0.1.6
|
||||
git+ssh://git@github.com/gitenberg-dev/metadata.git@0.1.11
|
||||
git+https://github.com/gitenberg-dev/gitberg-build
|
||||
#git+ssh://git@github.com/gitenberg-dev/metadata.git@0.1.11
|
||||
github3.py==0.9.5
|
||||
html5lib==1.0b3
|
||||
httplib2==0.7.5
|
||||
|
@ -101,3 +102,4 @@ pyasn1==0.1.9
|
|||
pycparser==2.14
|
||||
setuptools==25.0.0
|
||||
urllib3==1.16
|
||||
beautifulsoup4==4.6.0
|
||||
|
|
|
@ -75,7 +75,7 @@ def route53_records(domain_name, name, record_type):
|
|||
|
||||
def modify_rds_parameter(group_name, parameter, value, apply_immediate=False):
|
||||
"""change parameter in RDS parameter group_name to value
|
||||
http://stackoverflow.com/a/9085381/7782
|
||||
https://stackoverflow.com/a/9085381/7782
|
||||
Remember to make sure that the parameter group is actually associated with the db.
|
||||
You will likely need to reboot db too.
|
||||
"""
|
||||
|
|
|
@ -211,7 +211,7 @@ def test_relaunch(unglue_it_url = settings.LIVE_SERVER_TEST_URL, do_local=True,
|
|||
|
||||
# click on biggest campaign list
|
||||
# I have no idea why selenium thinks a is not displayed....so that's why I'm going up one element.
|
||||
# http://stackoverflow.com/a/6141678/7782
|
||||
# https://stackoverflow.com/a/6141678/7782
|
||||
#biggest_campaign_link = WebDriverWait(sel,20).until(lambda d: d.find_element_by_css_selector("li > a[href*='/campaigns/ending']"))
|
||||
#biggest_campaign_link.click()
|
||||
#time.sleep(1)
|
||||
|
|
|
@ -10,7 +10,7 @@ contributor:
|
|||
deathdate: 1933
|
||||
gutenberg_agent_id: '37155'
|
||||
url: http://www.gutenberg.org/2009/agents/37155
|
||||
wikipedia: http://en.wikipedia.org/wiki/E._W._Kemble
|
||||
wikipedia: https://en.wikipedia.org/wiki/E._W._Kemble
|
||||
edition_list:
|
||||
-
|
||||
_edition: rtc_isbn
|
||||
|
@ -32,8 +32,8 @@ creator:
|
|||
deathdate: 1910
|
||||
gutenberg_agent_id: '53'
|
||||
url: http://www.gutenberg.org/2009/agents/53
|
||||
wikipedia: http://en.wikipedia.org/wiki/Mark_Twain
|
||||
description: 'Wikipedia: http://en.wikipedia.org/wiki/Adventures_of_Huckleberry_Finn'
|
||||
wikipedia: https://en.wikipedia.org/wiki/Mark_Twain
|
||||
description: 'Wikipedia: https://en.wikipedia.org/wiki/Adventures_of_Huckleberry_Finn'
|
||||
gutenberg_bookshelf:
|
||||
- Banned Books from Anne Haight's list
|
||||
- Best Books Ever Listings
|
||||
|
@ -45,7 +45,7 @@ language: en
|
|||
publication_date: 2015-08-01
|
||||
publisher: Project Gutenberg
|
||||
rights: PD-US
|
||||
rights_url: http://creativecommons.org/licenses/by-nc/4.0/
|
||||
rights_url: https://creativecommons.org/licenses/by-nc/4.0/
|
||||
subjects:
|
||||
- !lcsh Male friendship -- Fiction
|
||||
- !lcsh Fugitive slaves -- Fiction
|
||||
|
|
|
@ -5,7 +5,7 @@ contributor:
|
|||
gutenberg_agent_id: 25396
|
||||
agent_name: John Schoenherr
|
||||
agent_sortname: Schoenherr, John
|
||||
wikipedia: http://en.wikipedia.org/wiki/John_Schoenherr
|
||||
wikipedia: https://en.wikipedia.org/wiki/John_Schoenherr
|
||||
covers:
|
||||
-
|
||||
image_path: 20728-h/images/illus-front.jpg
|
||||
|
@ -18,7 +18,7 @@ creator:
|
|||
gutenberg_agent_id: 8301
|
||||
agent_name: H. Beam Piper
|
||||
agent_sortname: Piper, H. Beam
|
||||
wikipedia: http://en.wikipedia.org/wiki/H._Beam_Piper
|
||||
wikipedia: https://en.wikipedia.org/wiki/H._Beam_Piper
|
||||
description:
|
||||
funding_info:
|
||||
identifiers:
|
||||
|
|
|
@ -3,7 +3,7 @@ import sys
|
|||
import unicodedata
|
||||
|
||||
|
||||
#http://stackoverflow.com/questions/1707890/fast-way-to-filter-illegal-xml-unicode-chars-in-python
|
||||
#https://stackoverflow.com/questions/1707890/fast-way-to-filter-illegal-xml-unicode-chars-in-python
|
||||
_illegal_unichrs = [(0x00, 0x08), (0x0B, 0x0C), (0x0E, 0x1F),
|
||||
(0x7F, 0x84), (0x86, 0x9F),
|
||||
(0xFDD0, 0xFDDF), (0xFFFE, 0xFFFF)]
|
||||
|
|
|
@ -37,8 +37,8 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
|||
end
|
||||
|
||||
|
||||
# 512MB not enough for compiling lxml: http://stackoverflow.com/a/25916353/7782
|
||||
# http://stackoverflow.com/a/26468913/7782 --> for how to get to this setting
|
||||
# 512MB not enough for compiling lxml: https://stackoverflow.com/a/25916353/7782
|
||||
# https://stackoverflow.com/a/26468913/7782 --> for how to get to this setting
|
||||
node.vm.provider "virtualbox" do |v|
|
||||
v.memory = 1024
|
||||
v.cpus = 2
|
||||
|
|
|
@ -156,7 +156,7 @@
|
|||
# installing mysql
|
||||
# https://github.com/bennojoy/mysql --> probably the right way
|
||||
# how do you make use of other people's playbooks in the right way?
|
||||
# http://stackoverflow.com/a/7740571/7782
|
||||
# https://stackoverflow.com/a/7740571/7782
|
||||
|
||||
- name: mysql setup
|
||||
raw: debconf-set-selections <<< 'mysql-server-5.5 mysql-server/root_password password {{mysql_root_pw}}'
|
||||
|
@ -190,8 +190,8 @@
|
|||
|
||||
|
||||
# running stuff within a virtualenv
|
||||
# http://stackoverflow.com/a/20572360
|
||||
# http://stackoverflow.com/questions/20575084/best-way-to-always-run-ansible-inside-a-virtualenv-on-remote-machines?rq=1
|
||||
# https://stackoverflow.com/a/20572360
|
||||
# https://stackoverflow.com/questions/20575084/best-way-to-always-run-ansible-inside-a-virtualenv-on-remote-machines?rq=1
|
||||
|
||||
|
||||
#sudo("ln -s /opt/regluit/deploy/please.conf /etc/apache2/sites-available/please")
|
||||
|
@ -276,7 +276,7 @@
|
|||
|
||||
#Run syncdb on the application
|
||||
# TO DO: syncdb might be deprecated
|
||||
# http://stackoverflow.com/a/29683785
|
||||
# https://stackoverflow.com/a/29683785
|
||||
|
||||
- name: django syncdb
|
||||
django_manage: >
|
||||
|
|
|
@ -103,7 +103,7 @@
|
|||
## installing mysql
|
||||
## https://github.com/bennojoy/mysql --> probably the right way
|
||||
## how do you make use of other people's playbooks in the right way?
|
||||
## http://stackoverflow.com/a/7740571/7782
|
||||
## https://stackoverflow.com/a/7740571/7782
|
||||
#
|
||||
#- name: mysql setup
|
||||
# raw: debconf-set-selections <<< 'mysql-server-5.5 mysql-server/root_password password {{mysql_root_pw}}'
|
||||
|
@ -126,8 +126,8 @@
|
|||
|
||||
|
||||
# running stuff within a virtualenv
|
||||
# http://stackoverflow.com/a/20572360
|
||||
# http://stackoverflow.com/questions/20575084/best-way-to-always-run-ansible-inside-a-virtualenv-on-remote-machines?rq=1
|
||||
# https://stackoverflow.com/a/20572360
|
||||
# https://stackoverflow.com/questions/20575084/best-way-to-always-run-ansible-inside-a-virtualenv-on-remote-machines?rq=1
|
||||
|
||||
|
||||
## hard coding of just
|
||||
|
|
|
@ -22,13 +22,13 @@
|
|||
# installing mysql
|
||||
# https://github.com/bennojoy/mysql --> probably the right way
|
||||
# how do you make use of other people's playbooks in the right way?
|
||||
# http://stackoverflow.com/a/7740571/7782
|
||||
# https://stackoverflow.com/a/7740571/7782
|
||||
|
||||
post_tasks:
|
||||
|
||||
# running stuff within a virtualenv
|
||||
# http://stackoverflow.com/a/20572360
|
||||
# http://stackoverflow.com/questions/20575084/best-way-to-always-run-ansible-inside-a-virtualenv-on-remote-machines?rq=1
|
||||
# https://stackoverflow.com/a/20572360
|
||||
# https://stackoverflow.com/questions/20575084/best-way-to-always-run-ansible-inside-a-virtualenv-on-remote-machines?rq=1
|
||||
|
||||
|
||||
## hard coding of {{config_name}}
|
||||
|
|
|
@ -129,7 +129,7 @@
|
|||
# installing mysql
|
||||
# https://github.com/bennojoy/mysql --> probably the right way
|
||||
# how do you make use of other people's playbooks in the right way?
|
||||
# http://stackoverflow.com/a/7740571/7782
|
||||
# https://stackoverflow.com/a/7740571/7782
|
||||
|
||||
- name: mysql setup
|
||||
raw: debconf-set-selections <<< 'mysql-server-5.5 mysql-server/root_password password {{mysql_root_pw}}'
|
||||
|
@ -158,8 +158,8 @@
|
|||
|
||||
|
||||
# running stuff within a virtualenv
|
||||
# http://stackoverflow.com/a/20572360
|
||||
# http://stackoverflow.com/questions/20575084/best-way-to-always-run-ansible-inside-a-virtualenv-on-remote-machines?rq=1
|
||||
# https://stackoverflow.com/a/20572360
|
||||
# https://stackoverflow.com/questions/20575084/best-way-to-always-run-ansible-inside-a-virtualenv-on-remote-machines?rq=1
|
||||
|
||||
|
||||
## hard coding of please
|
||||
|
|
|
@ -103,7 +103,7 @@
|
|||
# installing mysql
|
||||
# https://github.com/bennojoy/mysql --> probably the right way
|
||||
# how do you make use of other people's playbooks in the right way?
|
||||
# http://stackoverflow.com/a/7740571/7782
|
||||
# https://stackoverflow.com/a/7740571/7782
|
||||
|
||||
- name: mysql setup
|
||||
raw: debconf-set-selections <<< 'mysql-server-5.5 mysql-server/root_password password {{mysql_root_pw}}'
|
||||
|
@ -126,8 +126,8 @@
|
|||
|
||||
|
||||
# running stuff within a virtualenv
|
||||
# http://stackoverflow.com/a/20572360
|
||||
# http://stackoverflow.com/questions/20575084/best-way-to-always-run-ansible-inside-a-virtualenv-on-remote-machines?rq=1
|
||||
# https://stackoverflow.com/a/20572360
|
||||
# https://stackoverflow.com/questions/20575084/best-way-to-always-run-ansible-inside-a-virtualenv-on-remote-machines?rq=1
|
||||
|
||||
|
||||
## hard coding of please
|
||||
|
|
Loading…
Reference in New Issue