Merge branch 'master' of github.com:Gluejar/regluit into payment

pull/1/head
Raymond Yee 2012-03-02 09:32:01 -08:00
commit e1fc176031
37 changed files with 1611 additions and 376 deletions

View File

@ -80,6 +80,15 @@ Instructions for setting please are slightly different.
1. `sudo chown celery:celery /var/log/celery`
1. `sudo /etc/init.d/celeryd start`
Updating Production
--------------------
1. Study the latest changes in the master branch, especially keep in mind how
it has [changed from what's in production](https://github.com/Gluejar/regluit/compare/production...master).
1. Update the production branch accordingly. If everything in `master` is ready to be moved into `production`, you can just merge `master` into `production`. Otherwise, you can grab specific parts. (How to do so is something that should probably be described in greater detail.)
1. Login to unglue.it and run [`/opt/regluit/deploy/update-prod`](https://github.com/Gluejar/regluit/blob/master/deploy/update-prod)
OS X Developer Notes
-------------------

View File

@ -107,12 +107,12 @@ def get_google_isbn_results(isbn):
def add_ebooks(item, edition):
access_info = item.get('accessInfo')
if access_info:
edition.public_domain = item.get('public_domain', None)
edition.public_domain = access_info.get('publicDomain', None)
epub = access_info.get('epub')
if epub and epub.get('downloadLink'):
ebook = models.Ebook(edition=edition, format='epub',
url=epub.get('downloadLink'),
provider='google')
provider='Google Books')
try:
ebook.save()
except IntegrityError:
@ -123,7 +123,7 @@ def add_ebooks(item, edition):
if pdf and pdf.get('downloadLink'):
ebook = models.Ebook(edition=edition, format='pdf',
url=pdf.get('downloadLink', None),
provider='google')
provider='Google Books')
try:
ebook.save()
except IntegrityError:
@ -408,7 +408,7 @@ def thingisbn(isbn):
return [e.text for e in doc.findall('isbn')]
def merge_works(w1, w2):
def merge_works(w1, w2, user=None):
"""will merge the second work (w2) into the first (w1)
"""
logger.info("merging work %s into %s", w2, w1)
@ -433,7 +433,7 @@ def merge_works(w1, w2):
wishlist.remove_work(w2)
wishlist.add_work(w1, w2source)
models.WasWork(was=w2.pk,work=w1).save()
models.WasWork(was=w2.pk, work=w1, user=user).save()
for ww in models.WasWork.objects.filter(work = w2):
ww.work = w1
ww.save()
@ -557,7 +557,7 @@ def load_gutenberg_edition(title, gutenberg_etext_id, ol_work_id, seed_isbn, url
ebook.format = format
ebook.provider = 'gutenberg'
ebook.provider = 'Project Gutenberg'
ebook.url = url
ebook.rights = license
@ -636,6 +636,7 @@ def add_missing_isbn_to_editions(max_num=None, confirm=False):
'no_isbn_found': no_isbn_found,
'editions_to_merge': editions_to_merge,
'exceptions': exceptions,
'google_id_not_found': google_id_not_found,
'confirm': ok
}

View File

@ -0,0 +1,32 @@
"""
Load the Gutenberg editions
"""
from django.core.management.base import BaseCommand
from regluit.core import models
from regluit.test import booktests
class Command(BaseCommand):
help = "load Gutenberg editions"
args = "<max_num>"
def handle(self, max_num, **options):
try:
max_num = int(max_num)
except:
max_num = None
print "number of Gutenberg editions (before)", \
models.Edition.objects.filter(identifiers__type='gtbg').count()
print "number of Gutenberg ebooks (before)", \
models.Ebook.objects.filter(edition__identifiers__type='gtbg').count()
booktests.load_gutenberg_books(max_num=max_num)
print "number of Gutenberg editions (after)", \
models.Edition.objects.filter(identifiers__type='gtbg').count()
print "number of Gutenberg ebooks (after)", \
models.Ebook.objects.filter(edition__identifiers__type='gtbg').count()

View File

@ -0,0 +1,34 @@
"""
Dispose of the Frankenworks and recluster the works. Print out email addresses of those whose wishlists have been
affected.
"""
from django.core.management.base import BaseCommand
from regluit.test import booktests
class Command(BaseCommand):
help = "Dispose of the Frankenworks and recluster the works. Print out email addresses of those whose wishlists have been affected."
args = "<do>"
def handle(self, do, **options):
try:
do = str(do)
if do.lower() == 'true':
do = True
else:
do = False
except:
do = False
print "before..."
s = booktests.cluster_status()
print s['results']
booktests.clean_frankenworks(s, do=do)
s = booktests.cluster_status()
print "after cleanup...."
print "results ", s['results']
print "scattered clusters ", s['scattered_clusters']
print "franken works", s['franken_works']

View File

@ -0,0 +1,238 @@
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
no_dry_run = True
def forwards(self, orm):
# Adding field 'Ebook.user'
db.add_column('core_ebook', 'user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True), keep_default=False)
# Adding field 'WasWork.moved'
db.add_column('core_waswork', 'moved', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, default=datetime.date(2012, 2, 25), blank=True), keep_default=False)
# Adding field 'WasWork.user'
db.add_column('core_waswork', 'user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True), keep_default=False)
# change value of ebook.provider
for ebook in orm.Ebook.objects.filter(provider = 'google'):
ebook.provider ='Google Books'
ebook.rights = 'PD-US'
ebook.save()
for ebook in orm.Ebook.objects.filter(provider = 'gutenberg'):
ebook.provider ='Project Gutenberg'
ebook.rights = 'PD-US'
ebook.save()
def backwards(self, orm):
# Deleting field 'Ebook.user'
db.delete_column('core_ebook', 'user_id')
# Deleting field 'WasWork.moved'
db.delete_column('core_waswork', 'moved')
# Deleting field 'WasWork.user'
db.delete_column('core_waswork', 'user_id')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'core.author': {
'Meta': {'object_name': 'Author'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'editions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authors'", 'symmetrical': 'False', 'to': "orm['core.Edition']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '500'})
},
'core.campaign': {
'Meta': {'object_name': 'Campaign'},
'activated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'amazon_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'deadline': ('django.db.models.fields.DateTimeField', [], {}),
'description': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'details': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'left': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}),
'managers': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'campaigns'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '500', 'null': 'True'}),
'paypal_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'INITIALIZED'", 'max_length': '15', 'null': 'True'}),
'target': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}),
'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'to': "orm['core.Work']"})
},
'core.campaignaction': {
'Meta': {'object_name': 'CampaignAction'},
'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'actions'", 'to': "orm['core.Campaign']"}),
'comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '15'})
},
'core.celerytask': {
'Meta': {'object_name': 'CeleryTask'},
'active': ('django.db.models.fields.NullBooleanField', [], {'default': 'True', 'null': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 2, 25, 11, 41, 50, 264275)', 'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'null': 'True'}),
'function_args': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
'function_name': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'task_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tasks'", 'null': 'True', 'to': "orm['auth.User']"})
},
'core.claim': {
'Meta': {'object_name': 'Claim'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'rights_holder': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'claim'", 'to': "orm['core.RightsHolder']"}),
'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '7'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'claim'", 'to': "orm['auth.User']"}),
'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'claim'", 'to': "orm['core.Work']"})
},
'core.ebook': {
'Meta': {'object_name': 'Ebook'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'edition': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'ebooks'", 'to': "orm['core.Edition']"}),
'format': ('django.db.models.fields.CharField', [], {'max_length': '25'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'rights': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
'url': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'})
},
'core.edition': {
'Meta': {'object_name': 'Edition'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'public_domain': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'publication_date': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True'}),
'publisher': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'editions'", 'null': 'True', 'to': "orm['core.Work']"})
},
'core.identifier': {
'Meta': {'unique_together': "(('type', 'value'),)", 'object_name': 'Identifier'},
'edition': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'identifiers'", 'null': 'True', 'to': "orm['core.Edition']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '4'}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '31'}),
'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'identifiers'", 'to': "orm['core.Work']"})
},
'core.premium': {
'Meta': {'object_name': 'Premium'},
'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '10', 'decimal_places': '0'}),
'campaign': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'premiums'", 'null': 'True', 'to': "orm['core.Campaign']"}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '2'})
},
'core.rightsholder': {
'Meta': {'object_name': 'RightsHolder'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'rights_holder'", 'to': "orm['auth.User']"}),
'rights_holder_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'})
},
'core.subject': {
'Meta': {'ordering': "['name']", 'object_name': 'Subject'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'}),
'works': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'subjects'", 'symmetrical': 'False', 'to': "orm['core.Work']"})
},
'core.userprofile': {
'Meta': {'object_name': 'UserProfile'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'facebook_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
'goodreads_auth_secret': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'goodreads_auth_token': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'goodreads_user_id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
'goodreads_user_link': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
'goodreads_user_name': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
'home_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'librarything_id': ('django.db.models.fields.CharField', [], {'max_length': '31', 'blank': 'True'}),
'pic_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'tagline': ('django.db.models.fields.CharField', [], {'max_length': '140', 'blank': 'True'}),
'twitter_id': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"})
},
'core.waswork': {
'Meta': {'object_name': 'WasWork'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'moved': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
'was': ('django.db.models.fields.IntegerField', [], {'unique': 'True'}),
'work': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Work']"})
},
'core.wishes': {
'Meta': {'object_name': 'Wishes', 'db_table': "'core_wishlist_works'"},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'source': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}),
'wishlist': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Wishlist']"}),
'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'wishes'", 'to': "orm['core.Work']"})
},
'core.wishlist': {
'Meta': {'object_name': 'Wishlist'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'wishlist'", 'unique': 'True', 'to': "orm['auth.User']"}),
'works': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'wishlists'", 'symmetrical': 'False', 'through': "orm['core.Wishes']", 'to': "orm['core.Work']"})
},
'core.work': {
'Meta': {'ordering': "['title']", 'object_name': 'Work'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '2'}),
'num_wishes': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'openlibrary_lookup': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'})
}
}
complete_apps = ['core']

View File

@ -368,7 +368,10 @@ class Work(models.Model):
else:
status = percent;
return status;
def ebooks(self):
return Ebook.objects.filter(edition__work=self).order_by('-created')
def first_pdf(self):
return self.first_ebook('pdf')
@ -391,20 +394,18 @@ class Work(models.Model):
def first_ebook(self, ebook_format=None):
if ebook_format:
for ebook in Ebook.objects.filter(edition__work=self,
format=ebook_format):
for ebook in self.ebooks().filter(format=ebook_format):
return ebook
else:
for ebook in Ebook.objects.filter(edition__work=self):
for ebook in self.ebooks():
return ebook
return None
def wished_by(self):
return User.objects.filter(wishlist__works__in=[self])
def update_num_wishes(self):
self.num_wishes = Wishes.objects.filter(work=self).count()
self.save()
self.num_wishes = Wishes.objects.filter(work=self).count()
self.save()
def longest_description(self):
"""get the longest description from an edition of this work
@ -528,18 +529,56 @@ class Edition(models.Model):
return None
class WasWork(models.Model):
work = models.ForeignKey('Work')
was = models.IntegerField(unique = True)
work = models.ForeignKey('Work')
was = models.IntegerField(unique = True)
moved = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, null=True)
class Ebook(models.Model):
FORMAT_CHOICES = (('PDF','PDF'),( 'EPUB','EPUB'), ('HTML','HTML'), ('TEXT','TEXT'), ('MOBI','MOBI'))
RIGHTS_CHOICES = (('PD-US', 'Public Domain, US'),
('CC BY-NC-ND','CC BY-NC-ND'),
('CC BY-ND','CC BY-ND'),
('CC BY','CC BY'),
('CC BY-NC','CC BY-NC'),
( 'CC BY-NC-SA','CC BY-NC-SA'),
( 'CC BY-SA','CC BY-SA'),
( 'CC0','CC0'),
)
url = models.URLField(max_length=1024)
created = models.DateTimeField(auto_now_add=True)
format = models.CharField(max_length=25)
url = models.CharField(max_length=1024)
format = models.CharField(max_length=25, choices = FORMAT_CHOICES)
provider = models.CharField(max_length=255)
rights = models.CharField(max_length=255, null=True)
# use 'PD-US', 'CC BY', 'CC BY-NC-SA', 'CC BY-NC-ND', 'CC BY-NC', 'CC BY-ND', 'CC BY-SA', 'CC0'
rights = models.CharField(max_length=255, null=True, choices = RIGHTS_CHOICES)
edition = models.ForeignKey('Edition', related_name='ebooks')
user = models.ForeignKey(User, null=True)
def set_provider(self):
self.provider=Ebook.infer_provider(self.url)
return self.provider
@classmethod
def infer_provider(klass, url):
if not url:
return None
# provider derived from url. returns provider value. remember to call save() afterward
if url.startswith('http://books.google.com/'):
provider='Google Books'
elif url.startswith('http://www.gutenberg.org/'):
provider='Project Gutenberg'
elif url.startswith('http://www.archive.org/'):
provider='Internet Archive'
elif url.startswith('http://hdl.handle.net/2027/') or url.startswith('http://babel.hathitrust.org/'):
provider='Hathitrust'
elif re.match('http://\w\w\.wikisource\.org/', url):
provider='Wikisource'
else:
provider=None
return provider
def __unicode__(self):
return "%s (%s from %s)" % (self.edition.title, self.format, self.provider)

View File

@ -151,8 +151,8 @@ class BookLoaderTests(TestCase):
def test_merge_works(self):
# add two editions and see that there are two stub works
e1 = bookloader.add_by_isbn('0465019358')
e2 = bookloader.add_by_isbn('1458776204')
e1 = bookloader.add_by_isbn('0385722133')
e2 = bookloader.add_by_isbn('0385504187')
self.assertTrue(e1)
self.assertTrue(e2)
self.assertTrue(e1.work)
@ -201,9 +201,9 @@ class BookLoaderTests(TestCase):
comment2.save()
# now add related edition to make sure Works get merged
bookloader.add_related('1458776204')
bookloader.add_related('0385722133')
self.assertEqual(models.Work.objects.count(), 1)
w3 = models.Edition.get_by_isbn('1458776204').work
w3 = models.Edition.get_by_isbn('0385722133').work
# and that relevant Campaigns and Wishlists are updated
@ -227,13 +227,15 @@ class BookLoaderTests(TestCase):
#self.assertEqual(ebook_epub.url, 'http://books.google.com/books/download/The_Latin_language.epub?id=U3FXAAAAYAAJ&ie=ISO-8859-1&output=epub&source=gbs_api')
self.assertEqual(parse_qs(urlparse(ebook_epub.url).query).get("id"), ['U3FXAAAAYAAJ'])
self.assertEqual(parse_qs(urlparse(ebook_epub.url).query).get("output"), ['epub'])
self.assertEqual(ebook_epub.provider, 'google')
self.assertEqual(ebook_epub.provider, 'Google Books')
self.assertEqual(ebook_epub.set_provider(), 'Google Books')
ebook_pdf = edition.ebooks.filter(format='pdf')[0]
self.assertEqual(ebook_pdf.format, 'pdf')
#self.assertEqual(ebook_pdf.url, 'http://books.google.com/books/download/The_Latin_language.pdf?id=U3FXAAAAYAAJ&ie=ISO-8859-1&output=pdf&sig=ACfU3U2yLt3nmTncB8ozxOWUc4iHKUznCA&source=gbs_api')
self.assertEqual(parse_qs(urlparse(ebook_pdf.url).query).get("id"), ['U3FXAAAAYAAJ'])
self.assertEqual(parse_qs(urlparse(ebook_pdf.url).query).get("output"), ['pdf'])
self.assertEqual(ebook_pdf.provider, 'google')
self.assertEqual(ebook_pdf.provider, 'Google Books')
self.assertEqual(edition.public_domain, True)
w = edition.work
self.assertEqual(w.first_epub().url, ebook_epub.url)
@ -241,6 +243,9 @@ 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'
self.assertEqual(ebook_pdf.set_provider(), 'Wikisource')
def test_add_no_ebook(self):
# this edition lacks an ebook, but we should still be able to load it
e = bookloader.add_by_isbn('0465019358')

View File

@ -11,6 +11,7 @@ import random
random.seed()
import sys, os
import json
# a kludge to allow for isbn.py to be imported
# and not just in the context of the regluit Django app
@ -436,15 +437,14 @@ class FreebaseBooks(object):
self.freebase.login(username,password)
def books(self):
MQL = u"""[{
"type": "/book/book",
"id": null,
"key": [{
"namespace": "/wikipedia/en",
"value": null,
"type": "/type/key"
}]
}]
""".replace("\n"," ")
"type": "/book/book",
"id": null,
"key": [{
"namespace": "/wikipedia/en",
"value": null,
"type": "/type/key"
}]
}]""".replace("\n"," ")
query = json.loads(MQL)
resp = self.freebase.mqlreaditer(query)
for r in resp:
@ -452,18 +452,17 @@ class FreebaseBooks(object):
def book_editions(self):
MQL = u"""[{
"type": "/book/book_edition",
"id": null,
"isbn": [{}],
"ISBN": [{}],
"LCCN": [{}],
"OCLC_number": [{}],
"openlibrary_id": [{}],
"book": {
"id": null,
"name": null
}
}]""".replace("\n"," ")
"type": "/book/book_edition",
"id": null,
"isbn": [{}],
"ISBN": [{}],
"LCCN": [{}],
"OCLC_number": [{}],
"openlibrary_id": [{}],
"book": {
"id": null,
"name": null
}}]""".replace("\n"," ")
query = json.loads(MQL)
resp = self.freebase.mqlreaditer(query)
for r in resp:
@ -471,18 +470,17 @@ class FreebaseBooks(object):
def editions_for_book(self, book_id):
MQL = u"""[{
"type": "/book/book_edition",
"id": null,
"isbn": [{}],
"ISBN": [{}],
"LCCN": [{}],
"OCLC_number": [{}],
"openlibrary_id": [{}],
"book": {
"id": null,
"name": null
}
}]""".replace("\n"," ")
"type": "/book/book_edition",
"id": null,
"isbn": [{}],
"ISBN": [{}],
"LCCN": [{}],
"OCLC_number": [{}],
"openlibrary_id": [{}],
"book": {
"id": null,
"name": null
}}]""".replace("\n"," ")
query = json.loads(MQL)
query[0]["book"]["id"] = book_id
resp = self.freebase.mqlreaditer(query)
@ -491,18 +489,17 @@ class FreebaseBooks(object):
def book_edition_by_id(self,id,id_type):
MQL = u"""[{
"type": "/book/book_edition",
"id": null,
"isbn": [{}],
"ISBN": [{}],
"LCCN": [{}],
"OCLC_number": [{}],
"openlibrary_id": [{}],
"book": {
"id": null,
"name": null
}
}]""".replace("\n"," ")
"type": "/book/book_edition",
"id": null,
"isbn": [{}],
"ISBN": [{}],
"LCCN": [{}],
"OCLC_number": [{}],
"openlibrary_id": [{}],
"book": {
"id": null,
"name": null
}}]""".replace("\n"," ")
query = json.loads(MQL)
if id_type == 'isbn':
query[0][id_type][0].setdefault('name', id)
@ -526,18 +523,18 @@ class FreebaseBooks(object):
elif isbn_val is not None:
isbn_val = isbn_mod.ISBN(isbn_val).to_string('13')
MQL = """[{
"type": "/book/book_edition",
"isbn": {
"name": null
},
"book": {
"editions": [{
"isbn": {
"name": null
}
}]
}
}]""".replace("\n"," ")
"type": "/book/book_edition",
"isbn": {
"name": null
},
"book": {
"editions": [{
"isbn": {
"name": null
}
}]
}
}]""".replace("\n"," ")
query = json.loads(MQL)
query[0]["book"]["editions"][0]["isbn"]["name"] = isbn_val
resp = self.freebase.mqlreaditer(query)
@ -565,7 +562,91 @@ class WorkMapper(object):
yield work_id
if not complete_search:
raise StopIteration()
class LibraryThing(object):
"""
Provide cached access to thingisbn and LT whatwork interface. Allow for a cache file to be loaded and saved
"""
def __init__(self, fname=None):
self.__isbn_to_work_id = {}
self.fname = fname
def __del__(self):
self.save()
def thingisbn(self, isbn, return_work_id=False):
""" if return_work_id is True, we won't try to calculate all the relevant isbns"""
# first, normalize the isbn
isbn = isbn_mod.ISBN(isbn).to_string('13')
if isbn is None: return []
# check to see whether we have isbn already
if isbn in self.__isbn_to_work_id:
# return all isbns with the work id
# print "%s already cached" % (isbn)
work_id = self.__isbn_to_work_id.get(isbn)
if return_work_id:
return work_id
if work_id is not None:
return [k for (k, v) in self.__isbn_to_work_id.items() if v == work_id]
else:
return []
else:
# if isbn is not already cached, do look up and cache the results and return the results
print "calling thingisbn for %s" % (isbn)
results = [isbn_mod.ISBN(k).to_string('13') for k in thingisbn (isbn)]
if len(results):
# look up the librarything work id
work_id = self.whatwork(isbn)
if work_id is not None: # which should be the case since results is not zero-length
self.__isbn_to_work_id.update(dict([(isbn_mod.ISBN(result).to_string('13'), work_id) for result in results]))
else:
logger.exception("work_id should not be None for isbn %s", isbn)
return []
else:
self.__isbn_to_work_id[isbn] = None # mark as not recognized by LT
work_id = None
if return_work_id:
return work_id
else:
return results
def whatwork(self, isbn=None, title=None, author=None):
# if isbn is not None and title, author None then look up results, otherwise just pass along to lt_whatwork
# first, normalize the isbn
isbn = isbn_mod.ISBN(isbn).to_string('13')
if isbn is not None and (title is None and author is None):
if isbn in self.__isbn_to_work_id:
work_id = self.__isbn_to_work_id.get(isbn)
else:
work_id = lt_whatwork(isbn=isbn)
self.__isbn_to_work_id[isbn] = work_id
return work_id
else:
return lt_whatwork(isbn=isbn, title=title, author=author)
def load(self):
try:
f = open(self.fname)
input_data = json.load(f)
f.close()
if isinstance(input_data, dict):
self.__isbn_to_work_id = input_data
return True
else:
return False
except Exception, e:
print e
def save(self):
if self.fname is not None:
f = open(self.fname, "w")
json.dump(self.__isbn_to_work_id, f)
f.close()
return True
else:
return False
def look_up_my_zotero_books_in_hathi():
from regluit.experimental.zotero_books import MyZotero
@ -786,6 +867,17 @@ class LibraryThingTest(TestCase):
self.assertEqual(work_id, SURFACING_LT_WORK_ID)
work_id = lt_whatwork(title='Hamlet', author='Shakespeare')
self.assertEqual(work_id, '2199')
def test_cache(self):
lt = LibraryThing()
res = lt.thingisbn(SURFACING_ISBN)
res2 = lt.thingisbn(SURFACING_ISBN)
self.assertEqual(set(res), set(res2))
self.assertEqual(lt.whatwork(SURFACING_ISBN), SURFACING_LT_WORK_ID)
self.assertEqual(lt.thingisbn(SURFACING_ISBN, return_work_id=True), SURFACING_LT_WORK_ID)
def suite():
@ -793,7 +885,7 @@ def suite():
#testcases = [WorkMapperTest,FreebaseBooksTest, OpenLibraryTest,GoogleBooksTest]
testcases = []
suites = unittest.TestSuite([unittest.TestLoader().loadTestsFromTestCase(testcase) for testcase in testcases])
suites.addTest(LibraryThingTest('test_whatwork'))
suites.addTest(LibraryThingTest('test_cache'))
#suites.addTest(SettingsTest('test_dev_me_alignment')) # give option to test this alignment
return suites

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,7 @@ from urllib import urlencode
from pprint import pprint
from collections import defaultdict, OrderedDict
from itertools import islice, chain, izip
from itertools import islice, chain, izip, repeat
import operator
import time
@ -64,7 +64,8 @@ def grouper(iterable, page_size):
if len(page) == page_size:
yield page
page= []
yield page
if len(page):
yield page
def singleton(cls):
instances = {}
@ -103,6 +104,8 @@ class SeedISBN(Base):
id = Column(u'id', Integer(11), primary_key=True, nullable=False)
results = Column(u'results', MEDIUMTEXT())
seed_isbn = Column(u'seed_isbn', String(length=13))
title = Column(u'title', Text())
title_error = Column(u'title_error', Text())
class GutenbergText(object):
@ -710,8 +713,34 @@ def seed_isbn(olwk_ids, freebase_ids, lang='en'):
'len_all_isbns': len(all_isbns)}
return (candidate_seed_isbn, details)
def candidate_subcluster_from_lt_clusters_by_lang(lang, lt_clusters_by_lang):
"""
Boil the candidate down to a single ISBN: take a random ISBN from the list of all ISBNs in the requested
language subcluster within the largest cluster that has such a language subcluster.
Return None if there is no matching sub-language
Try to find an ISBN that has good overlap with Freebase and OpenLibrary
"""
candidate_subclusters = filter(lambda x: x[0] is not None,
[(c.get(lang), len(reduce(operator.add,c.values()))) for c in lt_clusters_by_lang]
)
if len(candidate_subclusters):
candidate_subcluster = max(candidate_subclusters, key=lambda x:x[1])
else:
candidate_subcluster = []
return candidate_seed_isbn
def report_on_seed_isbn(seed_isbn_result):
"""
return a dictionary interpreting the output of the seed isbn calculation
"""
s = seed_isbn_result
# what proportion of all the ISBNS does the largest cluster make of all the ISBNs
# x is an iterable of cluster lengths
dominance = lambda x: float(max(x))/float(sum(x)) if len(x) else None
report = OrderedDict([
("seed isbn", s[0]),
("the Google info we have on the seed isbn", s[1]['gbooks_data'].get(s[0])),
@ -730,7 +759,8 @@ def report_on_seed_isbn(seed_isbn_result):
for c in s[1]['lt_clusters_by_lang']]),
("size of the sub-cluster including the seed isbn", len(filter(lambda x: s[0] in x,
reduce(operator.add , [c.values() for c in s[1]['lt_clusters_by_lang']]))[0]) \
if s[0] is not None else None)
if s[0] is not None else None),
("dominance of largest cluster", dominance([len(cluster) for cluster in s[1]['lt_clusters']]))
])
return report
@ -813,7 +843,9 @@ def calc_seed_isbns(ids=None, max=None, override=False, max_consecutive_error=3)
def reports_in_db(max=None):
"""
a generator of all the Gutenberg seed isbn calculations
"""
gluejar_db = GluejarDB()
gutenberg_done = gluejar_db.session.query(SeedISBN).all()
for s in islice(gutenberg_done, max):
@ -874,6 +906,142 @@ def export_to_json(obj, max=None,fname=None):
return json.dumps(obj)
def calc_titles_for_seed_isbns(max_num=None, do=False):
"""
For the seedisbns, calculate the titles
"""
db = GluejarDB()
# title is Null and title_error is Null
#titles_to_calc = db.session.query(SeedISBN).filter(and_(SeedISBN.title==None, SeedISBN.title_error==None)).all()
titles_to_calc = db.session.query(SeedISBN, GutenbergText.lang, GutenbergText.title). \
join(GutenbergText, SeedISBN.gutenberg_etext_id==GutenbergText.etext_id). \
filter(and_(SeedISBN.title==None, SeedISBN.title_error==None)).all()
page_size = 5
for page in grouper(islice(titles_to_calc, max_num), page_size):
query = list(izip([edition.seed_isbn for (edition, lang, gt_title) in page], repeat('isbn')))
try:
res = OpenLibrary.read(query)
except Exception, e:
print e
for (edition, lang, gt_title) in page:
title_error = None
try:
title = res.get('isbn:{0}'.format(edition.seed_isbn))['records'].values()[0]['data']['title']
except Exception, e:
title = None
title_error = str(e)
if do and title is not None:
edition.title = title
edition.title_error = title_error
db.commit_db()
yield (edition.seed_isbn, title)
def repick_seed_isbn(max_num=None, do=False, print_progress=False):
"""
Let's try to get ISBNs in the cluster that are in OpenLibrary, Freebase, and Librarything if possible
"""
gluejar_db = GluejarDB()
gutenberg_done = gluejar_db.session.query(SeedISBN, GutenbergText.lang, GutenbergText.title).join(GutenbergText, SeedISBN.gutenberg_etext_id==GutenbergText.etext_id).all()
# need to join with GutenbergText table to get lang and Gutenberg title
for (i, (s, lang, gt_title)) in enumerate(islice(gutenberg_done, max_num)):
# calculate the dominant cluster
results = json.loads(s.results)
candidate_subclusters = filter(lambda x: x[0] is not None,
[(c.get(lang), len(reduce(operator.add,c.values()))) for c in results[1]['lt_clusters_by_lang']]
)
# remember that the cluster is the first element in the tuple and a length in the 2nd element
if len(candidate_subclusters):
candidate_subcluster = set(max(candidate_subclusters, key=lambda x:x[1])[0])
else:
candidate_subcluster = set([])
# confirm that the current seed isbn is in the candidate subcluster
current_seed_ok = s.seed_isbn in candidate_subcluster
# see whether we can get a seed isbn that, in addition to LibraryThing,
# is recognized by OpenLibrary and Freebase too...2nd priority
# is just OL, 3rd is Freebase and the 4th) just LT
fb_isbns = set(results[1]['fb_isbns'])
ol_isbns = set(results[1]['ol_isbns'])
seeds = (candidate_subcluster & fb_isbns & ol_isbns) or (candidate_subcluster & ol_isbns) or \
(candidate_subcluster & fb_isbns) or candidate_subcluster
new_seed_isbn = None
if do and len(seeds):
new_seed_isbn = seeds.pop()
s.seed_isbn = new_seed_isbn
gluejar_db.commit_db()
if print_progress:
print i, s.gutenberg_etext_id, s.seed_isbn, lang, gt_title, seeds, current_seed_ok, new_seed_isbn
yield (s.gutenberg_etext_id, s.seed_isbn, lang, gt_title, seeds, current_seed_ok, new_seed_isbn)
def compute_similarity_measures_for_seed_isbns(max_num=None):
"""
Output the current seedisbn calculations with some measures to help spot errors in mapping, including
the Levenshtein distance/ratio between the Gutenberg title and the title of the edition corresponding to the
ISBN -- and a dominance factor (the ratio of the size of the largest cluster of ISBNs
divided by all the number of ISBNs in all the clusters). Idea: editions whose titles have big distances
and low dominance factors should be looked at more closely.
"""
from Levenshtein import distance, ratio
# what proportion of all the ISBNs does the largest cluster make of all the ISBNs
# x is an iterable of cluster lengths
dominance = lambda x: float(max(x))/float(sum(x)) if len(x) else None
gluejar_db = GluejarDB()
seed_isbns = gluejar_db.session.query(SeedISBN, GutenbergText.lang, GutenbergText.title).join(GutenbergText, SeedISBN.gutenberg_etext_id==GutenbergText.etext_id).all()
for (i, (seed_isbn, lang, gt_title)) in enumerate(islice(seed_isbns, max_num)):
res = json.loads(seed_isbn.results)
yield OrderedDict([('etext_id', seed_isbn.gutenberg_etext_id),
('seed_isbn_title',seed_isbn.title),
('gt_title', gt_title),
('dominance', dominance([len(cluster) for cluster in res[1]['lt_clusters']])),
('title_l_ratio', ratio(seed_isbn.title, gt_title) if (seed_isbn.title is not None and gt_title is not None) else None)])
def output_to_csv(f, headers, rows, write_header=True, convert_values_to_unicode=True):
"""
take rows, an iterable of dicts (and corresponding headers) and output as a CSV file to f
"""
from unicode_csv import UnicodeDictWriter
cw = UnicodeDictWriter(f, headers)
if write_header:
cw.writerow(dict([(h,h) for h in headers]))
for row in rows:
if convert_values_to_unicode:
row = dict([(k, unicode(v)) for (k,v) in row.items()])
cw.writerow(row)
return f
def filtered_gutenberg_and_seed_isbn(min_l_ratio=None, min_dominance=None, max_num=None, include_olid=False):
# compute the similarity measures and pass through only the Gutenberg records that meet the minimum lt_ratio and dominance
measures = compute_similarity_measures_for_seed_isbns()
measures_map = dict()
for measure in measures:
measures_map[measure['etext_id']] = measure
for item in gutenberg_and_seed_isbn(max=max_num, include_olid=include_olid):
g_id = item['gutenberg_etext_id']
accept = True
if min_dominance is not None and measures_map[g_id]['dominance'] is not None and measures_map[g_id]['dominance'] < min_dominance:
accept = False
if min_l_ratio is not None and measures_map[g_id]['title_l_ratio'] is not None and measures_map[g_id]['title_l_ratio'] < min_l_ratio:
accept = False
if accept:
yield item
class FreebaseClient(object):
def __init__(self, username=None, password=None, main_or_sandbox='main'):
@ -1123,8 +1291,14 @@ if __name__ == '__main__':
#unittest.main()
print list(gutenberg_and_seed_isbn(max=10))
#print list(gutenberg_and_seed_isbn(max=10))
#print list(repick_seed_isbn(10))
# output a filtered gutenberg list
# 0.56 and 0.7 I got by eye-balling the results in Google Refine
y = list(filtered_gutenberg_and_seed_isbn(min_l_ratio=0.56, min_dominance=0.7))
export_to_json(y,fname="g_seed_isbn.json")
#suites = suite()
#suites = unittest.defaultTestLoader.loadTestsFromModule(__import__('__main__'))

View File

@ -11,14 +11,29 @@ from decimal import Decimal as D
from selectable.forms import AutoCompleteSelectMultipleWidget,AutoCompleteSelectMultipleField
from selectable.forms import AutoCompleteSelectWidget,AutoCompleteSelectField
from regluit.core.models import UserProfile, RightsHolder, Claim, Campaign, Premium
from regluit.core.models import UserProfile, RightsHolder, Claim, Campaign, Premium, Ebook
from regluit.core.lookups import OwnerLookup
import logging
logger = logging.getLogger(__name__)
class EbookForm(forms.ModelForm):
class Meta:
model = Ebook
exclude = 'created'
widgets = {
'edition': forms.HiddenInput,
'user': forms.HiddenInput,
'provider': forms.HiddenInput,
'url': forms.TextInput(attrs={'size' : 60}),
}
def clean_provider(self):
new_provider= Ebook.infer_provider(self.data['url'])
if not new_provider:
raise forms.ValidationError(_("At this time, ebook URLs must point at Internet Archive, Wikisources, Hathitrust, Project Gutenberg, or Google Books."))
return new_provider
def UserClaimForm ( user_instance, *args, **kwargs ):
class ClaimForm(forms.ModelForm):
i_agree=forms.BooleanField()
@ -194,30 +209,30 @@ class CampaignAdminForm(forms.Form):
pass
class EmailShareForm(forms.Form):
recipient = forms.EmailField()
sender = forms.EmailField(widget=forms.HiddenInput())
subject = forms.CharField(max_length=100)
message = forms.CharField(widget=forms.Textarea())
# allows us to return user to original page by passing it as hidden form input
# we can't rely on POST or GET since the emailshare view handles both
# and may iterate several times as it catches user errors, losing URL info
next = forms.CharField(widget=forms.HiddenInput())
recipient = forms.EmailField()
sender = forms.EmailField(widget=forms.HiddenInput())
subject = forms.CharField(max_length=100)
message = forms.CharField(widget=forms.Textarea())
# allows us to return user to original page by passing it as hidden form input
# we can't rely on POST or GET since the emailshare view handles both
# and may iterate several times as it catches user errors, losing URL info
next = forms.CharField(widget=forms.HiddenInput())
class FeedbackForm(forms.Form):
sender = forms.EmailField(widget=forms.TextInput(attrs={'size':50}), label="Your email")
subject = forms.CharField(max_length=500, widget=forms.TextInput(attrs={'size':50}))
message = forms.CharField(widget=forms.Textarea())
page = forms.CharField(widget=forms.HiddenInput())
notarobot = forms.IntegerField(label="Please prove you're not a robot")
answer = forms.IntegerField(widget=forms.HiddenInput())
num1 = forms.IntegerField(widget=forms.HiddenInput())
num2 = forms.IntegerField(widget=forms.HiddenInput())
def clean(self):
cleaned_data = self.cleaned_data
notarobot = str(cleaned_data.get("notarobot"))
answer = str(cleaned_data.get("answer"))
if notarobot!=answer:
raise forms.ValidationError(_("Whoops, try that sum again."))
return cleaned_data
sender = forms.EmailField(widget=forms.TextInput(attrs={'size':50}), label="Your email")
subject = forms.CharField(max_length=500, widget=forms.TextInput(attrs={'size':50}))
message = forms.CharField(widget=forms.Textarea())
page = forms.CharField(widget=forms.HiddenInput())
notarobot = forms.IntegerField(label="Please prove you're not a robot")
answer = forms.IntegerField(widget=forms.HiddenInput())
num1 = forms.IntegerField(widget=forms.HiddenInput())
num2 = forms.IntegerField(widget=forms.HiddenInput())
def clean(self):
cleaned_data = self.cleaned_data
notarobot = str(cleaned_data.get("notarobot"))
answer = str(cleaned_data.get("answer"))
if notarobot!=answer:
raise forms.ValidationError(_("Whoops, try that sum again."))
return cleaned_data

View File

@ -115,7 +115,7 @@ Welcome to the alpha version of Unglue.It. This site is a preview of our full f
<span>Contact</span>
<ul>
<li>General inquiries</li>
<li><a href="mailto:aqf@gluejar.com">aqf@gluejar.com</a></li>
<li><a href="mailto:faq@gluejar.com">faq@gluejar.com</a></li>
<li>Rights Holders</li>
<li><a href="mailto:rights@gluejar.com">rights@gluejar.com</a></li>
</ul>

View File

@ -2,6 +2,15 @@
{% block extra_js %}
<script type="text/javascript" src="/static/js/definitions.js"></script>
<script type="text/javascript">
var $j = jQuery.noConflict();
$j("#js-leftcol").ready(function() {
$j(".answer").hide();
$j(".faq").click(function() {
$j(this).next(".answer").toggle();
});
});
</script>
{% endblock %}
{% block extra_extra_head %}
<!-- extra head content in descendants goes in extra_extra_head, not extra_head, to avoid overwriting the documentation.css include -->
@ -16,7 +25,13 @@
<div id="main-container">
<div class="js-main">
<div id="js-leftcol">
{% include "faqmenu.html" %}
{% if faqmenu == "pledge" %}
{% include "faq_pledge.html" %}
{% else %}{% if faqmenu == "complete" %}
{% include "faq_pledge_complete.html" %}
{% else %}{% if faqmenu == "cancel" %}
{% include "faq_pledge_cancel.html" %}
{% endif %}{% endif %}{% endif %}
</div>
<div id="js-maincol-fr" class="have-right doc">
<div class="js-maincol-inner">

View File

@ -1,22 +1,22 @@
<div class="thewholebook listview tabs {% if status == 'SUCCESSFUL' or work.first_pdf_url or work.first_epub_url %}tabs-1{% else %}{% if status == 'ACTIVE' %}tabs-2{% else %}tabs-3{% endif %}{% endif %}">
<div class="thewholebook listview tabs {% if work.first_ebook %}tabs-1{% else %}{% if status == 'SUCCESSFUL' or status == 'ACTIVE' %}tabs-2{% else %}tabs-3{% endif %}{% endif %}">
<div class="listview book-list">
<div class="listview panelback side2">
<div class="greenpanel2">
<div class="greenpanel_top">
<div class="unglued_white">
<!-- top section: campaign info + optional action button. Varies by campaign status. -->
{% if work.first_pdf_url or work.first_epub_url %}
{% comment %}top section: campaign info + optional action button. Varies by campaign status.{% endcomment %}
{% if work.first_ebook %}
<b>AVAILABLE!</b>
</div>
<div class="read_itbutton">{% if work.first_epub_url %}<a href="{{ work.first_epub_url }}">{% else %}<a href="{{ work.first_pdf_url }}">{% endif %}Read it Now</a></div>
<div class="read_itbutton">{% if work.first_ebook %}<a href="{{ work.ebooks.0.url }}">{% endif %}Read it Now</a></div>
{% else %}{% if status == 'SUCCESSFUL' %}
<b>UNGLUED!</b>
<p><b>On:</b> {{ deadline|date:"M d, Y" }}</p>
<p><b>Raised:</b> {{ work.last_campaign.current_total }}</p>
</div>
<div class="read_itbutton">{% if work.first_epub_url %}<a href="{{ work.first_epub_url }}">{% else %}<a href="{{ work.first_pdf_url }}">{% endif %}Read it Now</a></div>
<div class="read_itbutton">{% if work.first_ebook %}<a href="{{ work.ebooks.0.url }}">{% endif %}Read it Now</a></div>
{% else %}{% if status == 'ACTIVE' %}
<b>UNGLUE IT!</b>
@ -49,7 +49,7 @@
{% endif %}{% endif %}{% endif %}{% endif %}{% endif %}{% endif %}{% endif %}
</div>
<!-- status of book vis-a-vis user's wishlist -->
{% comment %}status of book vis-a-vis user's wishlist{% endcomment %}
{% if request.user.id in work.last_campaign.supporters %}
<div class="moreinfo on-wishlist">
<span>Pledged!</span>
@ -73,13 +73,13 @@
</div>
{% endif %}{% endif %}{% endifequal %}{% endif %}
<!-- bibliographic data -->
{% comment %}bibliographic data{% endcomment %}
<div class="white_text">
<p><a href="{% if work.id %}{% url work work.id %}{% else %}{% url googlebooks work.googlebooks_id %}{% endif %}">{{ work.title }}</a></p>
<p>{{ work.author }}</p>
</div>
<!-- link to work page -->
{% comment %}link to work page{% endcomment %}
<div class="moreinfo">
<a href="{% if work.id %}{% url work work.id %}{% else %}{% url googlebooks work.googlebooks_id %}{% endif %}" target="_top">More Info</a>
</div>
@ -123,14 +123,11 @@
<div class="listview panelfront side1 icons">
{% if status == 'No campaign yet' or status == 'INITIALIZED' %}
<a href="{% url work work.id %}?tab=3" class="nobold"><span class="rounded"><span class="grey"><span class="panelnope">Wished by&nbsp;</span>{{ work.num_wishes }}</span></span></a>
{% else %}{% if work.first_pdf_url or work.first_epub_url %}
{% else %}{% if work.first_ebook %}
<span class="listview boolist-ebook">
{% if work.first_epub_url %}
<a href="{{ work.first_epub_url }}">EPUB</a>
{% endif %}
{% if work.first_pdf_url %}
<a href="{{ work.first_pdf_url }}">PDF</a>
{% endif %}
{% for ebook in work.ebooks|slice:":3" %}
<a href="{{ebook.url}}">{{ ebook.format }}</a>
{% endfor %}
</span>
{% else %}

View File

@ -8,6 +8,10 @@
<link type="text/css" rel="stylesheet" href="/static/css/book_panel.css" />
{% endblock %}
{% block extra_head %}
<script>
location.hash = "#2";
</script>
<script type="text/javascript" src="/static/js/wishlist.js"></script>
<script type="text/javascript" src={{ jquery_ui_home }}></script>
<script type="text/javascript" src="/static/js/greenpanel.js"></script>
@ -83,7 +87,8 @@
{% ifequal campaign_list.count 0 %}
There aren't any ungluing campaigns active yet. If you're a rights holder, you can <a href="/faq/rightsholders">start one</a>.
{% else %}
{% paginate 20 campaign_list %}
{% lazy_paginate 20 campaign_list using "campaign_list" %}
{% for campaign in campaign_list %}
<div class="{% cycle 'row1' 'row2' %}">
{% with campaign.status as status %}
@ -95,8 +100,11 @@
</div>
{% endfor %}
<br>
<div class="pagination content-block-heading">
{% show_pages %}
<div class="pagination content-block-heading tabs-1">
{% get_pages %}
{% for page in pages %}
<a href="{{ page.path }}#1">{{ page.number }}</a>
{% endfor %}
</div>
{% endifequal %}
{% endif %}

View File

@ -77,6 +77,11 @@ If you believe you are a rights holder and would like to discuss ungluing your w
<dt>Is there a widget that can be put on my own site to share my favorite books or campaigns?</dt>
<dd>Yes! Every book page has an "Embed" option. Click that and copy/paste the HTML code wherever you like.</dd>
{% comment %}
What is Open Access or Creative Commons versus PD?
TBA
{% endcomment %}
{% endif %}
{% if sublocation == 'account' or sublocation == 'all' %}
<h4>Your Account</h4>
@ -86,15 +91,15 @@ There's a <a href="/accounts/password/reset/">forgot your password</a> link at t
<dt>I never received a confirmation email after I signed up. What do I do?</dt>
<dd>TBA</dd>
<dd>Check your spam folder for mail from accounts@gluejar.com. If that doesn't work, contact us at <a href="mailto:support@gluejar.com">support@gluejar.com</a>.</dd>
<dt>How do I connect my Twitter, Facebook, GoodReads, and/or LibraryThing accounts? How can I link to my personal web site?</dt>
Click on "My Settings" near the top of your user profile page. (If you're logged in, your profile page is <a href="/">here</a>.) Some new options will appear. You can enter your personal URL and your LibraryThing username here. There are one-click options to connect your Twitter, Facebook, and GoodReads accounts. (You'll be asked to log in to those accounts, if you aren't logged in already.) Make sure to click on "Save Settings".
Click on "My Profile" near the top of your user profile page. (If you're logged in, your profile page is <a href="/">here</a>.) Some new options will appear. You can enter your personal URL and your LibraryThing username here. There are one-click options to connect your Twitter, Facebook, and GoodReads accounts. (You'll be asked to log in to those accounts, if you aren't logged in already.) Make sure to click on "Save Settings".
<dt>Why should I connect those accounts?</dt>
<dd>If you connect your Facebook or Twitter accounts, we'll use your user pics from those sites for your avatar here. If you connect LibraryThing or GoodReads, you can import your books there onto your wishlist here, using the My Settings area.</dd>
<dd>If you connect your Facebook or Twitter accounts, we'll use your user pics from those sites for your avatar here. If you connect LibraryThing or GoodReads, you can import your books there onto your wishlist here, using the My Profile area.</dd>
<dt>I don't want to connect my accounts. Do I have to?</dt>
@ -116,7 +121,7 @@ We have no functionality for these at this time. Should we?
{% endcomment %}
<dt>How do I unsubscribe from or adjust which emails I receive?</dt>
<dd>TBA<br /><br />
<dd>We're in the process of building out our notification system, including ways for you to control which notifications you receive, but we haven't finished that yet.<br /><br />
If you receive our newsletter, there's a link at the bottom of every message to manage your preferences. If you don't and would like to, you can <a href="http://eepurl.com/fKLfI">sign up here</a>.
</dd>
@ -157,12 +162,6 @@ These need to be put in proper order. Also this should be broken down into the
<dt>Why does it say a book's campaign has been suspended or withdrawn?</dt>
<dd>If there is a dispute between Gluejar and the book's rights holder, or if we discover evidence that a book's rights are in dispute after launching a campaign, it may be necessary for us to withdraw or suspend the campaign.</dd>
<dt>If I pledged to a suspended or withdrawn campaign, what happens to my pledge?</dt>
<dd>Your pledge will time out according to its original time limit. If the campaign is resolved and reactivated before your pledge has timed out, your pledge will become active again. If the campaign is not reactivated before your pledge's time limit, your pledge will expire and you will not be charged. As always, you will only be charged if a campaign is successful, within its original time limit.</dd>
<dt>What if I want to change or cancel a pledge?</dt>
{% endcomment %}
<dt>Who is eligible to start a campaign on Unglue.It?</dt>
<dd>To start a campaign, you need to be a verified rights holder who has signed a Platform Services Agreement with us. If you hold the electronic rights for one or more works, please contact <a href="mailto:rights@gluejar.com">rights@gluejar.com</a> to start the process.</dd>
@ -185,92 +184,49 @@ These need to be put in proper order. Also this should be broken down into the
If you want to find an interesting campaign and don't have a specific book in mind, see the Explore sidebar (on almost any page except the FAQs) for some of our favorites.</dd>
<h4>Campaign Pages</h4>
<dt>Can a campaign be edited after launching? (combine with changing funding goal/deadline?)</dt>
<dt>Can a campaign be edited after launching?</dt>
<dd>TBA</dd>
<dd>Yes. In fact, rights holders are encouraged to update their supporters about the progress of the campaign. (For an example of exceptionally successful updates, check out <a href="http://www.kickstarter.com/projects/599092525/the-order-of-the-stick-reprint-drive/posts">Rich Burlew's Kickstarter campaign</a>, which grossed over a million dollars, in part because of his frequent and creative communication and improvements to the rewards being offered.) Note that the campaign's deadline cannot be changed and its threshold may not be increased.</dd>
<dt>Can campaigns be edited after funding is completed?</dt>
<dd>TBA</dd>
<dd>Yes. Again, while the deadline and threshold cannot be changed, rights holders are encouraged to add thank-you messages for supporters and update them on the progress of ebook conversion (if needed).</dd>
<dt>What is a campaign page?</dt>
<dd>When a book has an active ungluing campaign, its book page turns into its campaign page. This means that, in addition to the standard book page information, it also features rewards for different pledge tiers; a Support button; and information supplied by the rights holder about the book and the campaign.</dd>
{% endif %}
{% if sublocation == 'funding' or sublocation == 'all' %}
<h4>Funding</h4>
<dt>Is a Paypal account required to launch a campaign?</dt>
<dd>TBA</dd>
{% if sublocation == 'supporting' or sublocation == 'all' %}
<h4>Supporting Campaigns</h4>
<dt>Are a funding goal and deadline required?</dt>
<dd>Yes.</dd>
<dt>How do I pledge? Do I need a PayPal account?</dt>
<dt>Can I change my funding goal?</dt>
<dd>TBA</dd>
<dd>There's a Support button on all campaign pages, or you can just click on the premium you'd like to receive. You'll be asked to select your premium and specify your pledge amount, and then you'll be taken to PayPal to complete the transaction. While you may certainly use your PayPal account if you have one, you do not need one; you can use your credit card normally.</dd>
<dt>Can I change my deadline?</dt>
<dd>TBA</dd>
<dt>When will I be charged?</dt>
<dt>Can funding be canceled?</dt>
<dd>In most cases, your account will be charged by the end of the next business day after the success of a campaign. If a campaign doesn't succeed, you won't be charged.</dd>
<dd>TBA</dd>
<dt>What if I want to change or cancel a pledge?</dt>
<dt>Can I offer tax deductions to people who pledge to my campaign?</dt>
<dd>You can modify your pledge by going to the book's page and clicking on the "Change Pledge" button. (The "Support" button you clicked on to make a pledge is replaced by the "Change Pledge" button after you pledge.)</dd>
<dd>TBA</dd>
<dt>What happens to my pledge if a campaign does not succeed?</dt>
<dt>Are contributions refundable?</dt>
<dd>TBA</dd>
<dt>How do I know when I have started fundraising?</dt>
<dd>TBA</dd>
<dt>How do I collect money for my campaign? (include registering & verifying info as needed)</dt>
<dd>TBA</dd>
<dt>Where can I check the status of my funds?</dt>
<dd>TBA</dd>
<dt>What happens if my campaign reaches its funding goal before its deadline? Can campaigns raise more money than their goal?</dt>
<dd>Campaigns close at midnight of the day when they hit their funding threshold. They may continue accruing funding up to that time, including funds over their goal.</dd>
<dd>Your credit card will only be charged when campaigns succeed. If they do not, your pledge will expire and you will not be charged.</dd>
<dt>Can people contribute anonymously?</dt>
<dd>We intend to implement this feature, but have not yet.</dd>
<dt>What happens if a supporter's credit card is declined?</dt>
<dt>If I pledged to a suspended or withdrawn campaign, what happens to my pledge?</dt>
<dd>TBA</dd>
<dd>Your pledge will time out according to its original time limit. If the campaign is resolved and reactivated before your pledge has timed out, your pledge will become active again. If the campaign is not reactivated before your pledge's time limit, your pledge will expire and you will not be charged. As always, you will only be charged if a campaign is successful, within its original time limit.</dd>
<dt>What fees does Unglue.It charge?</dt>
<dt>Is my contribution tax deductible?</dt>
<dd>Unglue.It charges a 6% commission on the gross amount raised. Rights holders may be liable for additional fees (e.g. Paypal, currency conversion, ebook conversion). Details are spelled out in the Platform Services Agreement that rights holders must sign before launching an Unglue.It campaign.</dd>
<dt>Does Paypal charge any fees?</dt>
<dd>TBA</dd>
<dt>Does it cost money to start a campaign on Unglue.It?</dt>
<dd>No.</dd>
<dt>I'm having problems verifying my account with Paypal. What do I need to do?</dt>
<dd>TBA</dd>
<dt>Will Paypal send me a 1099-K tax form?</dt>
<dd>TBA</dd>
<dt>What happens to my pledge if a campaign does not succeed?</dt>
<dd>Your credit card will only be charged when campaigns succeed. If they do not, your pledge will expire and you will not be charged.</dd>
<dd>If the rights holder for the work you're supporting is a nonprofit, please contact them directly to inquire. Unglue.It cannot offer tax deductions.</dd>
{% endif %}
{% if sublocation == 'premiums' or sublocation == 'all' %}
<h4>Premiums</h4>
@ -289,15 +245,7 @@ If you want to find an interesting campaign and don't have a specific book in mi
<li>$100 // Your name, link of your choice, and a brief message (140 characters max) under "bibliophiles"</li>
</ul>
Rights holders are encouraged to offer additional premiums to engage supporters' interest. Think about things that uniquely represent yourself or your work, that are appealing, and that you can deliver quickly and within your budget.</dd>
<dt>What can be offered as a premium? What items are prohibited as premiums?</dt>
<dd>TBA</dd>
<dt>Who is responsible for making sure Rights Holders deliver premiums?</dt>
<dd>The rights holder. (TBA: is this correct? do we need to say more?</dd>
Rights holders are encouraged to offer additional premiums to engage supporters' interest.</dd>
<dt>Who creates the premiums for each campaign?</dt>
@ -305,13 +253,8 @@ Rights holders are encouraged to offer additional premiums to engage supporters'
<dt>Is there a minimum or maximum for how much a premium can cost?</dt>
<dd>TBA</dd>
<dd>There's a $1 minimum and a $2000 maximum (this is the largest transaction we can authorize through PayPal).</dd>
<dt>How can I get my supporters' information (mailing address, T-shirt size, etc.) to fulfill premiums?</dt>
<dd>TBA</dd>
(TBA: questions about specifying estimated delivery dates/what the process timeline should be)
{% endif %}
{% endif %}
@ -321,7 +264,7 @@ Rights holders are encouraged to offer additional premiums to engage supporters'
<h4>General Questions</h4>
<dt>So what is an unglued ebook?</dt>
<dd>An unglued ebook is one that's been released under a <a href="http://creativecommons.org">Creative Commons</a> license, after obtaining permission and compensating the rights holder.<br /><br />
<dd>An unglued ebook is an already-published book that's been rereleased under a <a href="http://creativecommons.org">Creative Commons</a> license, after obtaining permission and compensating the 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 guaranteed up-front payment in lieu of royalties on this edition, while retaining copyright and all interests in other editions of your work.<br /><br />
@ -344,9 +287,13 @@ Unglued ebooks are a win-win solution for readers, libraries, and authors, publi
<dd>No. Unglued books can be anything in copyright, whether 75 years or 7 days old, whether they sell in paperback or hardcover and or can only be found in used bookstores -- or nowhere.</dd>
<dt>Does Gluejar help to fund self-published books?</dt>
<dt>Does Unglue.It help to fund self-published books?</dt>
<dd>No.</dd>{% comment %}should we provide pointers to other resources? encourage people to CC-license their own works?{% endcomment %}
<dd>In general, no. However, if you believe you represent a special case (for example, you own the house which has published your works), we're willing to be flexible. Drop us a line (<a href="mailto:rights@gluejar.com">rights@gluejar.com</a>).</dd>
<dt>Will Unglue.It help raise funds for books not yet written?</dt>
<dd>No. There are other crowdfunding sites devoted to new creative works, and we encourage you to investigate them. Once your book has been published, we'd love to talk to you.</dd>
{% endif %}
{% if sublocation == 'using' or sublocation == 'all' %}
@ -407,14 +354,10 @@ Unglue.It signs agreements concerning the copyright status of every work we ungl
{% endif %}
{% if sublocation == 'campaigns' or sublocation == 'all' %}
<h4>Launching Campaigns</h4>
<h4>Running Campaigns</h4>
<dt>What do I need to create a campaign?</dt>
<dd>TBA</dd>
<dt>How do I launch my first campaign?</dt>
<dd>TBA</dd>
<dd>First, you need to be an authorized rights holder with a signed Platform Services Agreement on file. Please contact <a href="mailto:rights@gluejar.com">rights@gluejar.com</a> to start this process. Once we have your PSA on file, you'll be able to claim your works and you'll have access to tools for launching and monitoring campaigns. We'll be happy to walk you through the process personally.</dd>
<dt>Can I Unglue only one of my books? Can I unglue all of them?</dt>
@ -426,13 +369,38 @@ Unglue.It signs agreements concerning the copyright status of every work we ungl
<dt>What should I include in my campaign page?</dt>
<dd>Show us why we should love your book. What makes it powerful, intriguing, moving, thought-provoking, important? How does it capture readers' imaginations, engage their minds, or connect to their emotions? Remind readers who loved the book why they loved it, and give people who haven't read it reasons they want to.<br /><br />
<dd>Show the world why we should love your book. What makes it powerful, intriguing, moving, thought-provoking, important? How does it capture readers' imaginations, engage their minds, or connect to their emotions? Remind readers who loved the book why they loved it, and give people who haven't read it reasons they want to.<br /><br />
We strongly encourage you to include video. You can upload it to YouTube and embed it here. We also strongly encourage links to help readers explore further -- authors' home pages, organizations which endorse the book, positive reviews, et cetera. Think about blurbs and awards which showcase your book. But don't just write a catalog entry or a laundry list: be relatable, and be concise.</dd>
<dt>What should I offer as premiums?</dt>
<dd>In addition to the required set of Unglue.It premiums, you're encouraged to sweeten the deal for your supporters by offering special premiums. Think about things that uniquely represent yourself or the work, that are appealing, and that you can deliver quickly and within your budget.</dd>
<dt>What can be offered as a premium? What items are prohibited as premiums?</dt>
<dd>TBA</dd>
<dt>Who is responsible for making sure a rights holder delivers premiums?</dt>
<dd>The rights holder.</dd>
<dt>How can I get my supporters' information (mailing address, T-shirt size, etc.) to fulfill premiums?</dt>
<dd>Unglue.It will share supporters' emails and the premiums they are owed with rights holders if, and only if, a campaign succeeds. If you need additional information to fulfill premiums, you may email ungluers directly. Rights holders are required to comply with the Unglue.It privacy policy.</dd>
<dt>What should I ask my supporters to do?</dt>
<dd>(TBA: support campaign and share the word)</dd>
<dd>Of course you'll want them to support your campaign financially, and make that pitch. But they also want to feel like they're part of the excitement of the campaign and the love for your book. Invite them to discuss the book in the Comments tab, to share the campaign via email and social media using the Share column links, and to embed the campaign widget in their blogs. And think about other ways you can get supporters involved, too: discussions on social reading sites? A Twitter hashtag? Uploading Flickr photos or YouTube videos of themselves with your book? (Prizes or acknowledgements for the person who photographs your book in the most unusual location?) Think of ways you can invite them to express and share their enthusiasm, and how you can reward them (not just monetarily) for doing so.</dd>
<dt>How can supporters contact me?</dt>
<dd>If you'd like supporters to contact you directly, please include (or link to) your contact information in your campaign page and profile. Make sure you're also monitoring the Comments tabs on your works' pages (whether or not they have active campaigns).</dd>
<dt>What is my responsibility for answering questions from supporters and non-supporters?</dt>
<dd>It's up to you. However, our experience watching and running crowdfunding campaigns suggests that you'll do best if you're engaged, prompt, and personable. In fact, don't just think in terms of answering questions (though you should absolutely do that) -- think about cultivating and contributing to the conversation.</dd>
{% endif %}
{% if sublocation == 'publicity' or sublocation == 'all' %}
<h4>Publicizing Campaigns</h4>
@ -441,11 +409,13 @@ We strongly encourage you to include video. You can upload it to YouTube and em
<dd>We're developing a social media toolkit for rights holders. Please tell us what you think should be in it so we can serve you better: <a href="mailto:andromeda@gluejar.com">andromeda@gluejar.com</a>. In the meantime we're happy to help you one-on-one, at the same address.</dd>
<dt>Can I contact my supporters directly?</dt>
<dd>TBA</dd>
<dd>We will not provide you contact information, except as needed to fulfill premiums after successful campaigns. Supporters may voluntarily disclose certain contact information (such as Twitter handles) on their profile pages. We encourage you to be thoughtful in your use of this information. Supporters have likely provided it as a way to connect with other like-minded people, not as a marketing tool, so please engage with it in that spirit.</dd>
<dt>How do I get feedback from supporters or fans about my campaign?</dt>
<dd>TBA</dd>
<dd>Ask them!<br /><br />
Also, pay attention to places they're talking about your campaign. This might be the Comments tab, a hashtag on Twitter, the comments section of your blog, a forum on your web site, your Facebook page, et cetera. Read these spaces regularly to see what people are saying, and to participate in the conversation.</dd>
<dt>How do I share my campaign?</dt>
@ -476,6 +446,69 @@ Need more ideas? We're happy to work with rights holders personally to craft a
<dt>Will Unglue.It raise money to help pay for conversion costs?</dt>
<dd>Yes. Your campaign target should include conversion costs.</dd>
{% endif %}
{% if sublocation == 'funding' or sublocation == 'all' %}
<h4>Funding</h4>
<dt>Is a PayPal account required to launch a campaign?</dt>
<dd>Unglue.It prefers that rights holders have PayPal accounts as it will simplify the process of paying you when a campaign succeeds. If you do not have a PayPal account, please address this with us before signing the Platform Services Agreement and we'll find a way to make sure you can be paid.</dd>
<dt>Are a funding goal and deadline required?</dt>
<dd>Yes.</dd>
<dt>Can I change my funding goal?</dt>
<dd>While a campaign is in progress you may lower, but not raise, your funding goal.</dd>
<dt>Can I change my deadline?</dt>
<dd>No.</dd>
<dt>Are contributions refundable?</dt>
<dd>Ungluers are free to modify or cancel their pledges before a campaign ends. At that point, their credit cards will be charged nonrefundably.</dd>
<dt>What if an ungluer contests a charge?</dt>
<dd>(TBA: link to PayPal, use the word "chargeback" somewhere) Ungluers may contest charges with their credit card issuers. Unglue.It is not liable for this and rights holders are liable for any chargebacks. We encourage rights holders to provide clear, timely communication to their supporters throughout the campaign to ensure they remember what they pledged to and why.</dd>
<dt>What happens if a supporter's credit card is declined?</dt>
<dd>Rights holders are liable for these costs.</dd>
<dt>Can I offer tax deductions to people who pledge to my campaign?</dt>
<dd>If you are a 501(c)3 or similar, please consult your own attorneys and accountants about the tax deductibility of ungluers' contributions. Unglue.It cannot offer tax deductions or advice.</dd>
<dt>How do I know when I have started fundraising?</dt>
<dd>Unglue.It will be in communication with you about when campaigns should go live. On your <a href="/rightsholders/">rights holder dashboard</a>, you will be able to see all works you have claimed and the status of any associated campaigns.</dd>
<dt>After a campaign succeeds, when and how will I be paid?</dt>
<dd>After we reach the threshold price Unglue.It will issue you a closing memo, which will cover your gross and net proceeds. Supporters' money will be put in escrow (through PayPal) until you deliver an ePub file meeting Unglue.It's quality standards, at which point payment will be released to you via your PayPal account. If Unglue.It does not receive a suitable ePub file within 90 days, we will deduct conversion costs from your funds and release the rest to you.</dd>
<dt>What happens if my campaign reaches its funding goal before its deadline? Can campaigns raise more money than their goal?</dt>
<dd>Campaigns close at midnight of the day when they hit their funding threshold. They may continue accruing funding up to that time, including funds over their goal.</dd>
<dt>What sorts of fees apply?</dt>
<dd>When a campaign succeeds, you will pay Unglue.It a 6% commission on the funds raised. PayPal also charges a small percentage in transaction fees (TBA:link). If you do not have a suitable ePub version of your book available, you will also need to cover conversion costs; please price this into your goal for a campaign.</dd>
<dt>What fees does Unglue.It charge?</dt>
<dd>When a campaign succeeds, you will pay Unglue.It a 6% commission on the funds raised. PayPal also charges a small percentage in transaction fees (TBA:link). If you do not have a suitable ePub version of your book available, you will also need to cover conversion costs; please price this into your goal for a campaign. Details are spelled out in the Platform Services Agreement that rights holders must sign before launching an Unglue.It campaign.</dd>
<dt>Does it cost money to start a campaign on Unglue.It?</dt>
<dd>No.</dd>
<dt>I'm having problems verifying my account with PayPal. What do I need to do?</dt>
<dd>TBA</dd>
<dt>Will PayPal send me a 1099-K tax form?</dt>
<dd>TBA</dd>
{% endif %}
{% if sublocation == 'rights' or sublocation == 'all' %}
<h4>Rights</h4>
@ -495,8 +528,6 @@ Need more ideas? We're happy to work with rights holders personally to craft a
<dd>Not necessarily. If you are the author and copyright holder but have a contract with a publisher, the publisher may be the authorized Rights Holder with respect to electronic rights, and they may be able to sign a licensing agreement on your behalf. Again, you must get advice about your individual contract, especially subsidiary rights clauses and exclusivity.</dd>
Questions about the Unglue.it license
<dt>Can I offer a book to be Unglued even if I cannot include all the illustrations from the print edition?</dt>
<dd>Yes. If permission to reprint cannot not be obtained for items such as photographs, drawings, color plates, as well as quotations from lyrics and poetry, we can produce an unglued edition which leaves them out. Please indicate the difference between the editions on your Campaign page.</dd>
@ -510,15 +541,6 @@ Questions about the Unglue.it license
<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 non-commercial limitations of the license.</dd>
{% endif %}
<dt>What is my responsibility for answering questions from supporters and non-supporters?</dt>
<dd>TBA</dd>
<dt>If I am unable to complete my campaign as listed, what should I do?</dt>
<dd>TBA</dd>
</dl>
{% endif %}

View File

@ -0,0 +1,43 @@
<div class="jsmodule">
<h3 class="jsmod-title"><span>Pledging FAQs</span></h3>
<div class="jsmod-content">
<ul class="menu level1">
<li class="first parent">
<span class="faq">How do I pledge?</span>
<span class="menu level2 answer">
Enter your pledge amount and select a premium. (You may select a premium at any level up to and including the amount you pledge.) After you click Pledge, you'll be directed through PayPal to complete the transaction.
</span>
</li>
<li class="parent">
<span class="faq">What happens after I pledge?</span>
<span class="menu level2 answer">
TBA
</span>
</li>
<li class="parent">
<span class="faq">When will I be charged?</span>
<span class="menu level2 answer">
If this campaign succeeds, you'll be charged at TBA. If it does not, your pledge will expire on {{ campaign.deadline }} and you will not be charged.
</span>
</li>
<li class="parent">
<span class="faq">What if I want to change my pledge?</span>
<span class="menu level2 answer">
You can change or cancel your pledge at any time before the campaign ends. This will be the campaign's deadline ({{ campaign.deadline }}) or midnight (Eastern US time) on the day the campaign succeeds, whichever comes first.
</span>
</li>
<li class="last parent">
<span class="faq">How and when will I receive my premiums?</span>
<span class="menu level2 answer">
TBA
</span>
</li>
</ul>
</div>
</div>

View File

@ -0,0 +1,67 @@
<div class="jsmodule">
<h3 class="jsmod-title"><span>FAQs</span></h3>
<div class="jsmod-content">
<ul class="menu level1">
{% comment %}
What happens after I pledge?/What kinds of communications should I expect?
privacy foo
how do I pledge?
when will I be charged?
("if this project is successfully funded, your credit card will be charged on {timestamp}" --gotta ask raymond about that)
what if I want to change my pledge?
("you can change or cancel any time before {timeline}")
how and when will I receive them
("when your reward is ready, {project creators} will send you a survey via email to request any info needed...")
{% endcomment %}
<li class="first parent">
<a href="#"><span>How do I pledge?</span></a>
<ul class="menu level2">
<li class="first">Enter your pledge amount and select a premium. (You may select a premium at any level up to and including the amount you pledge.)
</li>
</ul>
</li>
<li class="parent {% if location != 'basics' %}collapse{% endif %}">
<a href="/faq/basics/"><span>Basics</span></a>
<ul class="menu level2">
<li class="first"><a href="/faq/basics/howitworks"><span>How it Works</span></a></li>
<li><a href="/faq/basics/account"><span>Your Account</span></a></li>
<li class="last"><a href="/faq/basics/company/"><span>The Company</span></a></li>
</ul>
</li>
<li class="parent {% if location != 'campaigns' %}collapse{% endif %}">
<a href="/faq/campaigns/"><span>Campaigns</span></a>
<ul class="menu level2">
<li class="first"><a href="/faq/campaigns/overview"><span>Overview</span></a></li>
<li><a href="/faq/campaigns/funding"><span>Funding</span></a></li>
<li class="last"><a href="/faq/campaigns/premiums"><span>Premiums</span></a></li>
</ul>
</li>
<li class="parent {% if location != 'unglued_ebooks' %}collapse{% endif %}">
<a href="/faq/unglued_ebooks/"><span>Unglued Ebooks</span></a>
<ul class="menu level2">
<li class="first"><a href="/faq/unglued_ebooks/general"><span>General Questions</span></a></li>
<li><a href="/faq/unglued_ebooks/using"><span>Using Your Unglued Ebook</span></a></li>
<li class="last"><a href="/faq/unglued_ebooks/copyright"><span>Ungluing and Copyright</span></a></li>
</ul>
</li>
<li class="parent {% if location != 'rightsholders' %}collapse{% endif %}">
<a href="/faq/rightsholders/"><span>For Rights Holders</span></a>
<ul class="menu level2">
<li class="first"><a href="/faq/rightsholders/authorization"><span>Becoming Authorized</span></a></li>
<li><a href="/faq/rightsholders/campaigns"><span>Launching Campaigns</span></a></li>
<li><a href="/faq/rightsholders/publicity"><span>Publicizing Campaigns</span></a></li>
<li><a href="/faq/rightsholders/conversion"><span>Ebook Conversion</span></a></li>
<li class="last"><a href="/faq/rightsholders/rights/"><span>Rights</span></a></li>
</ul>
</li>
</ul>
</div>
</div>

View File

@ -0,0 +1,29 @@
<div class="jsmodule">
<h3 class="jsmod-title"><span>FAQs</span></h3>
<div class="jsmod-content">
<ul class="menu level1">
<li class="first parent">
<span class="faq">How will I know if the campaign succeeds?</span>
<span class="menu level2 answer">
We'll email you (from accounts@gluejar.com). You can also keep track of the campaign's progress from your <a href="{% url supporter user.username %}">supporter page</a> or from the campaign page for <a href="{% url work work.id %}">{{ work.title }}</a>.
</span>
</li>
<li class="parent">
<span class="faq">What kind of communications should I expect?</span>
<span class="menu level2 answer">
TBA
</span>
</li>
<li class="last parent">
<span class="faq">How and when will I receive my premiums?</span>
<span class="menu level2 answer">
TBA
</span>
</li>
</ul>
</div>
</div>

View File

@ -2,8 +2,12 @@
<h3 class="jsmod-title"><span>FAQs</span></h3>
<div class="jsmod-content">
<ul class="menu level1">
<li class="first parent">
<a href="/faq/"><span>All</span></a>
</li>
<li class="first parent {% if location != 'basics' %}collapse{% endif %}">
<li class="parent {% if location != 'basics' %}collapse{% endif %}">
<a href="/faq/basics/"><span>Basics</span></a>
<ul class="menu level2">
<li class="first"><a href="/faq/basics/howitworks"><span>How it Works</span></a></li>
@ -16,7 +20,7 @@
<a href="/faq/campaigns/"><span>Campaigns</span></a>
<ul class="menu level2">
<li class="first"><a href="/faq/campaigns/overview"><span>Overview</span></a></li>
<li><a href="/faq/campaigns/funding"><span>Funding</span></a></li>
<li><a href="/faq/campaigns/supporting"><span>Supporting Campaigns</span></a></li>
<li class="last"><a href="/faq/campaigns/premiums"><span>Premiums</span></a></li>
</ul>
</li>
@ -34,8 +38,9 @@
<a href="/faq/rightsholders/"><span>For Rights Holders</span></a>
<ul class="menu level2">
<li class="first"><a href="/faq/rightsholders/authorization"><span>Becoming Authorized</span></a></li>
<li><a href="/faq/rightsholders/campaigns"><span>Launching Campaigns</span></a></li>
<li><a href="/faq/rightsholders/campaigns"><span>Running Campaigns</span></a></li>
<li><a href="/faq/rightsholders/publicity"><span>Publicizing Campaigns</span></a></li>
<li><a href="/faq/rightsholders/funding"><span>Funding</span></a></li>
<li><a href="/faq/rightsholders/conversion"><span>Ebook Conversion</span></a></li>
<li class="last"><a href="/faq/rightsholders/rights/"><span>Rights</span></a></li>
</ul>

View File

@ -15,7 +15,7 @@
<div>You <a href="{{try_again_url}}">can finish the pledge transaction</a>.</div>
{% else %}
<div>What transaction are you talking about?</div>
<div>We're sorry; we can't figure out which transaction you're talking about. If you meant to cancel a pledge, please go to the book's page and hit the Modify link.</div>
{% endif %}
{% endblock %}

View File

@ -6,14 +6,31 @@
<link type="text/css" rel="stylesheet" href="/static/css/campaign.css" />
{% endblock %}
{% comment %}
this page needs to be laid out/styled
we need the share options and also something like the home page slide show to give people entry points back into the content
{% endcomment %}
{% block doccontent %}
<div class="thank-you">Thank you!</div>
<div>You just pledged ${{transaction.amount}} to <a href="{% url work work.id %}">{{work.title}}</a>.</div>
<div>If the campaign, which is slated to end at {{campaign.deadline}} reaches its target of ${{campaign.target}},
your PayPal account will be charged soon after the deadline.</div>
<div>If the campaign reaches its target of ${{campaign.target}} by {{campaign.deadline}},
your PayPal account will be charged.</div>
<div>Tell your friends about this campaign!</div>
<div class="jsmodule">
<h3 class="jsmod-title"><span>Share</span></h3>
<div class="jsmod-content">
<ul class="social menu">
<a href="https://www.facebook.com/sharer.php?u={{request.build_absolute_uri|urlencode:"" }}"><li class="facebook first"><span>Facebook</span></li></a>
<a href="https://twitter.com/intent/tweet?url={{request.build_absolute_uri|urlencode:"" }}&text=I'm%20ungluing%20{{ work.title|urlencode }}%20at%20%40unglueit"><li class="twitter"><span>Twitter</span></li></a>
{% if request.user.is_authenticated %}<a href="{% url emailshare %}?next={{request.build_absolute_uri|urlencode:""}}"><li class="email"><span>Email</span></li></a>{% endif %}
<a href="#" id="embed"><li class="embed"><span>Embed</span></li></a>
<div id="widgetcode">Copy/paste this into your site:<br /><textarea rows="7" cols="22">&lt;iframe src="https://{{request.META.HTTP_HOST}}/api/widget/{{work.first_isbn_13}}/" width="152" height="325" frameborder="0"&gt;&lt;/iframe&gt;</textarea></div>
</ul>
</div>
</div>
{% endblock %}

View File

@ -29,6 +29,11 @@
<a name="latest"></a><h2>Latest Press</h2>
<div class="pressarticles">
<div>
<a href="http://americanlibrariesmagazine.org/solutions-and-services/unglueit">Solutions and Services: Unglue.It</a><br />
American Libraries - February 14, 2012
</div>
<div class="pressarticles">
<div>
<a href="http://www.teleread.com/copy-right/web-site-hopes-to-unglue-e-book-versions-of-copyrighted-books-thorugh-crowdfunding/">Web site hopes to unglue e-book versions of copyrighted books through crowdfunding</a><br />
TeleRead - January 31, 2012
@ -37,10 +42,6 @@
<a href="http://paidcontent.org/article/419-the-unglued-model-crowdfunding-to-make-e-books-free/">A Crowdfunded Approach To Setting E-Books Free</a><br />
paidContent - January 30, 2012
</div>
<div>
<a href="http://newsbreaks.infotoday.com/NewsBreaks/New-Crowdfunding-Initiative-to-Unglue-Ebooks-Launches-in-Alpha-80293.asp">New Crowdfunding Initiative to Unglue Ebooks Launches in Alpha</a><br />
Information Today - January 30, 2012
</div>
</div>
<a name="overview"></a><h2>Overview</h2>
@ -68,6 +69,10 @@ For more background, read our president Eric Hellman's thoughts on <a href="http
<dd>Please consult our <a href="/faq/">FAQ</a> (sidebar at left); join the site and explore its features for yourself; or email us, <a href="press@gluejar.com">press@gluejar.com</a>.</dd>
<a name="press"></a><h2>Press Coverage</h2>
<div>
<a href="http://americanlibrariesmagazine.org/solutions-and-services/unglueit">Solutions and Services: Unglue.It</a><br />
American Libraries - February 14, 2012
</div>
<div class="pressarticles">
<div>
<a href="http://www.teleread.com/copy-right/web-site-hopes-to-unglue-e-book-versions-of-copyrighted-books-thorugh-crowdfunding/">Web site hopes to unglue e-book versions of copyrighted books through crowdfunding</a><br />
@ -81,6 +86,10 @@ For more background, read our president Eric Hellman's thoughts on <a href="http
<a href="http://newsbreaks.infotoday.com/NewsBreaks/New-Crowdfunding-Initiative-to-Unglue-Ebooks-Launches-in-Alpha-80293.asp">New Crowdfunding Initiative to Unglue Ebooks Launches in Alpha</a><br />
Information Today - January 30, 2012
</div>
<div>
<a href="http://www.cnetfrance.fr/blog/unglue-quand-les-internautes-financent-la-gratuite-des-ebooks-39767951.htm">UnGlue : quand les internautes financent la gratuité des ebooks</a> <I>(French)</I><br />
c|net France - January 29, 2012
</div>
<div>
<a href="http://publishingperspectives.com/2012/01/unglue-it-crowdfunds-unlimited-licenses-for-beloved-e-books/">Unglue.it Crowdfunds Unlimited Licenses for Beloved E-books</a><br />
Publishing Perspectives - January 23, 2012
@ -113,6 +122,22 @@ For more background, read our president Eric Hellman's thoughts on <a href="http
<a name="blogs"></a><h2>Blog Coverage (Highlights)</h2>
<div class="pressarticles">
<div>
<a href="http://plan3t.info/2012/02/08/unglue-it-was-ware-wenn/">Unglue.it: Was wäre, wenn…</a> <I>(German)</I><br />
Plan3t Info - February 8, 2012
</div>
<div>
<a href="http://chronicle.com/blogs/profhacker/unglue-a-book-crowdfunding-to-liberate-published-works/38096">Unglue a Book: Crowdfunding to Liberate Published Works</a><br />
Profhacker (Chronicle of Higher Education) - February 7, 2012
</div>
<div>
<a href="http://www.apogeonline.com/webzine/2012/02/06/anche-con-il-crowfunding-sembra-sempre-editoria">Anche con il crowfunding sembra sempre editoria</a> <I>(Italian)</I><br />
Apoge Online - February 6, 2012
</div>
<div>
<a href="http://www.ereaders.nl/article.php?article_id=100623">Unglue.It Maakt Ebooks Gratis Door Crowdfunding</a> <I>(Dutch)</I><br />
eReaders - February 3, 2012
</div>
<div>
<a href="http://pluggedinlibrarian.blogspot.com/2011/11/ebooks-brief-fix-on-moving-target.html">Ebooks: A Brief Fix On a Moving Target</a><br />
The Plugged-In Librarian - November 1, 2011

View File

@ -93,4 +93,3 @@ $j(document).ready(function() {
</div>
{% endblock %}
{% block counter %}{% endblock %}

View File

@ -20,13 +20,28 @@
<script type="text/javascript" src="/static/js/counter.js"></script>
{% endblock %}
{% block extra_js %}
<script>
var $j = jQuery.noConflict();
$j(document).ready(function(){
var highlighter = $j('#loadlt');
var target = $j('#connectlt');
highlighter.click(function(){
target.css({"background": "#8dc63f"}).animate(
{backgroundColor: "white"}, 1500
);
});
});
</script>
{% endblock %}
{% comment %}
To do:
create topsection file for inclusion in multiple contexts, if needed
figure out how to configure date display
decide if we're even including date joined in profile
Goodreads
be sure words display correctly
be sure words display correctlydja
do I need both add-wishlist and remove-wishlist classes? do they differ?
better alignment on I am ungluing & badges
@ -155,14 +170,14 @@ how do I integrate the your wishlist thing with the tabs thing?
<a href="{% url socialauth_associate_begin backend='facebook' %}">Connect your Facebook account</a> to Unglue.it
{% endif %}
</div>
<div class="check-list">
<div class="check-list" id="connectgr">
{% if user.profile.goodreads_user_id %}
<a href="{{goodreads_auth_url}}">Update your GoodReads connection</a> <br/> or disconnect GoodReads: {{ profile_form.clear_goodreads }}
{% else %}
<a href="{{goodreads_auth_url}}">Connect your GoodReads account</a> to Unglue.it
{% endif %}
</div>
<div class="check-list">
<div class="check-list" id="connectlt">
<label>{% if supporter.profile.librarything_id %}Change{% else %}Add{% endif %} your LibraryThing User ID:</label>
{{ profile_form.librarything_id }}{{ profile_form.librarything_id.errors }}
</div>
@ -221,7 +236,7 @@ how do I integrate the your wishlist thing with the tabs thing?
<ul class="tabs">
<li class="tabs1"><a href="#">Unglued</a></li>
<li class="tabs2"><a href="#">Active</a></li>
<li class="tabs3 active"><a href="#">Wishlisted</a></li>
<li class="tabs3"><a href="#">Wishlisted</a></li>
</ul>
<ul class="book-list-view">
@ -240,19 +255,20 @@ how do I integrate the your wishlist thing with the tabs thing?
</div>
<div id="content-block-content">
{% ifequal wishlist.works.all.count 0 %}
{% ifequal request.user supporter %}
<div class="empty-wishlist">
Your wishlist is currently empty.<br /><br />
Why not <span class="bounce-search">find</span> your favorite books, and add them to your Wishlist?<br /><br />
We'd also love to hear your <a href="/feedback">feedback</a>.
</div>
{% ifequal request.user supporter %}
<div class="empty-wishlist">
Your wishlist is currently empty.<br /><br />
Why not <span class="bounce-search">find</span> your favorite books, and add them to your Wishlist?<br /><br />
We'd also love to hear your <a href="/feedback">feedback</a>.
</div>
{% else %}
<div class="empty-wishlist">
It looks like {{ supporter.username }} is just getting started, and hasn't added books just yet.<br /><br />
{% endifequal %}
{% else %}
<div class="empty-wishlist">
It looks like {{ supporter.username }} is just getting started, and hasn't added books just yet.<br /><br />
{% endifequal %}
{% else %}
{% lazy_paginate 20 works %}
{% for work in works %}
{% lazy_paginate 20 works_unglued using "works_unglued" %}
{% for work in works_unglued %}
<div class="{% cycle 'row1' 'row2' %}">
{% with work.last_campaign_status as status %}
{% with work.last_campaign.deadline as deadline %}
@ -262,9 +278,49 @@ how do I integrate the your wishlist thing with the tabs thing?
</div>
{% endfor %}
<br>
<div class="pagination content-block-heading">
{% show_more %}
<div class="pagination content-block-heading tabs-1">
{% get_pages %}
{% for page in pages %}
<a href="{{ page.path }}#1" class="endless_page_link">{{ page.number }}</a>
{% endfor %}
</div>
{% lazy_paginate 20 works_active using "works_active" %}
{% for work in works_active %}
<div class="{% cycle 'row1' 'row2' %}">
{% with work.last_campaign_status as status %}
{% with work.last_campaign.deadline as deadline %}
{% with work.googlebooks_id as googlebooks_id %}
{% include "book_panel.html" %}
{% endwith %}{% endwith %}{% endwith %}
</div>
{% endfor %}
<br>
<div class="pagination content-block-heading tabs-2">
{% get_pages %}
{% for page in pages %}
<a href="{{ page.path }}#2" class="endless_page_link">{{ page.number }}</a>
{% endfor %}
</div>
{% lazy_paginate 20 works_wished using "works_wished" %}
{% for work in works_wished %}
<div class="{% cycle 'row1' 'row2' %}">
{% with work.last_campaign_status as status %}
{% with work.last_campaign.deadline as deadline %}
{% with work.googlebooks_id as googlebooks_id %}
{% include "book_panel.html" %}
{% endwith %}{% endwith %}{% endwith %}
</div>
{% endfor %}
<br>
<div class="pagination content-block-heading tabs-3">
{% get_pages %}
{% for page in pages %}
<a href="{{ page.path }}#3" class="endless_page_link">{{ page.number }}</a>
{% endfor %}
</div>
{% endifequal %}
</div>
</div>

View File

@ -8,8 +8,11 @@
<link type="text/css" rel="stylesheet" href="/static/css/book_panel.css" />
{% endblock %}
{% block extra_head %}
<script>
location.hash = "#1";
</script>
<script type="text/javascript" src="/static/js/wishlist.js"></script>
=<script type="text/javascript" src={{ jquery_ui_home }}></script>
<script type="text/javascript" src={{ jquery_ui_home }}></script>
<script type="text/javascript" src="/static/js/greenpanel.js"></script>
<script type="text/javascript" src="/static/js/toggle.js"></script>
<script type="text/javascript" src="/static/js/tabs.js"></script>
@ -51,7 +54,7 @@
<div id="js-maincol-fr">
<div class="js-maincol-inner">
<div id="content-block">
<div class="content-block-heading ungluing" id="tabs">
<div class="content-block-heading unglued" id="tabs">
<ul class="tabs">
<li class="tabs1"><a href="#">Unglued</a></li>
</ul>
@ -77,7 +80,7 @@
{% ifequal work_list.count 0 %}
There aren't any works in this list. Why don't you add some?
{% else %}
{% paginate 20 work_list %}
{% lazy_paginate 20 work_list using "work_list" %}
{% for work in work_list %}
<div class="{% cycle 'row1' 'row2' %}">
{% with work.last_campaign_status as status %}
@ -88,8 +91,11 @@
</div>
{% endfor %}
<br>
<div class="pagination content-block-heading">
{% show_pages %}
<div class="pagination content-block-heading tabs-1">
{% get_pages %}
{% for page in pages %}
<a href="{{ page.path }}#1">{{ page.number }}</a>
{% endfor %}
</div>
{% endifequal %}
</div>

View File

@ -19,7 +19,14 @@ jQuery(document).ready(function(){
jQuery('#embed').click(function(){
jQuery('div#widgetcode').toggle();
});
jQuery('.show_more_edition').click(function(){
jQuery(this).html('less...');
jQuery(this).next().toggle();
});
});
</script>
{% endblock %}
@ -31,7 +38,7 @@ jQuery(document).ready(function(){
<div class="jsmodule rounded">
<div class="jsmod-content{% if status == 'ACTIVE' %} active{{ work.percent_unglued}}{% endif %}">
<span>
{% if work.first_epub_url or work.first_pdf_url %}
{% if work.first_ebook %}
AVAILABLE!
{% else %}{% if work.last_campaign %}
{% if status == 'ACTIVE' %}
@ -70,7 +77,7 @@ jQuery(document).ready(function(){
<div class="js-maincol-inner">
<div id="content-block">
<div class="book-detail">
<div class="book-detail-img"><a href="#">
<div class="book-detail-img"><a href="{% url work_openlibrary work.id %}">
<img src="{{ work.cover_image_thumbnail }}" alt="{{ work.title }}" title="{{ work.title }}" width="131" height="192" /></a>
</div>
<div class="book-detail-info">
@ -106,7 +113,7 @@ jQuery(document).ready(function(){
</div>
</div>
<div class="pledged-info"><div class="pledged-group">
{% if work.first_epub_url or work.first_pdf_url %}
{% if work.first_ebook %}
{% if wishers == 1 %}
1 Ungluer is
{% else %}
@ -127,19 +134,9 @@ jQuery(document).ready(function(){
{% endif %} wished for this Work
{% endif %}{% endif %}
</div>
<div class="status"><img src="/static/images/images/icon-book-37by25-{% if work.first_epub_url or work.first_pdf_url %}6{%else%}{{ work.percent_unglued }}{%endif%}.png" /></div>
<div class="status"><img src="/static/images/images/icon-book-37by25-{% if work.first_ebook %}6{%else%}{{ work.percent_unglued }}{%endif%}.png" /></div>
</div>
<div class="btn_wishlist" id="wishlist_actions">
{% if work.first_epub_url or work.first_pdf_url %}
<span class="boolist-ebook">
{% if work.first_epub_url %}
<a href="{{ work.first_epub_url }}">EPUB</a>
{% endif %}
{% if work.first_pdf_url %}
<a href="{{ work.first_pdf_url }}">PDF</a>
{% endif %}
</span>
{% endif %}
{% if request.user.is_anonymous %}
<div class="create-account">
<span title="{% url work work.id %}">Login to Add</span>
@ -154,9 +151,16 @@ jQuery(document).ready(function(){
</div>
{% else %}
<div class="add-wishlist">
<span id="{{ work.googlebooks_id }}">Add to {% if work.first_epub_url or work.first_pdf_url %}Enjoying{% else %}Wishlist{% endif %}</span>
<span id="{{ work.googlebooks_id }}">Add to Wishlist</span>
</div>
{% endif %}{% endif %}{% endif %}
{% if work.first_ebook %}
<span class="boolist-ebook">
{% for ebook in work.ebooks %}
<a href="{{ ebook.url }}">{{ ebook.format }}</a> {{ebook.rights}} at {{ebook.provider}}<br />
{% endfor %}
</span>
{% endif %}
</div>
</div>
</div>
@ -224,7 +228,7 @@ jQuery(document).ready(function(){
<h4>Last campaign details</h4>
{{ work.last_campaign.details|safe }}
{% endif %}
<h4> Rights Information </h4>
{% if work.claim.count %}
<p> This work has been claimed by:</p>
@ -271,10 +275,11 @@ jQuery(document).ready(function(){
{% endif %}
<h4>Editions</h4>
{% if alert %}<div class="alert"><b>Ebook Contribution:</b><br />{{ alert }}</div>{% endif %}
{% for edition in editions %}
<div class="editions"><div class="image"><img src="http://bks{% cycle '1' '2' '3' '4' '5' '6' '7' '8' '9' %}.books.google.com/books?id={{ edition.googlebooks_id }}&printsec=frontcover&img=1&zoom=5" /></div>
<div class="metadata">{{edition.publisher}}<br />
{{edition.publication_date}}<br />
<div class="metadata" id="edition_{{edition.id}}">Publisher: {{edition.publisher}}<br />
Published: {{edition.publication_date}}<br />
{% with edition.isbn_13 as isbn %}
{% if isbn %}
ISBN: {{ isbn }}<br />
@ -284,6 +289,26 @@ jQuery(document).ready(function(){
{% endwith %}
See <a href="http://bks{% cycle '1' '2' '3' '4' '5' '6' '7' '8' '9' %}.books.google.com/books?id={{ edition.googlebooks_id }}">this edition on Google Books</a></div>
</div>
{% if edition.ebook_form %}
<div class="show_more_edition" >more...</div>
<div class="more_edition">
{% if edition.ebooks %}
<h5>eBooks for this Edition</h5>
{% for ebook in edition.ebooks.all %}
<a href="{{ebook.url}}">{{ ebook.format }}</a> {{ebook.rights}} at {{ebook.provider}}<br />
{% endfor %}
{% endif %}
<h5>Add an eBook for this Edition</h5>
<span>If you know that this edition is available as a public domain or Creative Commons ebook, you can enter the link here and "unglue" it. Right now, we're only accepting URLs that point to Internet Archive, Wikisources, Hathitrust, Project Gutenberg, or Google Books.</span>
<form method="POST" action="#edition_{{edition.id}}">
{% csrf_token %}{{ edition.ebook_form.edition }}{{ edition.ebook_form.user }}{{ edition.ebook_form.provider }}
URL: {{ edition.ebook_form.url }}<br />
File Format: {{ edition.ebook_form.format }}&nbsp;&nbsp;&nbsp;
License: {{ edition.ebook_form.rights }}<br />
<input type="submit" name="add_ebook" value="add ebook" id="submit" />
</form>
</div>
{% endif %}
{% endfor %}
</div>
</div>

View File

@ -52,10 +52,10 @@
<div id="js-maincol-fr">
<div class="js-maincol-inner">
<div id="content-block">
<div class="content-block-heading ungluing" id="tabs">
<div class="content-block-heading wantto" id="tabs">
<ul class="tabs">
<li class="tabs1"><a href="#">Unglued</a></li>
<li class="tabs2 active"><a href="#">Active</a></li>
<li class="tabs2"><a href="#">Active</a></li>
<li class="tabs3"><a href="#">Unglue It!</a></li>
</ul>
<div class="badges listspage">
@ -82,20 +82,59 @@
{% ifequal work_list.count 0 %}
There aren't any works in this list yet. Why not add your favorite books to your wishlist, so we can feature them here?
{% else %}
{% paginate 20 work_list %}
{% for work in work_list %}
<div class="{% cycle 'row1' 'row2' %}">
{% with work.last_campaign_status as status %}
{% with work.last_campaign.deadline as deadline %}
{% with work.googlebooks_id as googlebooks_id %}
{% include "book_panel.html" %}
{% endwith %}{% endwith %}{% endwith %}
</div>
{% endfor %}
<br>
<div class="pagination content-block-heading">
{% show_pages %}
</div>
{% lazy_paginate 20 works_unglued using "works_unglued" %}
{% for work in works_unglued %}
<div class="{% cycle 'row1' 'row2' %}">
{% with work.last_campaign_status as status %}
{% with work.last_campaign.deadline as deadline %}
{% with work.googlebooks_id as googlebooks_id %}
{% include "book_panel.html" %}
{% endwith %}{% endwith %}{% endwith %}
</div>
{% endfor %}
<br>
<div class="pagination content-block-heading tabs-1">
{% get_pages %}
{% for page in pages %}
<a href="{{ page.path }}#1" class="endless_page_link">{{ page.number }}</a>
{% endfor %}
</div>
{% lazy_paginate 20 works_active using "works_active" %}
{% for work in works_active %}
<div class="{% cycle 'row1' 'row2' %}">
{% with work.last_campaign_status as status %}
{% with work.last_campaign.deadline as deadline %}
{% with work.googlebooks_id as googlebooks_id %}
{% include "book_panel.html" %}
{% endwith %}{% endwith %}{% endwith %}
</div>
{% endfor %}
<br>
<div class="pagination content-block-heading tabs-2">
{% get_pages %}
{% for page in pages %}
<a href="{{ page.path }}#2" class="endless_page_link">{{ page.number }}</a>
{% endfor %}
</div>
{% lazy_paginate 20 works_wished using "works_wished" %}
{% for work in works_wished %}
<div class="{% cycle 'row1' 'row2' %}">
{% with work.last_campaign_status as status %}
{% with work.last_campaign.deadline as deadline %}
{% with work.googlebooks_id as googlebooks_id %}
{% include "book_panel.html" %}
{% endwith %}{% endwith %}{% endwith %}
</div>
{% endfor %}
<br>
<div class="pagination content-block-heading tabs-3">
{% get_pages %}
{% for page in pages %}
<a href="{{ page.path }}#3" class="endless_page_link">{{ page.number }}</a>
{% endfor %}
</div>
{% endifequal %}
</div>
</div>

View File

@ -1,7 +1,6 @@
import re
import sys
import json
import urllib
import logging
import datetime
from random import randint
@ -13,14 +12,15 @@ from xml.etree import ElementTree as ET
import requests
import oauth2 as oauth
from django import forms
from django.db.models import Q, Count, Sum
from django.conf import settings
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.core.exceptions import ObjectDoesNotExist
from django.core.mail import send_mail
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.comments import Comment
from django.db.models import Q, Count, Sum
from django.forms import Select
from django.forms.models import modelformset_factory
from django.http import HttpResponseRedirect, Http404
@ -30,10 +30,9 @@ from django.views.decorators.http import require_POST
from django.views.generic.edit import FormView
from django.views.generic.list import ListView
from django.views.generic.base import TemplateView
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, render_to_response, get_object_or_404
from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _
from regluit.core import tasks
from regluit.core import models, bookloader, librarything
from regluit.core import userlists
@ -42,6 +41,7 @@ from regluit.core.goodreads import GoodreadsClient
from regluit.frontend.forms import UserData, ProfileForm, CampaignPledgeForm, GoodreadsShelfLoadingForm
from regluit.frontend.forms import RightsHolderForm, UserClaimForm, LibraryThingForm, OpenCampaignForm
from regluit.frontend.forms import ManageCampaignForm, DonateForm, CampaignAdminForm, EmailShareForm, FeedbackForm
from regluit.frontend.forms import EbookForm
from regluit.payment.manager import PaymentManager
from regluit.payment.models import Transaction
from regluit.payment.parameters import TARGET_TYPE_CAMPAIGN, TARGET_TYPE_DONATION, PAYMENT_TYPE_AUTHORIZATION
@ -82,6 +82,7 @@ def home(request):
if j == count:
j = 0
events = models.Wishes.objects.order_by('-created')[0:2]
activetab = "2"
return render(request, 'home.html', {'suppress_search_box': True, 'works': works, 'works2': works2, 'events': events})
def stub(request):
@ -90,13 +91,29 @@ def stub(request):
def work(request, work_id, action='display'):
try:
work = models.Work.objects.get(id = work_id)
work = models.Work.objects.get(id = work_id)
except models.Work.DoesNotExist:
try:
work = models.WasWork.objects.get(was = work_id).work
except models.WasWork.DoesNotExist:
raise Http404
try:
work = models.WasWork.objects.get(was = work_id).work
except models.WasWork.DoesNotExist:
raise Http404
if request.method == 'POST' and not request.user.is_anonymous():
activetab = '4'
ebook_form= EbookForm( data = request.POST)
if ebook_form.is_valid():
ebook_form.save()
alert = 'Thanks for adding an ebook to unglue.it!'
else:
alert = ebook_form.errors
else:
alert=''
try:
activetab = request.GET['tab']
if activetab not in ['1', '2', '3', '4']:
activetab = '1';
except:
activetab = '1';
editions = work.editions.all().order_by('-publication_date')
campaign = work.last_campaign()
try:
@ -109,6 +126,9 @@ def work(request, work_id, action='display'):
pubdate = 'unknown'
if not request.user.is_anonymous():
claimform = UserClaimForm( request.user, data={'work':work.pk, 'user': request.user.id})
for edition in editions:
#edition.ebook_form = EbookForm( data = {'user':request.user.id, 'edition':edition.pk })
edition.ebook_form = EbookForm( instance= models.Ebook(user = request.user, edition = edition, provider = 'x' ) )
else:
claimform = None
if campaign:
@ -120,12 +140,6 @@ def work(request, work_id, action='display'):
wishers = work.num_wishes
base_url = request.build_absolute_uri("/")[:-1]
try:
activetab = request.GET['tab']
if activetab not in ['1', '2', '3', '4']:
activetab = '1';
except:
activetab = '1';
#may want to deprecate the following
if action == 'setup_campaign':
@ -142,6 +156,7 @@ def work(request, work_id, action='display'):
'pubdate': pubdate,
'pledged':pledged,
'activetab': activetab,
'alert':alert
})
def manage_campaign(request, id):
@ -205,14 +220,6 @@ class WorkListView(ListView):
template_name = "work_list.html"
context_object_name = "work_list"
def work_set_counts(self,work_set):
counts={}
# counts['unglued'] = work_set.annotate(ebook_count=Count('editions__ebooks')).filter(ebook_count__gt=0).count()
counts['unglued'] = work_set.filter(editions__ebooks__isnull=False).distinct().count()
counts['unglueing'] = work_set.filter(campaigns__status='ACTIVE').count()
counts['wished'] = work_set.count() - counts['unglued'] - counts['unglueing']
return counts
def get_queryset(self):
facet = self.kwargs['facet']
if (facet == 'popular'):
@ -227,9 +234,17 @@ class WorkListView(ListView):
def get_context_data(self, **kwargs):
context = super(WorkListView, self).get_context_data(**kwargs)
qs=self.get_queryset()
context['counts'] = self.work_set_counts(qs)
context['ungluers'] = userlists.work_list_users(qs,5)
context['facet'] =self.kwargs['facet']
context['works_unglued'] = qs.filter(editions__ebooks__isnull=False).distinct()
context['works_active'] = qs.exclude(editions__ebooks__isnull=False).filter(Q(campaigns__status='ACTIVE') | Q(campaigns__status='SUCCESSFUL')).distinct().order_by('-campaigns__status', 'campaigns__deadline')
context['works_wished'] = qs.exclude(editions__ebooks__isnull=False).exclude(campaigns__status='ACTIVE').exclude(campaigns__status='SUCCESSFUL').distinct().order_by('-num_wishes')
counts={}
counts['unglued'] = context['works_unglued'].count()
counts['unglueing'] = context['works_active'].count()
counts['wished'] = context['works_wished'].count()
context['counts'] = counts
return context
class UngluedListView(ListView):
@ -244,7 +259,7 @@ class UngluedListView(ListView):
def get_queryset(self):
facet = self.kwargs['facet']
if (facet == 'popular'):
return models.Work.objects.annotate(ebook_count=Count('editions__ebooks')).filter(ebook_count__gt=0).order_by('-num_wishes')
return models.Work.objects.filter(editions__ebooks__isnull=False).distinct().order_by('-num_wishes')
else:
#return models.Work.objects.annotate(ebook_count=Count('editions__ebooks')).filter(ebook_count__gt=0).order_by('-created')
return models.Work.objects.filter(editions__ebooks__isnull=False).distinct().order_by('-created')
@ -314,7 +329,7 @@ class PledgeView(FormView):
form = CampaignPledgeForm(data)
context.update({'work':work,'campaign':campaign, 'premiums':premiums, 'form':form, 'premium_id':premium_id})
context.update({'work':work,'campaign':campaign, 'premiums':premiums, 'form':form, 'premium_id':premium_id, 'faqmenu': 'pledge'})
return context
def form_valid(self, form):
@ -430,6 +445,7 @@ should briefly note next steps (e.g. if this campaign succeeds you will be email
context["correct_transaction_type"] = correct_transaction_type
context["work"] = work
context["campaign"] = campaign
context["faqmenu"] = "complete"
return context
@ -490,6 +506,7 @@ class PledgeCancelView(TemplateView):
context["try_again_url"] = try_again_url
context["work"] = work
context["campaign"] = campaign
context["faqmenu"] = "cancel"
return context
@ -725,25 +742,26 @@ def campaign_admin(request):
def supporter(request, supporter_username, template_name):
supporter = get_object_or_404(User, username=supporter_username)
wishlist = supporter.wishlist
works = wishlist.works.all().order_by('-num_wishes')
fromsupport = 1
backed = 0
backing = 0
transet = Transaction.objects.all().filter(user = supporter)
for transaction in transet:
try:
if(transaction.campaign.status == 'SUCCESSFUL'):
backed += 1
elif(transaction.campaign.status == 'ACTIVE'):
backing += 1
except:
continue
wished = supporter.wishlist.works.count()
# querysets for tabs
# unglued tab is anything with an existing ebook
## .order_by() may clash with .distinct() and this should be fixed
works_unglued = wishlist.works.all().filter(editions__ebooks__isnull=False).distinct().order_by('-num_wishes')
# take the set complement of the unglued tab and filter it for active works to get middle tab
result = wishlist.works.all().exclude(pk__in=works_unglued.values_list('pk', flat=True))
works_active = result.filter(Q(campaigns__status='ACTIVE') | Q(campaigns__status='SUCCESSFUL')).order_by('-campaigns__status', 'campaigns__deadline').distinct()
# everything else goes in tab 3
works_wished = result.exclude(pk__in=works_active.values_list('pk', flat=True)).order_by('-num_wishes')
# badge counts
backed = works_unglued.count()
backing = works_active.count()
wished = works_wished.count()
date = supporter.date_joined.strftime("%B %d, %Y")
# following block to support profile admin form in supporter page
if request.user.is_authenticated() and request.user.username == supporter_username:
@ -788,12 +806,12 @@ def supporter(request, supporter_username, template_name):
goodreads_id = None
librarything_id = None
context = {
"supporter": supporter,
"wishlist": wishlist,
"works": works,
"fromsupport": fromsupport,
"works_unglued": works_unglued,
"works_active": works_active,
"works_wished": works_wished,
"backed": backed,
"backing": backing,
"wished": wished,
@ -802,7 +820,7 @@ def supporter(request, supporter_username, template_name):
"ungluers": userlists.other_users(supporter, 5 ),
"goodreads_auth_url": reverse('goodreads_auth'),
"goodreads_id": goodreads_id,
"librarything_id": librarything_id
"librarything_id": librarything_id,
}
return render(request, template_name, context)
@ -1203,7 +1221,7 @@ def work_librarything(request, work_id):
url = "http://www.librarything.com/isbn/%s" % isbn
else:
term = work.title + " " + work.author()
q = urllib.urlencode({'searchtpe': 'work', 'term': term})
q = urlencode({'searchtpe': 'work', 'term': term})
url = "http://www.librarything.com/search.php?" + q
return HttpResponseRedirect(url)
@ -1224,7 +1242,7 @@ def work_openlibrary(request, work_id):
url = "http://openlibrary.org" + j[first]['key']
# fall back to doing a search on openlibrary
if not url:
q = urllib.urlencode({'q': work.title + " " + work.author()})
q = urlencode({'q': work.title + " " + work.author()})
url = "http://openlibrary.org/search?" + q
return HttpResponseRedirect(url)
@ -1236,7 +1254,7 @@ def work_goodreads(request, work_id):
elif isbn:
url = "http://www.goodreads.com/book/isbn/%s" % isbn
else:
q = urllib.urlencode({'query': work.title + " " + work.author()})
q = urlencode({'query': work.title + " " + work.author()})
url = "http://www.goodreads.com/search?" + q
return HttpResponseRedirect(url)
@ -1310,5 +1328,5 @@ def feedback(request):
return render(request, "feedback.html", {'form':form, 'num1':num1, 'num2':num2})
def comment(request):
latest_comments = Comment.objects.all()[:20]
latest_comments = Comment.objects.all().order_by('-submit_date')[:20]
return render(request, "comments.html", {'latest_comments': latest_comments})

View File

@ -113,7 +113,7 @@ div.book-list.listview div.book-name .title {
overflow: hidden;
height: 19px;
line-height: 19px;
padding-bottom: 5px;
margin-bottom: 5px;
font-weight: bold;
}
div.book-list.listview div.book-name .listview.author {

View File

@ -508,6 +508,9 @@ a {
.editions a:hover {
text-decoration: underline;
}
.show_more_edition {
cursor: pointer;
}
.thank-you {
font-size: 20px;
}
@ -526,3 +529,15 @@ a {
-webkit-border-radius: 5px;
border-radius: 5px;
}
.more_edition {
display: none;
clear: both;
padding-bottom: 10px;
padding-left: 60px;
}
.show_more_edition:hover {
text-decoration: underline;
}
.show_more_edition {
text-align: right;
}

View File

@ -99,3 +99,12 @@ form {
ul.support li, ul.support li:hover {
background-image: none;
}
.faq, .answer {
text-transform: none !important;
}
.faq {
cursor: pointer;
}
.faq:hover {
text-decoration: underline;
}

View File

@ -22,6 +22,21 @@
var tabsLink3 = $j('li.tabs3');
var contentBlockContent = $j('#content-block-content');
// on pageload we are showing only the Wishlisted tab, not Unglued or Active, by default
if(location.hash == "#1") {
tabsDash2.hide();
tabsDash3.hide();
tabsLink1.addClass('active');
} else if(location.hash =="#2") {
tabsDash1.hide();
tabsDash3.hide();
tabsLink2.addClass('active');
} else {
tabsDash1.hide();
tabsDash2.hide();
tabsLink3.addClass('active');
}
tabs1.click(function(){
tabs.find('.active').removeClass('active');
tabsLink1.addClass('active');
@ -30,6 +45,7 @@
tabsDash2.hide(200);
tabsDash3.hide(200);
tabsId.removeClass('wantto').removeClass('ungluing').addClass('unglued');
location.hash = "#1";
});
tabs2.click(function(){
tabs.find('.active').removeClass('active');
@ -39,6 +55,7 @@
tabsDash1.hide(200);
tabsDash3.hide(200);
tabsId.removeClass('unglued').removeClass('wantto').addClass('ungluing');
location.hash = "#2";
});
tabs3.click(function(){
tabs.find('.active').removeClass('active');
@ -48,6 +65,7 @@
tabsDash2.hide(200);
tabsDash1.hide(200);
tabsId.removeClass('unglued').removeClass('ungluing').addClass('wantto');
location.hash = "#3";
});
$j('.empty-wishlist span.bounce-search').click(function(){
$j('div.js-search-inner').effect("bounce", 500, function() {

View File

@ -48,7 +48,7 @@ div.book-list.listview{
line-height:normal;
overflow: hidden;
.height(19px);
padding-bottom: 5px;
margin-bottom: 5px;
font-weight:bold;
}

View File

@ -456,6 +456,10 @@ a{ color:#3d4e53; font-size:12px;}
}
}
.show_more_edition {
cursor: pointer;
}
.thank-you {
font-size: 20px;
}
@ -473,4 +477,17 @@ a{ color:#3d4e53; font-size:12px;}
img {
.one-border-radius(5px);
}
}
}
.more_edition {
display:none;
clear: both;
padding-bottom: 10px;
padding-left: 60px;
}
.show_more_edition:hover {
text-decoration: underline;
}
.show_more_edition {
text-align: right;
}

View File

@ -31,4 +31,16 @@ form {
ul.support li, ul.support li:hover {
background-image: none;
}
.faq, .answer {
text-transform: none !important;
}
.faq {
cursor: pointer;
&:hover {
text-decoration: underline;
}
}

View File

@ -1,12 +1,15 @@
from regluit.core import librarything, bookloader, models, tasks
from collections import OrderedDict
from itertools import izip, islice
from collections import OrderedDict, defaultdict, namedtuple
from itertools import izip, islice, repeat
import django
from django.db.models import Q, F
from regluit.core import bookloader
from django.contrib.comments.models import Comment
import warnings
import datetime
from regluit import experimental
from regluit.experimental import bookdata
from datetime import datetime
import json
@ -14,6 +17,20 @@ import json
import logging
logger = logging.getLogger(__name__)
def dictset(itertuple):
s = defaultdict(set)
for (k, v) in itertuple:
s[k].add(v)
return s
def dictlist(itertuple):
d = defaultdict(list)
for (k, v) in itertuple:
d[k].append(v)
return d
EdInfo = namedtuple('EdInfo', ['isbn', 'ed_id', 'ed_title', 'ed_created', 'work_id', 'work_created', 'lang'])
def ry_lt_books():
"""return parsing of rdhyee's LibraryThing collection"""
lt = librarything.LibraryThing('rdhyee')
@ -66,7 +83,7 @@ def load_gutenberg_moby_dick():
epub_url, format, license, lang, publication_date)
return ebook
def load_gutenberg_books(fname="/Users/raymondyee/D/Document/Gluejar/Gluejar.github/regluit/experimental/gutenberg/g_seed_isbn.json",
def load_gutenberg_books(fname="{0}/gutenberg/g_seed_isbn.json".format(experimental.__path__[0]),
max_num=None):
headers = ()
@ -85,26 +102,163 @@ def load_gutenberg_books(fname="/Users/raymondyee/D/Document/Gluejar/Gluejar.git
else:
logger.info("%d null seed_isbn: ebook %s", i, ebook)
def cluster_status():
def cluster_status(max_num=None):
"""Look at the current Work, Edition instances to figure out what needs to be fixed"""
results = OrderedDict([
('number of Works', models.Work.objects.count()),
('number of Editions', models.Edition.objects.count())
('number of Works w/o Identifier', models.Work.objects.filter(identifiers__isnull=True).count()),
('number of Editions', models.Edition.objects.count()),
('number of Editions with ISBN', models.Edition.objects.filter(identifiers__type='isbn').count()),
('number of Editions without ISBNs', models.Edition.objects.exclude(identifiers__type='isbn').count()),
('number of Edition that have both Google Books id and ISBNs',
models.Edition.objects.filter(identifiers__type='isbn').filter(identifiers__type='goog').count()),
('number of Editions with Google Books IDs but not ISBNs',
models.Edition.objects.filter(identifiers__type='goog').exclude(identifiers__type='isbn').count()),
])
# What needs to be done to recluster editions?
# models.Identifier.objects.filter(type='isbn').values_list('value', 'edition__id', 'edition__work__id', 'edition__work__language').count()
# 4 classes -- Edition have ISBN or not & ISBN is recognized or not by LT
# a) ISBN recognized by LT, b) ISBN not recognized by LT, c) no ISBN at all
# [w._meta.get_all_related_objects() for w in works_no_ids] -- try to figure out whether any related objects before deleting
# Are there Edition without ISBNs? Look up the corresponding ISBNs from Google Books and Are they all singletons?
# identify Editions that should be merged (e.g., if one Edition has a Google Books ID and another Edition has one with
# an ISBN tied to that Google Books ID)
import shutil
import time
import operator
return results
# let's form a key to map all the Editions into
# (lt_work_id (or None), lang, ISBN (if lt_work_id is None or None if we don't know it), ed_id (or None) )
work_clusters = defaultdict(set)
current_map = defaultdict(set)
#backup = '/Users/raymondyee/D/Document/Gluejar/Gluejar.github/regluit/experimental/lt_data_back.json'
backup = '{0}/lt_data_back.json'.format(experimental.__path__[0])
#fname = '/Users/raymondyee/D/Document/Gluejar/Gluejar.github/regluit/experimental/lt_data.json'
fname = '{0}/lt_data.json'.format(experimental.__path__[0])
shutil.copy(fname, backup)
lt = bookdata.LibraryThing(fname)
try:
input_file = open(fname, "r")
success = lt.load()
print "success: %s" % (success)
input_file.close()
except Exception, e:
print e
for (i, (isbn, ed_id, ed_title, ed_created, work_id, work_created, lang)) in enumerate(
islice(models.Identifier.objects.filter(type='isbn').values_list('value', 'edition__id',
'edition__title', 'edition__created', 'edition__work__id',
'edition__work__created', 'edition__work__language'), max_num)):
lt_work_id = lt.thingisbn(isbn, return_work_id=True)
key = (lt_work_id, lang, isbn if lt_work_id is None else None, None)
print i, isbn, lt_work_id, key
work_clusters[key].add(EdInfo(isbn=isbn, ed_id=ed_id, ed_title=ed_title, ed_created=ed_created,
work_id=work_id, work_created=work_created, lang=lang))
current_map[work_id].add(key)
lt.save()
# Now add the Editions without any ISBNs
print "editions w/o isbn"
for (i, (ed_id, ed_title, ed_created, work_id, work_created, lang)) in enumerate(
islice(models.Edition.objects.exclude(identifiers__type='isbn').values_list('id',
'title', 'created', 'work__id', 'work__created', 'work__language' ), None)):
key = (None, lang, None, ed_id)
print i, ed_id, ed_title.encode('ascii','ignore'), key
work_clusters[key].add(EdInfo(isbn=None, ed_id=ed_id, ed_title=ed_title, ed_created=ed_created,
work_id=work_id, work_created=work_created, lang=lang))
current_map[work_id].add(key)
print "number of clusters", len(work_clusters)
# all unglue.it Works that contain Editions belonging to more than one newly calculated cluster are "FrankenWorks"
franken_works = sorted([k for (k,v) in current_map.items() if len(v) > 1])
# let's calculate the list of users affected if delete the Frankenworks, the number of works deleted from their wishlist
# specifically a list of emails to send out
affected_works = [models.Work.objects.get(id=w_id) for w_id in franken_works]
affected_wishlists = set(reduce(operator.add, [list(w.wishlists.all()) for w in affected_works])) if len(affected_works) else set()
affected_emails = [w.user.email for w in affected_wishlists]
affected_editions = reduce(operator.add, [list(w.editions.all()) for w in affected_works]) if len(affected_works) else []
# calculate the Comments that would have to be deleted too.
affected_comments = reduce(operator.add, [list(Comment.objects.for_model(w)) for w in affected_works]) if len(affected_works) else []
# calculate the inverse of work_clusters
wcp = dict(reduce(operator.add, [ list( izip([ed.ed_id for ed in eds], repeat(k))) for (k,eds) in work_clusters.items()]))
# (I'm not completely sure of this calc -- but the datetime of the latest franken-event)
latest_franken_event = max([ max([min(map(lambda x: x[1], v)) for v in dictlist([(wcp[ed["id"]], (ed["id"], ed["created"].isoformat()))
for ed in models.Work.objects.get(id=w_id).editions.values('id', 'created')]).values()])
for w_id in franken_works]) if len(franken_works) else None
scattered_clusters = [(k, len(set(([e.work_id for e in v])))) for (k,v) in work_clusters.items() if len(set(([e.work_id for e in v]))) <> 1 ]
s = {'work_clusters':work_clusters, 'current_map':current_map, 'results':results, 'franken_works': franken_works,
'wcp':wcp, 'latest_franken_event': latest_franken_event, 'affected_works':affected_works,
'affected_comments': affected_comments, 'scattered_clusters': scattered_clusters,
'affected_emails': affected_emails}
return s
def clean_frankenworks(s, do=False):
# list out the email addresses of accounts with wishlists to be affected
print "number of email addresses: ", len(s['affected_emails'])
print ", ".join(s['affected_emails'])
# list the works we delete
print "number of FrankenWorks", len(s['franken_works'])
print s['franken_works']
# delete the affected comments
print "deleting comments"
for (i, comment) in enumerate(s['affected_comments']):
print i, "deleting ", comment
if do:
comment.delete()
# delete the Frankenworks
print "deleting Frankenworks"
for (i, work) in enumerate(s['affected_works']):
print i, "deleting ", work.id
if do:
work.delete()
# run reclustering surgically -- calculate a set of ISBNs to feed to bookloader.add_related
# assuming x is a set
popisbn = lambda x: list(x)[0].isbn if len(x) else None
# group scattered_clusters by LT work id
scattered_lt = dictlist([(k[0], k) for (k,v) in s['scattered_clusters']])
isbns = map(popisbn, [s['work_clusters'][k[0]] for k in scattered_lt.values()])
print "running bookloader"
for (i, isbn) in enumerate(isbns):
print i, isbn
if do:
bookloader.add_related(isbn)