diff --git a/api/onix.py b/api/onix.py index a504cb9e..1aba6b76 100644 --- a/api/onix.py +++ b/api/onix.py @@ -1,5 +1,6 @@ import datetime import pytz +import re from lxml import etree from regluit.core import models from regluit.core.cc import ccinfo @@ -134,6 +135,19 @@ def product(edition, facet=None): subj_node.append(text_node("SubjectSchemeIdentifier", "20")) subj_node.append(text_node("SubjectHeadingText", subject.name)) + # audience range composite + if work.age_level: + range_match = re.search(r'(\d?\d?)-(\d?\d?)', work.age_level) + if range_match: + audience_range_node = etree.SubElement(descriptive_node, "AudienceRange") + audience_range_node.append(text_node("AudienceRangeQualifier", "17")) #Interest age, years + if range_match.group(1): + audience_range_node.append(text_node("AudienceRangePrecision", "03")) #from + audience_range_node.append(text_node("AudienceRangeValue", range_match.group(1))) + if range_match.group(2): + audience_range_node.append(text_node("AudienceRangePrecision", "04")) #from + audience_range_node.append(text_node("AudienceRangeValue", range_match.group(2))) + # Collateral Detail Block coll_node = etree.SubElement(product_node, "CollateralDetail") desc_node = etree.SubElement(coll_node, "TextContent") diff --git a/api/opds.py b/api/opds.py index 711b9f30..f6cde35f 100644 --- a/api/opds.py +++ b/api/opds.py @@ -84,25 +84,29 @@ def work_node(work, facet=None): node.append(text_node('updated', work.first_ebook().created.isoformat())) # links for all ebooks - ebooks=facet.filter_model("Ebook",work.ebooks()) if facet else work.ebooks() + ebooks = facet.filter_model("Ebook",work.ebooks()) if facet else work.ebooks() + versions = set() for ebook in ebooks: - link_node = etree.Element("link") + if not ebook.version_label in versions: + versions.add(ebook.version_label) + link_node = etree.Element("link") - # ebook.download_url is an absolute URL with the protocol, domain, and path baked in - link_rel = "http://opds-spec.org/acquisition/open-access" - link_node.attrib.update({"href":add_query_component(ebook.download_url, "feed=opds"), - "rel":link_rel, - "{http://purl.org/dc/terms/}rights": str(ebook.rights)}) - if ebook.is_direct(): - link_node.attrib["type"] = FORMAT_TO_MIMETYPE.get(ebook.format, "") - else: - """ indirect acquisition, i.e. google books """ - link_node.attrib["type"] = "text/html" - indirect = etree.Element("{http://opds-spec.org/}indirectAcquisition",) - indirect.attrib["type"] = FORMAT_TO_MIMETYPE.get(ebook.format, "") - link_node.append(indirect) - - node.append(link_node) + # ebook.download_url is an absolute URL with the protocol, domain, and path baked in + link_rel = "http://opds-spec.org/acquisition/open-access" + link_node.attrib.update({"href":add_query_component(ebook.download_url, "feed=opds"), + "rel":link_rel, + "{http://purl.org/dc/terms/}rights": str(ebook.rights)}) + if ebook.is_direct(): + link_node.attrib["type"] = FORMAT_TO_MIMETYPE.get(ebook.format, "") + else: + """ indirect acquisition, i.e. google books """ + link_node.attrib["type"] = "text/html" + indirect = etree.Element("{http://opds-spec.org/}indirectAcquisition",) + indirect.attrib["type"] = FORMAT_TO_MIMETYPE.get(ebook.format, "") + link_node.append(indirect) + if ebook.version_label: + link_node.attrib.update({"{http://schema.org/}version": ebook.version_label}) + node.append(link_node) # get the cover -- assume jpg? @@ -163,6 +167,16 @@ def work_node(work, facet=None): # caused by control chars in subject.name logger.warning('Deleting subject: %s' % subject.name) subject.delete() + + # age level + # + if work.age_level: + category_node = etree.Element("category") + category_node.attrib["scheme"] = 'http://schema.org/typicalAgeRange' + category_node.attrib["term"] = work.age_level + category_node.attrib["label"] = work.get_age_level_display() + node.append(category_node) + # rating rating_node = etree.Element("{http://schema.org/}Rating") @@ -227,6 +241,9 @@ def opds_feed_for_work(work_id): works=models.Work.objects.filter(id=work_id) except models.Work.DoesNotExist: works=models.Work.objects.none() + except ValueError: + # not a valid work_id + works=models.Work.objects.none() self.works=works self.title='Unglue.it work #%s' % work_id self.feed_path='' diff --git a/core/admin.py b/core/admin.py index 9d85e843..f47b32f5 100644 --- a/core/admin.py +++ b/core/admin.py @@ -74,7 +74,8 @@ class WorkAdmin(ModelAdmin): ordering = ('title',) list_display = ('title', 'created') date_hierarchy = 'created' - fields = ('title', 'description', 'language', 'featured') + fields = ('title', 'description', 'language', 'featured', 'publication_range', + 'age_level', 'openlibrary_lookup') class AuthorAdmin(ModelAdmin): search_fields = ('name',) @@ -105,9 +106,9 @@ class EditionAdminForm(forms.ModelForm): ) publisher_name = AutoCompleteSelectField( PublisherNameLookup, - label='Name', + label='Publisher Name', widget=AutoCompleteSelectWidget(PublisherNameLookup), - required=True, + required=False, ) class Meta(object): model = models.Edition @@ -174,24 +175,76 @@ class CeleryTaskAdmin(ModelAdmin): class PressAdmin(ModelAdmin): list_display = ('title', 'source', 'date') ordering = ('-date',) + +class WorkRelationAdminForm(forms.ModelForm): + to_work = AutoCompleteSelectField( + WorkLookup, + label='To Work', + widget=AutoCompleteSelectWidget(WorkLookup), + required=True, + ) + from_work = AutoCompleteSelectField( + WorkLookup, + label='From Work', + widget=AutoCompleteSelectWidget(WorkLookup), + required=True, + ) + class Meta(object): + model = models.WorkRelation + exclude = () + +class WorkRelationAdmin(ModelAdmin): + form = WorkRelationAdminForm + list_display = ('to_work', 'relation', 'from_work') + +class IdentifierAdminForm(forms.ModelForm): + work = AutoCompleteSelectField( + WorkLookup, + label='Work', + widget=AutoCompleteSelectWidget(WorkLookup, attrs={'size':60}), + required=False, + ) + edition = AutoCompleteSelectField( + EditionLookup, + label='Edition', + widget=AutoCompleteSelectWidget(EditionLookup, attrs={'size':60}), + required=True, + ) + class Meta(object): + model = models.Identifier + exclude = () + +class IdentifierAdmin(ModelAdmin): + form = IdentifierAdminForm + list_display = ('type', 'value') + search_fields = ('type', 'value') + +class OfferAdmin(ModelAdmin): + list_display = ('work', 'license', 'price', 'active') + search_fields = ('work__title',) + readonly_fields = ('work',) admin_site.register(models.Acq, AcqAdmin) -admin_site.register(models.Work, WorkAdmin) -admin_site.register(models.Claim, ClaimAdmin) -admin_site.register(models.RightsHolder, RightsHolderAdmin) -admin_site.register(models.Premium, PremiumAdmin) -admin_site.register(models.Campaign, CampaignAdmin) admin_site.register(models.Author, AuthorAdmin) +admin_site.register(models.Badge, ModelAdmin) +admin_site.register(models.Campaign, CampaignAdmin) +admin_site.register(models.CeleryTask, CeleryTaskAdmin) +admin_site.register(models.Claim, ClaimAdmin) +admin_site.register(models.Ebook, EbookAdmin) +admin_site.register(models.Edition, EditionAdmin) +admin_site.register(models.Gift, GiftAdmin) +admin_site.register(models.Identifier, IdentifierAdmin) +admin_site.register(models.Offer, OfferAdmin) +admin_site.register(models.Premium, PremiumAdmin) +admin_site.register(models.Press, PressAdmin) admin_site.register(models.Publisher, PublisherAdmin) admin_site.register(models.PublisherName, PublisherNameAdmin) -admin_site.register(models.Subject, SubjectAdmin) -admin_site.register(models.Edition, EditionAdmin) -admin_site.register(models.Ebook, EbookAdmin) -admin_site.register(models.Wishlist, WishlistAdmin) -admin_site.register(models.UserProfile, UserProfileAdmin) -admin_site.register(models.CeleryTask, CeleryTaskAdmin) -admin_site.register(models.Press, PressAdmin) -admin_site.register(models.Gift, GiftAdmin) admin_site.register(models.Relation, RelationAdmin) +admin_site.register(models.RightsHolder, RightsHolderAdmin) +admin_site.register(models.Subject, SubjectAdmin) +admin_site.register(models.UserProfile, UserProfileAdmin) +admin_site.register(models.Wishlist, WishlistAdmin) +admin_site.register(models.Work, WorkAdmin) +admin_site.register(models.WorkRelation, WorkRelationAdmin) diff --git a/core/bookloader.py b/core/bookloader.py index a64fbf7a..d20b4b22 100755 --- a/core/bookloader.py +++ b/core/bookloader.py @@ -449,6 +449,7 @@ def add_related(isbn): for w in works_to_merge: logger.debug("merge_works path 2 %s %s", lang_edition.work.id, w.id ) merge_works(lang_edition.work, w) + models.WorkRelation.objects.get_or_create(to_work=lang_edition.work, from_work=work, relation='translation') return new_editions @@ -901,8 +902,8 @@ def load_from_yaml(yaml_url, test_mode=False): rights = cc.match_license(metadata.rights), format = ebook_format, edition = edition, - # version = metadata._version ) + ebook.set_version(metadata._version) return work.id diff --git a/core/cc.py b/core/cc.py index 0103ebcb..19993e39 100644 --- a/core/cc.py +++ b/core/cc.py @@ -106,7 +106,7 @@ def description(license): else: return '' -class ccinfo(): +class ccinfo(object): def __init__(self, license): value=license_value(license) self.license=value if value else license diff --git a/core/lookups.py b/core/lookups.py index 452c505c..df8764b3 100644 --- a/core/lookups.py +++ b/core/lookups.py @@ -3,7 +3,7 @@ from selectable.registry import registry from django.contrib.auth.models import User from django.db.models import Count -from regluit.core.models import Work, PublisherName, Edition, Subject +from regluit.core.models import Work, PublisherName, Edition, Subject, EditionNote class OwnerLookup(ModelLookup): model = User @@ -54,8 +54,17 @@ class SubjectLookup(ModelLookup): def get_query(self, request, term): return super(SubjectLookup, self).get_query( request, term).annotate(Count('works')).order_by('-works__count') +class EditionNoteLookup(ModelLookup): + model = EditionNote + search_fields = ('note__icontains',) + def create_item(self, value): + new_note, created = EditionNote.objects.get_or_create(note=value) + new_note.save() + return new_note + registry.register(OwnerLookup) registry.register(WorkLookup) registry.register(PublisherNameLookup) registry.register(EditionLookup) -registry.register(SubjectLookup) \ No newline at end of file +registry.register(SubjectLookup) +registry.register(EditionNoteLookup) \ No newline at end of file diff --git a/core/migrations/0003_auto_20160816_1645.py b/core/migrations/0003_auto_20160816_1645.py new file mode 100644 index 00000000..1ae7aa68 --- /dev/null +++ b/core/migrations/0003_auto_20160816_1645.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_auto_20160722_1716'), + ] + + operations = [ + migrations.CreateModel( + name='EditionNote', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('note', models.CharField(max_length=64, unique=True, null=True, blank=True)), + ], + ), + migrations.CreateModel( + name='WorkRelation', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('relation', models.CharField(max_length=15, choices=[(b'translation', b'translation'), (b'revision', b'revision'), (b'sequel', b'sequel'), (b'compilation', b'compilation')])), + ], + ), + migrations.AddField( + model_name='ebook', + name='version', + field=models.CharField(max_length=255, null=True), + ), + migrations.AddField( + model_name='work', + name='age_level', + field=models.CharField(default=b'', max_length=5, choices=[(b'', b'No Rating'), (b'5-6', b"Children's - Kindergarten, Age 5-6"), (b'6-7', b"Children's - Grade 1-2, Age 6-7"), (b'7-8', b"Children's - Grade 2-3, Age 7-8"), (b'8-9', b"Children's - Grade 3-4, Age 8-9"), (b'9-11', b"Children's - Grade 4-6, Age 9-11"), (b'12-14', b'Teen - Grade 7-9, Age 12-14'), (b'15-18', b'Teen - Grade 10-12, Age 15-18'), (b'18-', b'Adult/Advanced Reader')]), + ), + migrations.AddField( + model_name='workrelation', + name='from_work', + field=models.ForeignKey(related_name='works_related_from', to='core.Work'), + ), + migrations.AddField( + model_name='workrelation', + name='to_work', + field=models.ForeignKey(related_name='works_related_to', to='core.Work'), + ), + migrations.AddField( + model_name='edition', + name='note', + field=models.ForeignKey(to='core.EditionNote', null=True), + ), + migrations.AddField( + model_name='work', + name='related', + field=models.ManyToManyField(related_name='reverse_related', null=True, through='core.WorkRelation', to='core.Work'), + ), + ] diff --git a/core/migrations/0004_auto_20160808_1548.py b/core/migrations/0004_auto_20160808_1548.py new file mode 100644 index 00000000..52c7d0d7 --- /dev/null +++ b/core/migrations/0004_auto_20160808_1548.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models, transaction +from django.db.utils import IntegrityError + + +class Migration(migrations.Migration): + + def url_to_doi(apps, schema_editor): + Indentifier = apps.get_model('core', 'Identifier') + for doi in Indentifier.objects.filter(type='http', value__icontains='dx.doi.org'): + if doi.value.startswith('http://dx.doi.org/10.'): + doi.value = doi.value[18:] + elif doi.value.startswith('https://dx.doi.org/10.'): + doi.value = doi.value[19:] + else: + continue + doi.type = 'doi' + try: + with transaction.atomic(): + doi.save() + except IntegrityError: + continue + + def doi_to_url(apps, schema_editor): + Indentifier = apps.get_model('core', 'Identifier') + for doi in Indentifier.objects.filter(type='doi'): + doi.value = 'https://dx.doi.org/{}'.format(doi.value) + doi.type = 'http' + try: + with transaction.atomic(): + doi.save() + except IntegrityError: + continue + + + dependencies = [ + ('core', '0003_auto_20160816_1645'), + ] + + operations = [ + migrations.RunPython(url_to_doi, reverse_code=doi_to_url, hints={'core': 'Identifier'}), + ] diff --git a/core/migrations/0005_ebookfile_ebook.py b/core/migrations/0005_ebookfile_ebook.py new file mode 100644 index 00000000..c738940b --- /dev/null +++ b/core/migrations/0005_ebookfile_ebook.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_auto_20160808_1548'), + ] + + operations = [ + migrations.AddField( + model_name='ebookfile', + name='ebook', + field=models.ForeignKey(related_name='ebook_files', to='core.Ebook', null=True), + ), + ] diff --git a/core/migrations/0006_auto_20160818_1809.py b/core/migrations/0006_auto_20160818_1809.py new file mode 100644 index 00000000..5b3cbe4c --- /dev/null +++ b/core/migrations/0006_auto_20160818_1809.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from django.db import migrations, models + +class Migration(migrations.Migration): + + def add_ebooks_to_ebfs(apps, schema_editor): + """ + Now that EbookFile has ebook foreign key, this migration populates that key + """ + EbookFile = apps.get_model('core', 'EbookFile') + Ebook = apps.get_model('core', 'Ebook') + for ebf in EbookFile.objects.all(): + + # Connect each ebf (ebookfile) based on common edition (excluding the unglue.it provider) or URL. + + for ebook in Ebook.objects.filter(edition=ebf.edition, format=ebf.format).exclude(provider='Unglue.it'): + ebf.ebook = ebook + ebf.save() + for ebook in Ebook.objects.filter(url=ebf.file.url): + ebf.ebook = ebook + ebf.save() + + # if the ebookfile is still not connected to an ebook... + if not ebf.ebook: + # and the edition is associated with a THANKS campaign + if ebf.edition.work.campaigns.filter(type=3): + ebf.ebook = Ebook.objects.create( + edition=ebf.edition, + active=False, + url=ebf.file.url, + provider='Unglue.it', + format=ebf.format, + rights=ebf.edition.work.campaigns.order_by('-created')[0].license + ) + ebf.save() + + # Buy to unglue campaign + elif ebf.edition.work.campaigns.filter(type=2): + pass + else: + print 'ebf {} is dangling'.format(ebf.id) + + def noop(apps, schema_editor): + pass + + dependencies = [ + ('core', '0005_ebookfile_ebook'), + ] + + operations = [ + migrations.RunPython(add_ebooks_to_ebfs, reverse_code=noop, hints={'core': 'EbookFile'}), + ] diff --git a/core/migrations/0007_auto_20160923_1314.py b/core/migrations/0007_auto_20160923_1314.py new file mode 100644 index 00000000..3f1ee8b2 --- /dev/null +++ b/core/migrations/0007_auto_20160923_1314.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_auto_20160818_1809'), + ] + + operations = [ + migrations.RemoveField( + model_name='ebook', + name='version', + ), + migrations.AddField( + model_name='ebook', + name='version_iter', + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name='ebook', + name='version_label', + field=models.CharField(default=b'', max_length=255, blank=True), + ), + migrations.AlterField( + model_name='edition', + name='note', + field=models.ForeignKey(blank=True, to='core.EditionNote', null=True), + ), + migrations.AlterField( + model_name='edition', + name='publisher_name', + field=models.ForeignKey(related_name='editions', blank=True, to='core.PublisherName', null=True), + ), + migrations.AlterField( + model_name='userprofile', + name='badges', + field=models.ManyToManyField(related_name='holders', to='core.Badge', blank=True), + ), + migrations.AlterField( + model_name='userprofile', + name='facebook_id', + field=models.BigIntegerField(null=True, blank=True), + ), + migrations.AlterField( + model_name='work', + name='age_level', + field=models.CharField(default=b'', max_length=5, blank=True, choices=[(b'', b'No Rating'), (b'5-6', b"Children's - Kindergarten, Age 5-6"), (b'6-7', b"Children's - Grade 1-2, Age 6-7"), (b'7-8', b"Children's - Grade 2-3, Age 7-8"), (b'8-9', b"Children's - Grade 3-4, Age 8-9"), (b'9-11', b"Children's - Grade 4-6, Age 9-11"), (b'12-14', b'Teen - Grade 7-9, Age 12-14'), (b'15-18', b'Teen - Grade 10-12, Age 15-18'), (b'18-', b'Adult/Advanced Reader')]), + ), + migrations.AlterField( + model_name='work', + name='openlibrary_lookup', + field=models.DateTimeField(null=True, blank=True), + ), + migrations.AlterField( + model_name='work', + name='publication_range', + field=models.CharField(max_length=50, null=True, blank=True), + ), + ] diff --git a/core/models.py b/core/models.py deleted file mode 100755 index 19a0e73e..00000000 --- a/core/models.py +++ /dev/null @@ -1,2449 +0,0 @@ -''' -external library imports -''' -import binascii -import logging -import hashlib -import uuid -import re -import random -import urllib -import urllib2 -from urlparse import urlparse -import unicodedata -import math -import requests - -from ckeditor.fields import RichTextField -from datetime import timedelta, datetime -from decimal import Decimal -from notification import models as notification -from postmonkey import PostMonkey, MailChimpException -from sorl.thumbnail import get_thumbnail -from tempfile import SpooledTemporaryFile - -''' -django imports -''' -from django.apps import apps -from django.conf import settings -from django.contrib.auth.models import User -from django.contrib.sites.models import Site -from django.contrib.contenttypes.fields import GenericRelation -from django.core.urlresolvers import reverse -from django.core.files.base import ContentFile -from django.db import models -from django.db.models import F, Q -from django.db.models.signals import post_save, pre_delete -from django.utils.translation import ugettext_lazy as _ -''' -regluit imports -''' -import regluit -from regluit.libraryauth.auth import AVATARS -import regluit.core.isbn -import regluit.core.cc as cc -from regluit.core.epub import personalize, ungluify, test_epub, ask_epub -from regluit.core.pdf import ask_pdf, pdf_append -from regluit.core import mobi -from regluit.marc.models import MARCRecord as NewMARC -from regluit.core.signals import ( - successful_campaign, - unsuccessful_campaign, - wishlist_added -) - -from regluit.utils import crypto -from regluit.utils.localdatetime import now, date_today - -from regluit.payment.parameters import ( - TRANSACTION_STATUS_ACTIVE, - TRANSACTION_STATUS_COMPLETE, - TRANSACTION_STATUS_CANCELED, - TRANSACTION_STATUS_ERROR, - TRANSACTION_STATUS_FAILED, - TRANSACTION_STATUS_INCOMPLETE -) - -from regluit.core.parameters import ( - REWARDS, - BUY2UNGLUE, - THANKS, - INDIVIDUAL, - LIBRARY, - BORROWED, - TESTING, - RESERVE, - THANKED, -) -from regluit.questionnaire.models import Landing - -from regluit.booxtream import BooXtream -watermarker = BooXtream() - -from regluit.libraryauth.models import Library - -pm = PostMonkey(settings.MAILCHIMP_API_KEY) - -logger = logging.getLogger(__name__) - -class UnglueitError(RuntimeError): - pass - -class Key(models.Model): - """an encrypted key store""" - name = models.CharField(max_length=255, unique=True) - encrypted_value = models.TextField(null=True, blank=True) - - def _get_value(self): - return crypto.decrypt_string(binascii.a2b_hex(self.encrypted_value), settings.SECRET_KEY) - - def _set_value(self, value): - self.encrypted_value = binascii.b2a_hex(crypto.encrypt_string(value, settings.SECRET_KEY)) - - value = property(_get_value, _set_value) - - def __unicode__(self): - return "Key with name {0}".format(self.name) - -class CeleryTask(models.Model): - created = models.DateTimeField(auto_now_add=True) - task_id = models.CharField(max_length=255) - user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="tasks", null=True) - description = models.CharField(max_length=2048, null=True) # a description of what the task is - function_name = models.CharField(max_length=1024) # used to reconstitute the AsyncTask with which to get status - function_args = models.IntegerField(null=True) # not full generalized here -- takes only a single arg for now. - active = models.NullBooleanField(default=True) - - def __unicode__(self): - return "Task %s arg:%s ID# %s %s: State %s " % (self.function_name, self.function_args, self.task_id, self.description, self.state) - - @property - def AsyncResult(self): - f = getattr(regluit.core.tasks,self.function_name) - return f.AsyncResult(self.task_id) - @property - def state(self): - f = getattr(regluit.core.tasks,self.function_name) - return f.AsyncResult(self.task_id).state - @property - def result(self): - f = getattr(regluit.core.tasks,self.function_name) - return f.AsyncResult(self.task_id).result - @property - def info(self): - f = getattr(regluit.core.tasks,self.function_name) - return f.AsyncResult(self.task_id).info - -class Claim(models.Model): - STATUSES = (( - u'active', u'Claim has been accepted.'), - (u'pending', u'Claim is pending acceptance.'), - (u'release', u'Claim has not been accepted.'), - ) - created = models.DateTimeField(auto_now_add=True) - rights_holder = models.ForeignKey("RightsHolder", related_name="claim", null=False ) - work = models.ForeignKey("Work", related_name="claim", null=False ) - user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="claim", null=False ) - status = models.CharField(max_length=7, choices=STATUSES, default='active') - - @property - def can_open_new(self): - # whether a campaign can be opened for this claim - - #must be an active claim - if self.status != 'active': - return False - #can't already be a campaign - for campaign in self.campaigns: - if campaign.status in ['ACTIVE','INITIALIZED']: - return 0 # cannot open a new campaign - if campaign.status in ['SUCCESSFUL']: - return 2 # can open a THANKS campaign - return 1 # can open any type of campaign - - def __unicode__(self): - return self.work.title - - @property - def campaign(self): - return self.work.last_campaign() - - @property - def campaigns(self): - return self.work.campaigns.all() - -def notify_claim(sender, created, instance, **kwargs): - if 'example.org' in instance.user.email or hasattr(instance,'dont_notify'): - return - try: - (rights, new_rights) = User.objects.get_or_create(email='rights@gluejar.com',defaults={'username':'RightsatUnglueit'}) - except: - rights = None - if instance.user == instance.rights_holder.owner: - ul=(instance.user, rights) - else: - ul=(instance.user, instance.rights_holder.owner, rights) - notification.send(ul, "rights_holder_claim", {'claim': instance,}) -post_save.connect(notify_claim,sender=Claim) - -class RightsHolder(models.Model): - created = models.DateTimeField(auto_now_add=True) - email = models.CharField(max_length=100, blank=True) - rights_holder_name = models.CharField(max_length=100, blank=False) - owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="rights_holder", null=False ) - can_sell = models.BooleanField(default=False) - def __unicode__(self): - return self.rights_holder_name - -class Premium(models.Model): - PREMIUM_TYPES = ((u'00', u'Default'),(u'CU', u'Custom'),(u'XX', u'Inactive')) - TIERS = {"supporter":25, "patron":50, "bibliophile":100} #should load this from fixture - created = models.DateTimeField(auto_now_add=True) - type = models.CharField(max_length=2, choices=PREMIUM_TYPES) - campaign = models.ForeignKey("Campaign", related_name="premiums", null=True) - amount = models.DecimalField(max_digits=10, decimal_places=0, blank=False) - description = models.TextField(null=True, blank=False) - limit = models.IntegerField(default = 0) - - @property - def premium_count(self): - t_model=apps.get_model('payment','Transaction') - return t_model.objects.filter(premium=self).count() - @property - def premium_remaining(self): - t_model=apps.get_model('payment','Transaction') - return self.limit - t_model.objects.filter(premium=self).count() - def __unicode__(self): - return (self.campaign.work.title if self.campaign else '') + ' $' + str(self.amount) - -class PledgeExtra: - def __init__(self,premium=None,anonymous=False,ack_name='',ack_dedication='',offer=None): - self.anonymous = anonymous - self.premium = premium - self.offer = offer - self.extra = {} - if ack_name: - self.extra['ack_name']=ack_name - if ack_dedication: - self.extra['ack_dedication']=ack_dedication - -class CampaignAction(models.Model): - timestamp = models.DateTimeField(auto_now_add=True) - # anticipated types: activated, withdrawn, suspended, restarted, succeeded, failed, unglued - type = models.CharField(max_length=15) - comment = models.TextField(null=True, blank=True) - campaign = models.ForeignKey("Campaign", related_name="actions", null=False) - -class Offer(models.Model): - CHOICES = ((INDIVIDUAL,'Individual license'),(LIBRARY,'Library License')) - work = models.ForeignKey("Work", related_name="offers", null=False) - price = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=False) - license = models.PositiveSmallIntegerField(null = False, default = INDIVIDUAL, - choices=CHOICES) - active = models.BooleanField(default=False) - - @property - def days_per_copy(self): - return Decimal(float(self.price) / self.work.last_campaign().dollar_per_day ) - - @property - def get_thanks_display(self): - if self.license == LIBRARY: - return 'Suggested contribution for libraries' - else: - return 'Suggested contribution for individuals' - -class Acq(models.Model): - """ - Short for Acquisition, this is a made-up word to describe the thing you acquire when you buy or borrow an ebook - """ - CHOICES = ((INDIVIDUAL,'Individual license'),(LIBRARY,'Library License'),(BORROWED,'Borrowed from Library'), (TESTING,'Just for Testing'), (RESERVE,'On Reserve'),(THANKED,'Already Thanked'),) - created = models.DateTimeField(auto_now_add=True, db_index=True,) - expires = models.DateTimeField(null=True) - refreshes = models.DateTimeField(auto_now_add=True) - refreshed = models.BooleanField(default=True) - work = models.ForeignKey("Work", related_name='acqs', null=False) - user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='acqs') - license = models.PositiveSmallIntegerField(null = False, default = INDIVIDUAL, - choices=CHOICES) - watermarked = models.ForeignKey("booxtream.Boox", null=True) - nonce = models.CharField(max_length=32, null=True) - - # when the acq is a loan, this points at the library's acq it's derived from - lib_acq = models.ForeignKey("self", related_name="loans", null=True) - - class mock_ebook(object): - def __init__(self, acq): - self.url = acq.get_mobi_url() - self.format = 'mobi' - self.filesize = 0 - def save(self): - # TODO how to handle filesize? - return True - - def ebook(self): - return self.mock_ebook(self) - - def __unicode__(self): - if self.lib_acq: - return "%s, %s: %s for %s" % (self.work.title, self.get_license_display(), self.lib_acq.user, self.user) - else: - return "%s, %s for %s" % (self.work.title, self.get_license_display(), self.user,) - - @property - def expired(self): - if self.expires is None: - return False - else: - return self.expires < datetime.now() - - def get_mobi_url(self): - if self.expired: - return '' - return self.get_watermarked().download_link_mobi - - def get_epub_url(self): - if self.expired: - return '' - return self.get_watermarked().download_link_epub - - def get_watermarked(self): - if self.watermarked == None or self.watermarked.expired: - if self.on_reserve: - self.borrow(self.user) - do_watermark= self.work.last_campaign().do_watermark - params={ - 'customeremailaddress': self.user.email if do_watermark else '', - 'customername': self.user.username if do_watermark else 'an ungluer', - 'languagecode':'1033', - 'expirydays': 1, - 'downloadlimit': 7, - 'exlibris':0, - 'chapterfooter': 0, - 'disclaimer':0, - 'referenceid': '%s:%s:%s' % (self.work.id, self.user.id, self.id) if do_watermark else 'N/A', - 'kf8mobi': True, - 'epub': True, - } - personalized = personalize(self.work.epubfiles()[0].file, self) - personalized.seek(0) - self.watermarked = watermarker.platform(epubfile= personalized, **params) - self.save() - return self.watermarked - - def _hash(self): - return hashlib.md5('%s:%s:%s:%s'%(settings.SOCIAL_AUTH_TWITTER_SECRET,self.user.id,self.work.id,self.created)).hexdigest() - - def expire_in(self, delta): - self.expires = (now() + delta) if delta else now() - self.save() - if self.lib_acq: - self.lib_acq.refreshes = now() + delta - self.lib_acq.refreshed = False - self.lib_acq.save() - - @property - def on_reserve(self): - return self.license==RESERVE - - def borrow(self, user=None): - if self.on_reserve: - self.license=BORROWED - self.expire_in(timedelta(days=14)) - self.user.wishlist.add_work( self.work, "borrow") - notification.send([self.user], "library_borrow", {'acq':self}) - return self - elif self.borrowable and user: - user.wishlist.add_work( self.work, "borrow") - borrowed = Acq.objects.create(user=user,work=self.work,license= BORROWED, lib_acq=self) - from regluit.core.tasks import watermark_acq - notification.send([user], "library_borrow", {'acq':borrowed}) - watermark_acq.delay(borrowed) - return borrowed - - @property - def borrowable(self): - if self.license == RESERVE and not self.expired: - return True - if self.license == LIBRARY: - return self.refreshes < datetime.now() - else: - return False - - @property - def holds(self): - return Hold.objects.filter(library__user=self.user,work=self.work).order_by('created') - - -def config_acq(sender, instance, created, **kwargs): - if created: - instance.nonce=instance._hash() - instance.save() - if instance.license == RESERVE: - instance.expire_in(timedelta(hours=24)) - if instance.license == BORROWED: - instance.expire_in(timedelta(days=14)) - -post_save.connect(config_acq,sender=Acq) - -class Hold(models.Model): - created = models.DateTimeField(auto_now_add=True) - work = models.ForeignKey("Work", related_name='holds', null=False) - user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='holds', null=False) - library = models.ForeignKey(Library, related_name='holds', null=False) - - def __unicode__(self): - return '%s for %s at %s' % (self.work,self.user.username,self.library) - def ahead(self): - return Hold.objects.filter(work=self.work,library=self.library,created__lt=self.created).count() - -class Campaign(models.Model): - LICENSE_CHOICES = cc.FREECHOICES - created = models.DateTimeField(auto_now_add=True,) - name = models.CharField(max_length=500, null=True, blank=False) - description = RichTextField(null=True, blank=False) - details = RichTextField(null=True, blank=True) - target = models.DecimalField(max_digits=14, decimal_places=2, null=True, default=0.00) - license = models.CharField(max_length=255, choices = LICENSE_CHOICES, default='CC BY-NC-ND') - left = models.DecimalField(max_digits=14, decimal_places=2, null=True, db_index=True,) - deadline = models.DateTimeField(db_index=True, null=True) - dollar_per_day = models.FloatField(null=True) - cc_date_initial = models.DateTimeField(null=True) - activated = models.DateTimeField(null=True, db_index=True,) - paypal_receiver = models.CharField(max_length=100, blank=True) - amazon_receiver = models.CharField(max_length=100, blank=True) - work = models.ForeignKey("Work", related_name="campaigns", null=False) - managers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="campaigns", null=False) - # status: INITIALIZED, ACTIVE, SUSPENDED, WITHDRAWN, SUCCESSFUL, UNSUCCESSFUL - status = models.CharField(max_length=15, null=True, blank=False, default="INITIALIZED", db_index=True,) - type = models.PositiveSmallIntegerField(null = False, default = REWARDS, - choices=((REWARDS,'Pledge-to-unglue campaign'),(BUY2UNGLUE,'Buy-to-unglue campaign'),(THANKS,'Thanks-for-ungluing campaign'))) - edition = models.ForeignKey("Edition", related_name="campaigns", null=True) - email = models.CharField(max_length=100, blank=True) - publisher = models.ForeignKey("Publisher", related_name="campaigns", null=True) - do_watermark = models.BooleanField(default=True) - use_add_ask = models.BooleanField(default=True) - - def __init__(self, *args, **kwargs): - self.problems=[] - return super(Campaign, self).__init__(*args, **kwargs) - - def __unicode__(self): - try: - return u"Campaign for %s" % self.work.title - except: - return u"Campaign %s (no associated work)" % self.name - - def clone(self): - """use a previous UNSUCCESSFUL campaign's data as the basis for a new campaign - assume that B2U campaigns don't need cloning - """ - - if self.clonable(): - old_managers= self.managers.all() - - # copy custom premiums - new_premiums= self.premiums.filter(type='CU') - - # setting pk to None will insert new copy http://stackoverflow.com/a/4736172/7782 - self.pk = None - self.status = 'INITIALIZED' - - # set deadline far in future -- presumably RH will set deadline to proper value before campaign launched - self.deadline = date_today() + timedelta(days=int(settings.UNGLUEIT_LONGEST_DEADLINE)) - - # allow created, activated dates to be autoset by db - self.created = None - self.name = 'copy of %s' % self.name - self.activated = None - self.update_left() - self.save() - self.managers=old_managers - - # clone associated premiums - for premium in new_premiums: - premium.pk=None - premium.created = None - premium.campaign = self - premium.save() - return self - else: - return None - - def clonable(self): - """campaign clonable if it's UNSUCCESSFUL and is the last campaign associated with a Work""" - - if self.status == 'UNSUCCESSFUL' and self.work.last_campaign().id==self.id: - return True - else: - return False - - @property - def launchable(self): - may_launch=True - try: - if self.status != 'INITIALIZED': - if self.status == 'ACTIVE': - self.problems.append(_('The campaign is already launched')) - else: - self.problems.append(_('A campaign must initialized properly before it can be launched')) - may_launch = False - if not self.description: - self.problems.append(_('A campaign must have a description')) - may_launch = False - if self.type==REWARDS: - if self.deadline: - if self.deadline.date()- date_today() > timedelta(days=int(settings.UNGLUEIT_LONGEST_DEADLINE)): - self.problems.append(_('The chosen closing date is more than %s days from now' % settings.UNGLUEIT_LONGEST_DEADLINE)) - may_launch = False - else: - self.problems.append(_('A pledge campaign must have a closing date')) - may_launch = False - if self.target: - if self.target < Decimal(settings.UNGLUEIT_MINIMUM_TARGET): - self.problems.append(_('A pledge campaign may not be launched with a target less than $%s' % settings.UNGLUEIT_MINIMUM_TARGET)) - may_launch = False - else: - self.problems.append(_('A campaign must have a target')) - may_launch = False - if self.type==BUY2UNGLUE: - if self.work.offers.filter(price__gt=0,active=True).count()==0: - self.problems.append(_('You can\'t launch a buy-to-unglue campaign before setting a price for your ebooks' )) - may_launch = False - if EbookFile.objects.filter(edition__work=self.work).count()==0: - self.problems.append(_('You can\'t launch a buy-to-unglue campaign if you don\'t have any ebook files uploaded' )) - may_launch = False - if ((self.cc_date_initial is None) or (self.cc_date_initial > datetime.combine(settings.MAX_CC_DATE, datetime.min.time())) or (self.cc_date_initial < now())): - self.problems.append(_('You must set an initial Ungluing Date that is in the future and not after %s' % settings.MAX_CC_DATE )) - may_launch = False - if self.target: - if self.target < Decimal(settings.UNGLUEIT_MINIMUM_TARGET): - self.problems.append(_('A buy-to-unglue campaign may not be launched with a target less than $%s' % settings.UNGLUEIT_MINIMUM_TARGET)) - may_launch = False - else: - self.problems.append(_('A buy-to-unglue campaign must have a target')) - may_launch = False - if self.type==THANKS: - # the case in which there is no EbookFile and no Ebook associated with work (We have ebooks without ebook files.) - if EbookFile.objects.filter(edition__work=self.work).count()==0 and self.work.ebooks().count()==0: - self.problems.append(_('You can\'t launch a thanks-for-ungluing campaign if you don\'t have any ebook files uploaded' )) - may_launch = False - except Exception as e : - self.problems.append('Exception checking launchability ' + str(e)) - may_launch = False - return may_launch - - - def update_status(self, ignore_deadline_for_success=False, send_notice=False, process_transactions=False): - """Updates the campaign's status. returns true if updated. - for REWARDS: - Computes UNSUCCESSFUL only after the deadline has passed - Computes SUCCESSFUL only after the deadline has passed if ignore_deadline_for_success is TRUE -- otherwise looks just at amount of pledges accumulated - by default, send_notice is False so that we have to explicitly send specify delivery of successful_campaign notice - for BUY2UNGLUE: - Sets SUCCESSFUL when cc_date is in the past. - if process_transactions is True, also execute or cancel associated transactions - - """ - if not self.status=='ACTIVE': - return False - elif self.type==REWARDS: - if (ignore_deadline_for_success or self.deadline < now()) and self.current_total >= self.target: - self.status = 'SUCCESSFUL' - self.save() - action = CampaignAction(campaign=self, type='succeeded', comment = self.current_total) - action.save() - - if process_transactions: - p = PaymentManager() - results = p.execute_campaign(self) - - if send_notice: - successful_campaign.send(sender=None,campaign=self) - - # should be more sophisticated in whether to return True -- look at all the transactions? - return True - elif self.deadline < now() and self.current_total < self.target: - self.status = 'UNSUCCESSFUL' - self.save() - action = CampaignAction(campaign=self, type='failed', comment = self.current_total) - action.save() - - if process_transactions: - p = PaymentManager() - results = p.cancel_campaign(self) - - if send_notice: - regluit.core.signals.unsuccessful_campaign.send(sender=None,campaign=self) - # should be more sophisticated in whether to return True -- look at all the transactions? - return True - elif self.type==BUY2UNGLUE: - if self.cc_date < date_today(): - self.status = 'SUCCESSFUL' - self.save() - action = CampaignAction(campaign=self, type='succeeded', comment = self.current_total) - action.save() - self.watermark_success() - if send_notice: - successful_campaign.send(sender=None,campaign=self) - - # should be more sophisticated in whether to return True -- look at all the transactions? - return True - - return False - - _current_total = None - @property - def current_total(self): - if self._current_total is None: - p = PaymentManager() - self._current_total = p.query_campaign(self,summary=True, campaign_total=True) - return self._current_total - - def set_dollar_per_day(self): - if self.status!='INITIALIZED' and self.dollar_per_day: - return self.dollar_per_day - if self.cc_date_initial is None: - return None - - start_datetime= self.activated if self.activated else datetime.today() - - time_to_cc = self.cc_date_initial - start_datetime - - self.dollar_per_day = float(self.target)/float(time_to_cc.days) - if self.status!='DEMO': - self.save() - return self.dollar_per_day - - def set_cc_date_initial(self, a_date=settings.MAX_CC_DATE): - self.cc_date_initial = datetime.combine(a_date, datetime.min.time()) - - @property - def cc_date(self): - if self.type in { REWARDS, THANKS }: - return None - if self.dollar_per_day == None: - return self.cc_date_initial.date() - cc_advance_days = float(self.current_total) / self.dollar_per_day - return (self.cc_date_initial-timedelta(days=cc_advance_days)).date() - - - def update_left(self): - self._current_total=None - if self.type == THANKS: - self.left == Decimal(0.00) - elif self.type == BUY2UNGLUE: - self.left = Decimal(self.dollar_per_day*float((self.cc_date_initial - datetime.today()).days))-self.current_total - else: - self.left = self.target - self.current_total - if self.status != 'DEMO': - self.save() - - def transactions(self, **kwargs): - p = PaymentManager() - - # handle default parameter values - kw = {'summary':False, 'campaign_total':True} - kw.update(kwargs) - - return p.query_campaign(self, **kw) - - - def activate(self): - status = self.status - if status != 'INITIALIZED': - raise UnglueitError(_('Campaign needs to be initialized in order to be activated')) - try: - active_claim = self.work.claim.filter(status="active")[0] - except IndexError, e: - raise UnglueitError(_('Campaign needs to have an active claim in order to be activated')) - if not self.launchable: - raise UnglueitError('Configuration issues need to be addressed before campaign is activated: %s' % unicode(self.problems[0])) - self.status= 'ACTIVE' - self.left = self.target - self.activated = datetime.today() - if self.type == THANKS: - # make ebooks from ebookfiles - self.work.make_ebooks_from_ebfs() - self.save() - action = CampaignAction( campaign = self, type='activated', comment = self.get_type_display()) - ungluers = self.work.wished_by() - notification.queue(ungluers, "wishlist_active", {'campaign':self}, True) - return self - - - def suspend(self, reason): - status = self.status - if status != 'ACTIVE': - raise UnglueitError(_('Campaign needs to be active in order to be suspended')) - action = CampaignAction( campaign = self, type='suspended', comment = reason) - action.save() - self.status='SUSPENDED' - self.save() - return self - - def withdraw(self, reason): - status = self.status - if status != 'ACTIVE': - raise UnglueitError(_('Campaign needs to be active in order to be withdrawn')) - action = CampaignAction( campaign = self, type='withdrawn', comment = reason) - action.save() - self.status='WITHDRAWN' - self.save() - return self - - def resume(self, reason): - """Change campaign status from SUSPENDED to ACTIVE. We may want to track reason for resuming and track history""" - status = self.status - if status != 'SUSPENDED': - raise UnglueitError(_('Campaign needs to be suspended in order to be resumed')) - if not reason: - reason='' - action = CampaignAction( campaign = self, type='restarted', comment = reason) - action.save() - self.status= 'ACTIVE' - self.save() - return self - - def supporters(self): - # expensive query used in loop; stash it - if hasattr(self, '_translist_'): - return self._translist_ - - """nb: returns (distinct) supporter IDs, not supporter objects""" - self._translist_ = self.transactions().filter(status=TRANSACTION_STATUS_ACTIVE).values_list('user', flat=True).distinct() - return self._translist_ - - @property - def supporters_count(self): - # avoid transmitting the whole list if you don't need to; let the db do the count. - active = self.transactions().filter(status=TRANSACTION_STATUS_ACTIVE).values_list('user', flat=True).distinct().count() - complete = self.transactions().filter(status=TRANSACTION_STATUS_COMPLETE).values_list('user', flat=True).distinct().count() - return active+complete - @property - def anon_count(self): - # avoid transmitting the whole list if you don't need to; let the db do the count. - complete = self.transactions().filter(status=TRANSACTION_STATUS_COMPLETE,user=None).count() - return complete - - def transaction_to_recharge(self, user): - """given a user, return the transaction to be recharged if there is one -- None otherwise""" - - # only if a campaign is SUCCESSFUL, we allow for recharged - - if self.status == 'SUCCESSFUL': - if self.transaction_set.filter(Q(user=user) & (Q(status=TRANSACTION_STATUS_COMPLETE) | Q(status=TRANSACTION_STATUS_ACTIVE))).count(): - # presence of an active or complete transaction means no transaction to recharge - return None - else: - transactions = self.transaction_set.filter(Q(user=user) & (Q(status=TRANSACTION_STATUS_ERROR) | Q(status=TRANSACTION_STATUS_FAILED))) - # assumption --that the first failed/errored transaction has the amount we need to recharge - if transactions.count(): - return transactions[0] - else: - return None - else: - return None - - def ungluers(self): - # expensive query used in loop; stash it - if hasattr(self, '_ungluers_'): - return self._ungluers_ - p = PaymentManager() - ungluers={"all":[],"supporters":[], "patrons":[], "bibliophiles":[]} - if self.status == "ACTIVE": - translist = p.query_campaign(self, summary=False, pledged=True, authorized=True) - elif self.status == "SUCCESSFUL": - translist = p.query_campaign(self, summary=False, pledged=True, completed=True) - else: - translist = [] - for transaction in translist: - ungluers['all'].append(transaction.user) - if not transaction.anonymous: - if transaction.amount >= Premium.TIERS["bibliophile"]: - ungluers['bibliophiles'].append(transaction.user) - elif transaction.amount >= Premium.TIERS["patron"]: - ungluers['patrons'].append(transaction.user) - elif transaction.amount >= Premium.TIERS["supporter"]: - ungluers['supporters'].append(transaction.user) - - self._ungluers_= ungluers - return ungluers - - def ungluer_transactions(self): - """ - returns a list of authorized transactions for campaigns in progress, - or completed transactions for successful campaigns - used to build the acks page -- because ack_name, _link, _dedication adhere to transactions, - it's easier to return transactions than ungluers - """ - p = PaymentManager() - ungluers={"all":[],"supporters":[], "anon_supporters": 0, "patrons":[], "anon_patrons": 0, "bibliophiles":[]} - if self.status == "ACTIVE": - translist = p.query_campaign(self, summary=False, pledged=True, authorized=True) - elif self.status == "SUCCESSFUL": - translist = p.query_campaign(self, summary=False, pledged=True, completed=True) - else: - translist = [] - for transaction in translist: - ungluers['all'].append(transaction) - if transaction.amount >= Premium.TIERS["bibliophile"]: - ungluers['bibliophiles'].append(transaction) - elif transaction.amount >= Premium.TIERS["patron"]: - if transaction.anonymous: - ungluers['anon_patrons'] += 1 - else: - ungluers['patrons'].append(transaction) - elif transaction.amount >= Premium.TIERS["supporter"]: - if transaction.anonymous: - ungluers['anon_supporters'] += 1 - else: - ungluers['supporters'].append(transaction) - - return ungluers - - def effective_premiums(self): - """returns the available premiums for the Campaign including any default premiums""" - if self.type is BUY2UNGLUE: - return Premium.objects.none() - q = Q(campaign=self) | Q(campaign__isnull=True) - return Premium.objects.filter(q).exclude(type='XX').order_by('amount') - - def custom_premiums(self): - """returns only the active custom premiums for the Campaign""" - if self.type is BUY2UNGLUE: - return Premium.objects.none() - return Premium.objects.filter(campaign=self).filter(type='CU').order_by('amount') - - @property - def library_offer(self): - return self._offer(LIBRARY) - - @property - def individual_offer(self): - return self._offer(INDIVIDUAL) - - def _offer(self, license): - if self.type is REWARDS: - return None - try: - return Offer.objects.get(work=self.work, active=True, license=license) - except Offer.DoesNotExist: - return None - - @property - def ask_money(self): - # true if there's an offer asking for money - if self.type is REWARDS: - return True - try: - Offer.objects.get(work=self.work, active=True, price__gt=0.00) - return True - except Offer.DoesNotExist: - return False - except Offer.MultipleObjectsReturned: - return True - - @property - def days_per_copy(self): - if self.individual_offer: - return Decimal(float(self.individual_offer.price) / self.dollar_per_day ) - else: - return Decimal(0) - - @property - def rh(self): - """returns the rights holder for an active or initialized campaign""" - try: - q = Q(status='ACTIVE') | Q(status='INITIALIZED') - rh = self.work.claim.filter(q)[0].rights_holder - return rh - except: - return None - @property - def rightsholder(self): - """returns the name of the rights holder for an active or initialized campaign""" - try: - return self.rh.rights_holder_name - except: - return '' - - @property - def license_url(self): - return cc.CCLicense.url(self.license) - - @property - def license_badge(self): - return cc.CCLicense.badge(self.license) - - @property - def success_date(self): - if self.status == 'SUCCESSFUL': - try: - return self.actions.filter(type='succeeded')[0].timestamp - except: - return '' - return '' - - def percent_of_goal(self): - if self.type == THANKS: - return 100 - percent = 0 - if(self.status == 'SUCCESSFUL' or self.status == 'ACTIVE'): - if self.type == BUY2UNGLUE: - percent = int(100 - 100*self.left/self.target) - else: - percent = int(self.current_total/self.target*100) - return percent - - @property - def countdown(self): - from math import ceil - if not self.deadline: - return '' - time_remaining = self.deadline - now() - countdown = "" - - if time_remaining.days: - countdown = "%s days" % str(time_remaining.days + 1) - elif time_remaining.seconds > 3600: - countdown = "%s hours" % str(time_remaining.seconds/3600 + 1) - elif time_remaining.seconds > 60: - countdown = "%s minutes" % str(time_remaining.seconds/60 + 1) - else: - countdown = "Seconds" - - return countdown - - @property - def deadline_or_now(self): - return self.deadline if self.deadline else now() - - @classmethod - def latest_ending(cls): - return (timedelta(days=int(settings.UNGLUEIT_LONGEST_DEADLINE)) + now()) - - def make_mobi(self): - for ebf in self.work.ebookfiles().filter(format='epub').order_by('-created'): - if ebf.active: - new_mobi_ebf = EbookFile.objects.create(edition=ebf.edition, format='mobi', asking=ebf.asking) - new_mobi_ebf.file.save(path_for_file('ebf',None),ContentFile(mobi.convert_to_mobi(ebf.file.url))) - new_mobi_ebf.save() - self.work.make_ebooks_from_ebfs() - return True - return False - - def add_ask_to_ebfs(self, position=0): - if not self.use_add_ask or self.type != THANKS : - return - pdf_to_do = pdf_edition = None - epub_to_do = epub_edition = None - new_ebfs = {} - for ebf in self.work.ebookfiles().filter(asking = False).order_by('-created'): - if ebf.format=='pdf' and not pdf_to_do: - ebf.file.open() - pdf_to_do = ebf.file.read() - pdf_edition = ebf.edition - elif ebf.format=='epub' and not epub_to_do: - ebf.file.open() - epub_to_do = ebf.file.read() - epub_edition = ebf.edition - for ebook in self.work.ebooks_all().exclude(provider='Unglue.it'): - if ebook.format=='pdf' and not pdf_to_do: - r= requests.get(ebook.url) - pdf_to_do = r.content - pdf_edition = ebook.edition - elif ebook.format=='epub' and not epub_to_do: - r= requests.get(ebook.url) - epub_to_do = r.content - epub_edition = ebook.edition - if pdf_to_do: - try: - added = ask_pdf({'campaign':self, 'work':self.work, 'site':Site.objects.get_current()}) - new_file = SpooledTemporaryFile() - old_file = SpooledTemporaryFile() - old_file.write(pdf_to_do) - if position==0: - pdf_append(added, old_file, new_file) - else: - pdf_append(old_file, added, new_file) - new_file.seek(0) - new_pdf_ebf = EbookFile.objects.create(edition=pdf_edition, format='pdf', asking=True) - new_pdf_ebf.file.save(path_for_file('ebf',None),ContentFile(new_file.read())) - new_pdf_ebf.save() - new_ebfs['pdf']=new_pdf_ebf - except Exception as e: - logger.error("error appending pdf ask %s" % (e)) - if epub_to_do: - try: - old_file = SpooledTemporaryFile() - old_file.write(epub_to_do) - new_file= ask_epub(old_file, {'campaign':self, 'work':self.work, 'site':Site.objects.get_current()}) - new_file.seek(0) - new_epub_ebf = EbookFile.objects.create(edition=epub_edition, format='epub', asking=True) - new_epub_ebf.file.save(path_for_file(new_epub_ebf,None),ContentFile(new_file.read())) - new_epub_ebf.save() - new_ebfs['epub']=new_epub_ebf - # now make the mobi file - new_mobi_ebf = EbookFile.objects.create(edition=epub_edition, format='mobi', asking=True) - new_mobi_ebf.file.save(path_for_file('ebf',None),ContentFile(mobi.convert_to_mobi(new_epub_ebf.file.url))) - new_mobi_ebf.save() - new_ebfs['mobi']=new_mobi_ebf - except Exception as e: - logger.error("error making epub ask or mobi %s" % (e)) - for key in new_ebfs.keys(): - for old_ebf in self.work.ebookfiles().filter(asking = True, format=key).exclude(pk=new_ebfs[key].pk): - obsolete = Ebook.objects.filter(url=old_ebf.file.url) - for eb in obsolete: - eb.deactivate() - old_ebf.file.delete() - old_ebf.delete() - self.work.make_ebooks_from_ebfs(add_ask=True) - - def make_unglued_ebf(self, format, watermarked): - r=urllib2.urlopen(watermarked.download_link(format)) - ebf=EbookFile.objects.create(edition=self.work.preferred_edition, format=format) - ebf.file.save(path_for_file(ebf,None),ContentFile(r.read())) - ebf.file.close() - ebf.save() - ebook=Ebook.objects.create( - edition=self.work.preferred_edition, - format=format, - rights=self.license, - provider="Unglue.it", - url= settings.BASE_URL_SECURE + reverse('download_campaign',args=[self.work.id,format]), - ) - old_ebooks = Ebook.objects.exclude(pk=ebook.pk).filter( - edition=self.work.preferred_edition, - format=format, - rights=self.license, - provider="Unglue.it", - ) - for old_ebook in old_ebooks: - old_ebook.deactivate() - return ebook.pk - - - def watermark_success(self): - if self.status == 'SUCCESSFUL' and self.type == BUY2UNGLUE: - params={ - 'customeremailaddress': self.license, - 'customername': 'The Public', - 'languagecode':'1033', - 'expirydays': 1, - 'downloadlimit': 7, - 'exlibris':0, - 'chapterfooter':0, - 'disclaimer':0, - 'referenceid': '%s:%s:%s' % (self.work.id, self.id, self.license), - 'kf8mobi': True, - 'epub': True, - } - ungluified = ungluify(self.work.epubfiles()[0].file, self) - ungluified.filename.seek(0) - watermarked = watermarker.platform(epubfile= ungluified.filename, **params) - self.make_unglued_ebf('epub', watermarked) - self.make_unglued_ebf('mobi', watermarked) - return True - return False - - def is_pledge(self): - return self.type==REWARDS - - @property - def user_to_pay(self): - return self.rh.owner - - ### for compatibility with MARC output - def marc_records(self): - return self.work.marc_records() - -class Identifier(models.Model): - # olib, ltwk, goog, gdrd, thng, isbn, oclc, olwk, olib, gute, glue - type = models.CharField(max_length=4, null=False) - value = models.CharField(max_length=250, null=False) - work = models.ForeignKey("Work", related_name="identifiers", null=False) - edition = models.ForeignKey("Edition", related_name="identifiers", null=True) - - class Meta: - unique_together = ("type", "value") - - @staticmethod - def set(type=None, value=None, edition=None, work=None): - # if there's already an id of this type for this work and edition, change it - # if not, create it. if the id exists and points to something else, change it. - identifier= Identifier.get_or_add(type=type, value=value, edition = edition, work=work) - if identifier.work.id != work.id: - identifier.work=work - identifier.save() - if identifier.edition and edition: - if identifier.edition.id != edition.id: - identifier.edition = edition - identifier.save() - others= Identifier.objects.filter(type=type, work=work, edition=edition).exclude(value=value) - if others.count()>0: - for other in others: - other.delete() - return identifier - - @staticmethod - def get_or_add( type='goog', value=None, edition=None, work=None): - try: - return Identifier.objects.get(type=type, value=value) - except Identifier.DoesNotExist: - i=Identifier(type=type, value=value, edition=edition, work=work) - i.save() - return i - - def __unicode__(self): - return u'{0}:{1}'.format(self.type, self.value) - -class Work(models.Model): - created = models.DateTimeField(auto_now_add=True, db_index=True,) - title = models.CharField(max_length=1000) - language = models.CharField(max_length=5, default="en", null=False, db_index=True,) - openlibrary_lookup = models.DateTimeField(null=True) - num_wishes = models.IntegerField(default=0, db_index=True) - description = models.TextField(default='', null=True, blank=True) - selected_edition = models.ForeignKey("Edition", related_name = 'selected_works', null = True) - # repurposed earliest_publication to actually be publication range - publication_range = models.CharField(max_length=50, null = True) - featured = models.DateTimeField(null=True, blank=True, db_index=True,) - is_free = models.BooleanField(default=False) - landings = GenericRelation(Landing) - - class Meta: - ordering = ['title'] - def __unicode__(self): - return self.title - - def __init__(self, *args, **kwargs): - self._last_campaign = None - super(Work, self).__init__(*args, **kwargs) - - @property - def googlebooks_id(self): - try: - preferred_id=self.preferred_edition.googlebooks_id - # note that there should always be a preferred edition - except AttributeError: - # this work has no edition. - return '' - if preferred_id: - return preferred_id - try: - return self.identifiers.filter(type='goog')[0].value - except IndexError: - return '' - - @property - def googlebooks_url(self): - if self.googlebooks_id: - return "http://books.google.com/books?id=%s" % self.googlebooks_id - else: - return '' - - @property - def goodreads_id(self): - preferred_id=self.preferred_edition.goodreads_id - if preferred_id: - return preferred_id - try: - return self.identifiers.filter(type='gdrd')[0].value - except IndexError: - return '' - - @property - def goodreads_url(self): - return "http://www.goodreads.com/book/show/%s" % self.goodreads_id - - @property - def librarything_id(self): - try: - return self.identifiers.filter(type='ltwk')[0].value - except IndexError: - return '' - - @property - def librarything_url(self): - return "http://www.librarything.com/work/%s" % self.librarything_id - - @property - def openlibrary_id(self): - try: - return self.identifiers.filter(type='olwk')[0].value - except IndexError: - return '' - - @property - def openlibrary_url(self): - return "http://openlibrary.org" + self.openlibrary_id - - def cover_filetype(self): - if self.uses_google_cover(): - return 'jpeg' - else: - # consider the path only and not the params, query, or fragment - url = urlparse(self.cover_image_small().lower()).path - - if url.endswith('.png'): - return 'png' - elif url.endswith('.gif'): - return 'gif' - elif url.endswith('.jpg') or url.endswith('.jpeg'): - return 'jpeg' - else: - return 'image' - - def uses_google_cover(self): - if self.preferred_edition and self.preferred_edition.cover_image: - return False - else: - return self.googlebooks_id - - def cover_image_large(self): - if self.preferred_edition and self.preferred_edition.has_cover_image(): - return self.preferred_edition.cover_image_large() - return "/static/images/generic_cover_larger.png" - - def cover_image_small(self): - if self.preferred_edition and self.preferred_edition.has_cover_image(): - return self.preferred_edition.cover_image_small() - return "/static/images/generic_cover_larger.png" - - def cover_image_thumbnail(self): - try: - if self.preferred_edition and self.preferred_edition.has_cover_image(): - return self.preferred_edition.cover_image_thumbnail() - except IndexError: - pass - return "/static/images/generic_cover_larger.png" - - def authors(self): - # assumes that they come out in the same order they go in! - if self.preferred_edition and self.preferred_edition.authors.all().count()>0: - return self.preferred_edition.authors.all() - for edition in self.editions.all(): - if edition.authors.all().count()>0: - return edition.authors.all() - return Author.objects.none() - - def relators(self): - # assumes that they come out in the same order they go in! - if self.preferred_edition and self.preferred_edition.relators.all().count()>0: - return self.preferred_edition.relators.all() - for edition in self.editions.all(): - if edition.relators.all().count()>0: - return edition.relators.all() - return Relator.objects.none() - - def author(self): - # assumes that they come out in the same order they go in! - if self.relators().count()>0: - return self.relators()[0].name - return '' - - def authors_short(self): - # assumes that they come out in the same order they go in! - if self.relators().count()==1: - return self.relators()[0].name - elif self.relators().count()==2: - if self.relators()[0].relation == self.relators()[1].relation: - if self.relators()[0].relation.code == 'aut': - return "%s and %s" % (self.relators()[0].author.name, self.relators()[1].author.name) - else: - return "%s and %s, %ss" % (self.relators()[0].author.name, self.relators()[1].author.name, self.relators()[0].relation.name) - else: - return "%s (%s) and %s (%s)" % (self.relators()[0].author.name, self.relators()[0].relation.name, self.relators()[1].author.name, self.relators()[1].relation.name) - elif self.relators().count()>2: - auths = self.relators().order_by("relation__code") - if auths[0].relation.code == 'aut': - return "%s et al." % auths[0].author.name - else: - return "%s et al. (%ss)" % (auths[0].author.name , auths[0].relation.name ) - return '' - - def kindle_safe_title(self): - """ - Removes accents, keeps letters and numbers, replaces non-Latin characters with "#", and replaces punctuation with "_" - """ - safe = u'' - nkfd_form = unicodedata.normalize('NFKD', self.title) #unaccent accented letters - for c in nkfd_form: - ccat = unicodedata.category(c) - #print ccat - if ccat.startswith('L') or ccat.startswith('N'): # only letters and numbers - if ord(c) > 127: - safe = safe + '#' #a non latin script letter or number - else: - safe = safe + c - elif not unicodedata.combining(c): #not accents (combining forms) - safe = safe + '_' #punctuation - return safe - - def last_campaign(self): - # stash away the last campaign to prevent repeated lookups - if hasattr(self, '_last_campaign_'): - return self._last_campaign_ - try: - self._last_campaign_ = self.campaigns.order_by('-created')[0] - except IndexError: - self._last_campaign_ = None - return self._last_campaign_ - - @property - def preferred_edition(self): - if self.selected_edition: - return self.selected_edition - if self.last_campaign(): - if self.last_campaign().edition: - self.selected_edition = self.last_campaign().edition - self.save() - return self.last_campaign().edition - try: - self.selected_edition = self.editions.all().order_by('-cover_image', '-created')[0] # prefer editions with covers - self.save() - return self.selected_edition - except IndexError: - #should only happen if there are no editions for the work, - #which can happen when works are being merged - try: - return WasWork.objects.get(was=self.id).work.preferred_edition - except WasWork.DoesNotExist: - #should not happen - logger.warning('work {} has no edition'.format(self.id)) - return None - - def last_campaign_status(self): - campaign = self.last_campaign() - if campaign: - status = campaign.status - else: - if self.first_ebook(): - status = "Available" - else: - status = "No campaign yet" - return status - - def percent_unglued(self): - status = 0 - campaign = self.last_campaign() - if campaign is not None: - if(campaign.status == 'SUCCESSFUL'): - status = 6 - elif(campaign.status == 'ACTIVE'): - if campaign.target is not None: - target = float(campaign.target) - else: - #shouldn't happen, but did once because of a failed pdf conversion - return 0 - if target <= 0: - status = 6 - else: - if campaign.type == BUY2UNGLUE: - status = int( 6 - 6*campaign.left/campaign.target) - else: - status = int(float(campaign.current_total)*6/target) - if status >= 6: - status = 6 - return status - - def percent_of_goal(self): - campaign = self.last_campaign() - return 0 if campaign is None else campaign.percent_of_goal() - - def ebooks_all(self): - return self.ebooks(all=True) - - def ebooks(self, all=False): - if all: - return Ebook.objects.filter(edition__work=self).order_by('-created') - else: - return Ebook.objects.filter(edition__work=self,active=True).order_by('-created') - - def ebookfiles(self): - return EbookFile.objects.filter(edition__work=self).exclude(file='').order_by('-created') - - def epubfiles(self): - # filter out non-epub because that's what booxtream accepts - return EbookFile.objects.filter(edition__work=self, format='epub').exclude(file='').order_by('-created') - - def mobifiles(self): - return EbookFile.objects.filter(edition__work=self, format='mobi').exclude(file='').order_by('-created') - - def pdffiles(self): - return EbookFile.objects.filter(edition__work=self, format='pdf').exclude(file='').order_by('-created') - - def formats(self): - fmts=[] - for fmt in ['pdf', 'epub', 'mobi', 'html']: - for ebook in self.ebooks().filter(format=fmt): - fmts.append(fmt) - break - return fmts - - def make_ebooks_from_ebfs(self, add_ask=True): - # either the ebf has been uploaded or a created (perhaps an ask was added or mobi generated) - if self.last_campaign().type != THANKS: # just to make sure that ebf's can be unglued by mistake - return - ebfs=EbookFile.objects.filter(edition__work=self).exclude(file='').order_by('-created') - done_formats= [] - for ebf in ebfs: - previous_ebooks=Ebook.objects.filter(url= ebf.file.url,) - try: - previous_ebook = previous_ebooks[0] - for eb in previous_ebooks[1:]: #housekeeping - eb.deactivate() - except IndexError: - previous_ebook = None - - if ebf.format not in done_formats: - if ebf.asking==add_ask or ebf.format=='mobi': - if previous_ebook: - previous_ebook.activate() - else: - ebook=Ebook.objects.get_or_create( - edition=ebf.edition, - format=ebf.format, - rights=self.last_campaign().license, - provider="Unglue.it", - url= ebf.file.url, - ) - done_formats.append(ebf.format) - elif previous_ebook: - previous_ebook.deactivate() - elif previous_ebook: - previous_ebook.deactivate() - return - - def remove_old_ebooks(self): - old=Ebook.objects.filter(edition__work=self, active=True).order_by('-created') - done_formats= [] - for eb in old: - if eb.format in done_formats: - eb.deactivate() - else: - done_formats.append(eb.format) - null_files=EbookFile.objects.filter(edition__work=self, file='') - for ebf in null_files: - ebf.file.delete() - ebf.delete() - - @property - def download_count(self): - dlc=0 - for ebook in self.ebooks(all=True): - dlc += ebook.download_count - return dlc - - def first_pdf(self): - return self.first_ebook('pdf') - - def first_epub(self): - return self.first_ebook('epub') - - def first_pdf_url(self): - try: - url = self.first_ebook('pdf').url - return url - except: - return None - - def first_epub_url(self): - try: - url = self.first_ebook('epub').url - return url - except: - return None - - def first_ebook(self, ebook_format=None): - if ebook_format: - for ebook in self.ebooks().filter(format=ebook_format): - return ebook - else: - for ebook in self.ebooks(): - return ebook - - 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() - - def priority(self): - if self.last_campaign(): - return 5 - freedom = 1 if self.is_free else 0 - wishing = int(math.log(self.num_wishes )) + 1 if self.num_wishes else 0 - return min( freedom + wishing, 5 ) - - def first_oclc(self): - if self.preferred_edition == None: - return '' - preferred_id=self.preferred_edition.oclc - if preferred_id: - return preferred_id - try: - return self.identifiers.filter(type='oclc')[0].value - except IndexError: - return '' - - def first_isbn_13(self): - if self.preferred_edition == None: - return '' - preferred_id=self.preferred_edition.isbn_13 - if preferred_id: - return preferred_id - try: - return self.identifiers.filter(type='isbn')[0].value - except IndexError: - return '' - - @property - def earliest_publication_date(self): - for edition in Edition.objects.filter(work=self, publication_date__isnull=False).order_by('publication_date'): - if edition.publication_date and len(edition.publication_date)>=4: - return edition.publication_date - - @property - def publication_date(self): - if self.publication_range: - return self.publication_range - for edition in Edition.objects.filter(work=self, publication_date__isnull=False).order_by('publication_date'): - if edition.publication_date: - try: - earliest_publication = edition.publication_date[:4] - except IndexError: - continue - latest_publication = None - for edition in Edition.objects.filter(work=self, publication_date__isnull=False).order_by('-publication_date'): - if edition.publication_date: - try: - latest_publication = edition.publication_date[:4] - except IndexError: - continue - break - if earliest_publication == latest_publication: - publication_range = earliest_publication - else: - publication_range = earliest_publication + "-" + latest_publication - self.publication_range = publication_range - self.save() - return publication_range - return '' - - @property - def has_unglued_edition(self): - """ - allows us to distinguish successful campaigns with ebooks still in progress from successful campaigns with ebooks available - """ - if self.ebooks().filter(edition__unglued=True): - return True - return False - - @property - def user_with_rights(self): - """ - return queryset of users (should be at most one) who act for rights holders with active claims to the work - """ - claims = self.claim.filter(status='active') - assert claims.count() < 2, "There is more than one active claim on %r" % self.title - try: - return claims[0].user - except: - return False - - def get_absolute_url(self): - return reverse('work', args=[str(self.id)]) - - def publishers(self): - # returns a set of publishers associated with this Work - return Publisher.objects.filter(name__editions__work=self).distinct() - - def create_offers(self): - for choice in Offer.CHOICES: - if not self.offers.filter(license=choice[0]): - self.offers.create(license=choice[0],active=True,price=Decimal(10)) - return self.offers.all() - - def get_lib_license(self,user): - lib_user=(lib.user for lib in user.profile.libraries) - return self.get_user_license(lib_user) - - def borrowable(self, user): - if user.is_anonymous(): - return False - lib_license=self.get_lib_license(user) - if lib_license and lib_license.borrowable: - return True - return False - - def lib_thanked(self, user): - if user.is_anonymous(): - return False - lib_license=self.get_lib_license(user) - if lib_license and lib_license.thanked: - return True - return False - - def in_library(self,user): - if user.is_anonymous(): - return False - lib_license=self.get_lib_license(user) - if lib_license and lib_license.acqs.count(): - return True - return False - - @property - def lib_acqs(self): - return self.acqs.filter(license=LIBRARY) - - @property - def test_acqs(self): - return self.acqs.filter(license=TESTING).order_by('-created') - - class user_license: - acqs=Acq.objects.none() - def __init__(self,acqs): - self.acqs=acqs - - @property - def is_active(self): - return self.acqs.filter(expires__isnull = True).count()>0 or self.acqs.filter(expires__gt= now()).count()>0 - - @property - def borrowed(self): - loans = self.acqs.filter(license=BORROWED,expires__gt= now()) - if loans.count()==0: - return None - else: - return loans[0] - - @property - def purchased(self): - purchases = self.acqs.filter(license=INDIVIDUAL, expires__isnull = True) - if purchases.count()==0: - return None - else: - return purchases[0] - - @property - def thanked(self): - purchases = self.acqs.filter(license=THANKED) - if purchases.count()==0: - return None - else: - return purchases[0] - - @property - def lib_acqs(self): - return self.acqs.filter(license=LIBRARY) - - @property - def next_acq(self): - """ This is the next available copy in the user's libraries""" - loans = self.acqs.filter(license=LIBRARY, refreshes__gt=now()).order_by('refreshes') - if loans.count()==0: - return None - else: - return loans[0] - - @property - def borrowable(self): - return self.acqs.filter(license=LIBRARY, refreshes__lt=now()).count()>0 - - @property - def thanked(self): - return self.acqs.filter(license=THANKED).count()>0 - - @property - def borrowable_acq(self): - for acq in self.acqs.filter(license=LIBRARY, refreshes__lt=now()): - return acq - else: - return None - - @property - def is_duplicate(self): - # does user have two individual licenses? - pending = self.acqs.filter(license=INDIVIDUAL, expires__isnull = True, gifts__used__isnull = True).count() - return self.acqs.filter(license=INDIVIDUAL, expires__isnull = True).count() > pending - - - def get_user_license(self, user): - """ This is all the acqs, wrapped in user_license object for the work, user(s) """ - if user==None: - return None - if hasattr(user, 'is_anonymous'): - if user.is_anonymous(): - return None - return self.user_license(self.acqs.filter(user=user)) - else: - # assume it's several users - return self.user_license(self.acqs.filter(user__in=user)) - - @property - def has_marc(self): - for record in NewMARC.objects.filter(edition__work=self): - return True - return False - - ### for compatibility with MARC output - def marc_records(self): - record_list = [] - record_list.extend(NewMARC.objects.filter(edition__work=self)) - for obj in record_list: - break - else: - for ebook in self.ebooks(): - record_list.append(ebook.edition) - break - return record_list - - - -class Author(models.Model): - created = models.DateTimeField(auto_now_add=True) - name = models.CharField(max_length=255, unique=True) - editions = models.ManyToManyField("Edition", related_name="authors", through="Relator") - - def __unicode__(self): - return self.name - - @property - def last_name_first(self): - names = self.name.rsplit() - if len(names) == 0: - return '' - elif len(names) == 1: - return names[0] - elif len(names) == 2: - return names[1] + ", " + names[0] - else: - reversed_name= names[-1]+"," - for name in names[0:-1]: - reversed_name+=" " - reversed_name+=name - return reversed_name - -class Relation(models.Model): - code = models.CharField(max_length=3, blank=False, db_index=True, unique=True) - name = models.CharField(max_length=30, blank=True,) - -class Relator(models.Model): - relation = models.ForeignKey('Relation', default=1) #first relation should have code='aut' - author = models.ForeignKey('Author') - edition = models.ForeignKey('Edition', related_name='relators') - class Meta: - db_table = 'core_author_editions' - - @property - def name(self): - if self.relation.code == 'aut': - return self.author.name - else: - return "%s (%s)" % (self.author.name, self.relation.name) - - def set (self, relation_code): - if self.relation.code != relation_code: - try: - self.relation = Relation.objects.get(code = relation_code) - self.save() - except Relation.DoesNotExist: - logger.warning("relation not found: code = %s" % relation_code) - -class Subject(models.Model): - created = models.DateTimeField(auto_now_add=True) - name = models.CharField(max_length=200, unique=True) - works = models.ManyToManyField("Work", related_name="subjects") - is_visible = models.BooleanField(default = True) - authority = models.CharField(max_length=10, blank=False, default="") - - class Meta: - ordering = ['name'] - - def __unicode__(self): - return self.name - - - @property - def kw(self): - return 'kw.%s' % self.name - - def free_works(self): - return self.works.filter( is_free = True ) - -class Edition(models.Model): - created = models.DateTimeField(auto_now_add=True) - title = models.CharField(max_length=1000) - publisher_name = models.ForeignKey("PublisherName", related_name="editions", null=True) - publication_date = models.CharField(max_length=50, null=True, blank=True, db_index=True,) - work = models.ForeignKey("Work", related_name="editions", null=True) - cover_image = models.URLField(null=True, blank=True) - unglued = models.BooleanField(default=False) - - def __unicode__(self): - if self.isbn_13: - return "%s (ISBN %s) %s" % (self.title, self.isbn_13, self.publisher) - if self.oclc: - return "%s (OCLC %s) %s" % (self.title, self.oclc, self.publisher) - if self.googlebooks_id: - return "%s (GOOG %s) %s" % (self.title, self.googlebooks_id, self.publisher) - else: - return "%s (GLUE %s) %s" % (self.title, self.id, self.publisher) - - def cover_image_large(self): - #550 pixel high image - if self.cover_image: - im = get_thumbnail(self.cover_image, 'x550', crop='noop', quality=95) - if im.exists(): - return im.url - elif self.googlebooks_id: - url = "https://encrypted.google.com/books?id=%s&printsec=frontcover&img=1&zoom=0" % self.googlebooks_id - im = get_thumbnail(url, 'x550', crop='noop', quality=95) - if not im.exists() or im.storage.size(im.name)==16392: # check for "image not available" image - url = "https://encrypted.google.com/books?id=%s&printsec=frontcover&img=1&zoom=1" % self.googlebooks_id - im = get_thumbnail(url, 'x550', crop='noop', quality=95) - if im.exists(): - return im.url - else: - return '' - else: - return '' - - def cover_image_small(self): - #80 pixel high image - if self.cover_image: - im = get_thumbnail(self.cover_image, 'x80', crop='noop', quality=95) - if im.exists(): - return im.url - if self.googlebooks_id: - return "https://encrypted.google.com/books?id=%s&printsec=frontcover&img=1&zoom=5" % self.googlebooks_id - else: - return '' - - def cover_image_thumbnail(self): - #128 pixel wide image - if self.cover_image: - im = get_thumbnail(self.cover_image, '128', crop='noop', quality=95) - if im.exists(): - return im.url - if self.googlebooks_id: - return "https://encrypted.google.com/books?id=%s&printsec=frontcover&img=1&zoom=1" % self.googlebooks_id - else: - return '' - - def has_cover_image(self): - if self.cover_image: - return self.cover_image - elif self.googlebooks_id: - return True - else: - return False - - @property - def publisher(self): - if self.publisher_name: - return self.publisher_name.name - return '' - - @property - def isbn_10(self): - return regluit.core.isbn.convert_13_to_10(self.isbn_13) - - def id_for(self,type): - if not self.pk: - return '' - try: - return self.identifiers.filter(type=type)[0].value - except IndexError: - return '' - - @property - def isbn_13(self): - return self.id_for('isbn') - - @property - def googlebooks_id(self): - return self.id_for('goog') - - @property - def librarything_id(self): - return self.id_for('thng') - - @property - def oclc(self): - return self.id_for('oclc') - - @property - def goodreads_id(self): - return self.id_for('gdrd') - - @property - def http_id(self): - return self.id_for('http') - - @staticmethod - def get_by_isbn( isbn): - if len(isbn)==10: - isbn=regluit.core.isbn.convert_10_to_13(isbn) - try: - return Identifier.objects.get( type='isbn', value=isbn ).edition - except Identifier.DoesNotExist: - return None - - def add_author(self, author_name, relation='aut'): - if author_name: - (author, created) = Author.objects.get_or_create(name=author_name) - (relation,created) = Relation.objects.get_or_create(code=relation) - (new_relator,created) = Relator.objects.get_or_create(author=author, edition=self) - if new_relator.relation != relation: - new_relator.relation = relation - new_relator.save() - - def remove_author(self, author): - if author: - try: - relator = Relator.objects.get(author=author, edition=self) - relator.delete() - except Relator.DoesNotExist: - pass - - def set_publisher(self,publisher_name): - if publisher_name and publisher_name != '': - try: - pub_name = PublisherName.objects.get(name=publisher_name) - if pub_name.publisher: - pub_name = pub_name.publisher.name - except PublisherName.DoesNotExist: - pub_name = PublisherName.objects.create(name=publisher_name) - pub_name.save() - - self.publisher_name = pub_name - self.save() - - #### following methods for compatibility with marc outputter - def downloads(self): - return self.ebooks.filter(active=True) - - def download_via_url(self): - return settings.BASE_URL_SECURE + reverse('download', args=[self.work.id]) - - def authnames(self): - return [auth.last_name_first for auth in self.authors.all()] - - @property - def license(self): - try: - return self.ebooks.all()[0].rights - except: - return None - - @property - def funding_info(self): - if self.ebooks.all().count()==0: - return '' - if self.unglued: - return 'The book is available as a free download thanks to the generous support of interested readers and organizations, who made donations using the crowd-funding website Unglue.it.' - else: - if self.ebooks.all()[0].rights in cc.LICENSE_LIST: - return 'The book is available as a free download thanks to a Creative Commons license.' - else: - return 'The book is available as a free download because it is in the Public Domain.' - - @property - def description(self): - return self.work.description - - -class Publisher(models.Model): - created = models.DateTimeField(auto_now_add=True) - name = models.ForeignKey('PublisherName', related_name='key_publisher') - url = models.URLField(max_length=1024, null=True, blank=True) - logo_url = models.URLField(max_length=1024, null=True, blank=True) - description = models.TextField(default='', null=True, blank=True) - - def __unicode__(self): - return self.name.name - -class PublisherName(models.Model): - name = models.CharField(max_length=255, blank=False, unique=True) - - publisher = models.ForeignKey('Publisher', related_name='alternate_names', null=True) - - def __unicode__(self): - return self.name - - def save(self, *args, **kwargs): - super(PublisherName, self).save(*args, **kwargs) # Call the "real" save() method. - if self.publisher and self != self.publisher.name: - #this name is an alias, repoint all editions with this name to the other. - for edition in Edition.objects.filter(publisher_name=self): - edition.publisher_name = self.publisher.name - edition.save() - - -class WasWork(models.Model): - work = models.ForeignKey('Work') - was = models.IntegerField(unique = True) - moved = models.DateTimeField(auto_now_add=True) - user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True) - -def safe_get_work(work_id): - """ - use this rather than querying the db directly for a work by id - """ - try: - work = Work.objects.get(id = work_id) - except Work.DoesNotExist: - try: - work = WasWork.objects.get(was = work_id).work - except WasWork.DoesNotExist: - raise Work.DoesNotExist() - except ValueError: - #work_id is not a number - raise Work.DoesNotExist() - return work - -FORMAT_CHOICES = (('pdf','PDF'),( 'epub','EPUB'), ('html','HTML'), ('text','TEXT'), ('mobi','MOBI')) - -def path_for_file(instance, filename): - return "ebf/{}.{}".format(uuid.uuid4().get_hex(), instance.format) - -class EbookFile(models.Model): - file = models.FileField(upload_to=path_for_file) - format = models.CharField(max_length=25, choices = FORMAT_CHOICES) - edition = models.ForeignKey('Edition', related_name='ebook_files') - created = models.DateTimeField(auto_now_add=True) - asking = models.BooleanField(default=False) - - def check_file(self): - if self.format == 'epub': - return test_epub(self.file) - return None - - @property - def active(self): - try: - return Ebook.objects.filter(url=self.file.url)[0].active - except: - return False - -send_to_kindle_limit=7492232 - -class Ebook(models.Model): - FORMAT_CHOICES = settings.FORMATS - RIGHTS_CHOICES = cc.CHOICES - url = models.URLField(max_length=1024) #change to unique? - created = models.DateTimeField(auto_now_add=True, db_index=True,) - format = models.CharField(max_length=25, choices = FORMAT_CHOICES) - provider = models.CharField(max_length=255) - download_count = models.IntegerField(default=0) - active = models.BooleanField(default=True) - filesize = models.PositiveIntegerField(null=True) - version = None #placeholder - - # 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, db_index=True) - edition = models.ForeignKey('Edition', related_name='ebooks') - user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True) - - def kindle_sendable(self): - if not self.filesize or self.filesize < send_to_kindle_limit: - return True - else: - return False - - def get_archive(self): # returns an archived file - if self.edition.ebook_files.filter(format=self.format).count()==0: - if self.provider is not 'Unglue.it': - try: - r=urllib2.urlopen(self.url) - try: - self.filesize = int(r.info().getheaders("Content-Length")[0]) - if self.save: - self.filesize = self.filesize if self.filesize < 2147483647 else 2147483647 # largest safe positive integer - self.save() - ebf=EbookFile.objects.create(edition=self.edition, format=self.format) - ebf.file.save(path_for_file(ebf,None),ContentFile(r.read())) - ebf.file.close() - ebf.save() - ebf.file.open() - return ebf.file - except IndexError: - # response has no Content-Length header probably a bad link - logging.error( 'Bad link error: {}'.format(self.url) ) - except IOError: - logger.error(u'could not open {}'.format(self.url) ) - else: - # this shouldn't happen, except in testing perhaps - logger.error(u'couldn\'t find ebookfile for {}'.format(self.url) ) - # try the url instead - f = urllib.urlopen(self.url) - return f - else: - ebf = self.edition.ebook_files.filter(format=self.format).order_by('-created')[0] - try: - ebf.file.open() - except ValueError: - logger.error(u'couldn\'t open EbookFile {}'.format(ebf.id) ) - return None - except IOError: - logger.error(u'EbookFile {} does not exist'.format(ebf.id) ) - return None - return ebf.file - - def set_provider(self): - self.provider=Ebook.infer_provider(self.url) - return self.provider - - @property - def rights_badge(self): - if self.rights is None : - return cc.CCLicense.badge('PD-US') - return cc.CCLicense.badge(self.rights) - - @staticmethod - def infer_provider( url): - if not url: - return None - # provider derived from url. returns provider value. remember to call save() afterward - if re.match('https?://books.google.com/', url): - provider='Google Books' - elif re.match('https?://www.gutenberg.org/', url): - provider='Project Gutenberg' - elif re.match('https?://(www\.|)archive.org/', url): - provider='Internet Archive' - elif url.startswith('http://hdl.handle.net/2027/') or url.startswith('http://babel.hathitrust.org/'): - provider='Hathitrust' - elif re.match('https?://\w\w\.wikisource\.org/', url): - provider='Wikisource' - elif re.match('https?://\w\w\.wikibooks\.org/', url): - provider='Wikibooks' - elif re.match('https://github\.com/[^/ ]+/[^/ ]+/raw/[^ ]+', url): - provider='Github' - else: - provider=None - return provider - - def increment(self): - Ebook.objects.filter(id=self.id).update(download_count = F('download_count') +1) - - @property - def download_url(self): - return settings.BASE_URL_SECURE + reverse('download_ebook',args=[self.id]) - - def is_direct(self): - return self.provider not in ('Google Books', 'Project Gutenberg') - - def __unicode__(self): - return "%s (%s from %s)" % (self.edition.title, self.format, self.provider) - - def deactivate(self): - self.active=False - self.save() - - def activate(self): - self.active=True - self.save() - -def set_free_flag(sender, instance, created, **kwargs): - if created: - if not instance.edition.work.is_free and instance.active: - instance.edition.work.is_free = True - instance.edition.work.save() - elif not instance.active and instance.edition.work.is_free==True and instance.edition.work.ebooks().count()==0: - instance.edition.work.is_free = False - instance.edition.work.save() - elif instance.active and instance.edition.work.is_free==False and instance.edition.work.ebooks().count()>0: - instance.edition.work.is_free = True - instance.edition.work.save() - -post_save.connect(set_free_flag,sender=Ebook) - -def reset_free_flag(sender, instance, **kwargs): - # if the Work associated with the instance Ebook currenly has only 1 Ebook, then it's no longer a free Work - # once the instance Ebook is deleted. - if instance.edition.work.ebooks().count()==1: - instance.edition.work.is_free = False - instance.edition.work.save() - -pre_delete.connect(reset_free_flag,sender=Ebook) - -class Wishlist(models.Model): - created = models.DateTimeField(auto_now_add=True) - user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='wishlist') - works = models.ManyToManyField('Work', related_name='wishlists', through='Wishes') - - def __unicode__(self): - return "%s's Books" % self.user.username - - def add_work(self, work, source, notify=False): - try: - w = Wishes.objects.get(wishlist=self,work=work) - except: - Wishes.objects.create(source=source,wishlist=self,work=work) - work.update_num_wishes() - # only send notification in case of new wishes - # and only when they result from user action, not (e.g.) our tests - if notify: - wishlist_added.send(sender=self, work=work, supporter=self.user) - - def remove_work(self, work): - w = Wishes.objects.filter(wishlist=self, work=work) - if w: - w.delete() - work.update_num_wishes() - - def work_source(self, work): - w = Wishes.objects.filter(wishlist=self, work=work) - if w: - return w[0].source - else: - return '' - -class Wishes(models.Model): - created = models.DateTimeField(auto_now_add=True, db_index=True,) - source = models.CharField(max_length=15, blank=True, db_index=True,) - wishlist = models.ForeignKey('Wishlist') - work = models.ForeignKey('Work', related_name='wishes') - class Meta: - db_table = 'core_wishlist_works' - -class Badge(models.Model): - name = models.CharField(max_length=72, blank=True) - description = models.TextField(default='', null=True) - - @property - def path(self): - return '/static/images/%s.png' % self.name - def __unicode__(self): - return self.name - -def pledger(): - if not pledger.instance: - pledger.instance = Badge.objects.get(name='pledger') - return pledger.instance -pledger.instance=None - -def pledger2(): - if not pledger2.instance: - pledger2.instance = Badge.objects.get(name='pledger2') - return pledger2.instance -pledger2.instance=None - -ANONYMOUS_AVATAR = '/static/images/header/avatar.png' -(NO_AVATAR, GRAVATAR, TWITTER, FACEBOOK, UNGLUEITAR) = AVATARS - -class Libpref(models.Model): - user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='libpref') - marc_link_target = models.CharField( - max_length=6, - default = 'UNGLUE', - choices = settings.MARC_PREF_OPTIONS, - verbose_name="MARC record link targets" - ) - - -class UserProfile(models.Model): - created = models.DateTimeField(auto_now_add=True) - user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='profile') - tagline = models.CharField(max_length=140, blank=True) - pic_url = models.URLField(blank=True) - home_url = models.URLField(blank=True) - twitter_id = models.CharField(max_length=15, blank=True) - facebook_id = models.BigIntegerField(null=True) - librarything_id = models.CharField(max_length=31, blank=True) - badges = models.ManyToManyField('Badge', related_name='holders') - kindle_email = models.EmailField(max_length=254, blank=True) - - goodreads_user_id = models.CharField(max_length=32, null=True, blank=True) - goodreads_user_name = models.CharField(max_length=200, null=True, blank=True) - goodreads_auth_token = models.TextField(null=True, blank=True) - goodreads_auth_secret = models.TextField(null=True, blank=True) - goodreads_user_link = models.CharField(max_length=200, null=True, blank=True) - - avatar_source = models.PositiveSmallIntegerField(null = True, default = UNGLUEITAR, - choices=((NO_AVATAR,'No Avatar, Please'),(GRAVATAR,'Gravatar'),(TWITTER,'Twitter'),(FACEBOOK,'Facebook'),(UNGLUEITAR,'Unglueitar'))) - - def __unicode__(self): - return self.user.username - - def reset_pledge_badge(self): - #count user pledges - n_pledges = self.pledge_count - if self.badges.exists(): - self.badges.remove(pledger()) - self.badges.remove(pledger2()) - if n_pledges == 1: - self.badges.add(pledger()) - elif n_pledges > 1: - self.badges.add(pledger2()) - - @property - def pledge_count(self): - return self.user.transaction_set.exclude(status='NONE').exclude(status='Canceled',reason=None).exclude(anonymous=True).count() - - @property - def account(self): - # there should be only one active account per user - accounts = self.user.account_set.filter(date_deactivated__isnull=True) - if accounts.count()==0: - return None - else: - return accounts[0] - - @property - def old_account(self): - accounts = self.user.account_set.filter(date_deactivated__isnull=False).order_by('-date_deactivated') - if accounts.count()==0: - return None - else: - return accounts[0] - - @property - def pledges(self): - return self.user.transaction_set.filter(status=TRANSACTION_STATUS_ACTIVE, campaign__type=1) - - @property - def last_transaction(self): - from regluit.payment.models import Transaction - try: - return Transaction.objects.filter(user=self.user).order_by('-date_modified')[0] - except IndexError: - return None - - @property - def ack_name(self): - # use preferences from last transaction, if any - last = self.last_transaction - if last and last.extra: - return last.extra.get('ack_name', self.user.username) - else: - return self.user.username - - @property - def anon_pref(self): - # use preferences from last transaction, if any - last = self.last_transaction - if last: - return last.anonymous - else: - return None - - @property - def on_ml(self): - if "@example.org" in self.user.email: - # use @example.org email addresses for testing! - return False - try: - return settings.MAILCHIMP_NEWS_ID in pm.listsForEmail(email_address=self.user.email) - except MailChimpException, e: - if e.code!=215: # don't log case where user is not on a list - logger.error("error getting mailchimp status %s" % (e)) - except Exception, e: - logger.error("error getting mailchimp status %s" % (e)) - return False - - def ml_subscribe(self, **kwargs): - if "@example.org" in self.user.email: - # use @example.org email addresses for testing! - return True - from regluit.core.tasks import ml_subscribe_task - ml_subscribe_task.delay(self, **kwargs) - - def ml_unsubscribe(self): - if "@example.org" in self.user.email: - # use @example.org email addresses for testing! - return True - try: - return pm.listUnsubscribe(id=settings.MAILCHIMP_NEWS_ID, email_address=self.user.email) - except Exception, e: - logger.error("error unsubscribing from mailchimp list %s" % (e)) - return False - - def gravatar(self): - # construct the url - gravatar_url = "https://www.gravatar.com/avatar/" + hashlib.md5(self.user.email.lower()).hexdigest() + "?" - gravatar_url += urllib.urlencode({'d':'wavatar', 's':'50'}) - return gravatar_url - - def unglueitar(self): - # construct the url - gravatar_url = "https://www.gravatar.com/avatar/" + hashlib.md5(urllib.quote_plus(self.user.username.encode('utf-8')) + '@unglue.it').hexdigest() + "?" - gravatar_url += urllib.urlencode({'d':'wavatar', 's':'50'}) - return gravatar_url - - - @property - def avatar_url(self): - if self.avatar_source is None or self.avatar_source is TWITTER: - if self.pic_url: - return self.pic_url - else: - return ANONYMOUS_AVATAR - elif self.avatar_source == UNGLUEITAR: - return self.unglueitar() - elif self.avatar_source == GRAVATAR: - return self.gravatar() - elif self.avatar_source == FACEBOOK and self.facebook_id != None: - return 'https://graph.facebook.com/v2.3/' + str(self.facebook_id) + '/picture?redirect=true' - else: - return ANONYMOUS_AVATAR - - @property - def social_auths(self): - socials= self.user.social_auth.all() - auths={} - for social in socials: - auths[social.provider]=True - return auths - - @property - def libraries(self): - libs=[] - for group in self.user.groups.all(): - try: - libs.append(group.library) - except Library.DoesNotExist: - pass - return libs - - -class Press(models.Model): - url = models.URLField() - title = models.CharField(max_length=140) - source = models.CharField(max_length=140) - date = models.DateField( db_index=True,) - language = models.CharField(max_length=20, blank=True) - highlight = models.BooleanField(default=False) - note = models.CharField(max_length=140, blank=True) - -class Gift(models.Model): - # the acq will contain the recipient, and the work - acq = models.ForeignKey('Acq', related_name='gifts') - to = models.CharField(max_length = 75, blank = True) # store the email address originally sent to, not necessarily the email of the recipient - giver = models.ForeignKey(User, related_name='gifts') - message = models.TextField(max_length=512, default='') - used = models.DateTimeField(null=True) - - @staticmethod - def giftee(email, t_id): - # return a user (create a user if necessary) - (giftee, new_user) = User.objects.get_or_create(email=email,defaults={'username':'giftee%s' % t_id}) - giftee.new_user = new_user - return giftee - - def use(self): - self.used = now() - self.save() - notification.send([self.giver], "purchase_got_gift", {'gift': self }, True) - - -# this was causing a circular import problem and we do not seem to be using -# anything from regluit.core.signals after this line -# from regluit.core import signals -from regluit.payment.manager import PaymentManager - diff --git a/core/models/__init__.py b/core/models/__init__.py new file mode 100755 index 00000000..a00e475d --- /dev/null +++ b/core/models/__init__.py @@ -0,0 +1,1391 @@ +import binascii +import hashlib +import logging +import math +import random +import re +import urllib +import urllib2 +from datetime import timedelta, datetime +from decimal import Decimal +from tempfile import SpooledTemporaryFile + +import requests +from ckeditor.fields import RichTextField +from notification import models as notification +from postmonkey import PostMonkey, MailChimpException + +#django imports +from django.apps import apps +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.contrib.contenttypes.fields import GenericRelation +from django.core.urlresolvers import reverse +from django.core.files.base import ContentFile +from django.db import models +from django.db.models import F, Q +from django.db.models.signals import post_save +from django.utils.translation import ugettext_lazy as _ + +#regluit imports + +import regluit +import regluit.core.isbn +import regluit.core.cc as cc + +from regluit.booxtream import BooXtream +from regluit.libraryauth.auth import AVATARS +from regluit.libraryauth.models import Library +from regluit.payment.parameters import ( + TRANSACTION_STATUS_ACTIVE, + TRANSACTION_STATUS_COMPLETE, + TRANSACTION_STATUS_CANCELED, + TRANSACTION_STATUS_ERROR, + TRANSACTION_STATUS_FAILED, + TRANSACTION_STATUS_INCOMPLETE +) +from regluit.utils import crypto +from regluit.utils.localdatetime import now, date_today + +from regluit.core.parameters import ( + REWARDS, + BUY2UNGLUE, + THANKS, + INDIVIDUAL, + LIBRARY, + BORROWED, + TESTING, + RESERVE, + THANKED, + OFFER_CHOICES, + ACQ_CHOICES, +) +from regluit.core.epub import personalize, ungluify, ask_epub +from regluit.core.pdf import ask_pdf, pdf_append +from regluit.core import mobi +from regluit.core.signals import ( + successful_campaign, + unsuccessful_campaign, + wishlist_added +) + +watermarker = BooXtream() + +from .bibmodels import ( + Author, + Ebook, + EbookFile, + Edition, + EditionNote, + good_providers, + Identifier, + path_for_file, + Publisher, + PublisherName, + Relation, + Relator, + safe_get_work, + Subject, + WasWork, + Work, + WorkRelation, +) + +pm = PostMonkey(settings.MAILCHIMP_API_KEY) + +logger = logging.getLogger(__name__) + +class UnglueitError(RuntimeError): + pass + +class Key(models.Model): + """an encrypted key store""" + name = models.CharField(max_length=255, unique=True) + encrypted_value = models.TextField(null=True, blank=True) + + def _get_value(self): + return crypto.decrypt_string(binascii.a2b_hex(self.encrypted_value), settings.SECRET_KEY) + + def _set_value(self, value): + self.encrypted_value = binascii.b2a_hex(crypto.encrypt_string(value, settings.SECRET_KEY)) + + value = property(_get_value, _set_value) + + def __unicode__(self): + return "Key with name {0}".format(self.name) + +class CeleryTask(models.Model): + created = models.DateTimeField(auto_now_add=True) + task_id = models.CharField(max_length=255) + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="tasks", null=True) + description = models.CharField(max_length=2048, null=True) # a description of what the task is + function_name = models.CharField(max_length=1024) # used to reconstitute the AsyncTask with which to get status + function_args = models.IntegerField(null=True) # not full generalized here -- takes only a single arg for now. + active = models.NullBooleanField(default=True) + + def __unicode__(self): + return "Task %s arg:%s ID# %s %s: State %s " % (self.function_name, self.function_args, self.task_id, self.description, self.state) + + @property + def AsyncResult(self): + f = getattr(regluit.core.tasks, self.function_name) + return f.AsyncResult(self.task_id) + @property + def state(self): + f = getattr(regluit.core.tasks, self.function_name) + return f.AsyncResult(self.task_id).state + @property + def result(self): + f = getattr(regluit.core.tasks, self.function_name) + return f.AsyncResult(self.task_id).result + @property + def info(self): + f = getattr(regluit.core.tasks, self.function_name) + return f.AsyncResult(self.task_id).info + +class Claim(models.Model): + STATUSES = ((u'active', u'Claim has been accepted.'), + (u'pending', u'Claim is pending acceptance.'), + (u'release', u'Claim has not been accepted.'), + ) + created = models.DateTimeField(auto_now_add=True) + rights_holder = models.ForeignKey("RightsHolder", related_name="claim", null=False) + work = models.ForeignKey("Work", related_name="claim", null=False) + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="claim", null=False) + status = models.CharField(max_length=7, choices=STATUSES, default='active') + + @property + def can_open_new(self): + # whether a campaign can be opened for this claim + + #must be an active claim + if self.status != 'active': + return False + #can't already be a campaign + for campaign in self.campaigns: + if campaign.status in ['ACTIVE', 'INITIALIZED']: + return 0 # cannot open a new campaign + if campaign.status in ['SUCCESSFUL']: + return 2 # can open a THANKS campaign + return 1 # can open any type of campaign + + def __unicode__(self): + return self.work.title + + @property + def campaign(self): + return self.work.last_campaign() + + @property + def campaigns(self): + return self.work.campaigns.all() + +def notify_claim(sender, created, instance, **kwargs): + if 'example.org' in instance.user.email or hasattr(instance, 'dont_notify'): + return + try: + (rights, new_rights) = User.objects.get_or_create(email='rights@gluejar.com', defaults={'username':'RightsatUnglueit'}) + except: + rights = None + if instance.user == instance.rights_holder.owner: + ul = (instance.user, rights) + else: + ul = (instance.user, instance.rights_holder.owner, rights) + notification.send(ul, "rights_holder_claim", {'claim': instance,}) +post_save.connect(notify_claim, sender=Claim) + +class RightsHolder(models.Model): + created = models.DateTimeField(auto_now_add=True) + email = models.CharField(max_length=100, blank=True) + rights_holder_name = models.CharField(max_length=100, blank=False) + owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="rights_holder", null=False) + can_sell = models.BooleanField(default=False) + def __unicode__(self): + return self.rights_holder_name + +class Premium(models.Model): + PREMIUM_TYPES = ((u'00', u'Default'), (u'CU', u'Custom'), (u'XX', u'Inactive')) + TIERS = {"supporter":25, "patron":50, "bibliophile":100} #should load this from fixture + created = models.DateTimeField(auto_now_add=True) + type = models.CharField(max_length=2, choices=PREMIUM_TYPES) + campaign = models.ForeignKey("Campaign", related_name="premiums", null=True) + amount = models.DecimalField(max_digits=10, decimal_places=0, blank=False) + description = models.TextField(null=True, blank=False) + limit = models.IntegerField(default=0) + + @property + def premium_count(self): + t_model = apps.get_model('payment', 'Transaction') + return t_model.objects.filter(premium=self).count() + @property + def premium_remaining(self): + t_model = apps.get_model('payment', 'Transaction') + return self.limit - t_model.objects.filter(premium=self).count() + def __unicode__(self): + return (self.campaign.work.title if self.campaign else '') + ' $' + str(self.amount) + +class PledgeExtra: + def __init__(self, premium=None, anonymous=False, ack_name='', ack_dedication='', offer=None): + self.anonymous = anonymous + self.premium = premium + self.offer = offer + self.extra = {} + if ack_name: + self.extra['ack_name'] = ack_name + if ack_dedication: + self.extra['ack_dedication'] = ack_dedication + +class CampaignAction(models.Model): + timestamp = models.DateTimeField(auto_now_add=True) + # anticipated types: activated, withdrawn, suspended, restarted, succeeded, failed, unglued + type = models.CharField(max_length=15) + comment = models.TextField(null=True, blank=True) + campaign = models.ForeignKey("Campaign", related_name="actions", null=False) + +class Offer(models.Model): + work = models.ForeignKey("Work", related_name="offers", null=False) + price = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=False) + license = models.PositiveSmallIntegerField(null=False, default=INDIVIDUAL, + choices=OFFER_CHOICES) + active = models.BooleanField(default=False) + + @property + def days_per_copy(self): + return Decimal(float(self.price) / self.work.last_campaign().dollar_per_day) + + @property + def get_thanks_display(self): + if self.license == LIBRARY: + return 'Suggested contribution for libraries' + else: + return 'Suggested contribution for individuals' + +class Acq(models.Model): + """ + Short for Acquisition, this is a made-up word to describe the thing you acquire when you buy or borrow an ebook + """ + + created = models.DateTimeField(auto_now_add=True, db_index=True,) + expires = models.DateTimeField(null=True) + refreshes = models.DateTimeField(auto_now_add=True) + refreshed = models.BooleanField(default=True) + work = models.ForeignKey("Work", related_name='acqs', null=False) + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='acqs') + license = models.PositiveSmallIntegerField(null=False, default=INDIVIDUAL, + choices=ACQ_CHOICES) + watermarked = models.ForeignKey("booxtream.Boox", null=True) + nonce = models.CharField(max_length=32, null=True) + + # when the acq is a loan, this points at the library's acq it's derived from + lib_acq = models.ForeignKey("self", related_name="loans", null=True) + + class mock_ebook(object): + def __init__(self, acq): + self.url = acq.get_mobi_url() + self.format = 'mobi' + self.filesize = 0 + def save(self): + # TODO how to handle filesize? + return True + + def ebook(self): + return self.mock_ebook(self) + + def __unicode__(self): + if self.lib_acq: + return "%s, %s: %s for %s" % (self.work.title, self.get_license_display(), self.lib_acq.user, self.user) + else: + return "%s, %s for %s" % (self.work.title, self.get_license_display(), self.user,) + + @property + def expired(self): + if self.expires is None: + return False + else: + return self.expires < datetime.now() + + def get_mobi_url(self): + if self.expired: + return '' + return self.get_watermarked().download_link_mobi + + def get_epub_url(self): + if self.expired: + return '' + return self.get_watermarked().download_link_epub + + def get_watermarked(self): + if self.watermarked is None or self.watermarked.expired: + if self.on_reserve: + self.borrow(self.user) + do_watermark = self.work.last_campaign().do_watermark + params = { + 'customeremailaddress': self.user.email if do_watermark else '', + 'customername': self.user.username if do_watermark else 'an ungluer', + 'languagecode':'1033', + 'expirydays': 1, + 'downloadlimit': 7, + 'exlibris':0, + 'chapterfooter': 0, + 'disclaimer':0, + 'referenceid': '%s:%s:%s' % (self.work.id, self.user.id, self.id) if do_watermark else 'N/A', + 'kf8mobi': True, + 'epub': True, + } + personalized = personalize(self.work.epubfiles()[0].file, self) + personalized.seek(0) + self.watermarked = watermarker.platform(epubfile=personalized, **params) + self.save() + return self.watermarked + + def _hash(self): + return hashlib.md5('%s:%s:%s:%s'%(settings.SOCIAL_AUTH_TWITTER_SECRET, self.user.id, self.work.id, self.created)).hexdigest() + + def expire_in(self, delta): + self.expires = (now() + delta) if delta else now() + self.save() + if self.lib_acq: + self.lib_acq.refreshes = now() + delta + self.lib_acq.refreshed = False + self.lib_acq.save() + + @property + def on_reserve(self): + return self.license == RESERVE + + def borrow(self, user=None): + if self.on_reserve: + self.license = BORROWED + self.expire_in(timedelta(days=14)) + self.user.wishlist.add_work(self.work, "borrow") + notification.send([self.user], "library_borrow", {'acq':self}) + return self + elif self.borrowable and user: + user.wishlist.add_work(self.work, "borrow") + borrowed = Acq.objects.create(user=user, work=self.work, license=BORROWED, lib_acq=self) + from regluit.core.tasks import watermark_acq + notification.send([user], "library_borrow", {'acq':borrowed}) + watermark_acq.delay(borrowed) + return borrowed + + @property + def borrowable(self): + if self.license == RESERVE and not self.expired: + return True + if self.license == LIBRARY: + return self.refreshes < datetime.now() + else: + return False + + @property + def holds(self): + return Hold.objects.filter(library__user=self.user, work=self.work).order_by('created') + + +def config_acq(sender, instance, created, **kwargs): + if created: + instance.nonce = instance._hash() + instance.save() + if instance.license == RESERVE: + instance.expire_in(timedelta(hours=24)) + if instance.license == BORROWED: + instance.expire_in(timedelta(days=14)) + +post_save.connect(config_acq, sender=Acq) + +class Hold(models.Model): + created = models.DateTimeField(auto_now_add=True) + work = models.ForeignKey("Work", related_name='holds', null=False) + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='holds', null=False) + library = models.ForeignKey(Library, related_name='holds', null=False) + + def __unicode__(self): + return '%s for %s at %s' % (self.work, self.user.username, self.library) + def ahead(self): + return Hold.objects.filter(work=self.work, library=self.library, created__lt=self.created).count() + +class Campaign(models.Model): + LICENSE_CHOICES = cc.FREECHOICES + created = models.DateTimeField(auto_now_add=True,) + name = models.CharField(max_length=500, null=True, blank=False) + description = RichTextField(null=True, blank=False) + details = RichTextField(null=True, blank=True) + target = models.DecimalField(max_digits=14, decimal_places=2, null=True, default=0.00) + license = models.CharField(max_length=255, choices=LICENSE_CHOICES, default='CC BY-NC-ND') + left = models.DecimalField(max_digits=14, decimal_places=2, null=True, db_index=True,) + deadline = models.DateTimeField(db_index=True, null=True) + dollar_per_day = models.FloatField(null=True) + cc_date_initial = models.DateTimeField(null=True) + activated = models.DateTimeField(null=True, db_index=True,) + paypal_receiver = models.CharField(max_length=100, blank=True) + amazon_receiver = models.CharField(max_length=100, blank=True) + work = models.ForeignKey("Work", related_name="campaigns", null=False) + managers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="campaigns", null=False) + # status: INITIALIZED, ACTIVE, SUSPENDED, WITHDRAWN, SUCCESSFUL, UNSUCCESSFUL + status = models.CharField(max_length=15, null=True, blank=False, default="INITIALIZED", db_index=True,) + type = models.PositiveSmallIntegerField(null=False, default=REWARDS, + choices=((REWARDS, 'Pledge-to-unglue campaign'), + (BUY2UNGLUE, 'Buy-to-unglue campaign'), + (THANKS, 'Thanks-for-ungluing campaign'), + )) + edition = models.ForeignKey("Edition", related_name="campaigns", null=True) + email = models.CharField(max_length=100, blank=True) + publisher = models.ForeignKey("Publisher", related_name="campaigns", null=True) + do_watermark = models.BooleanField(default=True) + use_add_ask = models.BooleanField(default=True) + + def __init__(self, *args, **kwargs): + self.problems = [] + super(Campaign, self).__init__(*args, **kwargs) + + def __unicode__(self): + try: + return u"Campaign for %s" % self.work.title + except: + return u"Campaign %s (no associated work)" % self.name + + def clone(self): + """use a previous UNSUCCESSFUL campaign's data as the basis for a new campaign + assume that B2U campaigns don't need cloning + """ + + if self.clonable(): + old_managers = self.managers.all() + + # copy custom premiums + new_premiums = self.premiums.filter(type='CU') + + # setting pk to None will insert new copy http://stackoverflow.com/a/4736172/7782 + self.pk = None + self.status = 'INITIALIZED' + + # set deadline far in future + # presumably RH will set deadline to proper value before campaign launched + self.deadline = date_today() + timedelta(days=int(settings.UNGLUEIT_LONGEST_DEADLINE)) + + # allow created, activated dates to be autoset by db + self.created = None + self.name = 'copy of %s' % self.name + self.activated = None + self.update_left() + self.save() + self.managers = old_managers + + # clone associated premiums + for premium in new_premiums: + premium.pk = None + premium.created = None + premium.campaign = self + premium.save() + return self + else: + return None + + def clonable(self): + """campaign clonable if it's UNSUCCESSFUL and is the last campaign associated with a Work""" + + if self.status == 'UNSUCCESSFUL' and self.work.last_campaign().id == self.id: + return True + else: + return False + + @property + def launchable(self): + may_launch = True + try: + if self.status != 'INITIALIZED': + if self.status == 'ACTIVE': + self.problems.append(_('The campaign is already launched')) + else: + self.problems.append(_('A campaign must initialized properly before it can be launched')) + may_launch = False + if not self.description: + self.problems.append(_('A campaign must have a description')) + may_launch = False + if self.type == REWARDS: + if self.deadline: + if self.deadline.date()- date_today() > timedelta(days=int(settings.UNGLUEIT_LONGEST_DEADLINE)): + self.problems.append(_('The chosen closing date is more than %s days from now' % settings.UNGLUEIT_LONGEST_DEADLINE)) + may_launch = False + else: + self.problems.append(_('A pledge campaign must have a closing date')) + may_launch = False + if self.target: + if self.target < Decimal(settings.UNGLUEIT_MINIMUM_TARGET): + self.problems.append(_('A pledge campaign may not be launched with a target less than $%s' % settings.UNGLUEIT_MINIMUM_TARGET)) + may_launch = False + else: + self.problems.append(_('A campaign must have a target')) + may_launch = False + if self.type == BUY2UNGLUE: + if self.work.offers.filter(price__gt=0, active=True).count() == 0: + self.problems.append(_('You can\'t launch a buy-to-unglue campaign before setting a price for your ebooks')) + may_launch = False + if EbookFile.objects.filter(edition__work=self.work).count() == 0: + self.problems.append(_('You can\'t launch a buy-to-unglue campaign if you don\'t have any ebook files uploaded')) + may_launch = False + if (self.cc_date_initial is None) or (self.cc_date_initial > datetime.combine(settings.MAX_CC_DATE, datetime.min.time())) or (self.cc_date_initial < now()): + self.problems.append(_('You must set an initial Ungluing Date that is in the future and not after %s' % settings.MAX_CC_DATE)) + may_launch = False + if self.target: + if self.target < Decimal(settings.UNGLUEIT_MINIMUM_TARGET): + self.problems.append(_('A buy-to-unglue campaign may not be launched with a target less than $%s' % settings.UNGLUEIT_MINIMUM_TARGET)) + may_launch = False + else: + self.problems.append(_('A buy-to-unglue campaign must have a target')) + may_launch = False + if self.type == THANKS: + # the case in which there is no EbookFile and no Ebook associated with work (We have ebooks without ebook files.) + if EbookFile.objects.filter(edition__work=self.work).count() == 0 and self.work.ebooks().count() == 0: + self.problems.append(_('You can\'t launch a thanks-for-ungluing campaign if you don\'t have any ebook files uploaded')) + may_launch = False + except Exception as e: + self.problems.append('Exception checking launchability ' + str(e)) + may_launch = False + return may_launch + + + def update_status(self, ignore_deadline_for_success=False, send_notice=False, process_transactions=False): + """Updates the campaign's status. returns true if updated. + for REWARDS: + Computes UNSUCCESSFUL only after the deadline has passed + Computes SUCCESSFUL only after the deadline has passed if ignore_deadline_for_success is TRUE -- otherwise looks just at amount of pledges accumulated + by default, send_notice is False so that we have to explicitly send specify delivery of successful_campaign notice + for BUY2UNGLUE: + Sets SUCCESSFUL when cc_date is in the past. + if process_transactions is True, also execute or cancel associated transactions + + """ + if not self.status == 'ACTIVE': + return False + elif self.type == REWARDS: + if (ignore_deadline_for_success or self.deadline < now()) and self.current_total >= self.target: + self.status = 'SUCCESSFUL' + self.save() + action = CampaignAction(campaign=self, type='succeeded', comment=self.current_total) + action.save() + + if process_transactions: + p = PaymentManager() + results = p.execute_campaign(self) + + if send_notice: + successful_campaign.send(sender=None, campaign=self) + + # should be more sophisticated in whether to return True -- look at all the transactions? + return True + elif self.deadline < now() and self.current_total < self.target: + self.status = 'UNSUCCESSFUL' + self.save() + action = CampaignAction(campaign=self, type='failed', comment=self.current_total) + action.save() + + if process_transactions: + p = PaymentManager() + results = p.cancel_campaign(self) + + if send_notice: + regluit.core.signals.unsuccessful_campaign.send(sender=None, campaign=self) + # should be more sophisticated in whether to return True -- look at all the transactions? + return True + elif self.type == BUY2UNGLUE: + if self.cc_date < date_today(): + self.status = 'SUCCESSFUL' + self.save() + action = CampaignAction(campaign=self, type='succeeded', comment=self.current_total) + action.save() + self.watermark_success() + if send_notice: + successful_campaign.send(sender=None, campaign=self) + + # should be more sophisticated in whether to return True -- look at all the transactions? + return True + + return False + + _current_total = None + @property + def current_total(self): + if self._current_total is None: + p = PaymentManager() + self._current_total = p.query_campaign(self, summary=True, campaign_total=True) + return self._current_total + + def set_dollar_per_day(self): + if self.status != 'INITIALIZED' and self.dollar_per_day: + return self.dollar_per_day + if self.cc_date_initial is None: + return None + + start_datetime = self.activated if self.activated else datetime.today() + + time_to_cc = self.cc_date_initial - start_datetime + + self.dollar_per_day = float(self.target)/float(time_to_cc.days) + if self.status != 'DEMO': + self.save() + return self.dollar_per_day + + def set_cc_date_initial(self, a_date=settings.MAX_CC_DATE): + self.cc_date_initial = datetime.combine(a_date, datetime.min.time()) + + @property + def cc_date(self): + if self.type in {REWARDS, THANKS}: + return None + if self.dollar_per_day is None: + return self.cc_date_initial.date() + cc_advance_days = float(self.current_total) / self.dollar_per_day + return (self.cc_date_initial-timedelta(days=cc_advance_days)).date() + + + def update_left(self): + self._current_total = None + if self.type == THANKS: + self.left = Decimal(0.00) + elif self.type == BUY2UNGLUE: + self.left = Decimal(self.dollar_per_day*float((self.cc_date_initial - datetime.today()).days))-self.current_total + else: + self.left = self.target - self.current_total + if self.status != 'DEMO': + self.save() + + def transactions(self, **kwargs): + p = PaymentManager() + + # handle default parameter values + kw = {'summary':False, 'campaign_total':True} + kw.update(kwargs) + + return p.query_campaign(self, **kw) + + + def activate(self): + status = self.status + if status != 'INITIALIZED': + raise UnglueitError(_('Campaign needs to be initialized in order to be activated')) + try: + active_claim = self.work.claim.filter(status="active")[0] + except IndexError, e: + raise UnglueitError(_('Campaign needs to have an active claim in order to be activated')) + if not self.launchable: + raise UnglueitError('Configuration issues need to be addressed before campaign is activated: %s' % unicode(self.problems[0])) + self.status = 'ACTIVE' + self.left = self.target + self.activated = datetime.today() + if self.type == THANKS: + # make ebooks from ebookfiles + if self.use_add_ask: + self.add_ask_to_ebfs() + else: + self.revert_asks() + self.work.remove_old_ebooks() + self.save() + action = CampaignAction(campaign=self, type='activated', comment=self.get_type_display()) + ungluers = self.work.wished_by() + notification.queue(ungluers, "wishlist_active", {'campaign':self}, True) + return self + + + def suspend(self, reason): + status = self.status + if status != 'ACTIVE': + raise UnglueitError(_('Campaign needs to be active in order to be suspended')) + action = CampaignAction(campaign=self, type='suspended', comment=reason) + action.save() + self.status = 'SUSPENDED' + self.save() + return self + + def withdraw(self, reason): + status = self.status + if status != 'ACTIVE': + raise UnglueitError(_('Campaign needs to be active in order to be withdrawn')) + action = CampaignAction(campaign=self, type='withdrawn', comment=reason) + action.save() + self.status = 'WITHDRAWN' + self.save() + return self + + def resume(self, reason): + """Change campaign status from SUSPENDED to ACTIVE. We may want to track reason for resuming and track history""" + status = self.status + if status != 'SUSPENDED': + raise UnglueitError(_('Campaign needs to be suspended in order to be resumed')) + if not reason: + reason = '' + action = CampaignAction(campaign=self, type='restarted', comment=reason) + action.save() + self.status = 'ACTIVE' + self.save() + return self + + def supporters(self): + # expensive query used in loop; stash it + if hasattr(self, '_translist_'): + return self._translist_ + """nb: returns (distinct) supporter IDs, not supporter objects""" + self._translist_ = self.transactions().filter(status=TRANSACTION_STATUS_ACTIVE).values_list('user', flat=True).distinct() + return self._translist_ + + @property + def supporters_count(self): + # avoid transmitting the whole list if you don't need to; let the db do the count. + active = self.transactions().filter(status=TRANSACTION_STATUS_ACTIVE).values_list('user', flat=True).distinct().count() + complete = self.transactions().filter(status=TRANSACTION_STATUS_COMPLETE).values_list('user', flat=True).distinct().count() + return active+complete + + @property + def anon_count(self): + # avoid transmitting the whole list if you don't need to; let the db do the count. + complete = self.transactions().filter(status=TRANSACTION_STATUS_COMPLETE, user=None).count() + return complete + + def transaction_to_recharge(self, user): + """given a user, return the transaction to be recharged if there is one -- None otherwise""" + + # only if a campaign is SUCCESSFUL, we allow for recharged + + if self.status == 'SUCCESSFUL': + if self.transaction_set.filter(Q(user=user) & (Q(status=TRANSACTION_STATUS_COMPLETE) | Q(status=TRANSACTION_STATUS_ACTIVE))).count(): + # presence of an active or complete transaction means no transaction to recharge + return None + else: + transactions = self.transaction_set.filter(Q(user=user) & (Q(status=TRANSACTION_STATUS_ERROR) | Q(status=TRANSACTION_STATUS_FAILED))) + # assumption --that the first failed/errored transaction has the amount we need to recharge + if transactions.count(): + return transactions[0] + else: + return None + else: + return None + + def ungluers(self): + # expensive query used in loop; stash it + if hasattr(self, '_ungluers_'): + return self._ungluers_ + p = PaymentManager() + ungluers = {"all":[], "supporters":[], "patrons":[], "bibliophiles":[]} + if self.status == "ACTIVE": + translist = p.query_campaign(self, summary=False, pledged=True, authorized=True) + elif self.status == "SUCCESSFUL": + translist = p.query_campaign(self, summary=False, pledged=True, completed=True) + else: + translist = [] + for transaction in translist: + ungluers['all'].append(transaction.user) + if not transaction.anonymous: + if transaction.amount >= Premium.TIERS["bibliophile"]: + ungluers['bibliophiles'].append(transaction.user) + elif transaction.amount >= Premium.TIERS["patron"]: + ungluers['patrons'].append(transaction.user) + elif transaction.amount >= Premium.TIERS["supporter"]: + ungluers['supporters'].append(transaction.user) + + self._ungluers_ = ungluers + return ungluers + + def ungluer_transactions(self): + """ + returns a list of authorized transactions for campaigns in progress, + or completed transactions for successful campaigns + used to build the acks page -- because ack_name, _link, _dedication adhere to transactions, + it's easier to return transactions than ungluers + """ + p = PaymentManager() + ungluers = {"all":[], "supporters":[], "anon_supporters": 0, "patrons":[], "anon_patrons": 0, "bibliophiles":[]} + if self.status == "ACTIVE": + translist = p.query_campaign(self, summary=False, pledged=True, authorized=True) + elif self.status == "SUCCESSFUL": + translist = p.query_campaign(self, summary=False, pledged=True, completed=True) + else: + translist = [] + for transaction in translist: + ungluers['all'].append(transaction) + if transaction.amount >= Premium.TIERS["bibliophile"]: + ungluers['bibliophiles'].append(transaction) + elif transaction.amount >= Premium.TIERS["patron"]: + if transaction.anonymous: + ungluers['anon_patrons'] += 1 + else: + ungluers['patrons'].append(transaction) + elif transaction.amount >= Premium.TIERS["supporter"]: + if transaction.anonymous: + ungluers['anon_supporters'] += 1 + else: + ungluers['supporters'].append(transaction) + + return ungluers + + def effective_premiums(self): + """returns the available premiums for the Campaign including any default premiums""" + if self.type is BUY2UNGLUE: + return Premium.objects.none() + q = Q(campaign=self) | Q(campaign__isnull=True) + return Premium.objects.filter(q).exclude(type='XX').order_by('amount') + + def custom_premiums(self): + """returns only the active custom premiums for the Campaign""" + if self.type is BUY2UNGLUE: + return Premium.objects.none() + return Premium.objects.filter(campaign=self).filter(type='CU').order_by('amount') + + @property + def library_offer(self): + return self._offer(LIBRARY) + + @property + def individual_offer(self): + return self._offer(INDIVIDUAL) + + def _offer(self, license): + if self.type is REWARDS: + return None + try: + return Offer.objects.get(work=self.work, active=True, license=license) + except Offer.DoesNotExist: + return None + + @property + def ask_money(self): + # true if there's an offer asking for money + if self.type is REWARDS: + return True + try: + Offer.objects.get(work=self.work, active=True, price__gt=0.00) + return True + except Offer.DoesNotExist: + return False + except Offer.MultipleObjectsReturned: + return True + + @property + def days_per_copy(self): + if self.individual_offer: + return Decimal(float(self.individual_offer.price) / self.dollar_per_day) + else: + return Decimal(0) + + @property + def rh(self): + """returns the rights holder for an active or initialized campaign""" + try: + q = Q(status='ACTIVE') | Q(status='INITIALIZED') + rh = self.work.claim.filter(q)[0].rights_holder + return rh + except: + return None + @property + def rightsholder(self): + """returns the name of the rights holder for an active or initialized campaign""" + try: + return self.rh.rights_holder_name + except: + return '' + + @property + def license_url(self): + return cc.CCLicense.url(self.license) + + @property + def license_badge(self): + return cc.CCLicense.badge(self.license) + + @property + def success_date(self): + if self.status == 'SUCCESSFUL': + try: + return self.actions.filter(type='succeeded')[0].timestamp + except: + return '' + return '' + + def percent_of_goal(self): + if self.type == THANKS: + return 100 + percent = 0 + if self.status == 'SUCCESSFUL' or self.status == 'ACTIVE': + if self.type == BUY2UNGLUE: + percent = int(100 - 100*self.left/self.target) + else: + percent = int(self.current_total/self.target*100) + return percent + + @property + def countdown(self): + from math import ceil + if not self.deadline: + return '' + time_remaining = self.deadline - now() + countdown = "" + + if time_remaining.days: + countdown = "%s days" % str(time_remaining.days + 1) + elif time_remaining.seconds > 3600: + countdown = "%s hours" % str(time_remaining.seconds/3600 + 1) + elif time_remaining.seconds > 60: + countdown = "%s minutes" % str(time_remaining.seconds/60 + 1) + else: + countdown = "Seconds" + + return countdown + + @property + def deadline_or_now(self): + return self.deadline if self.deadline else now() + + @classmethod + def latest_ending(cls): + return timedelta(days=int(settings.UNGLUEIT_LONGEST_DEADLINE)) + now() + + def make_mobis(self): + # make archive files for ebooks, make mobi files for epubs + versions = set() + for ebook in self.work.ebooks().filter(provider__in=good_providers, format='mobi'): + versions.add(ebook.version_label) + for ebook in self.work.ebooks_all().exclude(provider='Unglue.it').filter(provider__in=good_providers, format='epub'): + if not ebook.version_label in versions: + # now make the mobi file + ebf = ebook.get_archive_ebf() + ebf.make_mobi() + + def add_ask_to_ebfs(self, position=0): + if not self.use_add_ask or self.type != THANKS: + return + format_versions = [] + to_dos = [] + for ebf in self.work.ebookfiles().filter(asking=False, ebook__provider='Unglue.it').order_by('-created'): + format_version = '{}_{}'.format(ebf.ebook.format, ebf.ebook.version_label) + if ebf.format in ('pdf', 'epub') and not format_version in format_versions: + ebf.file.open() + to_dos.append({'content': ebf.file.read(), 'ebook': ebf.ebook}) + format_versions.append(format_version) + for ebook in self.work.ebooks_all().exclude(provider='Unglue.it').filter(provider__in=good_providers): + format_version = '{}_{}'.format(ebook.format, ebook.version_label) + if ebook.format in ('pdf', 'epub') and not format_version in format_versions: + to_dos.append({'content': ebook.get_archive().read(), 'ebook': ebook}) + format_versions.append(format_version) + new_ebfs = [] + for to_do in to_dos: + edition = to_do['ebook'].edition + version = {'label':to_do['ebook'].version_label, 'iter':to_do['ebook'].version_iter} + if to_do['ebook'].format == 'pdf': + try: + added = ask_pdf({'campaign':self, 'work':self.work, 'site':Site.objects.get_current()}) + new_file = SpooledTemporaryFile() + old_file = SpooledTemporaryFile() + old_file.write(to_do['content']) + if position == 0: + pdf_append(added, old_file, new_file) + else: + pdf_append(old_file, added, new_file) + new_file.seek(0) + new_pdf_ebf = EbookFile.objects.create(edition=edition, format='pdf', asking=True) + new_pdf_ebf.version = version + new_pdf_ebf.file.save(path_for_file('ebf', None), ContentFile(new_file.read())) + new_pdf_ebf.save() + new_ebfs.append(new_pdf_ebf) + except Exception as e: + logger.error("error appending pdf ask %s" % (e)) + elif to_do['ebook'].format == 'epub': + try: + old_file = SpooledTemporaryFile() + old_file.write(to_do['content']) + new_file = ask_epub(old_file, {'campaign':self, 'work':self.work, 'site':Site.objects.get_current()}) + new_file.seek(0) + new_epub_ebf = EbookFile.objects.create(edition=edition, format='epub', asking=True) + new_epub_ebf.file.save(path_for_file(new_epub_ebf, None), ContentFile(new_file.read())) + new_epub_ebf.save() + new_epub_ebf.version = version + new_ebfs.append(new_epub_ebf) + + # now make the mobi file + new_mobi_ebf = EbookFile.objects.create(edition=edition, format='mobi', asking=True) + new_mobi_ebf.file.save(path_for_file('ebf', None), ContentFile(mobi.convert_to_mobi(new_epub_ebf.file.url))) + new_mobi_ebf.save() + new_mobi_ebf.version = version + new_ebfs.append(new_mobi_ebf) + except Exception as e: + logger.error("error making epub ask or mobi %s" % (e)) + for ebf in new_ebfs: + ebook = Ebook.objects.create( + edition=ebf.edition, + format=ebf.format, + rights=self.license, + provider="Unglue.it", + url=ebf.file.url, + version_label=ebf.version['label'], + version_iter=ebf.version['iter'], + ) + ebf.ebook = ebook + ebf.save() + new_ebf_pks = [ebf.pk for ebf in new_ebfs] + + for old_ebf in self.work.ebookfiles().filter(asking=True).exclude(pk__in=new_ebf_pks): + obsolete = Ebook.objects.filter(url=old_ebf.file.url) + old_ebf.ebook.deactivate() + old_ebf.file.delete() + old_ebf.delete() + + for non_asking in self.work.ebookfiles().filter(asking=False, ebook__active=True): + non_asking.ebook.deactivate() + + def revert_asks(self): + # there should be a deactivated non-asking ebook for every asking ebook + if self.type != THANKS: # just to make sure that ebf's can be unglued by mistake + return + format_versions = [] + for ebf in EbookFile.objects.filter(edition__work=self.work).exclude(file='').exclude(ebook=None).order_by('-created'): + format_version = '{}_{}'.format(ebf.format, ebf.ebook.version_label) + if ebf.asking: + ebf.ebook.deactivate() + elif format_version in format_versions: + # this ebook file has the wrong "asking" + ebf.ebook.deactivate() + else: + ebf.ebook.activate() + format_versions.append(format_version) + + def make_unglued_ebf(self, format, watermarked): + r = urllib2.urlopen(watermarked.download_link(format)) + ebf = EbookFile.objects.create(edition=self.work.preferred_edition, format=format) + ebf.file.save(path_for_file(ebf, None), ContentFile(r.read())) + ebf.file.close() + ebf.save() + ebook = Ebook.objects.create( + edition=self.work.preferred_edition, + format=format, + rights=self.license, + provider="Unglue.it", + url=settings.BASE_URL_SECURE + reverse('download_campaign', args=[self.work.id, format]), + version='unglued', + ) + old_ebooks = Ebook.objects.exclude(pk=ebook.pk).filter( + edition=self.work.preferred_edition, + format=format, + rights=self.license, + provider="Unglue.it", + ) + for old_ebook in old_ebooks: + old_ebook.deactivate() + return ebook.pk + + + def watermark_success(self): + if self.status == 'SUCCESSFUL' and self.type == BUY2UNGLUE: + params = { + 'customeremailaddress': self.license, + 'customername': 'The Public', + 'languagecode':'1033', + 'expirydays': 1, + 'downloadlimit': 7, + 'exlibris':0, + 'chapterfooter':0, + 'disclaimer':0, + 'referenceid': '%s:%s:%s' % (self.work.id, self.id, self.license), + 'kf8mobi': True, + 'epub': True, + } + ungluified = ungluify(self.work.epubfiles()[0].file, self) + ungluified.filename.seek(0) + watermarked = watermarker.platform(epubfile=ungluified.filename, **params) + self.make_unglued_ebf('epub', watermarked) + self.make_unglued_ebf('mobi', watermarked) + return True + return False + + def is_pledge(self): + return self.type == REWARDS + + @property + def user_to_pay(self): + return self.rh.owner + + ### for compatibility with MARC output + def marc_records(self): + return self.work.marc_records() + + +class Wishlist(models.Model): + created = models.DateTimeField(auto_now_add=True) + user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='wishlist') + works = models.ManyToManyField('Work', related_name='wishlists', through='Wishes') + + def __unicode__(self): + return "%s's Books" % self.user.username + + def add_work(self, work, source, notify=False): + try: + w = Wishes.objects.get(wishlist=self, work=work) + except: + Wishes.objects.create(source=source, wishlist=self, work=work) + work.update_num_wishes() + # only send notification in case of new wishes + # and only when they result from user action, not (e.g.) our tests + if notify: + wishlist_added.send(sender=self, work=work, supporter=self.user) + + def remove_work(self, work): + w = Wishes.objects.filter(wishlist=self, work=work) + if w: + w.delete() + work.update_num_wishes() + + def work_source(self, work): + w = Wishes.objects.filter(wishlist=self, work=work) + if w: + return w[0].source + else: + return '' + +class Wishes(models.Model): + created = models.DateTimeField(auto_now_add=True, db_index=True,) + source = models.CharField(max_length=15, blank=True, db_index=True,) + wishlist = models.ForeignKey('Wishlist') + work = models.ForeignKey('Work', related_name='wishes') + class Meta: + db_table = 'core_wishlist_works' + +class Badge(models.Model): + name = models.CharField(max_length=72, blank=True) + description = models.TextField(default='', null=True) + + @property + def path(self): + return '/static/images/%s.png' % self.name + def __unicode__(self): + return self.name + +def pledger(): + if not pledger.instance: + pledger.instance = Badge.objects.get(name='pledger') + return pledger.instance +pledger.instance = None + +def pledger2(): + if not pledger2.instance: + pledger2.instance = Badge.objects.get(name='pledger2') + return pledger2.instance +pledger2.instance = None + +ANONYMOUS_AVATAR = '/static/images/header/avatar.png' +(NO_AVATAR, GRAVATAR, TWITTER, FACEBOOK, UNGLUEITAR) = AVATARS + +class Libpref(models.Model): + user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='libpref') + marc_link_target = models.CharField( + max_length=6, + default='UNGLUE', + choices=settings.MARC_PREF_OPTIONS, + verbose_name="MARC record link targets", + ) + +class UserProfile(models.Model): + created = models.DateTimeField(auto_now_add=True) + user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='profile') + tagline = models.CharField(max_length=140, blank=True) + pic_url = models.URLField(blank=True) + home_url = models.URLField(blank=True) + twitter_id = models.CharField(max_length=15, blank=True) + facebook_id = models.BigIntegerField(null=True, blank=True) + librarything_id = models.CharField(max_length=31, blank=True) + badges = models.ManyToManyField('Badge', related_name='holders', blank=True) + kindle_email = models.EmailField(max_length=254, blank=True) + + goodreads_user_id = models.CharField(max_length=32, null=True, blank=True) + goodreads_user_name = models.CharField(max_length=200, null=True, blank=True) + goodreads_auth_token = models.TextField(null=True, blank=True) + goodreads_auth_secret = models.TextField(null=True, blank=True) + goodreads_user_link = models.CharField(max_length=200, null=True, blank=True) + + avatar_source = models.PositiveSmallIntegerField( + null=True, + default=UNGLUEITAR, + choices=( + (NO_AVATAR, 'No Avatar, Please'), + (GRAVATAR, 'Gravatar'), + (TWITTER, 'Twitter'), + (FACEBOOK, 'Facebook'), + (UNGLUEITAR, 'Unglueitar'), + ) + ) + + def __unicode__(self): + return self.user.username + + def reset_pledge_badge(self): + #count user pledges + n_pledges = self.pledge_count + if self.badges.exists(): + self.badges.remove(pledger()) + self.badges.remove(pledger2()) + if n_pledges == 1: + self.badges.add(pledger()) + elif n_pledges > 1: + self.badges.add(pledger2()) + + @property + def pledge_count(self): + return self.user.transaction_set.exclude(status='NONE').exclude(status='Canceled', reason=None).exclude(anonymous=True).count() + + @property + def account(self): + # there should be only one active account per user + accounts = self.user.account_set.filter(date_deactivated__isnull=True) + if accounts.count() == 0: + return None + else: + return accounts[0] + + @property + def old_account(self): + accounts = self.user.account_set.filter(date_deactivated__isnull=False).order_by('-date_deactivated') + if accounts.count() == 0: + return None + else: + return accounts[0] + + @property + def pledges(self): + return self.user.transaction_set.filter(status=TRANSACTION_STATUS_ACTIVE, campaign__type=1) + + @property + def last_transaction(self): + from regluit.payment.models import Transaction + try: + return Transaction.objects.filter(user=self.user).order_by('-date_modified')[0] + except IndexError: + return None + + @property + def ack_name(self): + # use preferences from last transaction, if any + last = self.last_transaction + if last and last.extra: + return last.extra.get('ack_name', self.user.username) + else: + return self.user.username + + @property + def anon_pref(self): + # use preferences from last transaction, if any + last = self.last_transaction + if last: + return last.anonymous + else: + return None + + @property + def on_ml(self): + if "@example.org" in self.user.email: + # use @example.org email addresses for testing! + return False + try: + return settings.MAILCHIMP_NEWS_ID in pm.listsForEmail(email_address=self.user.email) + except MailChimpException, e: + if e.code != 215: # don't log case where user is not on a list + logger.error("error getting mailchimp status %s" % (e)) + except Exception, e: + logger.error("error getting mailchimp status %s" % (e)) + return False + + def ml_subscribe(self, **kwargs): + if "@example.org" in self.user.email: + # use @example.org email addresses for testing! + return True + from regluit.core.tasks import ml_subscribe_task + ml_subscribe_task.delay(self, **kwargs) + + def ml_unsubscribe(self): + if "@example.org" in self.user.email: + # use @example.org email addresses for testing! + return True + try: + return pm.listUnsubscribe(id=settings.MAILCHIMP_NEWS_ID, email_address=self.user.email) + except Exception, e: + logger.error("error unsubscribing from mailchimp list %s" % (e)) + return False + + def gravatar(self): + # construct the url + gravatar_url = "https://www.gravatar.com/avatar/" + hashlib.md5(self.user.email.lower()).hexdigest() + "?" + gravatar_url += urllib.urlencode({'d':'wavatar', 's':'50'}) + return gravatar_url + + def unglueitar(self): + # construct the url + gravatar_url = "https://www.gravatar.com/avatar/" + hashlib.md5(urllib.quote_plus(self.user.username.encode('utf-8')) + '@unglue.it').hexdigest() + "?" + gravatar_url += urllib.urlencode({'d':'wavatar', 's':'50'}) + return gravatar_url + + + @property + def avatar_url(self): + if self.avatar_source is None or self.avatar_source is TWITTER: + if self.pic_url: + return self.pic_url + else: + return ANONYMOUS_AVATAR + elif self.avatar_source == UNGLUEITAR: + return self.unglueitar() + elif self.avatar_source == GRAVATAR: + return self.gravatar() + elif self.avatar_source == FACEBOOK and self.facebook_id != None: + return 'https://graph.facebook.com/v2.3/' + str(self.facebook_id) + '/picture?redirect=true' + else: + return ANONYMOUS_AVATAR + + @property + def social_auths(self): + socials = self.user.social_auth.all() + auths = {} + for social in socials: + auths[social.provider] = True + return auths + + @property + def libraries(self): + libs = [] + for group in self.user.groups.all(): + try: + libs.append(group.library) + except Library.DoesNotExist: + pass + return libs + + +class Press(models.Model): + url = models.URLField() + title = models.CharField(max_length=140) + source = models.CharField(max_length=140) + date = models.DateField(db_index=True,) + language = models.CharField(max_length=20, blank=True) + highlight = models.BooleanField(default=False) + note = models.CharField(max_length=140, blank=True) + +class Gift(models.Model): + # the acq will contain the recipient, and the work + acq = models.ForeignKey('Acq', related_name='gifts') + to = models.CharField(max_length=75, blank=True) # store the email address originally sent to, not necessarily the email of the recipient + giver = models.ForeignKey(User, related_name='gifts') + message = models.TextField(max_length=512, default='') + used = models.DateTimeField(null=True) + + @staticmethod + def giftee(email, t_id): + # return a user (create a user if necessary) + (giftee, new_user) = User.objects.get_or_create(email=email, defaults={'username':'giftee%s' % t_id}) + giftee.new_user = new_user + return giftee + + def use(self): + self.used = now() + self.save() + notification.send([self.giver], "purchase_got_gift", {'gift': self}, True) + + +# this was causing a circular import problem and we do not seem to be using +# anything from regluit.core.signals after this line +# from regluit.core import signals +from regluit.payment.manager import PaymentManager diff --git a/core/models/bibmodels.py b/core/models/bibmodels.py new file mode 100644 index 00000000..695e484d --- /dev/null +++ b/core/models/bibmodels.py @@ -0,0 +1,1191 @@ +import logging +import math +import re +import urllib +import urllib2 +import uuid + +from decimal import Decimal +import unicodedata +from urlparse import urlparse + +from sorl.thumbnail import get_thumbnail + +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.contenttypes.fields import GenericRelation +from django.core.files.base import ContentFile +from django.core.urlresolvers import reverse +from django.db import models +from django.db.models import F +from django.db.models.signals import post_save, pre_delete + +import regluit +from regluit.marc.models import MARCRecord as NewMARC +from regluit.utils.localdatetime import now +from regluit.questionnaire.models import Landing + +from regluit.core import mobi +import regluit.core.cc as cc +from regluit.core.epub import test_epub + +from regluit.core.parameters import ( + AGE_LEVEL_CHOICES, + BORROWED, + BUY2UNGLUE, + INDIVIDUAL, + LIBRARY, + OFFER_CHOICES, + TESTING, + TEXT_RELATION_CHOICES, + THANKED, + THANKS, +) + +logger = logging.getLogger(__name__) +good_providers = ('Internet Archive', 'Unglue.it', 'Github', 'OAPEN Library') + + +class Identifier(models.Model): + # olib, ltwk, goog, gdrd, thng, isbn, oclc, olwk, doab, gute, glue, doi + type = models.CharField(max_length=4, null=False) + value = models.CharField(max_length=250, null=False) + work = models.ForeignKey("Work", related_name="identifiers", null=False) + edition = models.ForeignKey("Edition", related_name="identifiers", null=True) + + class Meta: + unique_together = ("type", "value") + + @staticmethod + def set(type=None, value=None, edition=None, work=None): + # if there's already an id of this type for this work and edition, change it + # if not, create it. if the id exists and points to something else, change it. + identifier = Identifier.get_or_add(type=type, value=value, edition=edition, work=work) + if identifier.work.id != work.id: + identifier.work = work + identifier.save() + if identifier.edition and edition: + if identifier.edition.id != edition.id: + identifier.edition = edition + identifier.save() + others = Identifier.objects.filter(type=type, work=work, edition=edition).exclude(value=value) + if others.count() > 0: + for other in others: + other.delete() + return identifier + + @staticmethod + def get_or_add(type='goog', value=None, edition=None, work=None): + try: + return Identifier.objects.get(type=type, value=value) + except Identifier.DoesNotExist: + i = Identifier(type=type, value=value, edition=edition, work=work) + i.save() + return i + + def __unicode__(self): + return u'{0}:{1}'.format(self.type, self.value) + +class Work(models.Model): + created = models.DateTimeField(auto_now_add=True, db_index=True,) + title = models.CharField(max_length=1000) + language = models.CharField(max_length=5, default="en", null=False, db_index=True,) + openlibrary_lookup = models.DateTimeField(null=True, blank=True) + num_wishes = models.IntegerField(default=0, db_index=True) + description = models.TextField(default='', null=True, blank=True) + selected_edition = models.ForeignKey("Edition", related_name='selected_works', null=True) + # repurposed earliest_publication to actually be publication range + publication_range = models.CharField(max_length=50, null=True, blank=True) + featured = models.DateTimeField(null=True, blank=True, db_index=True,) + is_free = models.BooleanField(default=False) + landings = GenericRelation(Landing) + related = models.ManyToManyField('self', symmetrical=False, null=True, through='WorkRelation', related_name='reverse_related') + age_level = models.CharField(max_length=5, choices=AGE_LEVEL_CHOICES, default='', blank=True) + + class Meta: + ordering = ['title'] + def __unicode__(self): + return self.title + + def __init__(self, *args, **kwargs): + self._last_campaign = None + super(Work, self).__init__(*args, **kwargs) + + @property + def googlebooks_id(self): + try: + preferred_id = self.preferred_edition.googlebooks_id + # note that there should always be a preferred edition + except AttributeError: + # this work has no edition. + return '' + if preferred_id: + return preferred_id + try: + return self.identifiers.filter(type='goog')[0].value + except IndexError: + return '' + + @property + def googlebooks_url(self): + if self.googlebooks_id: + return "http://books.google.com/books?id=%s" % self.googlebooks_id + else: + return '' + + @property + def goodreads_id(self): + preferred_id = self.preferred_edition.goodreads_id + if preferred_id: + return preferred_id + try: + return self.identifiers.filter(type='gdrd')[0].value + except IndexError: + return '' + + @property + def goodreads_url(self): + return "http://www.goodreads.com/book/show/%s" % self.goodreads_id + + @property + def librarything_id(self): + try: + return self.identifiers.filter(type='ltwk')[0].value + except IndexError: + return '' + + @property + def librarything_url(self): + return "http://www.librarything.com/work/%s" % self.librarything_id + + @property + def openlibrary_id(self): + try: + return self.identifiers.filter(type='olwk')[0].value + except IndexError: + return '' + + @property + def openlibrary_url(self): + return "http://openlibrary.org" + self.openlibrary_id + + def cover_filetype(self): + if self.uses_google_cover(): + return 'jpeg' + else: + # consider the path only and not the params, query, or fragment + url = urlparse(self.cover_image_small().lower()).path + + if url.endswith('.png'): + return 'png' + elif url.endswith('.gif'): + return 'gif' + elif url.endswith('.jpg') or url.endswith('.jpeg'): + return 'jpeg' + else: + return 'image' + + def uses_google_cover(self): + if self.preferred_edition and self.preferred_edition.cover_image: + return False + else: + return self.googlebooks_id + + def cover_image_large(self): + if self.preferred_edition and self.preferred_edition.has_cover_image(): + return self.preferred_edition.cover_image_large() + return "/static/images/generic_cover_larger.png" + + def cover_image_small(self): + if self.preferred_edition and self.preferred_edition.has_cover_image(): + return self.preferred_edition.cover_image_small() + return "/static/images/generic_cover_larger.png" + + def cover_image_thumbnail(self): + try: + if self.preferred_edition and self.preferred_edition.has_cover_image(): + return self.preferred_edition.cover_image_thumbnail() + except IndexError: + pass + return "/static/images/generic_cover_larger.png" + + def authors(self): + # assumes that they come out in the same order they go in! + if self.preferred_edition and self.preferred_edition.authors.all().count() > 0: + return self.preferred_edition.authors.all() + for edition in self.editions.all(): + if edition.authors.all().count() > 0: + return edition.authors.all() + return Author.objects.none() + + def relators(self): + # assumes that they come out in the same order they go in! + if self.preferred_edition and self.preferred_edition.relators.all().count() > 0: + return self.preferred_edition.relators.all() + for edition in self.editions.all(): + if edition.relators.all().count() > 0: + return edition.relators.all() + return Relator.objects.none() + + def author(self): + # assumes that they come out in the same order they go in! + if self.relators().count() > 0: + return self.relators()[0].name + return '' + + def authors_short(self): + # assumes that they come out in the same order they go in! + if self.relators().count() == 1: + return self.relators()[0].name + elif self.relators().count() == 2: + if self.relators()[0].relation == self.relators()[1].relation: + if self.relators()[0].relation.code == 'aut': + return "%s and %s" % (self.relators()[0].author.name, self.relators()[1].author.name) + else: + return "%s and %s, %ss" % (self.relators()[0].author.name, self.relators()[1].author.name, self.relators()[0].relation.name) + else: + return "%s (%s) and %s (%s)" % (self.relators()[0].author.name, self.relators()[0].relation.name, self.relators()[1].author.name, self.relators()[1].relation.name) + elif self.relators().count() > 2: + auths = self.relators() + if auths[0].relation.code == 'aut': + return "%s et al." % auths[0].author.name + else: + return "%s et al. (%ss)" % (auths[0].author.name, auths[0].relation.name) + return '' + + def kindle_safe_title(self): + """ + Removes accents, keeps letters and numbers, replaces non-Latin characters with "#", and replaces punctuation with "_" + """ + safe = u'' + nkfd_form = unicodedata.normalize('NFKD', self.title) #unaccent accented letters + for c in nkfd_form: + ccat = unicodedata.category(c) + #print ccat + if ccat.startswith('L') or ccat.startswith('N'): # only letters and numbers + if ord(c) > 127: + safe = safe + '#' #a non latin script letter or number + else: + safe = safe + c + elif not unicodedata.combining(c): #not accents (combining forms) + safe = safe + '_' #punctuation + return safe + + def last_campaign(self): + # stash away the last campaign to prevent repeated lookups + if hasattr(self, '_last_campaign_'): + return self._last_campaign_ + try: + self._last_campaign_ = self.campaigns.order_by('-created')[0] + except IndexError: + self._last_campaign_ = None + return self._last_campaign_ + + @property + def preferred_edition(self): + if self.selected_edition: + return self.selected_edition + if self.last_campaign(): + if self.last_campaign().edition: + self.selected_edition = self.last_campaign().edition + self.save() + return self.last_campaign().edition + try: + self.selected_edition = self.editions.all().order_by('-cover_image', '-created')[0] # prefer editions with covers + self.save() + return self.selected_edition + except IndexError: + #should only happen if there are no editions for the work, + #which can happen when works are being merged + try: + return WasWork.objects.get(was=self.id).work.preferred_edition + except WasWork.DoesNotExist: + #should not happen + logger.warning('work {} has no edition'.format(self.id)) + return None + + def last_campaign_status(self): + campaign = self.last_campaign() + if campaign: + status = campaign.status + else: + if self.first_ebook(): + status = "Available" + else: + status = "No campaign yet" + return status + + def percent_unglued(self): + status = 0 + campaign = self.last_campaign() + if campaign is not None: + if campaign.status == 'SUCCESSFUL': + status = 6 + elif campaign.status == 'ACTIVE': + if campaign.target is not None: + target = float(campaign.target) + else: + #shouldn't happen, but did once because of a failed pdf conversion + return 0 + if target <= 0: + status = 6 + else: + if campaign.type == BUY2UNGLUE: + status = int(6 - 6*campaign.left/campaign.target) + else: + status = int(float(campaign.current_total)*6/target) + if status >= 6: + status = 6 + return status + + def percent_of_goal(self): + campaign = self.last_campaign() + return 0 if campaign is None else campaign.percent_of_goal() + + def ebooks_all(self): + return self.ebooks(all=True) + + def ebooks(self, all=False): + if all: + return Ebook.objects.filter(edition__work=self).order_by('-created') + else: + return Ebook.objects.filter(edition__work=self, active=True).order_by('-created') + + def ebookfiles(self): + return EbookFile.objects.filter(edition__work=self).exclude(file='').order_by('-created') + + def epubfiles(self): + # filter out non-epub because that's what booxtream accepts + return EbookFile.objects.filter(edition__work=self, format='epub').exclude(file='').order_by('-created') + + def mobifiles(self): + return EbookFile.objects.filter(edition__work=self, format='mobi').exclude(file='').order_by('-created') + + def pdffiles(self): + return EbookFile.objects.filter(edition__work=self, format='pdf').exclude(file='').order_by('-created') + + def versions(self): + version_labels = [] + for ebook in self.ebooks_all(): + if ebook.version_label and not ebook.version_label in version_labels: + version_labels.append(ebook.version_label) + return version_labels + + def formats(self): + fmts = [] + for fmt in ['pdf', 'epub', 'mobi', 'html']: + for ebook in self.ebooks().filter(format=fmt): + fmts.append(fmt) + break + return fmts + + def remove_old_ebooks(self): + # this method is triggered after an file upload or new ebook saved + old = Ebook.objects.filter(edition__work=self, active=True).order_by('-version_iter', '-created') + + # keep highest version ebook for each format and version label + done_format_versions = [] + for eb in old: + format_version = '{}_{}'.format(eb.format, eb.version_label) + if format_version in done_format_versions: + eb.deactivate() + else: + done_format_versions.append(format_version) + + # check for failed uploads. + null_files = EbookFile.objects.filter(edition__work=self, file='') + for ebf in null_files: + ebf.file.delete() + ebf.delete() + + @property + def download_count(self): + dlc = 0 + for ebook in self.ebooks(all=True): + dlc += ebook.download_count + return dlc + + def first_pdf(self): + return self.first_ebook('pdf') + + def first_epub(self): + return self.first_ebook('epub') + + def first_pdf_url(self): + try: + url = self.first_ebook('pdf').url + return url + except: + return None + + def first_epub_url(self): + try: + url = self.first_ebook('epub').url + return url + except: + return None + + def first_ebook(self, ebook_format=None): + if ebook_format: + for ebook in self.ebooks().filter(format=ebook_format): + return ebook + else: + for ebook in self.ebooks(): + return ebook + + def wished_by(self): + return User.objects.filter(wishlist__works__in=[self]) + + def update_num_wishes(self): + self.num_wishes = self.wishes.count() + self.save() + + def priority(self): + if self.last_campaign(): + return 5 + freedom = 1 if self.is_free else 0 + wishing = int(math.log(self.num_wishes)) + 1 if self.num_wishes else 0 + return min(freedom + wishing, 5) + + def first_oclc(self): + if self.preferred_edition is None: + return '' + preferred_id = self.preferred_edition.oclc + if preferred_id: + return preferred_id + try: + return self.identifiers.filter(type='oclc')[0].value + except IndexError: + return '' + + def first_isbn_13(self): + if self.preferred_edition is None: + return '' + preferred_id = self.preferred_edition.isbn_13 + if preferred_id: + return preferred_id + try: + return self.identifiers.filter(type='isbn')[0].value + except IndexError: + return '' + + @property + def earliest_publication_date(self): + for edition in Edition.objects.filter(work=self, publication_date__isnull=False).order_by('publication_date'): + if edition.publication_date and len(edition.publication_date) >= 4: + return edition.publication_date + + @property + def publication_date(self): + if self.publication_range: + return self.publication_range + for edition in Edition.objects.filter(work=self, publication_date__isnull=False).order_by('publication_date'): + if edition.publication_date: + try: + earliest_publication = edition.publication_date[:4] + except IndexError: + continue + latest_publication = None + for edition in Edition.objects.filter(work=self, publication_date__isnull=False).order_by('-publication_date'): + if edition.publication_date: + try: + latest_publication = edition.publication_date[:4] + except IndexError: + continue + break + if earliest_publication == latest_publication: + publication_range = earliest_publication + else: + publication_range = earliest_publication + "-" + latest_publication + self.publication_range = publication_range + self.save() + return publication_range + return '' + + @property + def has_unglued_edition(self): + """ + allows us to distinguish successful campaigns with ebooks still in progress from successful campaigns with ebooks available + """ + if self.ebooks().filter(edition__unglued=True): + return True + return False + + @property + def user_with_rights(self): + """ + return queryset of users (should be at most one) who act for rights holders with active claims to the work + """ + claims = self.claim.filter(status='active') + assert claims.count() < 2, "There is more than one active claim on %r" % self.title + try: + return claims[0].user + except: + return False + + def get_absolute_url(self): + return reverse('work', args=[str(self.id)]) + + def publishers(self): + # returns a set of publishers associated with this Work + return Publisher.objects.filter(name__editions__work=self).distinct() + + def create_offers(self): + for choice in OFFER_CHOICES: + if not self.offers.filter(license=choice[0]): + self.offers.create(license=choice[0], active=True, price=Decimal(10)) + return self.offers.all() + + def get_lib_license(self, user): + lib_user = (lib.user for lib in user.profile.libraries) + return self.get_user_license(lib_user) + + def borrowable(self, user): + if user.is_anonymous(): + return False + lib_license = self.get_lib_license(user) + if lib_license and lib_license.borrowable: + return True + return False + + def lib_thanked(self, user): + if user.is_anonymous(): + return False + lib_license = self.get_lib_license(user) + if lib_license and lib_license.thanked: + return True + return False + + def in_library(self, user): + if user.is_anonymous(): + return False + lib_license = self.get_lib_license(user) + if lib_license and lib_license.acqs.count(): + return True + return False + + @property + def lib_acqs(self): + return self.acqs.filter(license=LIBRARY) + + @property + def test_acqs(self): + return self.acqs.filter(license=TESTING).order_by('-created') + + class user_license: + acqs = Identifier.objects.none() # Identifier is just convenient. + def __init__(self, acqs): + self.acqs = acqs + + @property + def is_active(self): + return self.acqs.filter(expires__isnull=True).count() > 0 or self.acqs.filter(expires__gt=now()).count() > 0 + + @property + def borrowed(self): + loans = self.acqs.filter(license=BORROWED, expires__gt=now()) + if loans.count() == 0: + return None + else: + return loans[0] + + @property + def purchased(self): + purchases = self.acqs.filter(license=INDIVIDUAL, expires__isnull=True) + if purchases.count() == 0: + return None + else: + return purchases[0] + + @property + def lib_acqs(self): + return self.acqs.filter(license=LIBRARY) + + @property + def next_acq(self): + """ This is the next available copy in the user's libraries""" + loans = self.acqs.filter(license=LIBRARY, refreshes__gt=now()).order_by('refreshes') + if loans.count() == 0: + return None + else: + return loans[0] + + @property + def borrowable(self): + return self.acqs.filter(license=LIBRARY, refreshes__lt=now()).count() > 0 + + @property + def thanked(self): + return self.acqs.filter(license=THANKED).count() > 0 + + @property + def borrowable_acq(self): + for acq in self.acqs.filter(license=LIBRARY, refreshes__lt=now()): + return acq + + @property + def is_duplicate(self): + # does user have two individual licenses? + pending = self.acqs.filter(license=INDIVIDUAL, expires__isnull=True, gifts__used__isnull=True).count() + return self.acqs.filter(license=INDIVIDUAL, expires__isnull=True).count() > pending + + + def get_user_license(self, user): + """ This is all the acqs, wrapped in user_license object for the work, user(s) """ + if user is None: + return None + if hasattr(user, 'is_anonymous'): + if user.is_anonymous(): + return None + return self.user_license(self.acqs.filter(user=user)) + else: + # assume it's several users + return self.user_license(self.acqs.filter(user__in=user)) + + @property + def has_marc(self): + for record in NewMARC.objects.filter(edition__work=self): + return True + return False + + ### for compatibility with MARC output + def marc_records(self): + record_list = [] + record_list.extend(NewMARC.objects.filter(edition__work=self)) + for obj in record_list: + break + else: + for ebook in self.ebooks(): + record_list.append(ebook.edition) + break + return record_list + +class WorkRelation(models.Model): + to_work = models.ForeignKey('Work', related_name='works_related_to') + from_work= models.ForeignKey('Work', related_name='works_related_from') + relation = models.CharField(max_length=15, choices=TEXT_RELATION_CHOICES) + + +class Author(models.Model): + created = models.DateTimeField(auto_now_add=True) + name = models.CharField(max_length=255, unique=True) + editions = models.ManyToManyField("Edition", related_name="authors", through="Relator") + + def __unicode__(self): + return self.name + + @property + def last_name_first(self): + names = self.name.rsplit() + if len(names) == 0: + return '' + elif len(names) == 1: + return names[0] + elif len(names) == 2: + return names[1] + ", " + names[0] + else: + reversed_name = names[-1]+"," + for name in names[0:-1]: + reversed_name += " " + reversed_name += name + return reversed_name + +class Relation(models.Model): + code = models.CharField(max_length=3, blank=False, db_index=True, unique=True) + name = models.CharField(max_length=30, blank=True,) + +class Relator(models.Model): + relation = models.ForeignKey('Relation', default=1) #first relation should have code='aut' + author = models.ForeignKey('Author') + edition = models.ForeignKey('Edition', related_name='relators') + class Meta: + db_table = 'core_author_editions' + + @property + def name(self): + if self.relation.code == 'aut': + return self.author.name + else: + return "%s (%s)" % (self.author.name, self.relation.name) + + def set(self, relation_code): + if self.relation.code != relation_code: + try: + self.relation = Relation.objects.get(code=relation_code) + self.save() + except Relation.DoesNotExist: + logger.warning("relation not found: code = %s" % relation_code) + +class Subject(models.Model): + created = models.DateTimeField(auto_now_add=True) + name = models.CharField(max_length=200, unique=True) + works = models.ManyToManyField("Work", related_name="subjects") + is_visible = models.BooleanField(default=True) + authority = models.CharField(max_length=10, blank=False, default="") + + class Meta: + ordering = ['name'] + + def __unicode__(self): + return self.name + + + @property + def kw(self): + return 'kw.%s' % self.name + + def free_works(self): + return self.works.filter(is_free=True) + +class Edition(models.Model): + created = models.DateTimeField(auto_now_add=True) + title = models.CharField(max_length=1000) + publisher_name = models.ForeignKey("PublisherName", related_name="editions", null=True, blank=True) + publication_date = models.CharField(max_length=50, null=True, blank=True, db_index=True) + work = models.ForeignKey("Work", related_name="editions", null=True) + cover_image = models.URLField(null=True, blank=True) + unglued = models.BooleanField(default=False) + note = models.ForeignKey("EditionNote", null=True, blank=True) + + def __unicode__(self): + if self.isbn_13: + return "%s (ISBN %s) %s" % (self.title, self.isbn_13, self.publisher) + if self.oclc: + return "%s (OCLC %s) %s" % (self.title, self.oclc, self.publisher) + if self.googlebooks_id: + return "%s (GOOG %s) %s" % (self.title, self.googlebooks_id, self.publisher) + else: + return "%s (GLUE %s) %s" % (self.title, self.id, self.publisher) + + def cover_image_large(self): + #550 pixel high image + if self.cover_image: + im = get_thumbnail(self.cover_image, 'x550', crop='noop', quality=95) + if im.exists(): + return im.url + elif self.googlebooks_id: + url = "https://encrypted.google.com/books?id=%s&printsec=frontcover&img=1&zoom=0" % self.googlebooks_id + im = get_thumbnail(url, 'x550', crop='noop', quality=95) + if not im.exists() or im.storage.size(im.name) == 16392: # check for "image not available" image + url = "https://encrypted.google.com/books?id=%s&printsec=frontcover&img=1&zoom=1" % self.googlebooks_id + im = get_thumbnail(url, 'x550', crop='noop', quality=95) + if im.exists(): + return im.url + else: + return '' + else: + return '' + + def cover_image_small(self): + #80 pixel high image + if self.cover_image: + im = get_thumbnail(self.cover_image, 'x80', crop='noop', quality=95) + if im.exists(): + return im.url + if self.googlebooks_id: + return "https://encrypted.google.com/books?id=%s&printsec=frontcover&img=1&zoom=5" % self.googlebooks_id + else: + return '' + + def cover_image_thumbnail(self): + #128 pixel wide image + if self.cover_image: + im = get_thumbnail(self.cover_image, '128', crop='noop', quality=95) + if im.exists(): + return im.url + if self.googlebooks_id: + return "https://encrypted.google.com/books?id=%s&printsec=frontcover&img=1&zoom=1" % self.googlebooks_id + else: + return '' + + def has_cover_image(self): + if self.cover_image: + return self.cover_image + elif self.googlebooks_id: + return True + else: + return False + + @property + def publisher(self): + if self.publisher_name: + return self.publisher_name.name + return '' + + @property + def isbn_10(self): + return regluit.core.isbn.convert_13_to_10(self.isbn_13) + + def id_for(self, type): + if not self.pk: + return '' + try: + return self.identifiers.filter(type=type)[0].value + except IndexError: + return '' + + @property + def isbn_13(self): + return self.id_for('isbn') + + @property + def googlebooks_id(self): + return self.id_for('goog') + + @property + def librarything_id(self): + return self.id_for('thng') + + @property + def oclc(self): + return self.id_for('oclc') + + @property + def doi(self): + return self.id_for('doi') + + @property + def goodreads_id(self): + return self.id_for('gdrd') + + @property + def http_id(self): + return self.id_for('http') + + @staticmethod + def get_by_isbn(isbn): + if len(isbn) == 10: + isbn = regluit.core.isbn.convert_10_to_13(isbn) + try: + return Identifier.objects.get(type='isbn', value=isbn).edition + except Identifier.DoesNotExist: + return None + + def add_author(self, author_name, relation='aut'): + if author_name: + (author, created) = Author.objects.get_or_create(name=author_name) + (relation, created) = Relation.objects.get_or_create(code=relation) + (new_relator, created) = Relator.objects.get_or_create(author=author, edition=self) + if new_relator.relation != relation: + new_relator.relation = relation + new_relator.save() + + def remove_author(self, author): + if author: + try: + relator = Relator.objects.get(author=author, edition=self) + relator.delete() + except Relator.DoesNotExist: + pass + + def set_publisher(self, publisher_name): + if publisher_name and publisher_name != '': + try: + pub_name = PublisherName.objects.get(name=publisher_name) + if pub_name.publisher: + pub_name = pub_name.publisher.name + except PublisherName.DoesNotExist: + pub_name = PublisherName.objects.create(name=publisher_name) + pub_name.save() + + self.publisher_name = pub_name + self.save() + + #### following methods for compatibility with marc outputter + def downloads(self): + return self.ebooks.filter(active=True) + + def download_via_url(self): + return settings.BASE_URL_SECURE + reverse('download', args=[self.work.id]) + + def authnames(self): + return [auth.last_name_first for auth in self.authors.all()] + + @property + def license(self): + try: + return self.ebooks.all()[0].rights + except: + return None + + @property + def funding_info(self): + if self.ebooks.all().count() == 0: + return '' + if self.unglued: + return 'The book is available as a free download thanks to the generous support of interested readers and organizations, who made donations using the crowd-funding website Unglue.it.' + else: + if self.ebooks.all()[0].rights in cc.LICENSE_LIST: + return 'The book is available as a free download thanks to a Creative Commons license.' + else: + return 'The book is available as a free download because it is in the Public Domain.' + + @property + def description(self): + return self.work.description + +class EditionNote(models.Model): + note = models.CharField(max_length=64, null=True, blank=True, unique=True) + def __unicode__(self): + return self.note + +class Publisher(models.Model): + created = models.DateTimeField(auto_now_add=True) + name = models.ForeignKey('PublisherName', related_name='key_publisher') + url = models.URLField(max_length=1024, null=True, blank=True) + logo_url = models.URLField(max_length=1024, null=True, blank=True) + description = models.TextField(default='', null=True, blank=True) + + def __unicode__(self): + return self.name.name + +class PublisherName(models.Model): + name = models.CharField(max_length=255, blank=False, unique=True) + + publisher = models.ForeignKey('Publisher', related_name='alternate_names', null=True) + + def __unicode__(self): + return self.name + + def save(self, *args, **kwargs): + super(PublisherName, self).save(*args, **kwargs) # Call the "real" save() method. + if self.publisher and self != self.publisher.name: + #this name is an alias, repoint all editions with this name to the other. + for edition in Edition.objects.filter(publisher_name=self): + edition.publisher_name = self.publisher.name + edition.save() + + +class WasWork(models.Model): + work = models.ForeignKey('Work') + was = models.IntegerField(unique=True) + moved = models.DateTimeField(auto_now_add=True) + user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True) + +def safe_get_work(work_id): + """ + use this rather than querying the db directly for a work by id + """ + try: + work = Work.objects.get(id=work_id) + except Work.DoesNotExist: + try: + work = WasWork.objects.get(was=work_id).work + except WasWork.DoesNotExist: + raise Work.DoesNotExist() + except ValueError: + #work_id is not a number + raise Work.DoesNotExist() + return work + +def path_for_file(instance, filename): + return "ebf/{}.{}".format(uuid.uuid4().get_hex(), instance.format) + +class EbookFile(models.Model): + file = models.FileField(upload_to=path_for_file) + format = models.CharField(max_length=25, choices=settings.FORMATS) + edition = models.ForeignKey('Edition', related_name='ebook_files') + created = models.DateTimeField(auto_now_add=True) + asking = models.BooleanField(default=False) + ebook = models.ForeignKey('Ebook', related_name='ebook_files', null=True) + version = None + def check_file(self): + if self.format == 'epub': + return test_epub(self.file) + return None + + @property + def active(self): + try: + return Ebook.objects.filter(url=self.file.url)[0].active + except: + return False + + def make_mobi(self): + if not self.format == 'epub': + return False + new_mobi_ebf = EbookFile.objects.create(edition=self.edition, format='mobi', asking=self.asking) + new_mobi_ebf.file.save(path_for_file('ebf', None), ContentFile(mobi.convert_to_mobi(self.file.url))) + new_mobi_ebf.save() + if self.ebook: + new_ebook = Ebook.objects.create( + edition=self.edition, + format='mobi', + url=new_mobi_ebf.file.url, + rights=self.ebook.rights, + version_label=self.ebook.version_label, + version_iter=self.ebook.version_iter, + ) + new_mobi_ebf.ebook = new_ebook + new_mobi_ebf.save() + return True + +send_to_kindle_limit = 7492232 + +class Ebook(models.Model): + url = models.URLField(max_length=1024) #change to unique? + created = models.DateTimeField(auto_now_add=True, db_index=True,) + format = models.CharField(max_length=25, choices=settings.FORMATS) + provider = models.CharField(max_length=255) + download_count = models.IntegerField(default=0) + active = models.BooleanField(default=True) + filesize = models.PositiveIntegerField(null=True) + version_label = models.CharField(max_length=255, default="", blank=True) + version_iter = models.PositiveIntegerField(default=0) + + # 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=cc.CHOICES, db_index=True) + edition = models.ForeignKey('Edition', related_name='ebooks') + user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True) + + def kindle_sendable(self): + if not self.filesize or self.filesize < send_to_kindle_limit: + return True + else: + return False + + def get_archive(self): # returns an open file + ebf = self.get_archive_ebf() + if not ebf: + return None + try: + ebf.file.open() + except ValueError: + logger.error(u'couldn\'t open EbookFile {}'.format(ebf.id)) + return None + except IOError: + logger.error(u'EbookFile {} does not exist'.format(ebf.id)) + return None + return ebf.file + + def get_archive_ebf(self): # returns an ebf + if not self.ebook_files.filter(asking=False): + if not self.provider in good_providers: + return None + try: + r = urllib2.urlopen(self.url) + try: + self.filesize = int(r.info().getheaders("Content-Length")[0]) + if self.save: + self.filesize = self.filesize if self.filesize < 2147483647 else 2147483647 # largest safe positive integer + self.save() + ebf = EbookFile.objects.create(edition=self.edition, ebook=self, format=self.format) + ebf.file.save(path_for_file(ebf, None), ContentFile(r.read())) + ebf.file.close() + ebf.save() + return ebf + except IndexError: + # response has no Content-Length header probably a bad link + logging.error('Bad link error: {}'.format(self.url)) + except IOError: + logger.error(u'could not open {}'.format(self.url)) + else: + ebf = self.ebook_files.filter(asking=False).order_by('-created')[0] + return ebf + + def set_provider(self): + self.provider = Ebook.infer_provider(self.url) + return self.provider + + @property + def version(self): + if self.version_label is None: + return '.{}'.format(self.version_iter) + else: + return '().{}'.format(self.version_label, self.version_iter) + + def set_version(self, version): + #set both version_label and version_iter with one string with format "version.iter" + version_pattern = r'(.*)\.(\d+)$' + match = re.match(version_pattern,version) + if match: + (self.version_label, self.version_iter) = (match.group(1), match.group(2)) + else: + self.version_label = version + self.save() + + def set_next_iter(self): + # set the version iter to the next unused iter for that version + for ebook in Ebook.objects.filter( + edition=self.edition, + version_label=self.version_label, + format=self.format, + provider=self.provider + ).order_by('-version_iter'): + iter = ebook.version_iter + break + self.version_iter = iter + 1 + self.save() + + @property + def rights_badge(self): + if self.rights is None: + return cc.CCLicense.badge('PD-US') + return cc.CCLicense.badge(self.rights) + + @staticmethod + def infer_provider(url): + if not url: + return None + # provider derived from url. returns provider value. remember to call save() afterward + if re.match(r'https?://books.google.com/', url): + provider = 'Google Books' + elif re.match(r'https?://www.gutenberg.org/', url): + provider = 'Project Gutenberg' + elif re.match(r'https?://(www\.|)archive.org/', url): + provider = 'Internet Archive' + elif url.startswith('http://hdl.handle.net/2027/') or url.startswith('http://babel.hathitrust.org/'): + provider = 'Hathitrust' + elif re.match(r'https?://\w\w\.wikisource\.org/', url): + provider = 'Wikisource' + elif re.match(r'https?://\w\w\.wikibooks\.org/', url): + provider = 'Wikibooks' + elif re.match(r'https://github\.com/[^/ ]+/[^/ ]+/raw/[^ ]+', url): + provider = 'Github' + else: + provider = None + return provider + + def increment(self): + Ebook.objects.filter(id=self.id).update(download_count=F('download_count') +1) + + @property + def download_url(self): + return settings.BASE_URL_SECURE + reverse('download_ebook', args=[self.id]) + + def is_direct(self): + return self.provider not in ('Google Books', 'Project Gutenberg') + + def __unicode__(self): + return "%s (%s from %s)" % (self.edition.title, self.format, self.provider) + + def deactivate(self): + self.active = False + self.save() + + def activate(self): + self.active = True + self.save() + +def set_free_flag(sender, instance, created, **kwargs): + if created: + if not instance.edition.work.is_free and instance.active: + instance.edition.work.is_free = True + instance.edition.work.save() + elif not instance.active and instance.edition.work.is_free and instance.edition.work.ebooks().count() == 0: + instance.edition.work.is_free = False + instance.edition.work.save() + elif instance.active and not instance.edition.work.is_free and instance.edition.work.ebooks().count() > 0: + instance.edition.work.is_free = True + instance.edition.work.save() + +post_save.connect(set_free_flag, sender=Ebook) + +def reset_free_flag(sender, instance, **kwargs): + # if the Work associated with the instance Ebook currenly has only 1 Ebook, then it's no longer a free Work + # once the instance Ebook is deleted. + if instance.edition.work.ebooks().count() == 1: + instance.edition.work.is_free = False + instance.edition.work.save() + +pre_delete.connect(reset_free_flag, sender=Ebook) diff --git a/core/parameters.py b/core/parameters.py index 03707148..cfbf44ba 100644 --- a/core/parameters.py +++ b/core/parameters.py @@ -1,3 +1,30 @@ (REWARDS, BUY2UNGLUE, THANKS) = (1, 2, 3) (INDIVIDUAL, LIBRARY, BORROWED, RESERVE, THANKED) = (1, 2, 3, 4, 5) TESTING = 0 +OFFER_CHOICES = ((INDIVIDUAL,'Individual license'),(LIBRARY,'Library License')) +ACQ_CHOICES = ((INDIVIDUAL,'Individual license'),(LIBRARY,'Library License'),(BORROWED,'Borrowed from Library'), (TESTING,'Just for Testing'), (RESERVE,'On Reserve'),(THANKED,'Already Thanked'),) + +AGE_LEVEL_CHOICES = ( + ('', 'No Rating'), + ('5-6', 'Children\'s - Kindergarten, Age 5-6'), + ('6-7', 'Children\'s - Grade 1-2, Age 6-7'), + ('7-8', 'Children\'s - Grade 2-3, Age 7-8'), + ('8-9', 'Children\'s - Grade 3-4, Age 8-9'), + ('9-11', 'Children\'s - Grade 4-6, Age 9-11'), + ('12-14', 'Teen - Grade 7-9, Age 12-14'), + ('15-18', 'Teen - Grade 10-12, Age 15-18'), + ('18-', 'Adult/Advanced Reader') +) +TEXT_RELATION_CHOICES = ( + ('translation', 'translation'), + ('revision', 'revision'), + ('sequel', 'sequel'), + ('compilation', 'compilation') +) + + + + + + + diff --git a/core/tasks.py b/core/tasks.py index 37e43449..42a22c50 100644 --- a/core/tasks.py +++ b/core/tasks.py @@ -134,12 +134,13 @@ def process_ebfs(campaign): if campaign.use_add_ask: campaign.add_ask_to_ebfs() else: - campaign.work.make_ebooks_from_ebfs(add_ask=False) - campaign.work.remove_old_ebooks() + campaign.revert_asks() + campaign.make_mobis() + @task -def make_mobi(campaign): - return campaign.make_mobi() +def make_mobi(ebookfile): + return ebookfile.make_mobi() @task def refresh_acqs(): diff --git a/core/tests.py b/core/tests.py index 3fd7b8a8..f30a5808 100755 --- a/core/tests.py +++ b/core/tests.py @@ -192,6 +192,16 @@ class BookLoaderTests(TestCase): self.assertTrue(models.Edition.objects.count() > 15) self.assertEqual(models.Work.objects.filter(language=lang).count(), 1) self.assertTrue(edition.work.editions.count() > 9) + self.assertTrue(edition.work.reverse_related.count() > 0) + + # is edition.work found among the from_work of all the to_work of edition.work? + back_point = True + to_works = [wr.to_work for wr in edition.work.works_related_from.all()] + for to_work in to_works: + if edition.work.id not in [wr1.from_work.id for wr1 in to_work.works_related_to.all()]: + back_point = False + break + self.assertTrue(back_point) def test_populate_edition(self): @@ -1009,6 +1019,11 @@ class EbookFileTests(TestCase): dj_file = DjangoFile(temp_file) ebf = EbookFile( format='pdf', edition=e, file=dj_file) ebf.save() + eb = Ebook( format='pdf', edition=e, url=ebf.file.url, provider='Unglue.it') + eb.save() + ebf.ebook = eb + ebf.save() + temp_file.close() finally: @@ -1016,7 +1031,7 @@ class EbookFileTests(TestCase): os.remove(temp.name) #test the ask-appender c.add_ask_to_ebfs() - asking_pdf = c.work.ebookfiles().filter(asking = True)[0].file.url + asking_pdf = c.work.ebookfiles().filter(asking=True)[0].file.url assert test_pdf(asking_pdf) #Now do the same with epub @@ -1032,16 +1047,25 @@ class EbookFileTests(TestCase): dj_file = DjangoFile(temp_file) ebf = EbookFile( format='epub', edition=e, file=dj_file) ebf.save() - + eb = Ebook( format='epub', edition=e, url=ebf.file.url, provider='Unglue.it') + eb.save() + ebf.ebook = eb + ebf.save() temp_file.close() + ebf.make_mobi() finally: # make sure we get rid of temp file os.remove(temp.name) #test the ask-appender c.add_ask_to_ebfs() - self.assertTrue( c.work.ebookfiles().filter(asking = True, format='epub').count >0) - self.assertTrue( c.work.ebookfiles().filter(asking = True, format='mobi').count >0) - + self.assertTrue( c.work.ebookfiles().filter(asking = True, format='epub').count() > 0) + self.assertTrue( c.work.ebookfiles().filter(asking = True, format='mobi').count() > 0) + self.assertTrue( c.work.ebookfiles().filter(asking = True, ebook__active=True).count() > 0) + self.assertTrue( c.work.ebookfiles().filter(asking = False, ebook__active=True).count() == 0) + #test the unasker + c.revert_asks() + self.assertTrue( c.work.ebookfiles().filter(asking = True, ebook__active=True).count() == 0) + self.assertTrue( c.work.ebookfiles().filter(asking = False, ebook__active=True).count() > 0) class MobigenTests(TestCase): def test_convert_to_mobi(self): diff --git a/deploy/update-just b/deploy/update-just index ce2e90db..bc97a438 100755 --- a/deploy/update-just +++ b/deploy/update-just @@ -7,6 +7,9 @@ # ssh ubuntu@please.unglueit.com "/opt/regluit/deploy/update-regluit" cd /opt/regluit +find . -name "*.pyc" -delete +find . -type d -empty -delete + sudo -u ubuntu /usr/bin/git pull source ENV/bin/activate pip install --upgrade -r requirements_versioned.pip diff --git a/deploy/update-prod b/deploy/update-prod index 2175c378..52cc7705 100755 --- a/deploy/update-prod +++ b/deploy/update-prod @@ -1,6 +1,9 @@ #!/bin/bash cd /opt/regluit +find . -name "*.pyc" -delete +find . -type d -empty -delete + sudo -u ubuntu /usr/bin/git pull origin production source ENV/bin/activate pip install --upgrade -r requirements_versioned.pip diff --git a/deploy/update-regluit b/deploy/update-regluit index 68f38270..6f12c8a7 100755 --- a/deploy/update-regluit +++ b/deploy/update-regluit @@ -7,6 +7,9 @@ # ssh ubuntu@please.unglueit.com "/opt/regluit/deploy/update-regluit" cd /opt/regluit +find . -name "*.pyc" -delete +find . -type d -empty -delete + sudo -u ubuntu /usr/bin/git pull source ENV/bin/activate #pip install -r requirements.pip diff --git a/distro/push.py b/distro/push.py index 6a64feb6..30c4fb4b 100644 --- a/distro/push.py +++ b/distro/push.py @@ -4,7 +4,7 @@ from StringIO import StringIO from regluit.core.facets import BaseFacet -from regluit.core.models import Work +from regluit.core.models import Work, good_providers from regluit.api.onix import onix_feed from .models import Target @@ -45,7 +45,7 @@ def get_target_facet(target, start=datetime(1900,1,1), new=False): editions__ebooks__created__gt = start, identifiers__type="isbn", editions__ebooks__format__in = formats, - editions__ebooks__provider__in = ('Internet Archive', 'Unglue.it', 'Github', 'OAPEN Library'), + editions__ebooks__provider__in = good_providers, ).distinct().order_by('-featured') model_filters = {"Ebook": format_filter, "Edition": edition_format_filter} diff --git a/frontend/forms.py b/frontend/forms.py index 5d054ca2..a3faf19a 100644 --- a/frontend/forms.py +++ b/frontend/forms.py @@ -1,22 +1,17 @@ -""" -external library imports -""" +#external library imports + import logging import re -import zipfile -from datetime import timedelta, datetime, date +from datetime import timedelta, date from decimal import Decimal as D -""" -django imports -""" +#django imports + from django import forms from django.conf import settings from django.conf.global_settings import LANGUAGES from django.contrib.auth.models import User -from django.core.validators import validate_email -from django.db import models from django.forms.widgets import RadioSelect from django.forms.extras.widgets import SelectDateWidget from django.utils.translation import ugettext_lazy as _ @@ -32,10 +27,8 @@ from selectable.forms import ( from PyPDF2 import PdfFileReader +#regluit imports -""" -regluit imports -""" from regluit.core.models import ( UserProfile, RightsHolder, @@ -51,23 +44,28 @@ from regluit.core.models import ( Work, Press, Libpref, - Subject, TWITTER, FACEBOOK, - GRAVATAR, UNGLUEITAR ) from regluit.libraryauth.models import Library -from regluit.core.parameters import LIBRARY, REWARDS, BUY2UNGLUE, THANKS +from regluit.core.parameters import ( + LIBRARY, + REWARDS, + BUY2UNGLUE, + THANKS, + AGE_LEVEL_CHOICES, + TEXT_RELATION_CHOICES, +) from regluit.core.lookups import ( OwnerLookup, WorkLookup, PublisherNameLookup, - EditionLookup, SubjectLookup, + EditionNoteLookup, ) from regluit.utils.localdatetime import now -from regluit.utils.fields import EpubFileField, ISBNField +from regluit.utils.fields import ISBNField from regluit.mobi import Mobi from regluit.pyepub import EPUB from regluit.bisac.models import BisacHeading @@ -90,15 +88,15 @@ class SurveyForm(forms.Form): label = forms.CharField(max_length=64, required=True) survey = forms.ModelChoiceField(Questionnaire.objects.all(), widget=RadioSelect(), empty_label=None, required = True,) isbn = ISBNField( - label=_("ISBN"), - max_length=17, + label=_("ISBN"), + max_length=17, required = False, help_text = _("13 digits, no dash."), error_messages = { 'invalid': _("This must be a valid ISBN-13."), } ) - + def clean_isbn(self): isbn = self.cleaned_data['isbn'] if not isbn: @@ -115,24 +113,36 @@ class EditionForm(forms.ModelForm): add_author = forms.CharField(max_length=500, required=False) add_author_relation = forms.ChoiceField(choices=CREATOR_RELATIONS, initial=('aut', 'Author')) add_subject = AutoCompleteSelectField( - SubjectLookup, - widget=AutoCompleteSelectWidget(SubjectLookup,allow_new=True), - label='Keyword', - required =False + SubjectLookup, + widget=AutoCompleteSelectWidget(SubjectLookup, allow_new=True), + label='Keyword', + required=False, ) + add_related_work = AutoCompleteSelectField( + WorkLookup, + widget=AutoCompleteSelectWidget(WorkLookup, allow_new=False, attrs={'size': 40}), + label='Related Work', + required=False, + ) + add_work_relation = forms.ChoiceField( + choices=TEXT_RELATION_CHOICES, + initial=('translation', 'translation'), + required=False, + ) + bisac = forms.ModelChoiceField( bisac_headings, required=False ) - + publisher_name = AutoCompleteSelectField( - PublisherNameLookup, - label='Publisher Name', - widget=AutoCompleteSelectWidget(PublisherNameLookup,allow_new=True), - required=False, - allow_new=True, - ) + PublisherNameLookup, + label='Publisher Name', + widget=AutoCompleteSelectWidget(PublisherNameLookup,allow_new=True), + required=False, + allow_new=True, + ) isbn = ISBNField( - label=_("ISBN"), - max_length=17, + label=_("ISBN"), + max_length=17, required = False, help_text = _("13 digits, no dash."), error_messages = { @@ -140,8 +150,8 @@ class EditionForm(forms.ModelForm): } ) goog = forms.RegexField( - label=_("Google Books ID"), - max_length=12, + label=_("Google Books ID"), + max_length=12, regex=r'^([a-zA-Z0-9\-_]{12}|delete)$', required = False, help_text = _("12 alphanumeric characters, dash or underscore, case sensitive."), @@ -150,8 +160,8 @@ class EditionForm(forms.ModelForm): } ) gdrd = forms.RegexField( - label=_("GoodReads ID"), - max_length=8, + label=_("GoodReads ID"), + max_length=8, regex=r'^(\d+|delete)$', required = False, help_text = _("1-8 digits."), @@ -160,8 +170,8 @@ class EditionForm(forms.ModelForm): } ) thng = forms.RegexField( - label=_("LibraryThing ID"), - max_length=8, + label=_("LibraryThing ID"), + max_length=8, regex=r'(^\d+|delete)$', required = False, help_text = _("1-8 digits."), @@ -170,7 +180,7 @@ class EditionForm(forms.ModelForm): } ) oclc = forms.RegexField( - label=_("OCLCnum"), + label=_("OCLCnum"), regex=r'^(\d\d\d\d\d\d\d\d\d*|delete)$', required = False, help_text = _("8 or more digits."), @@ -182,132 +192,191 @@ class EditionForm(forms.ModelForm): label=_("HTTP URL"), # https://mathiasbynens.be/demo/url-regex regex=re.compile(r"(https?|ftp)://(-\.)?([^\s/?\.#]+\.?)+(/[^\s]*)?$", - flags=re.IGNORECASE|re.S ), + flags=re.IGNORECASE|re.S ), required = False, help_text = _("no spaces of funny stuff."), error_messages = { 'invalid': _("This value must be a valid http(s) URL."), } ) + doi = forms.RegexField( + label=_("DOI"), + regex=r'^(https?://dx\.doi\.org/)?(10.\d\d\d\d/\w+|delete)$', + required = False, + help_text = _("starts with '10.' or 'http://dx.doi.org'"), + error_messages = { + 'invalid': _("This value must be a valid DOI."), + } + ) language = forms.ChoiceField(choices=LANGUAGES) + age_level = forms.ChoiceField(choices=AGE_LEVEL_CHOICES, required=False) description = forms.CharField( required=False, widget=CKEditorWidget()) coverfile = forms.ImageField(required=False) - + note = AutoCompleteSelectField( + EditionNoteLookup, + widget=AutoCompleteSelectWidget(EditionNoteLookup, allow_new=True), + label='Edition Note', + required=False, + allow_new=True, + ) def __init__(self, *args, **kwargs): super(EditionForm, self).__init__(*args, **kwargs) self.relators = [] if self.instance: for relator in self.instance.relators.all(): select = forms.Select(choices=CREATOR_RELATIONS).render('change_relator_%s' % relator.id , relator.relation.code ) - self.relators.append({'relator':relator,'select':select}) - + self.relators.append({'relator':relator, 'select':select}) + + def clean_doi(self): + doi = self.cleaned_data["doi"] + if doi: + if doi.startswith("https"): + return doi[19:] + elif doi.startswith("http"): + return doi[18:] + return doi + def clean(self): has_isbn = self.cleaned_data.get("isbn", False) not in nulls has_oclc = self.cleaned_data.get("oclc", False) not in nulls has_goog = self.cleaned_data.get("goog", False) not in nulls has_http = self.cleaned_data.get("http", False) not in nulls - if not has_isbn and not has_oclc and not has_goog and not has_http: - raise forms.ValidationError(_("There must be either an ISBN or an OCLC number.")) + has_doi = self.cleaned_data.get("doi", False) not in nulls + try: + has_id = self.instance.work.identifiers.all().count() > 0 + except AttributeError: + has_id = False + if not has_id and not has_isbn and not has_oclc and not has_goog and not has_http and not has_doi: + raise forms.ValidationError(_("There must be either an ISBN, a DOI, a URL or an OCLC number.")) return self.cleaned_data - class Meta: model = Edition exclude = ('created', 'work') - widgets = { + widgets = { 'title': forms.TextInput(attrs={'size': 40}), 'add_author': forms.TextInput(attrs={'size': 30}), 'add_subject': forms.TextInput(attrs={'size': 30}), 'unglued': forms.CheckboxInput(), 'cover_image': forms.TextInput(attrs={'size': 60}), } - + +def test_file(the_file): + if the_file and the_file.name: + if format == 'epub': + try: + book = EPUB(the_file.file) + except Exception as e: + raise forms.ValidationError(_('Are you sure this is an EPUB file?: %s' % e) ) + elif format == 'mobi': + try: + book = Mobi(the_file.file) + book.parse() + except Exception as e: + raise forms.ValidationError(_('Are you sure this is a MOBI file?: %s' % e) ) + elif format == 'pdf': + try: + doc = PdfFileReader(the_file.file) + except Exception, e: + raise forms.ValidationError(_('%s is not a valid PDF file' % the_file.name) ) + class EbookFileForm(forms.ModelForm): - file = forms.FileField(max_length=16777216) - + file = forms.FileField(max_length=16777216) + version_label = forms.CharField(max_length=512, required=False) + new_version_label = forms.CharField(required=False) + def __init__(self, campaign_type=BUY2UNGLUE, *args, **kwargs): super(EbookFileForm, self).__init__(*args, **kwargs) self.campaign_type = campaign_type if campaign_type == BUY2UNGLUE: - self.fields['format'].widget=forms.HiddenInput() + self.fields['format'].widget = forms.HiddenInput() if campaign_type == THANKS: - self.fields['format'].widget=forms.Select(choices=(('pdf','PDF'),( 'epub','EPUB'), ('mobi','MOBI'))) - + self.fields['format'].widget = forms.Select( + choices = (('pdf', 'PDF'), ('epub', 'EPUB'), ('mobi', 'MOBI')) + ) + + def clean_version_label(self): + new_label = self.data.get('new_version_label','') + return new_label if new_label else self.cleaned_data['version_label'] + def clean_format(self): if self.campaign_type is BUY2UNGLUE: return 'epub' else: logger.info("EbookFileForm "+self.cleaned_data.get('format','')) return self.cleaned_data.get('format','') - + def clean(self): format = self.cleaned_data['format'] - the_file = self.cleaned_data.get('file',None) - if the_file and the_file.name: - if format == 'epub': - try: - book = EPUB(the_file.file) - except Exception as e: - raise forms.ValidationError(_('Are you sure this is an EPUB file?: %s' % e) ) - elif format == 'mobi': - try: - book = Mobi(the_file.file) - book.parse() - except Exception as e: - raise forms.ValidationError(_('Are you sure this is a MOBI file?: %s' % e) ) - elif format == 'pdf': - try: - doc = PdfFileReader(the_file.file) - except Exception, e: - raise forms.ValidationError(_('%s is not a valid PDF file' % the_file.name) ) + the_file = self.cleaned_data.get('file', None) + test_file(the_file) return self.cleaned_data class Meta: model = EbookFile widgets = { 'edition': forms.HiddenInput} - exclude = { 'created', 'asking' } - + exclude = { 'created', 'asking', 'ebook' } + class EbookForm(forms.ModelForm): + file = forms.FileField(max_length=16777216, required=False) + url = forms.CharField(required=False, widget=forms.TextInput(attrs={'size' : 60},)) + version_label = forms.CharField(required=False) + new_version_label = forms.CharField(required=False) + class Meta: model = Ebook - exclude =( 'created', 'download_count', 'active', 'filesize') - widgets = { - 'edition': forms.HiddenInput, - 'user': forms.HiddenInput, - 'provider': forms.HiddenInput, - 'url': forms.TextInput(attrs={'size' : 60}), + exclude = ('created', 'download_count', 'active', 'filesize', 'version_iter') + widgets = { + 'edition': forms.HiddenInput, + 'user': forms.HiddenInput, + 'provider': forms.HiddenInput, } + def clean_version_label(self): + new_label = self.data.get('new_version_label','') + return new_label if new_label else self.cleaned_data['version_label'] + def clean_provider(self): - new_provider= Ebook.infer_provider(self.data[self.prefix + '-url']) + new_provider = Ebook.infer_provider(self.cleaned_data['url']) if not new_provider: raise forms.ValidationError(_("At this time, ebook URLs must point at Internet Archive, Wikisources, Wikibooks, Hathitrust, Project Gutenberg, raw files at Github, or Google Books.")) return new_provider - + def clean_url(self): - url = self.data[self.prefix + '-url'] + url = self.cleaned_data['url'] try: Ebook.objects.get(url=url) except Ebook.DoesNotExist: return url raise forms.ValidationError(_("There's already an ebook with that url.")) - + + def clean(self): + format = self.cleaned_data['format'] + the_file = self.cleaned_data.get('file', None) + url = self.cleaned_data.get('url', None) + test_file(the_file) + if not the_file and not url: + raise forms.ValidationError(_("Either a link or a file is required.")) + if the_file and url: + self.cleaned_data['url'] = '' + return self.cleaned_data + def UserClaimForm ( user_instance, *args, **kwargs ): class ClaimForm(forms.ModelForm): - i_agree=forms.BooleanField(error_messages={'required': 'You must agree to the Terms in order to claim a work.'}) - rights_holder=forms.ModelChoiceField(queryset=user_instance.rights_holder.all(), empty_label=None) - + i_agree = forms.BooleanField(error_messages={'required': 'You must agree to the Terms in order to claim a work.'}) + rights_holder = forms.ModelChoiceField(queryset=user_instance.rights_holder.all(), empty_label=None) + class Meta: model = Claim exclude = ('status',) - widgets = { - 'user': forms.HiddenInput, - 'work': forms.HiddenInput, + widgets = { + 'user': forms.HiddenInput, + 'work': forms.HiddenInput, } def __init__(self): super(ClaimForm, self).__init__(*args, **kwargs) return ClaimForm() - + class RightsHolderForm(forms.ModelForm): owner = AutoCompleteSelectField( OwnerLookup, @@ -317,7 +386,7 @@ class RightsHolderForm(forms.ModelForm): error_messages={'required': 'Please ensure the owner is a valid Unglue.it account.'}, ) email = forms.EmailField( - label=_("notification email address for rights holder"), + label=_("notification email address for rights holder"), max_length=100, error_messages={'required': 'Please enter an email address for the rights holder.'}, ) @@ -333,12 +402,12 @@ class RightsHolderForm(forms.ModelForm): return rights_holder_name raise forms.ValidationError(_("Another rights holder with that name already exists.")) - + class ProfileForm(forms.ModelForm): - clear_facebook=forms.BooleanField(required=False) - clear_twitter=forms.BooleanField(required=False) - clear_goodreads=forms.BooleanField(required=False) - + clear_facebook = forms.BooleanField(required=False) + clear_twitter = forms.BooleanField(required=False) + clear_goodreads = forms.BooleanField(required=False) + class Meta: model = UserProfile fields = 'tagline', 'librarything_id', 'home_url', 'clear_facebook', 'clear_twitter', 'clear_goodreads', 'avatar_source' @@ -361,10 +430,10 @@ class ProfileForm(forms.ModelForm): def clean(self): # check that if a social net is cleared, we're not using it a avatar source - if self.cleaned_data.get("clear_facebook", False) and self.cleaned_data.get("avatar_source", None)==FACEBOOK: - self.cleaned_data["avatar_source"]==UNGLUEITAR - if self.cleaned_data.get("clear_twitter", False) and self.cleaned_data.get("avatar_source", None)==TWITTER: - self.cleaned_data["avatar_source"]==UNGLUEITAR + if self.cleaned_data.get("clear_facebook", False) and self.cleaned_data.get("avatar_source", None) == FACEBOOK: + self.cleaned_data["avatar_source"] == UNGLUEITAR + if self.cleaned_data.get("clear_twitter", False) and self.cleaned_data.get("avatar_source", None) == TWITTER: + self.cleaned_data["avatar_source"] == UNGLUEITAR return self.cleaned_data class CloneCampaignForm(forms.Form): @@ -395,30 +464,33 @@ def getTransferCreditForm(maximum, data=None, *args, **kwargs ): ) amount = forms.IntegerField( required=True, - min_value=1, + min_value=1, max_value=maximum, label="Transfer Amount", - error_messages={'min_value': 'Transfer amount must be positive', 'max_value': 'You only have %(limit_value)s available for transfer'}, + error_messages={ + 'min_value': 'Transfer amount must be positive', + 'max_value': 'You only have %(limit_value)s available for transfer' + }, ) return TransferCreditForm( data=data ) class WorkForm(forms.Form): - other_work = forms.ModelChoiceField(queryset=Work.objects.all(), - widget=forms.HiddenInput(), - required=True, + other_work = forms.ModelChoiceField(queryset=Work.objects.all(), + widget=forms.HiddenInput(), + required=True, error_messages={'required': 'Missing work to merge with.'}, ) - work=None + work = None def clean_other_work(self): - if self.cleaned_data["other_work"].id== self.work.id: + if self.cleaned_data["other_work"].id == self.work.id: raise forms.ValidationError(_("You can't merge a work into itself")) return self.cleaned_data["other_work"] def __init__(self, work=None, *args, **kwargs): super(WorkForm, self).__init__(*args, **kwargs) - self.work=work + self.work = work class OtherWorkForm(WorkForm): other_work = AutoCompleteSelectField( @@ -427,12 +499,12 @@ class OtherWorkForm(WorkForm): widget=AutoCompleteSelectWidget(WorkLookup), required=True, error_messages={'required': 'Missing work to merge with.'}, - ) + ) def __init__(self, *args, **kwargs): super(OtherWorkForm, self).__init__(*args, **kwargs) self.fields['other_work'].widget.update_query_parameters({'language':self.work.language}) - + class EditManagersForm(forms.ModelForm): managers = AutoCompleteSelectMultipleField( OwnerLookup, @@ -451,7 +523,7 @@ class CustomPremiumForm(forms.ModelForm): class Meta: model = Premium fields = 'campaign', 'amount', 'description', 'type', 'limit' - widgets = { + widgets = { 'description': forms.Textarea(attrs={'cols': 80, 'rows': 4}), 'campaign': forms.HiddenInput, 'type': forms.HiddenInput(attrs={'value':'XX'}), @@ -459,21 +531,24 @@ class CustomPremiumForm(forms.ModelForm): } def clean_type(self): return 'CU' - + class OfferForm(forms.ModelForm): class Meta: model = Offer fields = 'work', 'price', 'license' - widgets = { + widgets = { 'work': forms.HiddenInput, 'license': forms.HiddenInput, } - -date_selector=range(date.today().year, settings.MAX_CC_DATE.year+1) + +date_selector = range(date.today().year, settings.MAX_CC_DATE.year+1) class CCDateForm(object): - target = forms.DecimalField( min_value= D(settings.UNGLUEIT_MINIMUM_TARGET), error_messages={'required': 'Please specify a Revenue Target.'} ) + target = forms.DecimalField( + min_value= D(settings.UNGLUEIT_MINIMUM_TARGET), + error_messages={'required': 'Please specify a Revenue Target.'} + ) minimum_target = settings.UNGLUEIT_MINIMUM_TARGET maximum_target = settings.UNGLUEIT_MAXIMUM_TARGET max_cc_date = settings.MAX_CC_DATE @@ -490,26 +565,26 @@ class CCDateForm(object): new_cc_date_initial = self.cleaned_data['cc_date_initial'] if new_cc_date_initial.date() > settings.MAX_CC_DATE: raise forms.ValidationError('The initial Ungluing Date cannot be after %s'%settings.MAX_CC_DATE) - elif new_cc_date_initial - now() < timedelta(days=0): + elif new_cc_date_initial - now() < timedelta(days=0): raise forms.ValidationError('The initial Ungluing date must be in the future!') return new_cc_date_initial class DateCalculatorForm(CCDateForm, forms.ModelForm): revenue = forms.DecimalField() cc_date_initial = forms.DateTimeField( - widget = SelectDateWidget(years=date_selector) + widget = SelectDateWidget(years=date_selector) ) class Meta: model = Campaign fields = 'target', 'cc_date_initial', 'revenue', def getManageCampaignForm ( instance, data=None, initial=None, *args, **kwargs ): - + def get_queryset(): - work=instance.work + work = instance.work return Edition.objects.filter(work = work) - - class ManageCampaignForm(CCDateForm,forms.ModelForm): + + class ManageCampaignForm(CCDateForm, forms.ModelForm): target = forms.DecimalField( required= (instance.type in {REWARDS, BUY2UNGLUE})) deadline = forms.DateTimeField( required = (instance.type==REWARDS), @@ -520,91 +595,102 @@ def getManageCampaignForm ( instance, data=None, initial=None, *args, **kwargs ) widget = SelectDateWidget(years=date_selector) if instance.status=='INITIALIZED' else forms.HiddenInput ) paypal_receiver = forms.EmailField( - label=_("contact email address for this campaign"), - max_length=100, + label=_("contact email address for this campaign"), + max_length=100, error_messages={'required': 'You must enter the email we should contact you at for this campaign.'}, ) - edition = forms.ModelChoiceField(get_queryset(), widget=RadioSelect(),empty_label='no edition selected',required = False,) - publisher = forms.ModelChoiceField(instance.work.publishers(), empty_label='no publisher selected', required = False,) + edition = forms.ModelChoiceField( + get_queryset(), + widget=RadioSelect(), + empty_label='no edition selected', + required=False, + ) + publisher = forms.ModelChoiceField( + instance.work.publishers(), + empty_label='no publisher selected', + required=False, + ) work_description = forms.CharField( required=False , widget=CKEditorWidget()) - + class Meta: model = Campaign - fields = 'description', 'details', 'license', 'target', 'deadline', 'paypal_receiver', 'edition', 'email', 'publisher', 'cc_date_initial', "do_watermark", "use_add_ask", + fields = ('description', 'details', 'license', 'target', 'deadline', 'paypal_receiver', + 'edition', 'email', 'publisher', 'cc_date_initial', "do_watermark", "use_add_ask", + ) widgets = { 'deadline': SelectDateWidget } - + def clean_target(self): if self.instance.type == THANKS: return None - new_target = super(ManageCampaignForm,self).clean_target() + new_target = super(ManageCampaignForm, self).clean_target() if self.instance: if self.instance.status == 'ACTIVE' and self.instance.target < new_target: raise forms.ValidationError(_('The fundraising target for an ACTIVE campaign cannot be increased.')) return new_target def clean_cc_date_initial(self): - if self.instance.type in {REWARDS,THANKS} : + if self.instance.type in {REWARDS, THANKS} : return None if self.instance: if self.instance.status != 'INITIALIZED': # can't change this once launched return self.instance.cc_date_initial - return super(ManageCampaignForm,self).clean_cc_date_initial() - + return super(ManageCampaignForm, self).clean_cc_date_initial() + def clean_deadline(self): if self.instance.type in {BUY2UNGLUE, THANKS} : return None new_deadline_date = self.cleaned_data['deadline'] - new_deadline= new_deadline_date + timedelta(hours=23,minutes=59) + new_deadline = new_deadline_date + timedelta(hours=23, minutes=59) if self.instance: if self.instance.status == 'ACTIVE': return self.instance.deadline if new_deadline_date - now() > timedelta(days=int(settings.UNGLUEIT_LONGEST_DEADLINE)): raise forms.ValidationError(_('The chosen closing date is more than %s days from now' % settings.UNGLUEIT_LONGEST_DEADLINE)) - elif new_deadline - now() < timedelta(days=0): + elif new_deadline - now() < timedelta(days=0): raise forms.ValidationError(_('The chosen closing date is in the past')) return new_deadline - + def clean_license(self): new_license = self.cleaned_data['license'] if self.instance: if self.instance.status == 'ACTIVE' and self.instance.license != new_license: # should only allow change to a less restrictive license - if self.instance.license == 'CC BY-ND' and new_license in ['CC BY-NC-ND','CC BY-NC-SA','CC BY-NC']: + if self.instance.license == 'CC BY-ND' and new_license in ['CC BY-NC-ND', 'CC BY-NC-SA', 'CC BY-NC']: raise forms.ValidationError(_('The proposed license for an ACTIVE campaign may not add restrictions.')) elif self.instance.license == 'CC BY' and new_license != 'CC0': raise forms.ValidationError(_('The proposed license for an ACTIVE campaign may not add restrictions.')) - elif self.instance.license == 'CC BY-NC' and new_license in ['CC BY-NC-ND','CC BY-NC-SA','CC BY-SA','CC BY-ND']: + elif self.instance.license == 'CC BY-NC' and new_license in ['CC BY-NC-ND', 'CC BY-NC-SA', 'CC BY-SA', 'CC BY-ND']: raise forms.ValidationError(_('The proposed license for an ACTIVE campaign may not add restrictions.')) - elif self.instance.license == 'CC BY-ND' and new_license in ['CC BY-NC-ND','CC BY-NC-SA','CC BY-SA','CC BY-NC']: + elif self.instance.license == 'CC BY-ND' and new_license in ['CC BY-NC-ND', 'CC BY-NC-SA', 'CC BY-SA', 'CC BY-NC']: raise forms.ValidationError(_('The proposed license for an ACTIVE campaign may not add restrictions.')) - elif self.instance.license == 'CC BY-SA' and new_license in ['CC BY-NC-ND','CC BY-NC-SA','CC BY-ND','CC BY-NC']: + elif self.instance.license == 'CC BY-SA' and new_license in ['CC BY-NC-ND', 'CC BY-NC-SA', 'CC BY-ND', 'CC BY-NC']: raise forms.ValidationError(_('The proposed license for an ACTIVE campaign may not add restrictions.')) - elif self.instance.license == 'CC BY-NC-SA' and new_license in ['CC BY-NC-ND','CC BY-ND']: + elif self.instance.license == 'CC BY-NC-SA' and new_license in ['CC BY-NC-ND', 'CC BY-ND']: raise forms.ValidationError(_('The proposed license for an ACTIVE campaign may not add restrictions.')) elif self.instance.license == 'CC0' : raise forms.ValidationError(_('The proposed license for an ACTIVE campaign may not add restrictions.')) - elif self.instance.license in ['GDFL' , 'LAL']: + elif self.instance.license in ['GDFL', 'LAL']: raise forms.ValidationError(_('Once you start a campaign with GDFL or LAL, you can\'t use any other license.')) return new_license if initial and not initial.get('edition', None) and not instance.edition: - initial['edition']= instance.work.editions.all()[0] - return ManageCampaignForm(instance = instance, data=data, initial=initial) + initial['edition'] = instance.work.editions.all()[0] + return ManageCampaignForm(instance=instance, data=data, initial=initial) class CampaignPurchaseForm(forms.Form): anonymous = forms.BooleanField(required=False, label=_("Make this purchase anonymous, please")) offer_id = forms.IntegerField(required=False) - offer=None + offer = None library_id = forms.IntegerField(required=False) library = None - copies = forms.IntegerField(required=False,min_value=1) + copies = forms.IntegerField(required=False, min_value=1) give_to = forms.EmailField(required = False) give_message = forms.CharField(required = False, max_length=512, ) - + def clean_offer_id(self): offer_id = self.cleaned_data['offer_id'] try: - self.offer= Offer.objects.get(id=offer_id) + self.offer = Offer.objects.get(id=offer_id) except Offer.DoesNotExist: raise forms.ValidationError(_("Sorry, that offer is not valid.")) @@ -615,23 +701,23 @@ class CampaignPurchaseForm(forms.Form): self.library = Library.objects.get(id=library_id) except Library.DoesNotExist: raise forms.ValidationError(_("Sorry, that Library is not valid.")) - + def clean_copies(self): - copies = self.cleaned_data.get('copies',1) + copies = self.cleaned_data.get('copies', 1) return copies if copies else 1 - + def clean_anonymous(self): if self.data.get('give', False): return True else: return self.cleaned_data['anonymous'] - + def clean(self): if self.offer.license == LIBRARY: if not self.library: raise forms.ValidationError(_("No library specified." )) if self.data.get('give', False): - if not self.cleaned_data.get('give_to',None): + if not self.cleaned_data.get('give_to', None): raise forms.ValidationError(_("Gift recipient email is needed." )) else: if 'give_to' in self._errors: @@ -639,28 +725,31 @@ class CampaignPurchaseForm(forms.Form): return self.cleaned_data def amount(self): - - return self.offer.price * self.cleaned_data.get('copies',1) if self.offer else None - + + return self.offer.price * self.cleaned_data.get('copies', 1) if self.offer else None + @property def trans_extra(self): pe = PledgeExtra( anonymous=self.cleaned_data['anonymous'], offer = self.offer ) if self.library: - pe.extra['library_id']=self.library.id - pe.extra['copies']=self.cleaned_data.get('copies',1) + pe.extra['library_id'] = self.library.id + pe.extra['copies'] = self.cleaned_data.get('copies', 1) if self.data.get('give', False): - pe.extra['give_to']=self.cleaned_data['give_to'] - pe.extra['give_message']=self.cleaned_data['give_message'] + pe.extra['give_to'] = self.cleaned_data['give_to'] + pe.extra['give_message'] = self.cleaned_data['give_message'] return pe class CampaignThanksForm(forms.Form): - anonymous = forms.BooleanField(required=False, label=_("Make this contribution anonymous, please")) + anonymous = forms.BooleanField( + required=False, + label=_("Make this contribution anonymous, please") + ) preapproval_amount = forms.DecimalField( required = True, min_value=D('1.00'), - max_value=D('2000.00'), - decimal_places=2, + max_value=D('2000.00'), + decimal_places=2, label="Pledge Amount", ) @property @@ -672,43 +761,47 @@ class CampaignPledgeForm(forms.Form): preapproval_amount = forms.DecimalField( required = False, min_value=D('1.00'), - max_value=D('2000.00'), - decimal_places=2, + max_value=D('2000.00'), + decimal_places=2, label="Pledge Amount", ) def amount(self): return self.cleaned_data["preapproval_amount"] if self.cleaned_data else None - + anonymous = forms.BooleanField(required=False, label=_("Make this pledge anonymous, please")) - ack_name = forms.CharField(required=False, max_length=64, label=_("What name should we display?")) + ack_name = forms.CharField( + required=False, + max_length=64, + label=_("What name should we display?") + ) ack_dedication = forms.CharField(required=False, max_length=140, label=_("Your dedication:")) premium_id = forms.IntegerField(required=False) - premium=None - + premium = None + @property def trans_extra(self): return PledgeExtra( anonymous=self.cleaned_data['anonymous'], ack_name=self.cleaned_data['ack_name'], ack_dedication=self.cleaned_data['ack_dedication'], premium=self.premium) - + def clean_preapproval_amount(self): preapproval_amount = self.cleaned_data['preapproval_amount'] if preapproval_amount is None: raise forms.ValidationError(_("Please enter a pledge amount.")) return preapproval_amount - + def clean_premium_id(self): premium_id = self.cleaned_data['premium_id'] try: - self.premium= Premium.objects.get(id=premium_id) - if self.premium.limit>0: - if self.premium.limit<=self.premium.premium_count: + self.premium = Premium.objects.get(id=premium_id) + if self.premium.limit > 0: + if self.premium.limit <= self.premium.premium_count: raise forms.ValidationError(_("Sorry, that premium is fully subscribed.")) except Premium.DoesNotExist: raise forms.ValidationError(_("Sorry, that premium is not valid.")) - + def clean(self): # check on whether the preapproval amount is < amount for premium tier. If so, put an error message preapproval_amount = self.cleaned_data.get("preapproval_amount") @@ -716,7 +809,7 @@ class CampaignPledgeForm(forms.Form): # preapproval_amount failed validation, that error is the relevant one return self.cleaned_data elif self.premium is None: - raise forms.ValidationError(_("Please select a premium." )) + raise forms.ValidationError(_("Please select a premium." )) elif preapproval_amount < self.premium.amount: logger.info("raising form validating error") raise forms.ValidationError(_("Sorry, you must pledge at least $%s to select that premium." % (self.premium.amount))) @@ -727,11 +820,11 @@ class TokenCCMixin(forms.Form): class BaseCCMixin(forms.Form): work_id = forms.IntegerField(required=False, widget=forms.HiddenInput()) - preapproval_amount= forms.DecimalField( + preapproval_amount = forms.DecimalField( required=False, - min_value=D('1.00'), - max_value=D('100000.00'), - decimal_places=2, + min_value=D('1.00'), + max_value=D('100000.00'), + decimal_places=2, label="Amount", ) class UserCCMixin(forms.Form): @@ -751,7 +844,7 @@ class CCForm(UserCCMixin, BaseCCForm): class AccountCCForm( BaseCCMixin, UserCCMixin, forms.Form): pass - + class GoodreadsShelfLoadingForm(forms.Form): goodreads_shelf_name_number = forms.CharField(widget=forms.Select(choices=( ('all','all'), @@ -767,40 +860,60 @@ class PledgeCancelForm(forms.Form): class CampaignAdminForm(forms.Form): campaign_id = forms.IntegerField() - + class EmailShareForm(forms.Form): recipient = forms.EmailField(error_messages={'required': 'Please specify a recipient.'}) subject = forms.CharField(max_length=100, error_messages={'required': 'Please specify a subject.'}) - message = forms.CharField(widget=forms.Textarea(), error_messages={'required': 'Please include a message.'}) + message = forms.CharField( + widget=forms.Textarea(), + error_messages={'required': 'Please include a message.'} + ) # 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", error_messages={'required': 'Please specify your email address.'}) - subject = forms.CharField(max_length=500, widget=forms.TextInput(attrs={'size':50}), error_messages={'required': 'Please specify a subject.'}) - message = forms.CharField(widget=forms.Textarea(), error_messages={'required': 'Please specify a message.'}) + sender = forms.EmailField( + widget=forms.TextInput(attrs={'size':50}), + label="Your email", + error_messages={'required': 'Please specify your email address.'} + ) + subject = forms.CharField( + max_length=500, + widget=forms.TextInput(attrs={'size':50}), + error_messages={'required': 'Please specify a subject.'} + ) + message = forms.CharField( + widget=forms.Textarea(), + error_messages={'required': 'Please specify a message.'} + ) page = forms.CharField(widget=forms.HiddenInput()) - notarobot = forms.IntegerField(label="Please prove you're not a robot", error_messages={'required': "You must do the sum to prove you're not a robot."}) + notarobot = forms.IntegerField( + label="Please prove you're not a robot", + error_messages={'required': "You must do the sum to 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 class MsgForm(forms.Form): - msg = forms.CharField(widget=forms.Textarea(), error_messages={'required': 'Please specify a message.'}) + msg = forms.CharField( + widget=forms.Textarea(), + error_messages={'required': 'Please specify a message.'} + ) def full_clean(self): - super(MsgForm,self).full_clean() + super(MsgForm, self).full_clean() if self.data.has_key("supporter"): try: self.cleaned_data['supporter'] = User.objects.get(id=self.data["supporter"]) @@ -815,20 +928,20 @@ class MsgForm(forms.Form): raise ValidationError("Work does not exist") else: raise ValidationError("Work is not specified") - + class PressForm(forms.ModelForm): class Meta: model = Press exclude = () - widgets = { + widgets = { 'date': SelectDateWidget(years=range(2010,2025)), } - + class KindleEmailForm(forms.Form): kindle_email = forms.EmailField() - - + + class LibModeForm(forms.ModelForm): class Meta: model = Libpref @@ -836,7 +949,11 @@ class LibModeForm(forms.ModelForm): class RegiftForm(forms.Form): give_to = forms.EmailField(label="email address of recipient") - give_message = forms.CharField( max_length=512, label="your gift message", initial="Here's an ebook from unglue.it, I hope you like it! - me") + give_message = forms.CharField( + max_length=512, + label="your gift message", + initial="Here's an ebook from unglue.it, I hope you like it! - me", + ) class SubjectSelectForm(forms.Form): add_kw = AutoCompleteSelectField( @@ -855,4 +972,4 @@ class MapSubjectForm(forms.Form): widget=AutoCompleteSelectWidget(SubjectLookup,allow_new=False), label='Target Subject', ) - + diff --git a/frontend/templates/download.html b/frontend/templates/download.html index adb5fa1e..a651df00 100644 --- a/frontend/templates/download.html +++ b/frontend/templates/download.html @@ -149,7 +149,7 @@ $j(document).ready(function() { {{ ebook.rights}} {{ ebook.format }} - {{ ebook.format }} + {{ ebook.format }} {% if ebook.version_label %} ({{ ebook.version_label }}) {% endif %} {% if ebook.is_direct %}{% endif %} @@ -168,7 +168,7 @@ $j(document).ready(function() { {{ ebook.rights}} {{ ebook.format }} at {{ebook.provider}} - {{ ebook.format }} at {{ ebook.provider }} + {{ ebook.format }} {% if ebook.version_label %} ({{ ebook.version_label }}) {% endif %} at {{ ebook.provider }} {% if ebook.is_direct %}{% endif %} {% if not forloop.last %}

{% endif %} diff --git a/frontend/templates/edition_display.html b/frontend/templates/edition_display.html index a05bd15e..703011e1 100644 --- a/frontend/templates/edition_display.html +++ b/frontend/templates/edition_display.html @@ -16,6 +16,12 @@ {% endfor %}
{% endif %} + {% if edition.note %} + {{ edition.note }}.
+ {% endif %} + {% if edition.downloads.count %} + {{ edition.downloads.count }} ebooks
+ {% endif %} {% if edition.publisher %} Publisher: {{edition.publisher}}
{% endif %} @@ -28,6 +34,9 @@ {% if edition.oclc %} OCLC: {{ edition.oclc }}
{% endif %} + {% if edition.doi %} + DOI: {{ edition.doi }}
+ {% endif %} {% if edition.http_id %} web: {{ edition.http_id }}
{% endif %} diff --git a/frontend/templates/edition_upload.html b/frontend/templates/edition_upload.html index 721d6091..812accc3 100644 --- a/frontend/templates/edition_upload.html +++ b/frontend/templates/edition_upload.html @@ -1,31 +1,48 @@
- {% if edition.ebook_form %} - {% if show_ebook_form %} -
+ {% if ebook_form and show_ebook_form %} +
{% if alert %}
{{alert}}
{% endif %} - {% if edition.ebooks.all.0 %} -

eBooks for this Edition

- - {% for ebook in edition.ebooks.all %} - {{ ebook.format }} {{ebook.rights}} at {{ebook.provider}}. Downloaded {{ ebook.download_count }} times.
- {% endfor %} - {% endif %} - -

Add an eBook for this Edition:

- - 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, Wikibooks, Hathitrust, Project Gutenberg, raw files at Github, or Google Books. - -
- {% csrf_token %}{{ edition.ebook_form.edition.errors }}{{ edition.ebook_form.edition }}{{ edition.ebook_form.user.errors }}{{ edition.ebook_form.user }}{{ edition.ebook_form.provider.errors }}{{ edition.ebook_form.provider }} - {{ edition.ebook_form.url.errors }}URL: {{ edition.ebook_form.url }}
- {{ edition.ebook_form.format.errors }}File Format: {{ edition.ebook_form.format }}    - {{ edition.ebook_form.rights.errors }}License: {{ edition.ebook_form.rights }}
- -
-
- {% else %} -
Adding ebook links is disabled for this work.
- {% endif %} + {% if edition.ebooks.all.0 %} +

eBooks for this Edition

+ {% for ebook in edition.ebooks.all %} + {% if ebook.active %} + {{ ebook.format }} {{ebook.rights}} at {{ebook.provider}}. + {% if ebook.version_label %} {{ ebook.version_label }} (v{{ ebook.version_iter }}). {% endif %} + Downloaded {{ ebook.download_count }} times since {{ ebook.created }}
+ {% endif %} + {% endfor %} + + {% endif %} + +

Add an eBook for this Edition:

+ + 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, Wikibooks, Hathitrust, Project Gutenberg, OApen, raw files at Github, or Google Books. + +
+ {% csrf_token %} + {{ ebook_form.edition.errors }}{{ ebook_form.edition }}{{ ebook_form.user.errors }}{{ ebook_form.user }}{{ ebook_form.provider.errors }}{{ ebook_form.provider }} +
+ {{ ebook_form.url.errors }}Add a Link URL: {{ ebook_form.url }}
+ or...
+ {{ ebook_form.file.errors }}Upload an ebook file: {{ ebook_form.file }}

+ {{ ebook_form.format.errors }}File Format: {{ ebook_form.format }}    + {{ ebook_form.rights.errors }}License: {{ ebook_form.rights }}

+ Version Label (optional): {% if edition.work.versions %} + or add a new version label: + {% endif %} + {{ ebook_form.new_version_label.errors }} {{ ebook_form.new_version_label }}
+
+
+

Note on versions

+

+ If you want ebooks from two editions with the same format and provider to display, give them different version labels. +

+
+ {% else %} +
Adding ebook links is disabled for this work.
{% endif %}
\ No newline at end of file diff --git a/frontend/templates/edition_uploads.html b/frontend/templates/edition_uploads.html index 317af0e5..bf7d5bc3 100644 --- a/frontend/templates/edition_uploads.html +++ b/frontend/templates/edition_uploads.html @@ -20,12 +20,20 @@ LibraryThing ID: {{ edition.librarything_id }}
+{% if edition.ebooks.all.0 %} +

Active eBooks for this Work

+ {% for ebook in edition.work.ebooks %} + {{ ebook.format }} {{ebook.rights}} at {{ebook.provider}}. + {% if ebook.version_label %} {{ ebook.version_label }}. {% endif %} v{{ ebook.version_iter }} + Downloaded {{ ebook.download_count }} times since {{ ebook.created }}
+ {% endfor %} +{% endif %} {% if edition.ebook_files.all %}

Ebook Files for this Edition

@@ -66,15 +74,28 @@ For ePUB files, use the epubcheck tool to make sure everything will work properly.

{% csrf_token %} -{{form.as_p}} +{{form.edition.errors}}{{form.edition}} +

{{form.format.errors}}Format: {{form.format}}

+

Note on versions

+

+ If you want ebooks from two editions with the same format and provider to display, give them different version labels. +

+ Version Label (optional): {% if edition.work.versions %} + or add a new version label: + {% endif %} + {{ form.new_version_label.errors }} {{ form.new_version_label }}
+

{{form.file.errors}}Upload File: {{form.file}}

-{% if edition.work %} +

More Edition Management

Edit this edition
{% if edition.work.last_campaign %}
Manage this campaign
-{% endif %} + {% endif %} {% endblock %} diff --git a/frontend/templates/manage_campaign.html b/frontend/templates/manage_campaign.html index fc56270e..aac45e76 100644 --- a/frontend/templates/manage_campaign.html +++ b/frontend/templates/manage_campaign.html @@ -136,14 +136,13 @@ Please fix the following before launching your campaign:

Edition {{ edition.id }}:

@@ -276,6 +278,18 @@ {{ work.last_campaign.description|safe }} {% endif %} +
+ {% for work_rel in work.works_related_to.all %} +

+ This work is a {{ work_rel.relation }} of {{ work_rel.from_work }}. +

+ {% endfor %} + {% for work_rel in work.works_related_from.all %} +

+ {{ work_rel.to_work }} is a {{ work_rel.relation }} of this work. +

+ {% endfor %} +
@@ -417,7 +431,7 @@ This work has been downloaded {{ work.download_count }} times via unglue.it ebook links.
    {% for ebook in work.ebooks.all %} -
  1. {{ ebook.download_count }} - {{ ebook.format }} ({{ ebook.rights }}) at {{ ebook.provider }}
  2. +
  3. {{ ebook.download_count }} - {{ ebook.format }} {% if ebook.version_label %} ({{ ebook.version_label }}) {% endif %}({{ ebook.rights }}) at {{ ebook.provider }}.
  4. {% endfor %}
diff --git a/frontend/urls.py b/frontend/urls.py index d5d49da4..a059a965 100644 --- a/frontend/urls.py +++ b/frontend/urls.py @@ -27,7 +27,7 @@ urlpatterns = [ url(r"^rightsholders/$", views.rh_tools, name="rightsholders"), url(r"^rightsholders/campaign/(?P\d+)/$", views.manage_campaign, name="manage_campaign"), url(r"^rightsholders/campaign/(?P\d+)/results/$", views.manage_campaign, {'action': 'results'}, name="campaign_results"), - url(r"^rightsholders/campaign/(?P\d+)/makemobi/$", views.manage_campaign, {'action': 'makemobi'}, name="makemobi"), + url(r"^rightsholders/campaign/(?P\d+)/(?P\d+)/makemobi/$", views.manage_campaign, {'action': 'makemobi'}, name="makemobi"), url(r"^rightsholders/campaign/(?P\d+)/mademobi/$", views.manage_campaign, {'action': 'mademobi'}, name="mademobi"), url(r"^rightsholders/edition/(?P\d*)/(?P\d*)$", views.new_edition, {'by': 'rh'}, name="rh_edition"), url(r"^rightsholders/edition/(?P\d*)/upload/$", views.edition_uploads, name="edition_uploads"), diff --git a/frontend/views.py b/frontend/views.py index 6fc6d4e4..5ff5bec5 100755 --- a/frontend/views.py +++ b/frontend/views.py @@ -7,35 +7,27 @@ import json import logging import urllib import requests -import random -import oauth2 as oauth from datetime import timedelta, date from decimal import Decimal as D -from itertools import islice, chain +from itertools import chain from notification import models as notification from random import randint -from re import sub -from xml.etree import ElementTree as ET from tastypie.models import ApiKey -''' -django imports -''' -from django import forms +#django imports + from django.apps import apps from django.conf import settings from django.contrib import messages -from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User -from django.contrib.auth.views import login,password_reset, redirect_to_login +from django.contrib.auth.views import redirect_to_login from django_comments.models import Comment from django.contrib.sites.models import Site from django.core import signing -from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.exceptions import ValidationError from django.core.files.storage import default_storage -from django.core.files.temp import NamedTemporaryFile from django.core.mail import EmailMessage from django.core.urlresolvers import reverse, reverse_lazy from django.core.validators import validate_email @@ -48,7 +40,7 @@ from django.http import ( HttpResponse, HttpResponseNotFound ) -from django.shortcuts import render, render_to_response, get_object_or_404 +from django.shortcuts import render, get_object_or_404 from django.template import TemplateDoesNotExist from django.template.loader import render_to_string from django.utils.http import urlencode @@ -59,19 +51,16 @@ from django.views.generic.edit import FormView from django.views.generic.list import ListView from django.views.generic.base import ( TemplateView, - View ) -''' -regluit imports -''' +#regluit imports + from regluit.core import ( tasks, models, bookloader, librarything, userlists, - goodreads, ) import regluit.core.cc as cc from regluit.core.bookloader import merge_works, detach_edition @@ -79,7 +68,7 @@ from regluit.core.goodreads import GoodreadsClient from regluit.core.isbn import ISBN from regluit.core.search import gluejar_search from regluit.core.signals import supporter_message -from regluit.core.tasks import send_mail_task, emit_notifications, watermark_acq +from regluit.core.tasks import send_mail_task, watermark_acq from regluit.core.parameters import * from regluit.core.facets import get_facet_object, get_order_by @@ -126,24 +115,19 @@ from regluit.frontend.forms import ( from regluit.payment import baseprocessor, stripelib from regluit.payment.credit import credit_transaction from regluit.payment.manager import PaymentManager -from regluit.payment.models import Transaction, Account, Sent, CreditLog +from regluit.payment.models import Transaction, CreditLog from regluit.payment.parameters import ( TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE, TRANSACTION_STATUS_CANCELED, - TRANSACTION_STATUS_ERROR, - TRANSACTION_STATUS_FAILED, TRANSACTION_STATUS_INCOMPLETE, TRANSACTION_STATUS_NONE, TRANSACTION_STATUS_MODIFIED, PAYMENT_TYPE_AUTHORIZATION, - PAYMENT_TYPE_INSTANT, - PAYMENT_HOST_STRIPE, PAYMENT_HOST_NONE ) from regluit.utils.localdatetime import now, date_today -from regluit.pyepub import InvalidEpub from regluit.libraryauth.forms import UserNamePass from regluit.libraryauth.views import Authenticator, superlogin, login_user from regluit.libraryauth.models import Library @@ -160,7 +144,7 @@ def slideshow(): ending = models.Campaign.objects.filter(status='ACTIVE').order_by('deadline') count = ending.count() j = 0 - + worklist = [] if max > count: # add all the works with active campaigns @@ -172,13 +156,13 @@ def slideshow(): remainder_works = models.Work.objects.filter(campaigns__status="SUCCESSFUL").order_by('-campaigns__deadline')[:remainder] worklist.extend(remainder_works) else: - # if the active campaign list has more works than we can fit + # if the active campaign list has more works than we can fit # in our slideshow, it's the only source we need to draw from while j < max: worklist.append(ending[j].work) - j +=1 - - return (worklist[:4],worklist[4:8]) + j += 1 + + return (worklist[:4], worklist[4:8]) def process_kindle_email(request): """ @@ -198,7 +182,7 @@ def next(request): return response else: return HttpResponseRedirect('/') - + def safe_get_work(work_id): """ use this rather than querying the db directly for a work by id @@ -208,19 +192,19 @@ def safe_get_work(work_id): except models.Work.DoesNotExist: raise Http404 return work - + def cover_width(work): if work.percent_of_goal() < 100: cover_width = 100 - work.percent_of_goal() else: cover_width = 0 - + return cover_width def home(request, landing=False): faves = None if request.user.is_authenticated() : - next=request.GET.get('next', False) + next = request.GET.get('next', False) if next: # should happen only for new users return HttpResponseRedirect(next) @@ -236,14 +220,14 @@ def home(request, landing=False): except: #shouldn't occur except in tests featured = models.Work.objects.all()[0] - top_pledge = models.Campaign.objects.filter(status="ACTIVE",type=REWARDS).order_by('left')[:4] + top_pledge = models.Campaign.objects.filter(status="ACTIVE", type=REWARDS).order_by('left')[:4] top_b2u = models.Campaign.objects.filter(status="ACTIVE", type=BUY2UNGLUE).order_by('-work__num_wishes')[:4] - top_t4u = models.Campaign.objects.exclude(id = featured.id).filter(status="ACTIVE",type=THANKS).order_by('-work__num_wishes')[:4] + top_t4u = models.Campaign.objects.exclude(id = featured.id).filter(status="ACTIVE", type=THANKS).order_by('-work__num_wishes')[:4] most_wished = models.Work.objects.order_by('-num_wishes')[:4] - + unglued_books = models.Work.objects.filter(campaigns__status="SUCCESSFUL").order_by('-campaigns__deadline')[:4] - + cc_books = models.Work.objects.exclude(id = featured.id).filter( featured__isnull=False, ).distinct().order_by('-featured')[:4] @@ -275,40 +259,40 @@ def home(request, landing=False): lambda x: (x.submit_date, x, 'comment'), latest_comments ) - + latest_pledges_tuple = map( lambda x: (x.date_created, x, 'pledge'), latest_pledges ) - + latest_wishes_tuple = map( lambda x: (x.created, x, 'wish'), latest_wishes ) - + """ merge latest actions into a single list, sorted by date, to loop through in template """ latest_actions = sorted( - chain(latest_comments_tuple, latest_pledges_tuple, latest_wishes_tuple), + chain(latest_comments_tuple, latest_pledges_tuple, latest_wishes_tuple), key=lambda instance: instance[0], reverse=True ) - + if request.user.is_authenticated(): events = latest_actions[:12] else: events = latest_actions[:6] - + return render( request, - 'home.html', + 'home.html', { - 'events': events, - 'top_pledge': top_pledge, - 'top_b2u': top_b2u, - 'top_t4u': top_t4u, - 'unglued_books': unglued_books, + 'events': events, + 'top_pledge': top_pledge, + 'top_b2u': top_b2u, + 'top_t4u': top_t4u, + 'unglued_books': unglued_books, 'cc_books': cc_books, 'most_wished': most_wished, 'featured': featured, @@ -318,20 +302,20 @@ def home(request, landing=False): def stub(request): path = request.path[6:] # get rid of /stub/ - return render(request,'stub.html', {'path': path}) + return render(request, 'stub.html', {'path': path}) def acks(request, work): - return render(request,'front_matter.html', {'campaign': work.last_campaign()}) - + return render(request, 'front_matter.html', {'campaign': work.last_campaign()}) + def work(request, work_id, action='display'): work = safe_get_work(work_id) - alert='' + alert = '' formset = None if action == "acks": - return acks( request, work) + return acks(request, work) elif action == "editions": - EditionFormSet = inlineformset_factory(models.Work, models.Edition, fields=(), extra=0 ) + EditionFormSet = inlineformset_factory(models.Work, models.Edition, fields=(), extra=0) if request.method == "POST" and (request.user.is_staff or (work.last_campaign() and request.user in work.last_campaign().managers.all())): formset = EditionFormSet(data=request.POST, instance=work) if formset.is_valid(): @@ -339,10 +323,10 @@ def work(request, work_id, action='display'): detach_edition(form.instance) alert = 'editions have been split' if request.POST.has_key('select_edition'): - selected_id=request.POST['select_edition'] + selected_id = request.POST['select_edition'] try: - work.selected_edition= work.editions.get(id=selected_id) - work.title=work.selected_edition.title + work.selected_edition = work.editions.get(id=selected_id) + work.title = work.selected_edition.title work.save() alert = alert + 'edition selected' except models.Edition.DoesNotExist: @@ -355,9 +339,9 @@ def work(request, work_id, action='display'): if add_url == request.path: request.user.wishlist.add_work(work, "login", notify=True) request.session.pop("add_wishlist") - + process_kindle_email(request) - + if request.method == 'POST' and not request.user.is_anonymous(): activetab = '4' elif action == 'editions': @@ -366,40 +350,40 @@ def work(request, work_id, action='display'): try: activetab = request.GET['tab'] if activetab not in ['1', '2', '3', '4']: - activetab = '1'; + activetab = '1' except: - activetab = '1'; - + activetab = '1' + campaign = work.last_campaign() editions = work.editions.all().order_by('-publication_date')[:10] try: pledged = campaign.transactions().filter(user=request.user, status="ACTIVE") except: pledged = None - + cover_width_number = 0 - + if work.last_campaign_status() == 'ACTIVE': cover_width_number = cover_width(work) - + if action == 'preview': work.last_campaign_status = 'ACTIVE' - + if not request.user.is_anonymous(): - claimform = UserClaimForm( request.user, data={'claim-work':work.pk, 'claim-user': request.user.id}, prefix = 'claim') + claimform = UserClaimForm(request.user, data={'claim-work':work.pk, 'claim-user': request.user.id}, prefix = 'claim') else: claimform = None - + if campaign: # pull up premiums explicitly tied to the campaign # mandatory premiums are only displayed in pledge process premiums = campaign.custom_premiums() else: premiums = None - + wishers = work.num_wishes base_url = request.build_absolute_uri("/")[:-1] - + active_claims = work.claim.all().filter(status='active') if active_claims.count() == 1: claimstatus = 'one_active' @@ -409,17 +393,17 @@ def work(request, work_id, action='display'): pending_claims = work.claim.all().filter(status='pending') pending_claims_count = pending_claims.count() if pending_claims_count > 1: - claimstatus = 'disputed' + claimstatus = 'disputed' elif pending_claims_count == 1: - claimstatus = 'one_pending' - rights_holder_name = pending_claims[0].rights_holder.rights_holder_name + claimstatus = 'one_pending' + rights_holder_name = pending_claims[0].rights_holder.rights_holder_name else: - claimstatus = 'open' - + claimstatus = 'open' + return render(request, 'work.html', { - 'work': work, - 'premiums': premiums, - 'ungluers': userlists.supporting_users(work, 5), + 'work': work, + 'premiums': premiums, + 'ungluers': userlists.supporting_users(work, 5), 'claimform': claimform, 'wishers': wishers, 'base_url': base_url, @@ -433,7 +417,7 @@ def work(request, work_id, action='display'): 'action': action, 'formset': formset, 'kwform': SubjectSelectForm() - }) + }) def edition_uploads(request, edition_id): context = {} @@ -444,57 +428,69 @@ def edition_uploads(request, edition_id): except models.Edition.DoesNotExist: raise Http404 campaign_type = edition.work.last_campaign().type - if not request.user.is_staff : - if not request.user in edition.work.last_campaign().managers.all(): - return render(request, "admins_only.html") + if not user_can_edit_work(request.user, edition.work): + return render(request, "admins_only.html") if request.method == 'POST' : form = EbookFileForm(data=request.POST, files=request.FILES, campaign_type=campaign_type) if form.is_valid() : logger.info("EbookFileForm is_valid") form.save() edition.work.last_campaign().save() - context['uploaded']=True + context['uploaded'] = True if campaign_type == BUY2UNGLUE: if edition.work.last_campaign().status == 'SUCCESSFUL': try: edition.work.last_campaign().watermark_success() except Exception as e: - context['upload_error']= e + context['upload_error'] = e form.instance.delete() else: # campaign mangager gets a copy - test_acq = models.Acq.objects.create(user=request.user,work=edition.work,license= TESTING) + test_acq = models.Acq.objects.create(user=request.user, work=edition.work, license= TESTING) try: test_acq.get_watermarked() - context['watermarked']= test_acq.watermarked + context['watermarked'] = test_acq.watermarked except Exception as e: - context['upload_error']= e + context['upload_error'] = e form.instance.delete() if campaign_type == THANKS: e = form.instance.check_file() if e != None: logger.error(e) - context['upload_error']= e + context['upload_error'] = e form.instance.delete() else: tasks.process_ebfs.delay(edition.work.last_campaign()) + if form.instance.id: + new_ebook = models.Ebook.objects.create( + edition=edition, + format=form.instance.format, + url=form.instance.file.url, + rights=edition.work.last_campaign().license, + version_label=form.cleaned_data.get('version_label', ''), + active=False, + provider="Unglue.it", + ) + form.instance.ebook = new_ebook + form.instance.ebook.set_next_iter() + else: - context['upload_error']= form.errors - form = EbookFileForm(initial={'edition':edition,'format':'epub'}, campaign_type=campaign_type) + context['upload_error'] = form.errors + form = EbookFileForm(initial={'edition':edition, 'format':'epub'}, campaign_type=campaign_type) context.update({ - 'form': form, 'edition': edition, + 'form': form, 'edition': edition, 'ebook_files': models.EbookFile.objects.filter(edition = edition) }) - return render(request, 'edition_uploads.html', context ) + return render(request, 'edition_uploads.html', context) -def add_subject(subject_name,work, authority=''): +def add_subject(subject_name, work, authority=''): try: - subject= models.Subject.objects.get(name=subject_name) + subject = models.Subject.objects.get(name=subject_name) except models.Subject.DoesNotExist: - subject=models.Subject.objects.create(name=subject_name, authority=authority) + subject = models.Subject.objects.create(name=subject_name, authority=authority) subject.works.add(work) -def user_can_edit_work(user,work): +def user_can_edit_work(user, work): if user.is_staff : return True elif work and work.last_campaign(): @@ -503,26 +499,28 @@ def user_can_edit_work(user,work): return True else: return False - + @login_required def new_edition(request, work_id, edition_id, by=None): - if not request.user.is_authenticated() : + if not request.user.is_authenticated(): return render(request, "admins_only.html") - # if the work and edition are set, we save the edition and set the work - language='en' - description='' - title='' + # if the work and edition are set, we save the edition and set the work + language = 'en' + age_level = '' + description = '' + title = '' if work_id: work = safe_get_work(work_id) - language=work.language - description=work.description - title=work.title + language = work.language + age_level = work.age_level + description = work.description + title = work.title else: - work=None - - alert = '' + work = None + + alert = '' admin = user_can_edit_work(request.user, work) if edition_id: try: @@ -530,93 +528,120 @@ def new_edition(request, work_id, edition_id, by=None): except models.Edition.DoesNotExist: raise Http404 if work: - edition.work = work - language=edition.work.language - description=edition.work.description + edition.work = work + language = edition.work.language + age_level = edition.work.age_level + description = edition.work.description else: edition = models.Edition() if work: - edition.work = work + edition.work = work + edition.publication_date = work.earliest_publication_date + edition.new_authors = [] + for relator in work.relators(): + edition.new_authors.append((relator.author.name, relator.relation.code)) - initial={ - 'language':language, - 'publisher_name':edition.publisher_name, - 'isbn':edition.isbn_13, - 'oclc':edition.oclc, - 'description':description, - 'title': title, - 'goog': edition.googlebooks_id, - 'gdrd': edition.goodreads_id, - 'thng': edition.librarything_id, - 'http': edition.http_id, - } - if request.method == 'POST' : + initial = { + 'language': language, + 'age_level': age_level, + 'publisher_name': edition.publisher_name, + 'isbn': edition.isbn_13, + 'oclc': edition.oclc, + 'description': description, + 'title': title, + 'goog': edition.googlebooks_id, + 'gdrd': edition.goodreads_id, + 'thng': edition.librarything_id, + 'http': edition.http_id, + 'doi': edition.id_for('doi'), + } + if request.method == 'POST': form = None - edition.new_authors=zip(request.POST.getlist('new_author'),request.POST.getlist('new_author_relation')) - edition.new_subjects=request.POST.getlist('new_subject') + edition.new_authors = zip(request.POST.getlist('new_author'), request.POST.getlist('new_author_relation')) + edition.new_subjects = request.POST.getlist('new_subject') if edition.id and admin: for author in edition.authors.all(): if request.POST.has_key('delete_author_%s' % author.id): edition.remove_author(author) form = EditionForm(instance=edition, data=request.POST, files=request.FILES) break + work_rels = models.WorkRelation.objects.filter(Q(to_work=work) | Q(from_work=work)) + for work_rel in work_rels: + if request.POST.has_key('delete_work_rel_%s' % work_rel.id): + work_rel.delete() + form = EditionForm(instance=edition, data=request.POST, files=request.FILES) + break + if request.POST.has_key('add_author_submit') and admin: new_author_name = request.POST['add_author'].strip() new_author_relation = request.POST['add_author_relation'] try: - author= models.Author.objects.get(name=new_author_name) + author = models.Author.objects.get(name=new_author_name) except models.Author.DoesNotExist: - author=models.Author.objects.create(name=new_author_name) - edition.new_authors.append((new_author_name,new_author_relation)) + author = models.Author.objects.create(name=new_author_name) + edition.new_authors.append((new_author_name, new_author_relation)) form = EditionForm(instance=edition, data=request.POST, files=request.FILES) - elif not form and admin: + elif not form and admin: form = EditionForm(instance=edition, data=request.POST, files=request.FILES) if form.is_valid(): form.save() if not work: - work= models.Work(title=form.cleaned_data['title'],language=form.cleaned_data['language'],description=form.cleaned_data['description']) + work = models.Work( + title=form.cleaned_data['title'], + language=form.cleaned_data['language'], + age_level=form.cleaned_data['age_level'], + description=form.cleaned_data['description'], + ) work.save() - edition.work=work + edition.work = work edition.save() else: - work.description=form.cleaned_data['description'] - work.title=form.cleaned_data['title'] + work.description = form.cleaned_data['description'] + work.title = form.cleaned_data['title'] work.publication_range = None # will reset on next access work.language = form.cleaned_data['language'] + work.age_level = form.cleaned_data['age_level'] work.save() - - id_msg="" - for id_type in ('isbn', 'oclc', 'goog', 'thng', 'gdrd', 'http'): + + id_msg = "" + for id_type in ('isbn', 'oclc', 'goog', 'thng', 'gdrd', 'http', 'doi'): id_val = form.cleaned_data[id_type] - if id_val=='delete': + if id_val == 'delete': edition.identifiers.filter(type=id_type).delete() elif id_val: - existing= models.Identifier.objects.filter(type=id_type, value=form.cleaned_data[id_type]) + existing = models.Identifier.objects.filter(type=id_type, value=form.cleaned_data[id_type]) if existing.count() and existing[0].edition != edition: - return render(request, 'new_edition.html', { - 'form': form, 'edition': edition, 'admin': admin, - 'id_msg': "%s = %s already exists"%( id_type, id_val ), - }) + return render(request, 'new_edition.html', { + 'form': form, 'edition': edition, 'admin': admin, + 'id_msg': "%s = %s already exists"%(id_type, id_val), + }) else: models.Identifier.set(type=id_type, value=id_val, edition=edition, work=work) for relator in edition.relators.all(): if request.POST.has_key('change_relator_%s' % relator.id): new_relation = request.POST['change_relator_%s' % relator.id] relator.set(new_relation) + related_work = form.cleaned_data['add_related_work'] + if related_work: + models.WorkRelation.objects.get_or_create( + to_work=work, + from_work=related_work, + relation=form.cleaned_data['add_work_relation'], + ) for (author_name, author_relation) in edition.new_authors: - edition.add_author(author_name,author_relation) + edition.add_author(author_name, author_relation) if form.cleaned_data.has_key('bisac'): - bisacsh=form.cleaned_data['bisac'] + bisacsh = form.cleaned_data['bisac'] while bisacsh: add_subject(bisacsh.full_label, work, authority="bisacsh") bisacsh = bisacsh.parent for subject_name in edition.new_subjects: add_subject(subject_name, work) work_url = reverse('work', kwargs={'work_id': edition.work.id}) - cover_file=form.cleaned_data.get("coverfile",None) + cover_file = form.cleaned_data.get("coverfile", None) if cover_file: - # save it - cover_file_name= '/Users/%s/covers/%s/%s' % ( request.user.username, edition.pk, cover_file.name) + # save it + cover_file_name = '/Users/%s/covers/%s/%s' % (request.user.username, edition.pk, cover_file.name) file = default_storage.open(cover_file_name, 'w') file.write(cover_file.read()) file.close() @@ -628,7 +653,6 @@ def new_edition(request, work_id, edition_id, by=None): form = EditionForm(instance=edition, initial=initial) return render(request, 'new_edition.html', { 'form': form, 'edition': edition, 'admin':admin, 'alert':alert, - }) @@ -642,58 +666,56 @@ def manage_ebooks(request, edition_id, by=None): work = edition.work else: raise Http404 - if not request.user.is_authenticated() : - return render(request, "admins_only.html") - # if the work and edition are set, we save the edition and set the work - - alert = '' - admin = False - if request.user.is_staff : - admin = True - elif work and work.last_campaign(): - if request.user in work.last_campaign().managers.all(): - admin = True - elif work==None and request.user.rights_holder.count(): - admin = True + # if the work and edition are set, we save the edition and set the work + + alert = '' + admin = user_can_edit_work(request.user, work) if request.method == 'POST' : - edition.new_authors=zip(request.POST.getlist('new_author'),request.POST.getlist('new_author_relation')) - edition.new_subjects=request.POST.getlist('new_subject') - if edition.id and admin: - for author in edition.authors.all(): - if request.POST.has_key('delete_author_%s' % author.id): - edition.remove_author(author) - form = EditionForm(instance=edition, data=request.POST, files=request.FILES) - break - if request.POST.has_key('ebook_%d-edition' % edition.id): - edition.ebook_form= EbookForm( data = request.POST, prefix = 'ebook_%d'%edition.id) - if edition.ebook_form.is_valid(): - edition.ebook_form.save() - alert = 'Thanks for adding an ebook to unglue.it!' - else: - alert = 'your submitted ebook had errors' + ebook_form = EbookForm(data = request.POST, files=request.FILES,) + if ebook_form.is_valid(): + if ebook_form.cleaned_data.get('file', None): + new_ebf = models.EbookFile.objects.create( + file=ebook_form.cleaned_data['file'], + format=ebook_form.cleaned_data['format'], + edition=edition, + active=True, + + ) + ebook_form.instance.url = new_ebf.file.url + ebook_form.instance.provider = "Unglue.it" + ebook_form.instance.save() + new_ebf.ebook = ebook_form.instance + new_ebf.save() + else: + ebook_form.save() + ebook_form.instance.set_next_iter() + edition.work.remove_old_ebooks() + alert = 'Thanks for adding an ebook to unglue.it!' + else: + alert = 'your submitted ebook had errors' else: - edition.ebook_form = EbookForm( instance= models.Ebook(user = request.user, edition = edition, provider = 'x' ), prefix = 'ebook_%d'%edition.id) + ebook_form = EbookForm(instance=models.Ebook(user=request.user, edition=edition, provider='x')) try: - show_ebook_form = edition.work.last_campaign().status not in ['ACTIVE','INITIALIZED'] + show_ebook_form = admin or edition.work.last_campaign().status not in ['ACTIVE','INITIALIZED'] except: show_ebook_form = True return render(request, 'manage_ebooks.html', { 'edition': edition, 'admin':admin, 'alert':alert, - 'show_ebook_form':show_ebook_form, + 'ebook_form': ebook_form, 'show_ebook_form':show_ebook_form, }) def campaign_results(request, campaign): return render(request, 'campaign_results.html', { - 'campaign': campaign, + 'campaign': campaign, }) - - -def manage_campaign(request, id, action='manage'): + + +def manage_campaign(request, id, ebf=None, action='manage'): campaign = get_object_or_404(models.Campaign, id=id) - campaign.not_manager=False - campaign.problems=[] + campaign.not_manager = False + campaign.problems = [] if (not request.user.is_authenticated) or (not request.user in campaign.managers.all() and not request.user.is_staff): - campaign.not_manager=True + campaign.not_manager = True return render(request, 'manage_campaign.html', {'campaign': campaign}) if action == 'results': return campaign_results(request, campaign) @@ -701,7 +723,7 @@ def manage_campaign(request, id, action='manage'): activetab = '#1' offers = campaign.work.offers.all() for offer in offers: - offer.offer_form=OfferForm(instance=offer,prefix='offer_%d'%offer.id) + offer.offer_form = OfferForm(instance=offer, prefix='offer_%d'%offer.id) if request.method == 'POST' : if request.POST.has_key('add_premium') : @@ -711,20 +733,20 @@ def manage_campaign(request, id, action='manage'): alerts.append(_('New premium has been added')) new_premium_form = CustomPremiumForm(initial={'campaign': campaign}) else: - alerts.append(_('New premium has not been added')) + alerts.append(_('New premium has not been added')) form = getManageCampaignForm(instance=campaign) activetab = '#2' elif request.POST.has_key('save') or request.POST.has_key('launch') : - form= getManageCampaignForm(instance=campaign, data=request.POST) - if form.is_valid(): - form.save() + form = getManageCampaignForm(instance=campaign, data=request.POST) + if form.is_valid(): + form.save() campaign.dollar_per_day = None campaign.set_dollar_per_day() - campaign.work.selected_edition=campaign.edition - if campaign.type in {BUY2UNGLUE,THANKS} : - offers= campaign.work.create_offers() + campaign.work.selected_edition = campaign.edition + if campaign.type in {BUY2UNGLUE, THANKS} : + offers = campaign.work.create_offers() for offer in offers: - offer.offer_form=OfferForm(instance=offer,prefix='offer_%d'%offer.id) + offer.offer_form = OfferForm(instance=offer, prefix='offer_%d'%offer.id) campaign.update_left() if campaign.type is THANKS : campaign.work.description = form.cleaned_data['work_description'] @@ -751,37 +773,38 @@ def manage_campaign(request, id, action='manage'): if selected_premium.type == 'CU': selected_premium.type = 'XX' selected_premium.save() - alerts.append(_('Premium %s has been inactivated'% premium_to_stop)) + alerts.append(_('Premium %s has been inactivated'% premium_to_stop)) form = getManageCampaignForm(instance=campaign) new_premium_form = CustomPremiumForm(initial={'campaign': campaign}) elif request.POST.has_key('change_offer'): for offer in offers : if request.POST.has_key('offer_%d-work' % offer.id) : - offer.offer_form=OfferForm(instance=offer, data = request.POST, prefix='offer_%d'%offer.id) + offer.offer_form = OfferForm(instance=offer, data = request.POST, prefix='offer_%d'%offer.id) if offer.offer_form.is_valid(): offer.offer_form.save() offer.active = True offer.save() alerts.append(_('Offer has been changed')) else: - alerts.append(_('Offer has not been changed')) + alerts.append(_('Offer has not been changed')) form = getManageCampaignForm(instance=campaign) new_premium_form = CustomPremiumForm(data={'campaign': campaign}) activetab = '#2' else: if action == 'makemobi': - tasks.make_mobi.delay(campaign) + ebookfile = get_object_or_404(models.EbookFile, id=ebf) + tasks.make_mobi.delay(ebookfile) return HttpResponseRedirect(reverse('mademobi', args=[campaign.id])) - elif action =='mademobi': + elif action == 'mademobi': alerts.append('A MOBI file is being generated') form = getManageCampaignForm(instance=campaign, initial={'work_description':campaign.work.description}) new_premium_form = CustomPremiumForm(initial={'campaign': campaign}) - + return render(request, 'manage_campaign.html', { - 'campaign': campaign, - 'form':form, - 'problems': campaign.problems, - 'alerts': alerts, + 'campaign': campaign, + 'form':form, + 'problems': campaign.problems, + 'alerts': alerts, 'premiums' : campaign.custom_premiums(), 'premium_form' : new_premium_form, 'work': campaign.work, @@ -789,10 +812,10 @@ def manage_campaign(request, id, action='manage'): 'offers':offers, 'action':action, }) - + def googlebooks(request, googlebooks_id): - try: - edition = models.Identifier.objects.get(type='goog',value=googlebooks_id).edition + try: + edition = models.Identifier.objects.get(type='goog', value=googlebooks_id).edition except models.Identifier.DoesNotExist: try: edition = bookloader.add_by_googlebooks_id(googlebooks_id) @@ -839,29 +862,29 @@ class MapSubjectView(FormView): Allows a staffer to add given subject to all works with given the onto_subject keyword. e.g., subject = "Language" onto_subject="English language" """ - template_name="map_subject.html" + template_name = "map_subject.html" form_class = MapSubjectForm - + def dispatch(self, request, *args, **kwargs): if not request.user.is_staff: return render(request, "admins_only.html") else: return super(MapSubjectView, self).dispatch(request, *args, **kwargs) - + def form_valid(self, form): - context=self.get_context_data() - context['subject']=form.cleaned_data['subject'] - context['onto_subject']=form.cleaned_data['onto_subject'] + context = self.get_context_data() + context['subject'] = form.cleaned_data['subject'] + context['onto_subject'] = form.cleaned_data['onto_subject'] if self.request.POST.has_key('confirm_map_subject'): initial_count = context['onto_subject'].works.all().count() initial_free_count = context['onto_subject'].works.filter(is_free=True).count() context['onto_subject'].works.add(*list(context['subject'].works.all())) - context['map_complete']=True + context['map_complete'] = True context['form'] = MapSubjectForm(initial=form.cleaned_data) context['added'] = context['onto_subject'].works.all().count() - initial_count context['added_free'] = context['onto_subject'].works.filter(is_free=True).count() - initial_free_count else: - context['form']=MapSubjectForm(initial=form.cleaned_data) + context['form'] = MapSubjectForm(initial=form.cleaned_data) return render(self.request, self.template_name, context) class FilterableListView(ListView): @@ -874,30 +897,30 @@ class FilterableListView(ListView): return self.get_queryset_all().filter(language=self.request.GET['pub_lang']) else: return self.get_queryset_all() - + def get_context_data(self, **kwargs): context = super(FilterableListView, self).get_context_data(**kwargs) if self.request.GET.has_key('pub_lang'): - context['pub_lang']=self.request.GET['pub_lang'] + context['pub_lang'] = self.request.GET['pub_lang'] else: - context['pub_lang']='' - context['show_langs']=True - context['WISHED_LANGS']=settings.WISHED_LANGS + context['pub_lang'] = '' + context['show_langs'] = True + context['WISHED_LANGS'] = settings.WISHED_LANGS return context - + def render_to_response(self, context, **response_kwargs): if self.send_marc: return qs_marc_records(self.request, qs=self.object_list) else: - return super(FilterableListView,self).render_to_response(context, **response_kwargs) + return super(FilterableListView, self).render_to_response(context, **response_kwargs) -recommended_user = User.objects.filter( username=settings.UNGLUEIT_RECOMMENDED_USERNAME) +recommended_user = User.objects.filter(username=settings.UNGLUEIT_RECOMMENDED_USERNAME) class WorkListView(FilterableListView): template_name = "work_list.html" context_object_name = "work_list" - max_works=100000 - + max_works = 100000 + def get_queryset_all(self): facet = self.kwargs.get('facet', None) if (facet == 'popular'): @@ -909,31 +932,31 @@ class WorkListView(FilterableListView): return models.Work.objects.exclude(num_wishes=0).order_by('-created', '-num_wishes' ,'id') else: return models.Work.objects.all().order_by('-created', 'id') - + def get_context_data(self, **kwargs): - context = super(WorkListView, self).get_context_data(**kwargs) - qs=self.get_queryset() - context['ungluers'] = userlists.work_list_users(qs,5) - context['facet'] = self.kwargs.get('facet','') - works_unglued = qs.filter(is_free = True).distinct() | qs.filter(campaigns__status='SUCCESSFUL').distinct() - context['works_unglued'] = works_unglued[:self.max_works] - context['works_active'] = qs.filter(campaigns__status='ACTIVE').distinct()[:self.max_works] - context['works_wished'] = qs.filter(is_free=False).exclude(campaigns__status='ACTIVE').exclude(campaigns__status='SUCCESSFUL').distinct()[:self.max_works] - - counts={} - counts['unglued'] = context['works_unglued'].count() - counts['unglueing'] = context['works_active'].count() - counts['wished'] = context['works_wished'].count() - context['counts'] = counts - - if counts['unglueing']: - context['activetab'] = "#2" - elif counts['unglued']: - context['activetab'] = "#1" - else: - context['activetab'] = "#3" - - return context + context = super(WorkListView, self).get_context_data(**kwargs) + qs = self.get_queryset() + context['ungluers'] = userlists.work_list_users(qs, 5) + context['facet'] = self.kwargs.get('facet','') + works_unglued = qs.filter(is_free = True).distinct() | qs.filter(campaigns__status='SUCCESSFUL').distinct() + context['works_unglued'] = works_unglued[:self.max_works] + context['works_active'] = qs.filter(campaigns__status='ACTIVE').distinct()[:self.max_works] + context['works_wished'] = qs.filter(is_free=False).exclude(campaigns__status='ACTIVE').exclude(campaigns__status='SUCCESSFUL').distinct()[:self.max_works] + + counts = {} + counts['unglued'] = context['works_unglued'].count() + counts['unglueing'] = context['works_active'].count() + counts['wished'] = context['works_wished'].count() + context['counts'] = counts + + if counts['unglueing']: + context['activetab'] = "#2" + elif counts['unglued']: + context['activetab'] = "#1" + else: + context['activetab'] = "#3" + + return context class FacetedView(FilterableListView): template_name = "faceted_list.html" @@ -941,7 +964,7 @@ class FacetedView(FilterableListView): if not hasattr(self,'vertex'): facet_path = self.kwargs.get('path', '') self.vertex = get_facet_object(facet_path) - + order_by = self.request.GET.get('order_by', 'newest') #special cases if order_by == 'subjects': @@ -951,7 +974,7 @@ class FacetedView(FilterableListView): def get_context_data(self, **kwargs): context = super(FacetedView, self).get_context_data(**kwargs) facet = self.kwargs.get('facet','all') - qs=self.get_queryset() + qs = self.get_queryset() if self.request.GET.has_key('setkw') and self.request.user.is_staff: setkw = self.request.GET['setkw'] try: @@ -965,26 +988,26 @@ class FacetedView(FilterableListView): context['order_by'] = self.request.GET.get('order_by', 'newest') context['view_as'] = self.request.GET.get('view_as', None) return context - - + + class ByPubView(WorkListView): template_name = "bypub_list.html" context_object_name = "work_list" max_works = 100000 publisher_name = None publisher = None - + def get_publisher_name(self): self.publisher_name = get_object_or_404(models.PublisherName, id=self.kwargs['pubname']) self.set_publisher() - + def set_publisher(self): if self.publisher_name.key_publisher.count(): self.publisher = self.publisher_name.key_publisher.all()[0] elif self.publisher_name.publisher: self.publisher = self.publisher_name.publisher self.publisher_name = self.publisher.name - + def get_queryset_all(self): facet = self.kwargs.get('facet','') self.get_publisher_name() @@ -999,12 +1022,12 @@ class ByPubView(WorkListView): return objects.order_by('title', 'id') def get_context_data(self, **kwargs): - context = super(ByPubView, self).get_context_data(**kwargs) - context['pubname'] = self.publisher_name - context['publisher'] = self.publisher - context['facet'] = self.kwargs.get('facet','all') + context = super(ByPubView, self).get_context_data(**kwargs) + context['pubname'] = self.publisher_name + context['publisher'] = self.publisher + context['facet'] = self.kwargs.get('facet','all') - return context + return context class ByPubListView(ByPubView): def get_publisher_name(self): @@ -1015,7 +1038,7 @@ class ByPubListView(ByPubView): class UngluedListView(FilterableListView): template_name = "unglued_list.html" context_object_name = "work_list" - + def get_queryset_all(self): facet = self.kwargs['facet'] if (facet == 'popular'): @@ -1037,8 +1060,8 @@ class UngluedListView(FilterableListView): def get_context_data(self, **kwargs): context = super(UngluedListView, self).get_context_data(**kwargs) - qs=self.get_queryset() - context['ungluers'] = userlists.work_list_users(qs,5) + qs = self.get_queryset() + context['ungluers'] = userlists.work_list_users(qs, 5) facet = self.kwargs['facet'] context['facet'] = facet if facet == 'cc' or facet == 'creativecommons': @@ -1058,7 +1081,7 @@ class CampaignListView(FilterableListView): template_name = "campaign_list.html" context_object_name = "campaign_list" model = models.Campaign - + def get_queryset_all(self): facet = self.kwargs['facet'] if (facet == 'newest'): @@ -1068,7 +1091,7 @@ class CampaignListView(FilterableListView): elif (facet == 'pledges'): return models.Campaign.objects.filter(status='ACTIVE').annotate(pledges=Count('transaction')).order_by('-pledges') elif (facet == 'almost'): - return models.Campaign.objects.filter(status='ACTIVE').all() # STUB: will need to make db changes to make this work + return models.Campaign.objects.filter(status='ACTIVE').all() # STUB: will need to make db changes to make this work elif (facet == 'ending'): return models.Campaign.objects.filter(status='ACTIVE').order_by('deadline') elif (facet == 'soon'): @@ -1085,27 +1108,27 @@ class CampaignListView(FilterableListView): return models.Campaign.objects.all() def get_context_data(self, **kwargs): - context = super(CampaignListView, self).get_context_data(**kwargs) - qs=self.get_queryset() - context['ungluers'] = userlists.campaign_list_users(qs,5) - facet = self.kwargs['facet'] - context['facet'] =facet - context['facet_label'] = FACET_LABELS.get(facet, 'Active') - return context + context = super(CampaignListView, self).get_context_data(**kwargs) + qs = self.get_queryset() + context['ungluers'] = userlists.campaign_list_users(qs, 5) + facet = self.kwargs['facet'] + context['facet'] = facet + context['facet_label'] = FACET_LABELS.get(facet, 'Active') + return context class MergeView(FormView): - template_name="merge.html" - work=None - + template_name = "merge.html" + work = None + def dispatch(self, request, *args, **kwargs): if not request.user.is_staff: return render(request, "admins_only.html") else: return super(MergeView, self).dispatch(request, *args, **kwargs) - + def get_context_data(self, **kwargs): context = super(MergeView, self).get_context_data(**kwargs) - context['work']=self.work + context['work'] = self.work return context def get_form_class(self): @@ -1113,67 +1136,67 @@ class MergeView(FormView): return WorkForm else: return OtherWorkForm - + def get_form_kwargs(self): self.work = safe_get_work(self.kwargs["work_id"]) - form_kwargs= {'work':self.work} + form_kwargs = {'work':self.work} if self.request.method == 'POST': form_kwargs.update({'data':self.request.POST}) return form_kwargs def form_valid(self, form): - other_work=form.cleaned_data['other_work'] - context=self.get_context_data() + other_work = form.cleaned_data['other_work'] + context = self.get_context_data() if self.request.POST.has_key('confirm_merge_works'): - context['old_work_id']=other_work.id - merge_works(self.work,other_work,self.request.user) - context['merge_complete']=True + context['old_work_id'] = other_work.id + merge_works(self.work, other_work, self.request.user) + context['merge_complete'] = True else: - context['form']=WorkForm(initial={'other_work':other_work}) - context['other_work']=other_work + context['form'] = WorkForm(initial={'other_work':other_work}) + context['other_work'] = other_work return render(self.request, self.template_name, context) class GiftView(TemplateView): template_name = "gift.html" - - def get(self, request, *args, **kwargs): + + def get(self, request, *args, **kwargs): context = self.get_context_data() - context['transfer_form']=getTransferCreditForm(self.request.user.credit.available) + context['transfer_form'] = getTransferCreditForm(self.request.user.credit.available) return self.render_to_response(context) - - def post(self, request, *args, **kwargs): + + def post(self, request, *args, **kwargs): context = self.get_context_data() - transfer_form=getTransferCreditForm(self.request.user.credit.available, data=self.request.POST) + transfer_form = getTransferCreditForm(self.request.user.credit.available, data=self.request.POST) if transfer_form.is_valid(): if self.request.user.credit.transfer_to(transfer_form.cleaned_data['recipient'], transfer_form.cleaned_data['amount']): #successful transfer context['transfer_message'] = 'Your transfer has been successfully executed.' - context['recipient']= transfer_form.cleaned_data['recipient'] + context['recipient'] = transfer_form.cleaned_data['recipient'] context['transfer_amount'] = transfer_form.cleaned_data['amount'] - context['transfer_form']=getTransferCreditForm(self.request.user.credit.available) + context['transfer_form'] = getTransferCreditForm(self.request.user.credit.available) else: #unsuccessful transfer context['transfer_message'] = 'Your transfer was not successful.' - context['transfer_form']=transfer_form + context['transfer_form'] = transfer_form else: #not valid - context['transfer_form']=transfer_form + context['transfer_form'] = transfer_form return self.render_to_response(context) - + def get_context_data(self, *args, **kwargs): context = {'user' : self.request.user} return context - + class PledgeView(FormView): action = "pledge" - template_name="pledge.html" + template_name = "pledge.html" form_class = CampaignPledgeForm transaction = None campaign = None work = None premiums = None data = {} - + def get_preapproval_amount(self): preapproval_amount = self.request.GET.get('preapproval_amount', self.request.POST.get('preapproval_amount', None)) if preapproval_amount: @@ -1185,24 +1208,24 @@ class PledgeView(FormView): except: preapproval_amount = None if self.transaction: - if preapproval_amount: - preapproval_amount = preapproval_amount if preapproval_amount>self.transaction.amount else self.transaction.amount + if preapproval_amount: + preapproval_amount = preapproval_amount if preapproval_amount > self.transaction.amount else self.transaction.amount else: preapproval_amount = self.transaction.amount return preapproval_amount - + def get_form_kwargs(self): - + assert self.request.user.is_authenticated() self.work = safe_get_work(self.kwargs["work_id"]) - + # if there is no campaign or if campaign is not active, we should raise an error try: self.campaign = self.work.last_campaign() # TODO need to sort the premiums self.premiums = self.campaign.custom_premiums() | models.Premium.objects.filter(id=150) # Campaign must be ACTIVE - assert self.campaign.status == 'ACTIVE' + assert self.campaign.status == 'ACTIVE' except Exception, e: # this used to raise an exception, but that seemed pointless. This now has the effect of preventing any pledges. return {} @@ -1210,11 +1233,11 @@ class PledgeView(FormView): transactions = self.campaign.transactions().filter(user=self.request.user, status=TRANSACTION_STATUS_ACTIVE, type=PAYMENT_TYPE_AUTHORIZATION) premium_id = self.request.GET.get('premium_id', self.request.POST.get('premium_id', 150)) if transactions.count() == 0: - ack_name=self.request.user.profile.ack_name - ack_dedication='' - anonymous=self.request.user.profile.anon_pref + ack_name = self.request.user.profile.ack_name + ack_dedication = '' + anonymous = self.request.user.profile.anon_pref else: - self.transaction = transactions[0] + self.transaction = transactions[0] if premium_id == 150 and self.transaction.premium is not None: premium_id = self.transaction.premium.id if self.transaction.extra : @@ -1223,9 +1246,9 @@ class PledgeView(FormView): else: ack_name = self.request.user.profile.ack_name ack_dedication = '' - anonymous=self.transaction.anonymous + anonymous = self.transaction.anonymous - self.data = {'preapproval_amount':self.get_preapproval_amount(), 'premium_id':premium_id, + self.data = {'preapproval_amount':self.get_preapproval_amount(), 'premium_id':premium_id, 'ack_name':ack_name, 'ack_dedication':ack_dedication, 'anonymous':anonymous} if self.request.method == 'POST': self.data.update(self.request.POST.dict()) @@ -1238,38 +1261,38 @@ class PledgeView(FormView): return {'data':self.data} else: return {'initial':self.data} - + def get_context_data(self, **kwargs): """set up the pledge page""" - + context = super(PledgeView, self).get_context_data(**kwargs) - + context.update({ 'work':self.work, - 'campaign':self.campaign, - 'premiums':self.premiums, - 'premium_id':self.data.get('premium_id',None), - 'faqmenu': 'modify' if self.transaction else 'pledge', + 'campaign':self.campaign, + 'premiums':self.premiums, + 'premium_id':self.data.get('premium_id', None), + 'faqmenu': 'modify' if self.transaction else 'pledge', 'transaction': self.transaction, 'tid': self.transaction.id if self.transaction else None, 'cover_width': cover_width(self.work) }) - + return context - + def form_valid(self, form): # right now, if there is a non-zero pledge amount, go with that. otherwise, do the pre_approval - + p = PaymentManager() if self.transaction: # modifying the transaction... - assert self.transaction.type == PAYMENT_TYPE_AUTHORIZATION and self.transaction.status == TRANSACTION_STATUS_ACTIVE - status, url = p.modify_transaction(self.transaction, form.cleaned_data["preapproval_amount"], - paymentReason="Unglue.it %s for %s"% (self.action,self.campaign.name) , - pledge_extra=form.trans_extra + assert self.transaction.type == PAYMENT_TYPE_AUTHORIZATION and self.transaction.status == TRANSACTION_STATUS_ACTIVE + status, url = p.modify_transaction(self.transaction, form.cleaned_data["preapproval_amount"], + paymentReason="Unglue.it %s for %s"% (self.action, self.campaign.name) , + pledge_extra = form.trans_extra ) logger.info("status: {0}, url:{1}".format(status, url)) - + if status and url is not None: logger.info("PledgeView (Modify): " + url) return HttpResponseRedirect(url) @@ -1278,13 +1301,13 @@ class PledgeView(FormView): else: return HttpResponse("No modification made") else: - t, url = p.process_transaction('USD', form.amount(), - host = PAYMENT_HOST_NONE, - campaign=self.campaign, + t, url = p.process_transaction('USD', form.amount(), + host = PAYMENT_HOST_NONE, + campaign=self.campaign, user=self.request.user, - paymentReason="Unglue.it Pledge for {0}".format(self.campaign.name), + paymentReason="Unglue.it Pledge for {0}".format(self.campaign.name), pledge_extra=form.trans_extra - ) + ) if url: logger.info("PledgeView url: " + url) return HttpResponseRedirect(url) @@ -1292,8 +1315,8 @@ class PledgeView(FormView): logger.error("Attempt to produce transaction id {0} failed".format(t.id)) return HttpResponse("Our attempt to enable your transaction failed. We have logged this error.") -class PurchaseView(PledgeView): - template_name="purchase.html" +class PurchaseView(PledgeView): + template_name = "purchase.html" form_class = CampaignPurchaseForm action = "purchase" offer_id = None @@ -1302,8 +1325,8 @@ class PurchaseView(PledgeView): context = super(PledgeView, self).get_context_data(**kwargs) context.update({ 'work':self.work, - 'campaign':self.campaign, - 'faqmenu': 'purchase' , + 'campaign':self.campaign, + 'faqmenu': 'purchase' , 'transaction': self.transaction, 'tid': self.transaction.id if self.transaction else None, 'cover_width': cover_width(self.work), @@ -1311,18 +1334,18 @@ class PurchaseView(PledgeView): 'user_license': self.work.get_user_license(self.request.user), 'give': self.give }) - + return context def get_form_kwargs(self): assert self.request.user.is_authenticated() self.work = safe_get_work(self.kwargs["work_id"]) - + # if there is no campaign or if campaign is not active, we should raise an error try: self.campaign = self.work.last_campaign() # Campaign must be ACTIVE - assert self.campaign.status == 'ACTIVE' + assert self.campaign.status == 'ACTIVE' except Exception, e: # this used to raise an exception, but that seemed pointless. This now has the effect of preventing any pledges. return {} @@ -1359,24 +1382,24 @@ class PurchaseView(PledgeView): def form_valid(self, form): p = PaymentManager() - t, url = p.process_transaction('USD', form.amount(), - host = PAYMENT_HOST_NONE, - campaign=self.campaign, + t, url = p.process_transaction('USD', form.amount(), + host = PAYMENT_HOST_NONE, + campaign=self.campaign, user=self.request.user, - paymentReason="Unglue.it Purchase for {0}".format(self.campaign.name), + paymentReason="Unglue.it Purchase for {0}".format(self.campaign.name), pledge_extra=form.trans_extra - ) + ) if url: return HttpResponseRedirect(url) else: logger.error("Attempt to produce transaction id {0} failed".format(t.id)) return HttpResponse("Our attempt to enable your transaction failed. We have logged this error.") - + class FundView(FormView): - template_name="fund_the_pledge.html" + template_name = "fund_the_pledge.html" transaction = None action = None - + def get_form_class(self): if self.request.user.is_anonymous(): return AnonCCForm @@ -1387,7 +1410,7 @@ class FundView(FormView): def get_form_kwargs(self): kwargs = super(FundView, self).get_form_kwargs() - + #assert self.request.user.is_authenticated() if self.transaction is None: self.transaction = get_object_or_404(Transaction, id=self.kwargs["t_id"]) @@ -1405,48 +1428,48 @@ class FundView(FormView): else: data = {} kwargs['initial'] = data - + data.update( {'preapproval_amount':self.transaction.needed_amount, 'username':self.request.user.username if self.request.user.is_authenticated else None, 'work_id':self.transaction.campaign.work.id, 'title':self.transaction.campaign.work.title} - ) + ) return kwargs def get_context_data(self, **kwargs): context = super(FundView, self).get_context_data(**kwargs) - context['modified'] = self.transaction.status==TRANSACTION_STATUS_MODIFIED - context['preapproval_amount']=self.transaction.max_amount + context['modified'] = self.transaction.status == TRANSACTION_STATUS_MODIFIED + context['preapproval_amount'] = self.transaction.max_amount context['needed'] = self.transaction.needed_amount - context['transaction']=self.transaction + context['transaction'] = self.transaction context['STRIPE_PK'] = stripelib.STRIPE_PK context['action'] = self.action return context - + def form_valid(self, form): p = PaymentManager() stripe_token = form.cleaned_data.get("stripe_token", None) self.transaction.host = settings.PAYMENT_PROCESSOR - return_url = "%s?tid=%s" % (reverse('pledge_complete'),self.transaction.id) + return_url = "%s?tid=%s" % (reverse('pledge_complete'), self.transaction.id) if self.transaction.campaign.type == THANKS and self.transaction.user == None: #anonymous user, just charge the card! if self.request.user.is_authenticated(): self.transaction.user = self.request.user # if there's an email address, put it in the receipt column, so far unused. - self.transaction.receipt = form.cleaned_data.get("email",None) - t, url = p.charge(self.transaction, return_url = return_url, token=stripe_token ) + self.transaction.receipt = form.cleaned_data.get("email", None) + t, url = p.charge(self.transaction, return_url = return_url, token=stripe_token) elif self.request.user.is_anonymous(): #somehow the user lost their login return HttpResponseRedirect(reverse('superlogin')) elif self.transaction.user.id != self.request.user.id: # other sort of strange trouble! - return render(self.request, "pledge_user_error.html", {'transaction': self.transaction, 'action': self.action }) + return render(self.request, "pledge_user_error.html", {'transaction': self.transaction, 'action': self.action }) else: # if the user has active account, use it. Otherwise... if not self.request.user.profile.account: - + # if we get a stripe_token, create a new stripe account for the user if stripe_token: try: @@ -1458,106 +1481,106 @@ class FundView(FormView): return render(self.request, "pledge_card_error.html", {'transaction': self.transaction, 'exception':e }) # with the Account in hand, now do the transaction if self.action == 'pledge': - t, url = p.authorize(self.transaction, return_url = return_url ) + t, url = p.authorize(self.transaction, return_url = return_url) else: - t, url = p.charge(self.transaction, return_url = return_url ) - + t, url = p.charge(self.transaction, return_url = return_url) + # redirecting user to pledge_complete/payment_complete on successful preapproval (in the case of stripe) if url is not None: return HttpResponseRedirect(url) else: - return render(self.request, "pledge_card_error.html", {'transaction': self.transaction }) - + return render(self.request, "pledge_card_error.html", {'transaction': self.transaction }) + class GiftCredit(TemplateView): - template_name="gift_credit.html" + template_name = "gift_credit.html" def get_context_data(self, **kwargs): context = super(GiftCredit, self).get_context_data(**kwargs) - context['faqmenu']="gift" + context['faqmenu'] = "gift" try: - envelope=signing.loads(kwargs['token']) - context['envelope']=envelope + envelope = signing.loads(kwargs['token']) + context['envelope'] = envelope except signing.BadSignature: - self.template_name="gift_error.html" + self.template_name = "gift_error.html" return context try: work = models.Work.objects.get(id=envelope['work_id']) - campaign=work.last_campaign() + campaign = work.last_campaign() except models.Work.DoesNotExist: campaign = None - context['work']=work + context['work'] = work try: user = User.objects.get(username=envelope['username']) except User.DoesNotExist: - self.template_name="gift_user_error.html" - context['error']='user does not exist' + self.template_name = "gift_user_error.html" + context['error'] = 'user does not exist' return context if user != self.request.user: - self.template_name="gift_user_error.html" - context['error']='wrong user logged in' + self.template_name = "gift_user_error.html" + context['error'] = 'wrong user logged in' return context try: # check token not used CreditLog.objects.get(sent=envelope['sent']) - context['error']='credit already registered' + context['error'] = 'credit already registered' return context except CreditLog.DoesNotExist: #not used yet! - amount=envelope['amount']+envelope['cents']/D(100) - CreditLog.objects.create(user=user,amount=amount,action='deposit',sent=envelope['sent']) - ts=Transaction.objects.filter(user=user,campaign=campaign,status=TRANSACTION_STATUS_NONE).order_by('-pk') + amount = envelope['amount']+envelope['cents']/D(100) + CreditLog.objects.create(user=user, amount=amount, action='deposit', sent=envelope['sent']) + ts = Transaction.objects.filter(user=user, campaign=campaign, status=TRANSACTION_STATUS_NONE).order_by('-pk') if ts.count()==0: - ts=Transaction.objects.filter(user=user,campaign=campaign,status=TRANSACTION_STATUS_MODIFIED).order_by('-pk') + ts = Transaction.objects.filter(user=user, campaign=campaign, status=TRANSACTION_STATUS_MODIFIED).order_by('-pk') if ts.count()>0: - t=ts[0] - credit_transaction(t,user, amount) + t = ts[0] + credit_transaction(t, user, amount) for t in ts[1:]: - t.status=TRANSACTION_STATUS_CANCELED + t.status = TRANSACTION_STATUS_CANCELED t.save() - context['transaction']=t + context['transaction'] = t return context else: user.credit.add_to_balance(amount) return context - - + + class PledgeRechargeView(TemplateView): """ a view to allow for recharge of a transaction for failed transactions or ones with errors """ - template_name="pledge_recharge.html" + template_name = "pledge_recharge.html" def get_context_data(self, **kwargs): - + context = super(PledgeRechargeView, self).get_context_data(**kwargs) - + # the following should be true since PledgeView.as_view is wrapped in login_required assert self.request.user.is_authenticated() user = self.request.user - + work = safe_get_work(self.kwargs["work_id"]) campaign = work.last_campaign() - + if campaign is None: return Http404 - + transaction = campaign.transaction_to_recharge(user) - + # calculate a URL to do a preapproval -- in the future, we may want to do a straight up payment - + return_url = None nevermind_url = None - + if transaction is not None: - # the recipients of this authorization is not specified here but rather by the PaymentManager. + # the recipients of this authorization is not specified here but rather by the PaymentManager. paymentReason = "Unglue.it Recharge for {0}".format(campaign.name) - + p = PaymentManager() t, url = p.authorize(transaction, return_url=return_url, paymentReason=paymentReason) logger.info("Recharge url: {0}".format(url)) else: url = None - + context.update({ 'work':work, 'transaction':transaction, @@ -1565,69 +1588,69 @@ class PledgeRechargeView(TemplateView): 'recharge_url': url }) return context - + class FundCompleteView(TemplateView): """A callback for Payment to tell unglue.it that a payment transaction has completed successfully. - + Possible things to implement: - + after pledging, supporter receives email including thanks, work pledged, amount, expiry date, any next steps they should expect; others? study other confirmation emails for their contents should note that a confirmation email has been sent to $email from $sender - should briefly note next steps (e.g. if this campaign succeeds you will be emailed on date X) - + should briefly note next steps (e.g. if this campaign succeeds you will be emailed on date X) + """ - - template_name="pledge_complete.html" - + + template_name = "pledge_complete.html" + def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) - + if self.transaction: if self.transaction.campaign.type == THANKS: return DownloadView.as_view()(request, work=self.transaction.campaign.work) - + else: if request.user.is_authenticated: if self.user_is_ok(): return self.render_to_response(context) else: - return HttpResponseRedirect(reverse('work', kwargs={'work_id': self.transaction.campaign.work.id})) + return HttpResponseRedirect(reverse('work', kwargs={'work_id': self.transaction.campaign.work.id})) else: return redirect_to_login(request.get_full_path()) else: return HttpResponseRedirect(reverse('home')) - + def user_is_ok(self): if not self.transaction: - return False + return False if self.transaction.campaign.type == THANKS and self.transaction.user == None: # to handle anonymous donors- leakage not an issue return True - else: + else: return self.request.user.id == self.transaction.user.id - - + + def get_context_data(self): # pick up all get and post parameters and display context = super(FundCompleteView, self).get_context_data() self.transaction = None - + # pull out the transaction id and try to get the corresponding Transaction - transaction_id = self.request.POST.get("tid",self.request.GET.get("tid", None)) - + transaction_id = self.request.POST.get("tid", self.request.GET.get("tid", None)) + if not transaction_id: return context try: self.transaction = Transaction.objects.get(id=transaction_id) except (ValueError, Transaction.DoesNotExist): self.transaction = None - + if not self.transaction: return context - + # work and campaign in question try: campaign = self.transaction.campaign @@ -1635,12 +1658,12 @@ class FundCompleteView(TemplateView): except Exception, e: campaign = None work = None - + # # we need to check whether the user tied to the transaction is indeed the authenticated user. - + if not self.user_is_ok(): return context - + gift = self.transaction.extra.has_key('give_to') if not gift: # add the work corresponding to the Transaction on the user's wishlist if it's not already on the wishlist @@ -1648,52 +1671,51 @@ class FundCompleteView(TemplateView): self.transaction.user.wishlist.add_work(work, 'pledging', notify=True) #put info into session for download page to pick up. - self.request.session['amount']= int(self.transaction.amount * 100) + self.request.session['amount'] = int(self.transaction.amount * 100) if self.transaction.receipt: - self.request.session['receipt']= self.transaction.receipt - - + self.request.session['receipt'] = self.transaction.receipt + context["transaction"] = self.transaction context["work"] = work context["campaign"] = campaign context["faqmenu"] = "complete" context["site"] = Site.objects.get_current() - - return context + + return context class PledgeModifiedView(FundCompleteView): def get_context_data(self): context = super(PledgeModifiedView, self).get_context_data() - context['modified']=True + context['modified'] = True return context - + class PledgeCancelView(FormView): """A view for allowing a user to cancel the active transaction for specified campaign""" - template_name="pledge_cancel.html" + template_name = "pledge_cancel.html" form_class = PledgeCancelForm - + def get_context_data(self, **kwargs): context = super(PledgeCancelView, self).get_context_data(**kwargs) - + # initialize error to be None context["error"] = None - + # the following should be true since PledgeCancelView.as_view is wrapped in login_required - + if self.request.user.is_authenticated(): user = self.request.user else: context["error"] = "You are not logged in." return context - + campaign = get_object_or_404(models.Campaign, id=self.kwargs["campaign_id"]) if campaign.status != 'ACTIVE': context["error"] = "{0} is not an active campaign".format(campaign) return context - + work = campaign.work transactions = campaign.transactions().filter(user=user, status=TRANSACTION_STATUS_ACTIVE) - + if transactions.count() < 1: context["error"] = "You don't have an active transaction for this campaign." return context @@ -1707,29 +1729,29 @@ class PledgeCancelView(FormView): logger.error("Transaction id {0} transaction type, which should be {1}, is actually {2}".format(transaction.id, PAYMENT_TYPE_AUTHORIZATION, transaction.type)) context["error"] = "Your transaction type, which should be {0}, is actually {1}".format(PAYMENT_TYPE_AUTHORIZATION, transaction.type) return context - + # we've located the transaction, work, and campaign referenced in the view - + context["transaction"] = transaction context["work"] = work context["campaign"] = campaign context["faqmenu"] = "cancel" - + return context - + def form_valid(self, form): # check that user does, in fact, have an active transaction for specified campaign - + logger.info("arrived at pledge_cancel form_valid") # pull campaign_id from form, not from URI as we do from GET - campaign_id = self.request.POST.get('campaign_id',self.request.GET.get('campaign_id')) - + campaign_id = self.request.POST.get('campaign_id', self.request.GET.get('campaign_id')) + # this following logic should be extraneous. if self.request.user.is_authenticated(): user = self.request.user else: return HttpResponse("You need to be logged in.") - + try: # look up the specified campaign and attempt to pull up the appropriate transaction # i.e., the transaction actually belongs to user, that the transaction is active @@ -1759,10 +1781,10 @@ class PledgeCancelView(FormView): return HttpResponse("Our attempt to cancel your transaction failed. We have logged this error.") except Exception, e: logger.error("Exception from attempt to cancel pledge for campaign id {0} for username {1}: {2}".format(campaign_id, user.username, e)) - return HttpResponse("Sorry, something went wrong in canceling your campaign pledge. We have logged this error.") - + return HttpResponse("Sorry, something went wrong in canceling your campaign pledge. We have logged this error.") + def claim(request): - if request.method == 'GET': + if request.method == 'GET': data = request.GET else: data = request.POST @@ -1784,7 +1806,7 @@ def claim(request): active_claims = work.claim.exclude(status = 'release') context = {'form': form, 'work': work, 'rights_holder':rights_holder , 'active_claims':active_claims} return render(request, "claim.html", context) - + def new_survey(request, work_id): if not request.user.is_authenticated() : return HttpResponseRedirect(reverse('surveys')) @@ -1792,23 +1814,23 @@ def new_survey(request, work_id): Q(claim__user = request.user) | Q(claim__rights_holder__owner = request.user) ) if work_id: - work =safe_get_work(work_id) + work = safe_get_work(work_id) for my_work in my_works: - if my_work==work: - form=SurveyForm() + if my_work == work: + form = SurveyForm() break else: return HttpResponseRedirect(reverse('surveys')) else: work = None form = SurveyForm() - if request.method == 'POST': - form = SurveyForm( data=request.POST) + if request.method == 'POST': + form = SurveyForm(data=request.POST) if form.is_valid(): if not work and form.work: for my_work in my_works: - print '{} {}'.format(my_work.id,form.work.id) - if my_work==form.work: + print '{} {}'.format(my_work.id, form.work.id) + if my_work == form.work: work = form.work break else: @@ -1826,7 +1848,7 @@ def surveys(request): Q(claim__user = request.user) | Q(claim__rights_holder__owner = request.user) ) return render(request, "surveys.html", {"works":works}) - + def rh_tools(request): if not request.user.is_authenticated() : return render(request, "rh_tools.html") @@ -1838,15 +1860,15 @@ def rh_tools(request): if claim.can_open_new: if request.method == 'POST' and request.POST.has_key('cl_%s-work' % claim.id) and int(request.POST['cl_%s-work' % claim.id]) == claim.work.id : claim.campaign_form = OpenCampaignForm(data = request.POST, prefix = 'cl_'+str(claim.id),) - if claim.campaign_form.is_valid(): + if claim.campaign_form.is_valid(): new_campaign = claim.campaign_form.save(commit=False) - if new_campaign.type==BUY2UNGLUE: + if new_campaign.type == BUY2UNGLUE: new_campaign.target = D(settings.UNGLUEIT_MAXIMUM_TARGET) new_campaign.set_cc_date_initial() - elif new_campaign.type==REWARDS: + elif new_campaign.type == REWARDS: new_campaign.deadline = date_today() + timedelta(days=int(settings.UNGLUEIT_LONGEST_DEADLINE)) new_campaign.target = D(settings.UNGLUEIT_MINIMUM_TARGET) - elif new_campaign.type==THANKS: + elif new_campaign.type == THANKS: new_campaign.target = D(settings.UNGLUEIT_MINIMUM_TARGET) new_campaign.save() claim.campaign_form.save_m2m() @@ -1860,23 +1882,23 @@ def rh_tools(request): if claim.campaign: if claim.campaign.status in ['ACTIVE','INITIALIZED']: if request.method == 'POST' and request.POST.has_key('edit_managers_%s'% claim.campaign.id) : - claim.campaign.edit_managers_form=EditManagersForm( instance=claim.campaign, data=request.POST, prefix=claim.campaign.id) + claim.campaign.edit_managers_form = EditManagersForm(instance=claim.campaign, data=request.POST, prefix=claim.campaign.id) if claim.campaign.edit_managers_form.is_valid(): claim.campaign.edit_managers_form.save() claim.campaign.edit_managers_form = EditManagersForm(instance=claim.campaign, prefix=claim.campaign.id) else: - claim.campaign.edit_managers_form=EditManagersForm(instance=claim.campaign, prefix=claim.campaign.id) + claim.campaign.edit_managers_form = EditManagersForm(instance=claim.campaign, prefix=claim.campaign.id) campaigns = request.user.campaigns.all() new_campaign = None for campaign in campaigns: if campaign.clonable(): if request.method == 'POST' and request.POST.has_key('c%s-campaign_id'% campaign.id): - clone_form= CloneCampaignForm(data=request.POST, prefix = 'c%s' % campaign.id) + clone_form = CloneCampaignForm(data=request.POST, prefix = 'c%s' % campaign.id) if clone_form.is_valid(): campaign.clone() else: - campaign.clone_form= CloneCampaignForm(initial={'campaign_id':campaign.id}, prefix = 'c%s' % campaign.id) - return render(request, "rh_tools.html", {'claims': claims ,'campaigns': campaigns}) + campaign.clone_form = CloneCampaignForm(initial={'campaign_id':campaign.id}, prefix='c%s' % campaign.id) + return render(request, "rh_tools.html", {'claims': claims , 'campaigns': campaigns}) def rh_admin(request, facet='top'): if not request.user.is_authenticated() : @@ -1886,7 +1908,7 @@ def rh_admin(request, facet='top'): PendingFormSet = modelformset_factory(models.Claim, fields=['status'], extra=0) pending_data = models.Claim.objects.filter(status = 'pending') active_data = models.Claim.objects.filter(status = 'active') - if request.method == 'POST': + if request.method == 'POST': if 'create_rights_holder' in request.POST.keys(): form = RightsHolderForm(data=request.POST) pending_formset = PendingFormSet (queryset=pending_data) @@ -1903,12 +1925,12 @@ def rh_admin(request, facet='top'): form = RightsHolderForm() pending_formset = PendingFormSet(queryset=pending_data) rights_holders = models.RightsHolder.objects.all() - - context = { - 'request': request, - 'rights_holders': rights_holders, + + context = { + 'request': request, + 'rights_holders': rights_holders, 'form': form, - 'pending': zip(pending_data,pending_formset), + 'pending': zip(pending_data, pending_formset), 'pending_formset': pending_formset, 'active_data': active_data, 'facet': facet, @@ -1917,37 +1939,37 @@ def rh_admin(request, facet='top'): def campaign_admin(request): if not request.user.is_authenticated() : - return render(request, "admins_only.html") + return render(request, "admins_only.html") if not request.user.is_staff : return render(request, "admins_only.html") - + context = {} - + def campaigns_types(): # pull out Campaigns with Transactions that are ACTIVE -- and hence can be executed # Campaign.objects.filter(transaction__status='ACTIVE') - + campaigns_with_active_transactions = models.Campaign.objects.filter(transaction__status=TRANSACTION_STATUS_ACTIVE) - + # pull out Campaigns with Transactions that are INCOMPLETE - + campaigns_with_incomplete_transactions = models.Campaign.objects.filter(transaction__status=TRANSACTION_STATUS_INCOMPLETE) - + # show all Campaigns with Transactions that are COMPLETED - + campaigns_with_completed_transactions = models.Campaign.objects.filter(transaction__status=TRANSACTION_STATUS_COMPLETE) - + # show Campaigns with Transactions that are CANCELED - + campaigns_with_canceled_transactions = models.Campaign.objects.filter(transaction__status=TRANSACTION_STATUS_CANCELED) - + return (campaigns_with_active_transactions, campaigns_with_incomplete_transactions, campaigns_with_completed_transactions, campaigns_with_canceled_transactions) - + form = CampaignAdminForm() pm = PaymentManager() check_status_results = None command_status = None - + if request.method == 'GET': pass elif request.method == 'POST': @@ -1964,15 +1986,15 @@ def campaign_admin(request): check_status_results += "

No preapprovals needed updating

" if len(status["payments"]): for t in status["payments"]: - info = ", ".join(["%s:%s" % (k,v) for (k,v) in t.items()]) + info = ", ".join(["%s:%s" % (k, v) for (k, v) in t.items()]) check_status_results += "

Payment updated: %s

" % (info) - + else: - check_status_results += "

No payments needed updating

" + check_status_results += "

No payments needed updating

" command_status = _("Transactions updated based on PaymentDetails and PreapprovalDetails") except Exception, e: check_status_results = e - elif 'execute_campaigns' in request.POST.keys(): + elif 'execute_campaigns' in request.POST.keys(): c_id = request.POST.get('active_campaign', None) if c_id is not None: try: @@ -1989,8 +2011,8 @@ def campaign_admin(request): results = pm.finish_campaign(campaign) command_status = str(results) except Exception, e: - command_status = "Error in finishing transactions for campaign %s " % (str(e)) - + command_status = "Error in finishing transactions for campaign %s " % (str(e)) + elif 'cancel_campaigns' in request.POST.keys(): c_id = request.POST.get('active_campaign', None) if c_id is not None: @@ -1999,11 +2021,11 @@ def campaign_admin(request): results = pm.cancel_campaign(campaign) command_status = str(results) except Exception, e: - command_status = "Error in canceling transactions for campaign %s " % (str(e)) - + command_status = "Error in canceling transactions for campaign %s " % (str(e)) + (campaigns_with_active_transactions, campaigns_with_incomplete_transactions, campaigns_with_completed_transactions, campaigns_with_canceled_transactions) = campaigns_types() - + context.update({ 'form': form, 'check_status_results':check_status_results, @@ -2023,7 +2045,7 @@ def supporter(request, supporter_username, template_name, extra_context={}): works_active = [] works_wished = [] works_on_wishlist = wishlist.works.all() - + if (works_on_wishlist): # querysets for tabs # unglued tab is anything with an existing ebook or successful campaign @@ -2034,17 +2056,17 @@ def supporter(request, supporter_username, template_name, extra_context={}): works_unglued = works_unglued.order_by('-campaigns__status', 'campaigns__deadline', '-num_wishes') works_active = works_on_wishlist.filter(campaigns__status='ACTIVE').order_by('campaigns__deadline').distinct() - + # everything else goes in tab 3 works_wished = works_on_wishlist.exclude(pk__in=works_active.values_list('pk', flat=True)).exclude(pk__in=works_unglued.values_list('pk', flat=True)).order_by('-num_wishes') - + slidelist = [] # badge counts backed = works_unglued.count() backing = works_active.count() wished = works_wished.count() - - else: + + else: backed = 0 backing = 0 wished = 0 @@ -2056,22 +2078,22 @@ def supporter(request, supporter_username, template_name, extra_context={}): activetab = "#1" else: activetab = "#3" - + # following block to support profile admin form in supporter page if request.user.is_authenticated() and request.user.username == supporter_username: - profile_obj=request.user.profile + profile_obj = request.user.profile - if request.method == 'POST': - profile_form = ProfileForm(data=request.POST,instance=profile_obj) + if request.method == 'POST': + profile_form = ProfileForm(data=request.POST, instance=profile_obj) if profile_form.is_valid(): if profile_form.cleaned_data['clear_facebook'] or profile_form.cleaned_data['clear_twitter'] or profile_form.cleaned_data['clear_goodreads'] : if profile_form.cleaned_data['clear_facebook']: - profile_obj.facebook_id=0 + profile_obj.facebook_id = 0 if profile_obj.avatar_source == models.FACEBOOK: profile_obj.avatar_source = models.UNGLUEITAR if profile_form.cleaned_data['clear_twitter']: - profile_obj.twitter_id="" + profile_obj.twitter_id = "" if profile_obj.avatar_source == models.TWITTER: profile_obj.avatar_source = models.UNGLUEITAR if profile_form.cleaned_data['clear_goodreads']: @@ -2085,8 +2107,8 @@ def supporter(request, supporter_username, template_name, extra_context={}): profile_form.save() else: - profile_form= ProfileForm(instance=profile_obj) - + profile_form = ProfileForm(instance=profile_obj) + else: profile_form = '' @@ -2102,39 +2124,39 @@ def supporter(request, supporter_username, template_name, extra_context={}): "backing": backing, "wished": wished, "profile_form": profile_form, - "ungluers": userlists.other_users(supporter, 5 ), + "ungluers": userlists.other_users(supporter, 5), "activetab": activetab, } context.update(extra_context) return render(request, template_name, context) -def library(request,library_name): - context={} +def library(request, library_name): + context = {} try: # determine if the supporter is a library - authenticator = Authenticator(request,library_name) + authenticator = Authenticator(request, library_name) context['authenticator'] = authenticator context['library'] = library = authenticator.library except Library.DoesNotExist: raise Http404 - works_active= models.Work.objects.filter(acqs__user=library.user,acqs__license=LIBRARY).distinct() - if works_active.count()>0: + works_active = models.Work.objects.filter(acqs__user=library.user, acqs__license=LIBRARY).distinct() + if works_active.count() > 0: context['works_active'] = works_active context['activetab'] = "#2" - context['ungluers'] = userlists.library_users(library, 5 ) - return supporter(request,library_name,template_name='libraryauth/library.html', extra_context=context) - - + context['ungluers'] = userlists.library_users(library, 5) + return supporter(request, library_name, template_name='libraryauth/library.html', extra_context=context) + + class ManageAccount(FormView): - template_name="manage_account.html" + template_name = "manage_account.html" form_class = PlainCCForm def get_context_data(self, **kwargs): context = super(ManageAccount, self).get_context_data(**kwargs) context['STRIPE_PK'] = stripelib.STRIPE_PK return context - + def form_valid(self, form): """ save the token, make an account""" @@ -2155,20 +2177,20 @@ class ManageAccount(FormView): def search(request): q = request.GET.get('q', '') ty = request.GET.get('ty', 'g') # ge= 'general, au= 'author' - request.session['q']=q + request.session['q'] = q try: page = int(request.GET.get('page', 1)) except ValueError: # garbage in page page = 1 gbo = request.GET.get('gbo', 'n') # gbo is flag for google books only - our_stuff = Q(is_free=True) | Q(campaigns__isnull=False ) - if q != '' and page==1 and not gbo=='y': + our_stuff = Q(is_free=True) | Q(campaigns__isnull=False) + if q != '' and page == 1 and not gbo == 'y': isbnq = ISBN(q) if isbnq.valid: work_query = Q(identifiers__value=str(isbnq), identifiers__type="isbn") - elif ty=='au': - work_query = Q(editions__authors__name=q) + elif ty == 'au': + work_query = Q(editions__authors__name=q) else: work_query = Q(title__icontains=q) | Q(editions__authors__name__icontains=q) | Q(subjects__name__iexact=q) campaign_works = models.Work.objects.filter(our_stuff).filter(work_query).distinct() @@ -2180,17 +2202,17 @@ def search(request): gbo = 'y' else: if gbo == 'n': - page=page-1 # because page=1 is the unglue.it results + page = page-1 # because page=1 is the unglue.it results results = gluejar_search(q, user_ip=request.META['REMOTE_ADDR'], page=page) campaign_works = None # flag search result as on wishlist as appropriate - works=[] + works = [] for result in results: try: - work = models.Identifier.objects.get(type='goog',value=result['googlebooks_id']).work + work = models.Identifier.objects.get(type='goog', value=result['googlebooks_id']).work works.append(work) - except models.Identifier.DoesNotExist: + except models.Identifier.DoesNotExist: works.append(result) context = { "q": q, @@ -2220,23 +2242,23 @@ def wishlist(request): work.subjects.remove(subject) return HttpResponse('removed work from '+setkw) elif add_work_id: - work =safe_get_work(add_work_id) + work = safe_get_work(add_work_id) work.subjects.add(subject) return HttpResponse('added work to '+setkw) - + if googlebooks_id: try: edition = bookloader.add_by_googlebooks_id(googlebooks_id) if edition.new: # add related editions asynchronously tasks.populate_edition.delay(edition.isbn_13) - request.user.wishlist.add_work(edition.work,'user', notify=True) + request.user.wishlist.add_work(edition.work, 'user', notify=True) return HttpResponse('added googlebooks id') except bookloader.LookupFailure: logger.warning("failed to load googlebooks_id %s" % googlebooks_id) return HttpResponse('error adding googlebooks id') except Exception, e: - logger.warning("Error in wishlist adding %s" % (e)) + logger.warning("Error in wishlist adding %s" % (e)) return HttpResponse('error adding googlebooks id') # TODO: redirect to work page, when it exists elif remove_work_id: @@ -2245,8 +2267,8 @@ def wishlist(request): return HttpResponse('removed work from wishlist') elif add_work_id: # if adding from work page, we have may work.id, not googlebooks_id - work =safe_get_work(add_work_id) - request.user.wishlist.add_work(work,'user', notify=True) + work = safe_get_work(add_work_id) + request.user.wishlist.add_work(work, 'user', notify=True) return HttpResponse('added work to wishlist') @require_POST @@ -2262,9 +2284,9 @@ def kw_edit(request, work_id): except models.Subject.DoesNotExist: return HttpResponse('invalid subject') work.subjects.remove(subject) - return HttpResponse('removed ' + remove_kw ) + return HttpResponse('removed ' + remove_kw) elif add_form: - form= SubjectSelectForm(data=request.POST) + form = SubjectSelectForm(data=request.POST) if form.is_valid(): add_kw = form.cleaned_data['add_kw'] try: @@ -2272,22 +2294,22 @@ def kw_edit(request, work_id): except models.Subject.DoesNotExist: return HttpResponse('invalid subject') work.subjects.add(subject) - return HttpResponse( add_kw.name ) + return HttpResponse(add_kw.name) else: - return HttpResponse('xxbadform' ) + return HttpResponse('xxbadform') else: return HttpResponse(str(add_form)) return HttpResponse(str(add_form)) - + class InfoPageView(TemplateView): - + def get_template_names(self, **kwargs): if self.kwargs['template_name']: return (self.kwargs['template_name']) else: return ('metrics.html') - + def get_context_data(self, **kwargs): users = User.objects users.today = users.filter(date_joined__range = (date_today(), now())) @@ -2329,7 +2351,7 @@ class InfoPageView(TemplateView): ebookfiles.year = ebookfiles.filter(created__year = date_today().year) ebookfiles.month = ebookfiles.year.filter(created__month = date_today().month) ebookfiles.yesterday = ebookfiles.filter(created__range = (date_today()-timedelta(days=1), date_today())) - wishlists= models.Wishlist.objects.exclude(wishes__isnull=True) + wishlists = models.Wishlist.objects.exclude(wishes__isnull=True) wishlists.today = wishlists.filter(created__range = (date_today(), now())) wishlists.days7 = wishlists.filter(created__range = (date_today()-timedelta(days=7), now())) wishlists.year = wishlists.filter(created__year = date_today().year) @@ -2338,7 +2360,7 @@ class InfoPageView(TemplateView): wishlists.yesterday = wishlists.filter(created__range = (date_today()-timedelta(days=1), date_today())) else: wishlists.yesterday = wishlists.month.filter(created__day = date_today().day-1) - + transactions = Transaction.objects.filter(status__in = [TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE]) transactions.sum = transactions.aggregate(Sum('amount'))['amount__sum'] transactions.today = transactions.filter(date_created__range = (date_today(), now())) @@ -2356,9 +2378,9 @@ class InfoPageView(TemplateView): marc.days7 = marc.filter(created__range = (date_today()-timedelta(days=7), now())) marc.year = marc.filter(created__year = date_today().year) marc.month = marc.year.filter(created__month = date_today().month) - marc.yesterday = marc.filter(created__range = (date_today()-timedelta(days=1),date_today())) + marc.yesterday = marc.filter(created__range = (date_today()-timedelta(days=1), date_today())) return { - 'users': users, + 'users': users, 'works': works, 'ebooks': ebooks, 'ebookfiles': ebookfiles, @@ -2368,38 +2390,38 @@ class InfoPageView(TemplateView): } class InfoLangView(TemplateView): - + def get_template_names(self, **kwargs): if self.kwargs['template_name']: return (self.kwargs['template_name']) else: return ('languages.html') - + def get_context_data(self, **kwargs): - languages=models.Work.objects.filter(num_wishes__gte = 1).values('language').annotate(lang_count=Count('language')).order_by('-lang_count') + languages = models.Work.objects.filter(num_wishes__gte = 1).values('language').annotate(lang_count=Count('language')).order_by('-lang_count') return { - 'wished_languages': languages, + 'wished_languages': languages, } - + class FAQView(FormView): template_name = "faq.html" form_class = DateCalculatorForm def form_valid(self, form): - form.instance.status='DEMO' - form.instance.type=BUY2UNGLUE + form.instance.status = 'DEMO' + form.instance.type = BUY2UNGLUE form.instance.set_dollar_per_day() form.instance.update_left() - form.instance._current_total=form.cleaned_data['revenue'] + form.instance._current_total = form.cleaned_data['revenue'] return self.render_to_response(self.get_context_data(form=form)) - + def get_initial(self): - return {'target':10000, 'cc_date_initial': date_today()+timedelta(days=1461),'revenue':0, 'type':BUY2UNGLUE, 'status':'DEMO'} + return {'target':10000, 'cc_date_initial': date_today()+timedelta(days=1461), 'revenue':0, 'type':BUY2UNGLUE, 'status':'DEMO'} def get_context_data(self, **kwargs): - cd = super(FAQView,self).get_context_data(**kwargs) + cd = super(FAQView, self).get_context_data(**kwargs) cd.update({ - 'location': self.kwargs["location"], + 'location': self.kwargs["location"], 'sublocation': self.kwargs["sublocation"], }) return cd @@ -2410,13 +2432,13 @@ class GoodreadsDisplayView(TemplateView): context = super(GoodreadsDisplayView, self).get_context_data(**kwargs) session = self.request.session gr_client = GoodreadsClient(key=settings.GOODREADS_API_KEY, secret=settings.GOODREADS_API_SECRET) - + user = self.request.user if user.is_authenticated(): api_key = ApiKey.objects.filter(user=user)[0].key context['api_key'] = api_key - if user.profile.goodreads_user_id is None: + if user.profile.goodreads_user_id is None: # calculate the Goodreads authorization URL (context["goodreads_auth_url"], request_token) = gr_client.begin_authorization(self.request.build_absolute_uri(reverse('goodreads_cb'))) logger.info("goodreads_auth_url: %s" %(context["goodreads_auth_url"])) @@ -2429,14 +2451,14 @@ class GoodreadsDisplayView(TemplateView): gr_shelf_load_form = GoodreadsShelfLoadingForm() # load the shelves into the form choices = [('all:%d' % (gr_shelves["total_book_count"]),'all (%d)' % (gr_shelves["total_book_count"]))] + \ - [("%s:%d" % (s["name"], s["book_count"]) ,"%s (%d)" % (s["name"],s["book_count"])) for s in gr_shelves["user_shelves"]] + [("%s:%d" % (s["name"], s["book_count"]) ,"%s (%d)" % (s["name"], s["book_count"])) for s in gr_shelves["user_shelves"]] gr_shelf_load_form.fields['goodreads_shelf_name_number'].widget = Select(choices=tuple(choices)) - + context["gr_shelf_load_form"] = gr_shelf_load_form - + # also load any CeleryTasks associated with the user context["celerytasks"] = models.CeleryTask.objects.filter(user=user) - + return context @login_required @@ -2449,13 +2471,13 @@ def goodreads_auth(request): # store request token in session so that we can redeem it for auth_token if authorization works request.session['goodreads_request_token'] = request_token['oauth_token'] request.session['goodreads_request_secret'] = request_token['oauth_token_secret'] - + return HttpResponseRedirect(goodreads_auth_url) -@login_required +@login_required def goodreads_cb(request): """handle callback from Goodreads""" - + session = request.session authorized_flag = request.GET['authorize'] # is it '1'? request_oauth_token = request.GET['oauth_token'] @@ -2464,21 +2486,21 @@ def goodreads_cb(request): request_token = {'oauth_token': session.get('goodreads_request_token'), 'oauth_token_secret': session.get('goodreads_request_secret')} gr_client = GoodreadsClient(key=settings.GOODREADS_API_KEY, secret=settings.GOODREADS_API_SECRET) - + access_token = gr_client.complete_authorization(request_token) - + # store the access token in the user profile profile = request.user.profile profile.goodreads_auth_token = access_token["oauth_token"] profile.goodreads_auth_secret = access_token["oauth_token_secret"] - + # let's get the userid, username user = gr_client.auth_user() - + profile.goodreads_user_id = user["userid"] profile.goodreads_user_name = user["name"] profile.goodreads_user_link = user["link"] - + profile.save() # is this needed? # redirect to the Goodreads display page -- should observe some next later @@ -2486,7 +2508,7 @@ def goodreads_cb(request): @require_POST @login_required -@csrf_exempt +@csrf_exempt def goodreads_flush_assoc(request): user = request.user if user.is_authenticated(): @@ -2499,9 +2521,9 @@ def goodreads_flush_assoc(request): profile.save() logger.info('Goodreads association flushed for user %s', user) return HttpResponseRedirect(reverse('goodreads_display')) - + @require_POST -@login_required +@login_required @csrf_exempt def goodreads_load_shelf(request): """ @@ -2518,16 +2540,16 @@ def goodreads_load_shelf(request): load_task_name = "load_goodreads_shelf_into_wishlist" load_task = getattr(tasks, load_task_name) task_id = load_task.delay(user.id, shelf_name, expected_number_of_books=expected_number_of_books) - + ct = models.CeleryTask() ct.task_id = task_id ct.function_name = load_task_name ct.user = user ct.description = "Loading Goodread shelf %s to user %s with %s books" % (shelf_name, user, expected_number_of_books) ct.save() - + return HttpResponse("We're on it! Reload the page to see the books we've snagged so far.") - except Exception,e: + except Exception, e: return HttpResponse("Error in loading shelf: %s " % (e)) logger.info("Error in loading shelf for user %s: %s ", user, e) @@ -2537,23 +2559,23 @@ def goodreads_calc_shelves(request): # we should move towards calculating this only if needed (perhaps with Ajax), caching previous results, etc to speed up # performance - + if request.user.profile.goodreads_user_id is not None: gr_client = GoodreadsClient(key=settings.GOODREADS_API_KEY, secret=settings.GOODREADS_API_SECRET) goodreads_shelves = gr_client.shelves_list(user_id=request.user.profile.goodreads_user_id) #goodreads_shelf_load_form = GoodreadsShelfLoadingForm() ## load the shelves into the form #choices = [('all:%d' % (goodreads_shelves["total_book_count"]),'all (%d)' % (goodreads_shelves["total_book_count"]))] + \ - # [("%s:%d" % (s["name"], s["book_count"]) ,"%s (%d)" % (s["name"],s["book_count"])) for s in goodreads_shelves["user_shelves"]] + # [("%s:%d" % (s["name"], s["book_count"]) ,"%s (%d)" % (s["name"], s["book_count"])) for s in goodreads_shelves["user_shelves"]] #goodreads_shelf_load_form.fields['goodreads_shelf_name_number'].widget = Select(choices=tuple(choices)) else: goodreads_shelf_load_form = None - + return HttpResponse(json.dumps(goodreads_shelves), content_type="application/json") - + @require_POST -@login_required +@login_required @csrf_exempt def librarything_load(request): """ @@ -2562,30 +2584,30 @@ def librarything_load(request): # Should be moved to the API user = request.user - - try: + + try: # figure out expected_number_of_books later - + lt_username = request.user.profile.librarything_id - logger.info('Adding task to load librarything %s to user %s', lt_username, user ) + logger.info('Adding task to load librarything %s to user %s', lt_username, user) load_task_name = "load_librarything_into_wishlist" load_task = getattr(tasks, load_task_name) task_id = load_task.delay(user.id, lt_username, None) - + ct = models.CeleryTask() ct.task_id = task_id ct.function_name = load_task_name ct.user = user ct.description = "Loading LibraryThing collection of %s to user %s." % (lt_username, user) ct.save() - + return HttpResponse("We're on it! Reload the page to see the books we've snagged so far.") - except Exception,e: + except Exception, e: return HttpResponse("Error in loading LibraryThing library: %s " % (e)) logger.info("Error in loading LibraryThing for user %s: %s ", user, e) @require_POST -@login_required +@login_required @csrf_exempt def clear_wishlist(request): try: @@ -2593,53 +2615,53 @@ def clear_wishlist(request): logger.info("Wishlist for user %s cleared", request.user) return HttpResponse('wishlist cleared') except Exception, e: - return HttpResponse("Error in clearing wishlist: %s " % (e)) logger.info("Error in clearing wishlist for user %s: %s ", request.user, e) + return HttpResponse("Error in clearing wishlist: %s " % (e)) @require_POST -@login_required +@login_required def msg(request): form = MsgForm(data=request.POST) if form.is_valid(): if not request.user.is_staff and request.user not in form.cleaned_data['work'].last_campaign().managers.all(): - logger.warning("unauthorized attempt to send message by %s for %s"% (request.user,form.cleaned_data['work'])) + logger.warning("unauthorized attempt to send message by %s for %s"% (request.user, form.cleaned_data['work'])) raise Http404 - supporter_message.send(sender=request.user,msg=form.cleaned_data["msg"], work=form.cleaned_data["work"],supporter=form.cleaned_data["supporter"]) + supporter_message.send(sender=request.user, msg=form.cleaned_data["msg"], work=form.cleaned_data["work"], supporter=form.cleaned_data["supporter"]) return HttpResponse("message sent") else: logger.info("Invalid form for user %s", request.user) raise Http404 - + class LibraryThingView(FormView): - template_name="librarything.html" + template_name = "librarything.html" form_class = LibraryThingForm - + def get_context_data(self, **kwargs): context = super(LibraryThingView, self).get_context_data(**kwargs) form = kwargs['form'] # get the books for the lt_username in the form - lt_username=self.request.GET.get("lt_username",None) + lt_username = self.request.GET.get("lt_username", None) if lt_username is not None: lt = librarything.LibraryThing(username=lt_username) context.update({'books':list(lt.parse_user_catalog(view_style=5))}) else: context.update({'books':None}) - + # try picking up the LibraryThing api key -- and set to None if not available. Not being used for # anything crucial at this moment, so a None is ok here try: context.update({'lt_api_key':settings.LIBRARYTHING_API_KEY}) except: pass - + return context - def form_valid(self,form): + def form_valid(self, form): return super(LibraryThingView, self).form_valid(form) - + @require_POST -@login_required +@login_required @csrf_exempt def clear_celery_tasks(request): try: @@ -2647,15 +2669,15 @@ def clear_celery_tasks(request): logger.info("Celery tasks for user %s cleared", request.user) return HttpResponse('Celery Tasks List cleared') except Exception, e: + logger.info("Error in clearing Celery Tasks for user %s: %s ", request.user, e) return HttpResponse("Error in clearing Celery Tasks: %s " % (e)) - logger.info("Error in clearing Celery Tasks for user %s: %s ", request.user, e) def celery_test(request): return HttpResponse("celery_test") # routing views that try to redirect to the works page on a 3rd party site # -# TODO: need to queue up a task to look up IDs if we have to fallback to +# TODO: need to queue up a task to look up IDs if we have to fallback to # routing based on ISBN or search def work_librarything(request, work_id): @@ -2687,10 +2709,10 @@ def work_openlibrary(request, work_id): # as long as there were some matches get the first one and route to it if len(j.keys()) > 0: first = j.keys()[0] - url = "http://openlibrary.org" + j[first]['key'] + url = "http://openlibrary.org" + j[first]['key'] except ValueError: # fail at openlibrary - logger.warning("failed to get OpenLibrary json at %s" % u) + logger.warning("failed to get OpenLibrary json at %s" % u) # fall back to doing a search on openlibrary if not url: q = urlencode({'q': work.title + " " + work.author()}) @@ -2712,11 +2734,11 @@ def work_goodreads(request, work_id): @login_required def emailshare(request, action): if request.method == 'POST': - form=EmailShareForm(request.POST) + form = EmailShareForm(request.POST) if form.is_valid(): subject = form.cleaned_data['subject'] message = form.cleaned_data['message'] - sender = '%s via Unglue.it <%s>'%(request.user.username, request.user.email) + sender = '%s via Unglue.it <%s>' % (request.user.username, request.user.email) recipient = form.cleaned_data['recipient'] send_mail_task.delay(subject, message, sender, [recipient]) try: @@ -2725,11 +2747,11 @@ def emailshare(request, action): # if we totally failed to have a next value, we should still redirect somewhere useful next = 'https://unglue.it' return HttpResponseRedirect(next) - - else: + + else: work = None status = None - + try: next = request.GET['next'] work_id = next.split('/')[-2] @@ -2739,11 +2761,11 @@ def emailshare(request, action): status = work.last_campaign().status except: pass - context = {'request':request,'work':work,'site': Site.objects.get_current(), 'action': action} + context = {'request':request, 'work':work, 'site': Site.objects.get_current(), 'action': action} if work and action : message = render_to_string('emails/i_just_pledged.txt', context) subject = "Help me unglue "+work.title - else: + else: # customize the call to action depending on campaign status if status == 'ACTIVE': message = render_to_string('emails/pledge_this.txt', context) @@ -2758,42 +2780,42 @@ def emailshare(request, action): form = EmailShareForm(initial={ 'next':next, 'subject': subject, 'message': message}) - return render(request, "emailshare.html", {'form':form}) - + return render(request, "emailshare.html", {'form':form}) + def ask_rh(request, campaign_id): campaign = get_object_or_404(models.Campaign, id=campaign_id) - return feedback(request, recipient=campaign.email, template="ask_rh.html", - message_template="ask_rh.txt", + return feedback(request, recipient=campaign.email, template="ask_rh.html", + message_template="ask_rh.txt", redirect_url = reverse('work', args=[campaign.work.id]), - extra_context={'campaign':campaign, 'subject':campaign }) - + extra_context={'campaign':campaign, 'subject':campaign }) + def feedback(request, recipient='support@gluejar.com', template='feedback.html', message_template='feedback.txt', extra_context=None, redirect_url=None): context = extra_context or {} - context['num1'] = randint(0,10) - context['num2'] = randint(0,10) + context['num1'] = randint(0, 10) + context['num2'] = randint(0, 10) context['answer'] = context['num1'] + context['num2'] - + if request.method == 'POST': - form=FeedbackForm(request.POST) + form = FeedbackForm(request.POST) if form.is_valid(): context.update(form.cleaned_data) - context['request']=request + context['request'] = request if extra_context: context.update(extra_context) - message = render_to_string(message_template,context) + message = render_to_string(message_template, context) send_mail_task.delay(context['subject'], message, context['sender'], [recipient]) if redirect_url: return HttpResponseRedirect(redirect_url) else: - return render(request, "thanks.html", context) - + return render(request, "thanks.html", context) + else: context['num1'] = request.POST['num1'] context['num2'] = request.POST['num2'] - + else: if request.user.is_authenticated(): - context['sender']=request.user.email; + context['sender'] = request.user.email try: context['page'] = request.GET['page'] except: @@ -2802,8 +2824,8 @@ def feedback(request, recipient='support@gluejar.com', template='feedback.html', context['subject'] = "Feedback on page "+context['page'] form = FeedbackForm(initial=context) context['form'] = form - return render(request, template, context) - + return render(request, template, context) + def comment(request): latest_comments = Comment.objects.all().order_by('-submit_date')[:20] return render(request, "comments.html", {'latest_comments': latest_comments}) @@ -2827,13 +2849,13 @@ def lockss(request, work_id): except: ebooks = None authors = work.authors() - + return render(request, "lockss.html", {'work':work, 'ebooks':ebooks, 'authors':authors}) - + def lockss_manifest(request, year): """ manifest pages for lockss harvester -- yearly indices - (lockss needs pages listing all books unglued by year, with + (lockss needs pages listing all books unglued by year, with programmatically determinable URLs) """ year = int(year) @@ -2843,12 +2865,12 @@ def lockss_manifest(request, year): ebooks = models.Edition.objects.filter(unglued=True).filter(created__range=(start_date, end_date)) except: ebooks = None - + return render(request, "lockss_manifest.html", {'ebooks':ebooks, 'year': year}) class DownloadView(PurchaseView): - template_name="download.html" - form_class = CampaignThanksForm + template_name = "download.html" + form_class = CampaignThanksForm def show_beg(self): if not self.campaign or self.campaign.type != THANKS: return False @@ -2858,18 +2880,18 @@ class DownloadView(PurchaseView): return False elif self.campaign.status != 'ACTIVE': return self.request.GET.has_key('testmode') or self.request.POST.has_key('testmode') - else: + else: return True - + def form_valid(self, form): p = PaymentManager() - t, url = p.process_transaction('USD', form.cleaned_data["preapproval_amount"], - host = PAYMENT_HOST_NONE, - campaign=self.campaign, - user=self.request.user, - paymentReason="Unglue.it Contribution for {0}".format(self.campaign.name), - pledge_extra=form.trans_extra, - ) + t, url = p.process_transaction('USD', form.cleaned_data["preapproval_amount"], + host = PAYMENT_HOST_NONE, + campaign = self.campaign, + user = self.request.user, + paymentReason="Unglue.it Contribution for {0}".format(self.campaign.name), + pledge_extra = form.trans_extra, + ) if url: return HttpResponseRedirect(url) else: @@ -2879,7 +2901,7 @@ class DownloadView(PurchaseView): def get_form_kwargs(self): if self.kwargs.has_key('work'): self.work = self.kwargs["work"] - self.show_beg= lambda: False + self.show_beg = lambda: False else: self.work = safe_get_work(self.kwargs["work_id"]) self.campaign = self.work.last_campaign() @@ -2900,23 +2922,23 @@ class DownloadView(PurchaseView): def get_context_data(self, **kwargs): context = super(FormView, self).get_context_data(**kwargs) # adapt funtion view to class view - work = self.work + work = self.work request = self.request site = Site.objects.get_current() unglued_ebooks = work.ebooks().filter(edition__unglued=True) other_ebooks = work.ebooks().filter(edition__unglued=False) xfer_url = kindle_url = None - acq=None + acq = None formats = {} # a dict of format name and url for ebook in work.ebooks().all(): - formats[ebook.format] = reverse('download_ebook', args=[ebook.id] ) - - if request.user.is_authenticated(): + formats[ebook.format] = reverse('download_ebook', args=[ebook.id]) + + if request.user.is_authenticated(): #add a fave request.user.wishlist.add_work(work,'download') - - all_acqs=request.user.acqs.filter(work=work).order_by('-created') + + all_acqs = request.user.acqs.filter(work=work).order_by('-created') for an_acq in all_acqs: if not an_acq.expired: # skip for THANKS @@ -2928,20 +2950,20 @@ class DownloadView(PurchaseView): if not an_acq.on_reserve: watermark_acq.delay(an_acq) acq = an_acq - formats['epub']= reverse('download_acq', kwargs={'nonce':acq.nonce, 'format':'epub'}) - formats['mobi']= reverse('download_acq', kwargs={'nonce':acq.nonce, 'format':'mobi'}) + formats['epub'] = reverse('download_acq', kwargs={'nonce':acq.nonce, 'format':'epub'}) + formats['mobi'] = reverse('download_acq', kwargs={'nonce':acq.nonce, 'format':'mobi'}) xfer_url = settings.BASE_URL_SECURE + formats['epub'] kindle_url = settings.BASE_URL_SECURE + formats['mobi'] can_kindle = True break - - + + if not acq: # google ebooks have a captcha which breaks some of our services non_google_ebooks = work.ebooks().exclude(provider='Google Books') - - #send to kindle - + + #send to kindle + try: kindle_ebook = non_google_ebooks.filter(format='mobi')[0] can_kindle = kindle_ebook.kindle_sendable() @@ -2954,10 +2976,10 @@ class DownloadView(PurchaseView): # configure the xfer url try: xfer_epub_ebook = non_google_ebooks.filter(format='epub')[0] - xfer_url = settings.BASE_URL_SECURE + reverse('download_ebook',args=[xfer_epub_ebook.id]) + xfer_url = settings.BASE_URL_SECURE + reverse('download_ebook', args=[xfer_epub_ebook.id]) except: xfer_url = None - agent = request.META.get('HTTP_USER_AGENT','') + agent = request.META.get('HTTP_USER_AGENT','') iOS = 'iPad' in agent or 'iPhone' in agent or 'iPod' in agent iOS_app = iOS and not 'Safari' in agent android = 'Android' in agent @@ -2980,7 +3002,7 @@ class DownloadView(PurchaseView): 'acq':acq, 'show_beg': self.show_beg, 'preapproval_amount': self.get_preapproval_amount(), - 'work': work, + 'work': work, 'site': site, 'action': "Contribution", 'user_license': self.user_license, @@ -3001,7 +3023,7 @@ def feature(request, work_id): if work.is_free: work.featured = now() work.save() - return HttpResponseRedirect(reverse('landing', args=[] )) + return HttpResponseRedirect(reverse('landing', args=[])) else: return HttpResponse('can\'t feature an work without an ebook') @@ -3019,7 +3041,7 @@ def borrow(request, work_id): if libuser: acq = work.get_user_license(libuser).borrowable_acq if not libuser or not acq: - acq=work.get_lib_license(request.user).borrowable_acq + acq = work.get_lib_license(request.user).borrowable_acq if acq: borrowed = acq.borrow(request.user) return DownloadView.as_view()(request, work=work) @@ -3039,12 +3061,12 @@ def reserve(request, work_id): library = work.get_lib_license(request.user).next_acq.library except: library = None - - models.Hold.objects.get_or_create(library=library,work=work,user=request.user) - return PurchaseView.as_view()(request,work_id=work_id) - + + models.Hold.objects.get_or_create(library=library, work=work, user=request.user) + return PurchaseView.as_view()(request, work_id=work_id) + def download_ebook(request, ebook_id): - ebook = get_object_or_404(models.Ebook,id=ebook_id) + ebook = get_object_or_404(models.Ebook, id=ebook_id) ebook.increment() logger.info("ebook: {0}, user_ip: {1}".format(ebook_id, request.META['REMOTE_ADDR'])) return HttpResponseRedirect(ebook.url) @@ -3057,15 +3079,15 @@ def download_purchased(request, work_id): def download_campaign(request, work_id, format): work = safe_get_work(work_id) - # Raise 404 unless there is a SUCCESSFUL BUY2UNGLUE campaign associated with work + # Raise 404 unless there is a SUCCESSFUL BUY2UNGLUE campaign associated with work try: campaign = work.campaigns.get(status='SUCCESSFUL', type=BUY2UNGLUE) except Campaign.DoesNotExist as e: raise Http404 - ebfs= models.EbookFile.objects.filter(edition__work=campaign.work, format=format).exclude(file='').order_by('-created') + ebfs = models.EbookFile.objects.filter(edition__work=campaign.work, format=format).exclude(file='').order_by('-created') logger.info(ebfs.count()) - # return the link to the most recently created EbookFile (if any) with specified format for the campaign + # return the link to the most recently created EbookFile (if any) with specified format for the campaign for ebf in ebfs: logger.info(ebf.file.url) return HttpResponseRedirect(ebf.file.url) @@ -3074,14 +3096,14 @@ def download_campaign(request, work_id, format): raise Http404 def download_acq(request, nonce, format): - acq = get_object_or_404(models.Acq,nonce=nonce) + acq = get_object_or_404(models.Acq, nonce=nonce) if acq.on_reserve: acq.borrow() if format == 'epub': - return HttpResponseRedirect( acq.get_epub_url() ) + return HttpResponseRedirect(acq.get_epub_url()) else: - return HttpResponseRedirect( acq.get_mobi_url() ) - + return HttpResponseRedirect(acq.get_mobi_url()) + def about(request, facet): template = "about_" + facet + ".html" try: @@ -3093,7 +3115,7 @@ def receive_gift(request, nonce): try: gift = models.Gift.objects.get(acq__nonce=nonce) except models.Gift.DoesNotExist: - return render(request, 'gift_error.html', ) + return render(request, 'gift_error.html',) context = {'gift': gift, "site": Site.objects.get_current() } work = gift.acq.work context['work'] = work @@ -3103,8 +3125,8 @@ def receive_gift(request, nonce): if request.user.is_authenticated(): #check that user hasn't redeemed the gift themselves if (gift.acq.user.id == request.user.id) and not gift.acq.expired: - return HttpResponseRedirect( reverse('display_gift', args=[gift.id,'existing'] )) - return render(request, 'gift_error.html', context ) + return HttpResponseRedirect(reverse('display_gift', args=[gift.id,'existing'])) + return render(request, 'gift_error.html', context) if request.user.is_authenticated(): user_license = work.get_user_license(request.user) if user_license and user_license.purchased: @@ -3112,7 +3134,7 @@ def receive_gift(request, nonce): if user_license.is_duplicate or request.user.id == gift.giver.id: # regift if request.method == 'POST': - form=RegiftForm( data=request.POST) + form = RegiftForm(data=request.POST) if form.is_valid(): giftee = models.Gift.giftee(form.cleaned_data['give_to'], request.user.username) new_acq = models.Acq.objects.create(user=giftee, work=gift.acq.work, license= gift.acq.license) @@ -3122,20 +3144,20 @@ def receive_gift(request, nonce): gift.use() notification.send([giftee], "purchase_gift", context, True) return render(request, 'gift_duplicate.html', context) - context['form']= RegiftForm() + context['form'] = RegiftForm() return render(request, 'gift_duplicate.html', context) else: # new book! - gift.use() + gift.use() request.user.wishlist.add_work(gift.acq.work, 'gift') - return HttpResponseRedirect( reverse('display_gift', args=[gift.id,'existing'] )) + return HttpResponseRedirect(reverse('display_gift', args=[gift.id,'existing'])) else: # we'll just leave the old user inactive. gift.acq.user = request.user gift.acq.save() - gift.use() + gift.use() request.user.wishlist.add_work(gift.acq.work, 'gift') - return HttpResponseRedirect( reverse('display_gift', args=[gift.id,'existing'] )) + return HttpResponseRedirect(reverse('display_gift', args=[gift.id,'existing'])) if (gift.acq.created - gift.acq.user.date_joined) > timedelta(minutes=1) or gift.used: # giftee is established user (or gift has been used), ask them to log in return superlogin(request, extra_context=context, template_name='gift_login.html') @@ -3144,21 +3166,21 @@ def receive_gift(request, nonce): gift.use() gift.acq.user.wishlist.add_work(gift.acq.work, 'gift') login_user(request, gift.acq.user) - - return HttpResponseRedirect( reverse('display_gift', args=[gift.id, 'newuser'] )) -@login_required -def display_gift(request, gift_id, message): + return HttpResponseRedirect(reverse('display_gift', args=[gift.id, 'newuser'])) + +@login_required +def display_gift(request, gift_id, message): try: gift = models.Gift.objects.get(id=gift_id) except models.Gift.DoesNotExist: - return render(request, 'gift_error.html', ) + return render(request, 'gift_error.html',) if request.user.id != gift.acq.user.id : return HttpResponse("this is not your gift") redeemed_gift = request.session.get('gift_nonce', None) == gift.acq.nonce context = {'gift': gift, 'work': gift.acq.work , 'message':message } if request.method == 'POST' and redeemed_gift: - form=UserNamePass(data=request.POST) + form = UserNamePass(data=request.POST) form.oldusername = request.user.username context['form'] = form if form.is_valid(): @@ -3170,28 +3192,32 @@ def display_gift(request, gift_id, message): return render(request, 'gift_welcome.html', context) else: if redeemed_gift: - form = UserNamePass(initial={'username':request.user.username}) + form = UserNamePass(initial={'username':request.user.username}) form.oldusername = request.user.username context['form'] = form return render(request, 'gift_welcome.html', context) - -@login_required -@csrf_exempt + +@login_required +@csrf_exempt def ml_status(request): return render(request, "ml_status.html") @require_POST -@login_required +@login_required def ml_subscribe(request): - request.user.profile.ml_subscribe(double_optin=False,send_welcome=True, merge_vars = {"OPTIN_IP":request.META['REMOTE_ADDR'],"OPTIN_TIME":now().isoformat()}) + request.user.profile.ml_subscribe( + double_optin=False, + send_welcome=True, + merge_vars = {"OPTIN_IP":request.META['REMOTE_ADDR'], "OPTIN_TIME":now().isoformat()} + ) return HttpResponseRedirect(reverse("notification_notice_settings")) @require_POST -@login_required +@login_required def ml_unsubscribe(request): request.user.profile.ml_unsubscribe() return HttpResponseRedirect(reverse("notification_notice_settings")) - + def press(request): latest_items = models.Press.objects.order_by('-date')[:3] highlighted_items = models.Press.objects.filter(highlight=True).order_by('-date') @@ -3201,7 +3227,7 @@ def press(request): 'highlighted_items': highlighted_items, 'all_items': all_items }) - + def press_submitterator(request): if not request.user.is_staff: return render(request, "admins_only.html") @@ -3214,7 +3240,7 @@ def press_submitterator(request): title = form.cleaned_data['title'] else: form = PressForm() - + return render(request, 'press_submitterator.html', { 'form':form, 'title':title @@ -3234,10 +3260,10 @@ def kindle_config(request, work_id=None): request.user.profile.save() template = "kindle_change_successful.html" else: - form = KindleEmailForm() + form = KindleEmailForm() return render(request, template, { - 'form': form, - 'work': work, + 'form': form, + 'work': work, 'ok_email': request.user.profile.kindle_email and ('kindle' in request.user.profile.kindle_email), }) @@ -3249,15 +3275,15 @@ def send_to_kindle(request, work_id, javascript='0'): def local_response(request, javascript, context, message): context['message'] = message if javascript == '1': - return render(request,'kindle_response_message.html',context ) + return render(request, 'kindle_response_message.html', context) else: return render(request, 'kindle_response_graceful_degradation.html', context) - - work=safe_get_work(work_id) - context= {'work':work} + + work = safe_get_work(work_id) + context = {'work':work} acq = None - if request.user.is_authenticated(): - all_acqs=request.user.acqs.filter(work=work).order_by('-created') + if request.user.is_authenticated(): + all_acqs = request.user.acqs.filter(work=work).order_by('-created') for an_acq in all_acqs: if not an_acq.expired: # skip for THANKS @@ -3270,7 +3296,7 @@ def send_to_kindle(request, work_id, javascript='0'): watermark_acq.delay(an_acq) acq = an_acq break - + if acq: ebook = acq.ebook() title = acq.work.kindle_safe_title() @@ -3298,10 +3324,10 @@ def send_to_kindle(request, work_id, javascript='0'): return local_response(request, javascript, context, 3) request.session['kindle_email'] = kindle_email elif request.user.is_authenticated(): - kindle_email = request.user.profile.kindle_email + kindle_email = request.user.profile.kindle_email context['kindle_email'] = kindle_email - + """ Amazon SES has a 10 MB size limit (http://aws.amazon.com/ses/faqs/#49) in messages sent to determine whether the file will meet this limit, we probably need to compare the @@ -3325,7 +3351,7 @@ def send_to_kindle(request, work_id, javascript='0'): if ebook.filesize > models.send_to_kindle_limit: logger.info('ebook %s is too large to be emailed' % work.id) return local_response(request, javascript, context, 0) - + try: email = EmailMessage(from_email='notices@gluejar.com', to=[kindle_email]) @@ -3338,29 +3364,29 @@ def send_to_kindle(request, work_id, javascript='0'): if request.POST.has_key('kindle_email') and not request.user.is_authenticated(): return HttpResponseRedirect(reverse('superlogin')) return local_response(request, javascript, context, 2) - - + + def userlist_marc(request, userlist=None): if userlist: - user = get_object_or_404(User,username=userlist) + user = get_object_or_404(User, username=userlist) return qs_marc_records(request, qs=user.wishlist.works.all()) else: return qs_marc_records(request, qs=request.user.wishlist.works.all()) - return render( request,'marc.html',{'userlist' : [] }) + return render(request, 'marc.html', {'userlist' : [] }) def work_marc(request, work_id): work = safe_get_work(work_id) return qs_marc_records(request, qs=[ work ]) - + class LibModeView(FormView): template_name = 'marc_config.html' form_class = LibModeForm success_url = reverse_lazy('marc') - + def form_valid(self, form): - enable= form.data.has_key('enable') + enable = form.data.has_key('enable') if enable: try: libpref = self.request.user.libpref @@ -3373,11 +3399,11 @@ class LibModeView(FormView): self.request.user.libpref.delete() except: pass - messages.success(self.request,"Tools are disabled." ) + messages.success(self.request,"Tools are disabled.") if reverse('marc_config', args=[]) in self.request.META['HTTP_REFERER']: return HttpResponseRedirect(reverse('marc_config', args=[])) else: return super(LibModeView, self).form_valid(form) - - + + diff --git a/marc/load.py b/marc/load.py index 8ab5d05d..7482d364 100644 --- a/marc/load.py +++ b/marc/load.py @@ -89,6 +89,17 @@ def stub(edition): ) record.add_ordered_field(field245) + #edition statement + if edition.note: + field250 = pymarc.Field( + tag='250', + indicators = [' ', ' '], + subfields = [ + 'a', unicode(edition.note), + ] + ) + record.add_ordered_field(field250) + # publisher, date if edition.publisher: field260 = pymarc.Field( @@ -101,7 +112,7 @@ def stub(edition): if edition.publication_date: field260.add_subfield('c', unicode(edition.publication_date)) record.add_ordered_field(field260) - + if edition.description: #add 520 field (description) field520 = pymarc.Field( diff --git a/marc/models.py b/marc/models.py index d7402f26..65bf14a6 100644 --- a/marc/models.py +++ b/marc/models.py @@ -56,6 +56,7 @@ class AbstractEdition: publisher = '' title = '' publication_date = '' + note = '' # the edition should be able to report ebook downloads, with should have format and url attributes def downloads(self): diff --git a/static/css/campaign2.css b/static/css/campaign2.css index 16623ea5..dcdb33e2 100644 --- a/static/css/campaign2.css +++ b/static/css/campaign2.css @@ -1 +1 @@ -.header-text{display:block;text-decoration:none;font-weight:bold;letter-spacing:-0.05em}.panelborders{border-width:1px 0;border-style:solid none;border-color:#fff}.roundedspan{border:1px solid #d4d4d4;-moz-border-radius:7px;-webkit-border-radius:7px;border-radius:7px;padding:1px;color:#fff;margin:0 8px 0 0;display:inline-block}.roundedspan>span{padding:7px 7px;min-width:15px;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;text-align:center;display:inline-block}.roundedspan>span .hovertext{display:none}.roundedspan>span:hover .hovertext{display:inline}.mediaborder{padding:5px;border:solid 5px #edf3f4}.actionbuttons{width:auto;height:36px;line-height:36px;background:#8dc63f;border:1px solid transparent;color:white;cursor:pointer;font-size:13px;font-weight:normal;padding:0 15px;margin:5px 0}.errors{-moz-border-radius:16px;-webkit-border-radius:16px;border-radius:16px;border:solid #e35351 3px;clear:both;width:90%;height:auto;line-height:16px;padding:7px 0;font-weight:bold;font-size:13px;text-align:center}.errors li{list-style:none;border:0}#tabs{border-bottom:4px solid #6994a3;clear:both;float:left;margin-top:10px;width:100%}#tabs ul.book-list-view{margin-bottom:4px!important}#tabs-1,#tabs-2,#tabs-3,#tabs-4{display:none}#tabs-1.active,#tabs-2.active,#tabs-3.active,#tabs-4.active{display:inherit}#tabs-2 textarea{width:95%}ul.tabs{float:left;padding:0;margin:0;list-style:none;width:100%}ul.tabs li{float:left;height:46px;line-height:20px;padding-right:2px;width:116px;background:0;margin:0;padding:0 2px 0 0}ul.tabs li.tabs4{padding-right:0}ul.tabs li a{height:41px;line-height:18px;display:block;text-align:center;padding:0 10px;min-width:80px;-moz-border-radius:7px 7px 0 0;-webkit-border-radius:7px 7px 0 0;border-radius:7px 7px 0 0;background:#d6dde0;color:#3d4e53;padding-top:5px}ul.tabs li a:hover{text-decoration:none}ul.tabs li a div{padding-top:8px}ul.tabs li a:hover,ul.tabs li.active a{background:#6994a3;color:#fff}.header-text{display:block;text-decoration:none;font-weight:bold;letter-spacing:-0.05em}.panelborders{border-width:1px 0;border-style:solid none;border-color:#fff}.roundedspan{border:1px solid #d4d4d4;-moz-border-radius:7px;-webkit-border-radius:7px;border-radius:7px;padding:1px;color:#fff;margin:0 8px 0 0;display:inline-block}.roundedspan>span{padding:7px 7px;min-width:15px;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;text-align:center;display:inline-block}.roundedspan>span .hovertext{display:none}.roundedspan>span:hover .hovertext{display:inline}.mediaborder{padding:5px;border:solid 5px #edf3f4}.actionbuttons{width:auto;height:36px;line-height:36px;background:#8dc63f;border:1px solid transparent;color:white;cursor:pointer;font-size:13px;font-weight:normal;padding:0 15px;margin:5px 0}.errors{-moz-border-radius:16px;-webkit-border-radius:16px;border-radius:16px;border:solid #e35351 3px;clear:both;width:90%;height:auto;line-height:16px;padding:7px 0;font-weight:bold;font-size:13px;text-align:center}.errors li{list-style:none;border:0}.book-detail{float:left;width:100%;clear:both;display:block}#book-detail-img{float:left;margin-right:10px;width:151px}#book-detail-img img{padding:5px;border:solid 5px #edf3f4}.book-detail-info{float:left;width:309px}.book-detail-info h2.book-name,.book-detail-info h3.book-author,.book-detail-info h3.book-year{padding:0;margin:0;line-height:normal}.book-detail-info h2.book-name{font-size:19px;font-weight:bold;color:#3d4e53}.book-detail-info h3.book-author,.book-detail-info h3.book-year{font-size:13px;font-weight:normal;color:#3d4e53}.book-detail-info h3.book-author span a,.book-detail-info h3.book-year span a{font-size:13px;font-weight:normal;color:#6994a3}.book-detail-info>div{width:100%;clear:both;display:block;overflow:hidden;border-top:1px solid #edf3f4;padding:10px 0}.book-detail-info>div.layout{border:0;padding:0}.book-detail-info>div.layout div.pubinfo{float:left;width:auto;padding-bottom:7px}.book-detail-info .btn_wishlist span{text-align:right}.book-detail-info .find-book label{float:left;line-height:31px}.book-detail-info .find-link{float:right}.book-detail-info .find-link img{padding:2px;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px}.book-detail-info .pledged-info{padding:10px 0;position:relative}.book-detail-info .pledged-info.noborder{border-top:0;padding-top:0}.book-detail-info .pledged-info .campaign-status-info{float:left;width:50%;margin-top:13px}.book-detail-info .pledged-info .campaign-status-info span{font-size:15px;color:#6994a3;font-weight:bold}.book-detail-info .thermometer{-moz-border-radius:10px;-webkit-border-radius:10px;border-radius:10px;border:solid 2px #d6dde0;width:291px;padding:7px;position:relative;overflow:visible;background:-webkit-gradient(linear,left top,right top,from(#8dc63f),to(#cf6944));background:-webkit-linear-gradient(left,#cf6944,#8dc63f);background:-moz-linear-gradient(left,#cf6944,#8dc63f);background:-ms-linear-gradient(left,#cf6944,#8dc63f);background:-o-linear-gradient(left,#cf6944,#8dc63f);background:linear-gradient(left,#cf6944,#8dc63f);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='@alert',endColorstr='@call-to-action');-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr='@alert', endColorstr='@call-to-action')"}.book-detail-info .thermometer.successful{border-color:#8ac3d7;background:#edf3f4}.book-detail-info .thermometer .cover{position:absolute;right:0;-moz-border-radius:0 10px 10px 0;-webkit-border-radius:0 10px 10px 0;border-radius:0 10px 10px 0;width:50px;height:14px;margin-top:-7px;background:#f3f5f6}.book-detail-info .thermometer span{display:none}.book-detail-info .thermometer:hover span{display:block;position:absolute;z-index:200;right:0;top:-7px;font-size:19px;color:#6994a3;background:white;border:2px solid #d6dde0;-moz-border-radius:10px;-webkit-border-radius:10px;border-radius:10px;padding:5px}.book-detail-info .explainer span.explanation{display:none}.book-detail-info .explainer:hover span.explanation{display:block;position:absolute;z-index:200;right:0;top:12px;font-size:13px;font-weight:normal;color:#3d4e53;background:white;border:2px solid #d6dde0;-moz-border-radius:10px;-webkit-border-radius:10px;border-radius:10px;padding:5px}.book-detail-info .status{position:absolute;top:50%;right:0;height:25px;margin-top:-12px}.header-text{display:block;text-decoration:none;font-weight:bold;letter-spacing:-0.05em}.panelborders{border-width:1px 0;border-style:solid none;border-color:#fff}.roundedspan{border:1px solid #d4d4d4;-moz-border-radius:7px;-webkit-border-radius:7px;border-radius:7px;padding:1px;color:#fff;margin:0 8px 0 0;display:inline-block}.roundedspan>span{padding:7px 7px;min-width:15px;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;text-align:center;display:inline-block}.roundedspan>span .hovertext{display:none}.roundedspan>span:hover .hovertext{display:inline}.mediaborder{padding:5px;border:solid 5px #edf3f4}.actionbuttons{width:auto;height:36px;line-height:36px;background:#8dc63f;border:1px solid transparent;color:white;cursor:pointer;font-size:13px;font-weight:normal;padding:0 15px;margin:5px 0}.errors{-moz-border-radius:16px;-webkit-border-radius:16px;border-radius:16px;border:solid #e35351 3px;clear:both;width:90%;height:auto;line-height:16px;padding:7px 0;font-weight:bold;font-size:13px;text-align:center}.errors li{list-style:none;border:0}ul.social a:hover{text-decoration:none}ul.social li{padding:5px 0 5px 30px!important;height:28px;line-height:28px!important;margin:0!important;-moz-border-radius:0;-webkit-border-radius:0;border-radius:0}ul.social li.facebook{background:url("/static/images/icons/facebook.png") 10px center no-repeat;cursor:pointer}ul.social li.facebook span{padding-left:10px}ul.social li.facebook:hover{background:#8dc63f url("/static/images/icons/facebook-hover.png") 10px center no-repeat}ul.social li.facebook:hover span{color:#fff}ul.social li.twitter{background:url("/static/images/icons/twitter.png") 10px center no-repeat;cursor:pointer}ul.social li.twitter span{padding-left:10px}ul.social li.twitter:hover{background:#8dc63f url("/static/images/icons/twitter-hover.png") 10px center no-repeat}ul.social li.twitter:hover span{color:#fff}ul.social li.email{background:url("/static/images/icons/email.png") 10px center no-repeat;cursor:pointer}ul.social li.email span{padding-left:10px}ul.social li.email:hover{background:#8dc63f url("/static/images/icons/email-hover.png") 10px center no-repeat}ul.social li.email:hover span{color:#fff}ul.social li.embed{background:url("/static/images/icons/embed.png") 10px center no-repeat;cursor:pointer}ul.social li.embed span{padding-left:10px}ul.social li.embed:hover{background:#8dc63f url("/static/images/icons/embed-hover.png") 10px center no-repeat}ul.social li.embed:hover span{color:#fff}#js-page-wrap{overflow:hidden}#main-container{margin-top:20px}#js-leftcol .jsmodule,.pledge.jsmodule{margin-bottom:10px}#js-leftcol .jsmodule.rounded .jsmod-content,.pledge.jsmodule.rounded .jsmod-content{-moz-border-radius:20px;-webkit-border-radius:20px;border-radius:20px;background:#edf3f4;color:#3d4e53;padding:10px 20px;font-weight:bold;border:0;margin:0;line-height:16px}#js-leftcol .jsmodule.rounded .jsmod-content.ACTIVE,.pledge.jsmodule.rounded .jsmod-content.ACTIVE{background:#8dc63f;color:white;font-size:19px;font-weight:normal;line-height:20px}#js-leftcol .jsmodule.rounded .jsmod-content.No.campaign.yet,.pledge.jsmodule.rounded .jsmod-content.No.campaign.yet{background:#e18551;color:white}#js-leftcol .jsmodule.rounded .jsmod-content span,.pledge.jsmodule.rounded .jsmod-content span{display:inline-block;vertical-align:middle}#js-leftcol .jsmodule.rounded .jsmod-content span.spacer,.pledge.jsmodule.rounded .jsmod-content span.spacer{visibility:none}#js-leftcol .jsmodule.rounded .jsmod-content span.findtheungluers,.pledge.jsmodule.rounded .jsmod-content span.findtheungluers{cursor:pointer}.jsmodule.pledge{float:left;margin-left:10px}#js-slide .jsmodule{width:660px!important}a{color:#3d4e53}#js-search{margin:0 15px 0 15px!important}.alert>.errorlist{list-style-type:none;font-size:15px;border:0;text-align:left;font-weight:normal;font-size:13px}.alert>.errorlist>li{margin-bottom:14px}.alert>.errorlist .errorlist{margin-top:7px}.alert>.errorlist .errorlist li{width:auto;height:auto;padding-left:32px;padding-right:32px;font-size:13px}#js-maincol{float:left;width:470px;margin:0 10px}#js-maincol div#content-block{background:0;padding:0}.status{font-size:19px;color:#8dc63f}.add-wishlist,.add-wishlist-workpage,.remove-wishlist-workpage,.remove-wishlist,.on-wishlist,.create-account{float:right;cursor:pointer}.add-wishlist span,.add-wishlist-workpage span,.remove-wishlist-workpage span,.remove-wishlist span,.on-wishlist span,.create-account span{font-weight:normal;color:#3d4e53;text-transform:none;padding-left:20px}.add-wishlist span.on-wishlist,.add-wishlist-workpage span.on-wishlist,.remove-wishlist-workpage span.on-wishlist,.remove-wishlist span.on-wishlist,.on-wishlist span.on-wishlist,.create-account span.on-wishlist{background:url("/static/images/checkmark_small.png") left center no-repeat;cursor:default}.btn_wishlist .add-wishlist span,.add-wishlist-workpage span,.create-account span{background:url("/static/images/booklist/add-wishlist.png") left center no-repeat}.remove-wishlist-workpage span,.remove-wishlist span{background:url("/static/images/booklist/remove-wishlist-blue.png") left center no-repeat}div#content-block-content{padding-left:5px}div#content-block-content a{color:#6994a3}div#content-block-content #tabs-1 img{padding:5px;border:solid 5px #edf3f4}div#content-block-content #tabs-3{margin-left:-5px}.tabs-content{padding-right:5px}.tabs-content iframe{padding:5px;border:solid 5px #edf3f4}.tabs-content form{margin-left:-5px}.tabs-content .clearfix{margin-bottom:10px;border-bottom:2px solid #d6dde0}.work_supporter{height:auto;min-height:50px;margin-top:5px;vertical-align:middle}.work_supporter_avatar{float:left;margin-right:5px}.work_supporter_avatar img{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px}.work_supporter_name{height:50px;line-height:50px;float:left}.work_supporter_nocomment{height:50px;margin-top:5px;vertical-align:middle;min-width:235px;float:left}.show_supporter_contact_form{display:block;margin-left:5px;float:right}.supporter_contact_form{display:none;margin-left:5px}.contact_form_result{display:block;margin-left:5px}.work_supporter_wide{display:block;height:65px;margin-top:5px;float:none;margin-left:5px}.info_for_managers{display:block}.show_supporter_contact_form{cursor:pointer;opacity:.5}.show_supporter_contact_form:hover{cursor:pointer;opacity:1}.official{border:3px #8ac3d7 solid;padding:3px;margin-left:-5px}.editions div{float:left;padding-bottom:5px;margin-bottom:5px}.editions .image{width:60px;overflow:hidden}.editions .metadata{display:block;overflow:hidden;margin-left:5px}.editions a:hover{text-decoration:underline}.show_more_edition,.show_more_ebooks{cursor:pointer}.show_more_edition{text-align:right}.show_more_edition:hover{text-decoration:underline}.more_edition{display:none;clear:both;padding-bottom:10px;padding-left:60px}.more_ebooks{display:none}.show_more_ebooks:hover{text-decoration:underline}#js-rightcol .add-wishlist,#js-rightcol .on-wishlist,#js-rightcol .create-account{float:none}#js-rightcol .on-wishlist{margin-left:20px}#js-rightcol,#pledge-rightcol{float:right;width:235px;margin-bottom:20px}#js-rightcol h3.jsmod-title,#pledge-rightcol h3.jsmod-title{background:#a7c1ca;-moz-border-radius:10px;-webkit-border-radius:10px;border-radius:10px;padding:10px;height:auto;font-style:normal;font-size:15px;margin:0 0 10px 0;color:white}#js-rightcol h3.jsmod-title span,#pledge-rightcol h3.jsmod-title span{padding:0;color:#fff;font-style:normal;height:22px;line-height:22px}#js-rightcol .jsmodule,#pledge-rightcol .jsmodule{margin-bottom:10px}#js-rightcol .jsmodule a:hover,#pledge-rightcol .jsmodule a:hover{text-decoration:none}#pledge-rightcol{margin-top:7px}.js-rightcol-pad{border:1px solid #d6dde0;-moz-border-radius:10px;-webkit-border-radius:10px;border-radius:10px;padding:10px}#widgetcode{display:none;border:1px solid #d6dde0;-moz-border-radius:10px;-webkit-border-radius:10px;border-radius:10px;padding:10px}ul.support li{border-bottom:1px solid #d6dde0;padding:10px 5px 10px 10px;background:url("/static/images/icons/pledgearrow.png") 98% center no-repeat}ul.support li.no_link{background:0}ul.support li.last{border-bottom:0}ul.support li span{display:block;padding-right:10px}ul.support li span.menu-item-price{font-size:19px;float:left;display:inline;margin-bottom:3px}ul.support li span.menu-item-desc{float:none;clear:both;font-size:15px;font-weight:normal;line-height:19.5px}ul.support li:hover{color:#fff;background:#8dc63f url("/static/images/icons/pledgearrow-hover.png") 98% center no-repeat}ul.support li:hover a{color:#fff;text-decoration:none}ul.support li:hover.no_link{background:#fff;color:#8dc63f}.you_pledged{float:left;line-height:21px;font-weight:normal;color:#3d4e53;padding-left:20px;background:url("/static/images/checkmark_small.png") left center no-repeat}.thank-you{font-size:19px;font-weight:bold;margin:20px auto}div#libtools{border:1px solid #d6dde0;-moz-border-radius:10px;-webkit-border-radius:10px;border-radius:10px;margin-left:0;margin-top:1em;padding:10px}div#libtools p{margin-top:0;margin-bottom:0}div#libtools span{margin-top:0;margin-left:.5em;display:inline-block}div#libtools input[type="submit"]{margin-left:4em} \ No newline at end of file +.header-text{display:block;text-decoration:none;font-weight:bold;letter-spacing:-0.05em}.panelborders{border-width:1px 0;border-style:solid none;border-color:#fff}.roundedspan{border:1px solid #d4d4d4;-moz-border-radius:7px;-webkit-border-radius:7px;border-radius:7px;padding:1px;color:#fff;margin:0 8px 0 0;display:inline-block}.roundedspan>span{padding:7px 7px;min-width:15px;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;text-align:center;display:inline-block}.roundedspan>span .hovertext{display:none}.roundedspan>span:hover .hovertext{display:inline}.mediaborder{padding:5px;border:solid 5px #edf3f4}.actionbuttons{width:auto;height:36px;line-height:36px;background:#8dc63f;border:1px solid transparent;color:white;cursor:pointer;font-size:13px;font-weight:normal;padding:0 15px;margin:5px 0}.errors{-moz-border-radius:16px;-webkit-border-radius:16px;border-radius:16px;border:solid #e35351 3px;clear:both;width:90%;height:auto;line-height:16px;padding:7px 0;font-weight:bold;font-size:13px;text-align:center}.errors li{list-style:none;border:0}#tabs{border-bottom:4px solid #6994a3;clear:both;float:left;margin-top:10px;width:100%}#tabs ul.book-list-view{margin-bottom:4px!important}#tabs-1,#tabs-2,#tabs-3,#tabs-4{display:none}#tabs-1.active,#tabs-2.active,#tabs-3.active,#tabs-4.active{display:inherit}#tabs-2 textarea{width:95%}ul.tabs{float:left;padding:0;margin:0;list-style:none;width:100%}ul.tabs li{float:left;height:46px;line-height:20px;padding-right:2px;width:116px;background:0;margin:0;padding:0 2px 0 0}ul.tabs li.tabs4{padding-right:0}ul.tabs li a{height:41px;line-height:18px;display:block;text-align:center;padding:0 10px;min-width:80px;-moz-border-radius:7px 7px 0 0;-webkit-border-radius:7px 7px 0 0;border-radius:7px 7px 0 0;background:#d6dde0;color:#3d4e53;padding-top:5px}ul.tabs li a:hover{text-decoration:none}ul.tabs li a div{padding-top:8px}ul.tabs li a:hover,ul.tabs li.active a{background:#6994a3;color:#fff}.header-text{display:block;text-decoration:none;font-weight:bold;letter-spacing:-0.05em}.panelborders{border-width:1px 0;border-style:solid none;border-color:#fff}.roundedspan{border:1px solid #d4d4d4;-moz-border-radius:7px;-webkit-border-radius:7px;border-radius:7px;padding:1px;color:#fff;margin:0 8px 0 0;display:inline-block}.roundedspan>span{padding:7px 7px;min-width:15px;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;text-align:center;display:inline-block}.roundedspan>span .hovertext{display:none}.roundedspan>span:hover .hovertext{display:inline}.mediaborder{padding:5px;border:solid 5px #edf3f4}.actionbuttons{width:auto;height:36px;line-height:36px;background:#8dc63f;border:1px solid transparent;color:white;cursor:pointer;font-size:13px;font-weight:normal;padding:0 15px;margin:5px 0}.errors{-moz-border-radius:16px;-webkit-border-radius:16px;border-radius:16px;border:solid #e35351 3px;clear:both;width:90%;height:auto;line-height:16px;padding:7px 0;font-weight:bold;font-size:13px;text-align:center}.errors li{list-style:none;border:0}.book-detail{float:left;width:100%;clear:both;display:block}#book-detail-img{float:left;margin-right:10px;width:151px}#book-detail-img img{padding:5px;border:solid 5px #edf3f4}.book-detail-info{float:left;width:309px}.book-detail-info h2.book-name,.book-detail-info h3.book-author,.book-detail-info h3.book-year{padding:0;margin:0;line-height:normal}.book-detail-info h2.book-name{font-size:19px;font-weight:bold;color:#3d4e53}.book-detail-info h3.book-author,.book-detail-info h3.book-year{font-size:13px;font-weight:normal;color:#3d4e53}.book-detail-info h3.book-author span a,.book-detail-info h3.book-year span a{font-size:13px;font-weight:normal;color:#6994a3}.book-detail-info>div{width:100%;clear:both;display:block;overflow:hidden;border-top:1px solid #edf3f4;padding:10px 0}.book-detail-info>div.layout{border:0;padding:0}.book-detail-info>div.layout div.pubinfo{float:left;width:auto;padding-bottom:7px}.book-detail-info .btn_wishlist span{text-align:right}.book-detail-info .find-book label{float:left;line-height:31px}.book-detail-info .find-link{float:right}.book-detail-info .find-link img{padding:2px;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px}.book-detail-info .pledged-info{padding:10px 0;position:relative}.book-detail-info .pledged-info.noborder{border-top:0;padding-top:0}.book-detail-info .pledged-info .campaign-status-info{float:left;width:50%;margin-top:13px}.book-detail-info .pledged-info .campaign-status-info span{font-size:15px;color:#6994a3;font-weight:bold}.book-detail-info .thermometer{-moz-border-radius:10px;-webkit-border-radius:10px;border-radius:10px;border:solid 2px #d6dde0;width:291px;padding:7px;position:relative;overflow:visible;background:-webkit-gradient(linear,left top,right top,from(#8dc63f),to(#cf6944));background:-webkit-linear-gradient(left,#cf6944,#8dc63f);background:-moz-linear-gradient(left,#cf6944,#8dc63f);background:-ms-linear-gradient(left,#cf6944,#8dc63f);background:-o-linear-gradient(left,#cf6944,#8dc63f);background:linear-gradient(left,#cf6944,#8dc63f);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='@alert',endColorstr='@call-to-action');-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr='@alert', endColorstr='@call-to-action')"}.book-detail-info .thermometer.successful{border-color:#8ac3d7;background:#edf3f4}.book-detail-info .thermometer .cover{position:absolute;right:0;-moz-border-radius:0 10px 10px 0;-webkit-border-radius:0 10px 10px 0;border-radius:0 10px 10px 0;width:50px;height:14px;margin-top:-7px;background:#f3f5f6}.book-detail-info .thermometer span{display:none}.book-detail-info .thermometer:hover span{display:block;position:absolute;z-index:200;right:0;top:-7px;font-size:19px;color:#6994a3;background:white;border:2px solid #d6dde0;-moz-border-radius:10px;-webkit-border-radius:10px;border-radius:10px;padding:5px}.book-detail-info .explainer span.explanation{display:none}.book-detail-info .explainer:hover span.explanation{display:block;position:absolute;z-index:200;right:0;top:12px;font-size:13px;font-weight:normal;color:#3d4e53;background:white;border:2px solid #d6dde0;-moz-border-radius:10px;-webkit-border-radius:10px;border-radius:10px;padding:5px}.book-detail-info .status{position:absolute;top:50%;right:0;height:25px;margin-top:-12px}.header-text{display:block;text-decoration:none;font-weight:bold;letter-spacing:-0.05em}.panelborders{border-width:1px 0;border-style:solid none;border-color:#fff}.roundedspan{border:1px solid #d4d4d4;-moz-border-radius:7px;-webkit-border-radius:7px;border-radius:7px;padding:1px;color:#fff;margin:0 8px 0 0;display:inline-block}.roundedspan>span{padding:7px 7px;min-width:15px;-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;text-align:center;display:inline-block}.roundedspan>span .hovertext{display:none}.roundedspan>span:hover .hovertext{display:inline}.mediaborder{padding:5px;border:solid 5px #edf3f4}.actionbuttons{width:auto;height:36px;line-height:36px;background:#8dc63f;border:1px solid transparent;color:white;cursor:pointer;font-size:13px;font-weight:normal;padding:0 15px;margin:5px 0}.errors{-moz-border-radius:16px;-webkit-border-radius:16px;border-radius:16px;border:solid #e35351 3px;clear:both;width:90%;height:auto;line-height:16px;padding:7px 0;font-weight:bold;font-size:13px;text-align:center}.errors li{list-style:none;border:0}ul.social a:hover{text-decoration:none}ul.social li{padding:5px 0 5px 30px!important;height:28px;line-height:28px!important;margin:0!important;-moz-border-radius:0;-webkit-border-radius:0;border-radius:0}ul.social li.facebook{background:url("/static/images/icons/facebook.png") 10px center no-repeat;cursor:pointer}ul.social li.facebook span{padding-left:10px}ul.social li.facebook:hover{background:#8dc63f url("/static/images/icons/facebook-hover.png") 10px center no-repeat}ul.social li.facebook:hover span{color:#fff}ul.social li.twitter{background:url("/static/images/icons/twitter.png") 10px center no-repeat;cursor:pointer}ul.social li.twitter span{padding-left:10px}ul.social li.twitter:hover{background:#8dc63f url("/static/images/icons/twitter-hover.png") 10px center no-repeat}ul.social li.twitter:hover span{color:#fff}ul.social li.email{background:url("/static/images/icons/email.png") 10px center no-repeat;cursor:pointer}ul.social li.email span{padding-left:10px}ul.social li.email:hover{background:#8dc63f url("/static/images/icons/email-hover.png") 10px center no-repeat}ul.social li.email:hover span{color:#fff}ul.social li.embed{background:url("/static/images/icons/embed.png") 10px center no-repeat;cursor:pointer}ul.social li.embed span{padding-left:10px}ul.social li.embed:hover{background:#8dc63f url("/static/images/icons/embed-hover.png") 10px center no-repeat}ul.social li.embed:hover span{color:#fff}#js-page-wrap{overflow:hidden}#main-container{margin-top:20px}#js-leftcol .jsmodule,.pledge.jsmodule{margin-bottom:10px}#js-leftcol .jsmodule.rounded .jsmod-content,.pledge.jsmodule.rounded .jsmod-content{-moz-border-radius:20px;-webkit-border-radius:20px;border-radius:20px;background:#edf3f4;color:#3d4e53;padding:10px 20px;font-weight:bold;border:0;margin:0;line-height:16px}#js-leftcol .jsmodule.rounded .jsmod-content.ACTIVE,.pledge.jsmodule.rounded .jsmod-content.ACTIVE{background:#8dc63f;color:white;font-size:19px;font-weight:normal;line-height:20px}#js-leftcol .jsmodule.rounded .jsmod-content.No.campaign.yet,.pledge.jsmodule.rounded .jsmod-content.No.campaign.yet{background:#e18551;color:white}#js-leftcol .jsmodule.rounded .jsmod-content span,.pledge.jsmodule.rounded .jsmod-content span{display:inline-block;vertical-align:middle}#js-leftcol .jsmodule.rounded .jsmod-content span.spacer,.pledge.jsmodule.rounded .jsmod-content span.spacer{visibility:none}#js-leftcol .jsmodule.rounded .jsmod-content span.findtheungluers,.pledge.jsmodule.rounded .jsmod-content span.findtheungluers{cursor:pointer}.jsmodule.pledge{float:left;margin-left:10px}#js-slide .jsmodule{width:660px!important}#js-search{margin:0 15px 0 15px!important}.alert>.errorlist{list-style-type:none;font-size:15px;border:0;text-align:left;font-weight:normal;font-size:13px}.alert>.errorlist>li{margin-bottom:14px}.alert>.errorlist .errorlist{margin-top:7px}.alert>.errorlist .errorlist li{width:auto;height:auto;padding-left:32px;padding-right:32px;font-size:13px}#js-maincol{float:left;width:470px;margin:0 10px}#js-maincol div#content-block{background:0;padding:0}.status{font-size:19px;color:#8dc63f}.add-wishlist,.add-wishlist-workpage,.remove-wishlist-workpage,.remove-wishlist,.on-wishlist,.create-account{float:right;cursor:pointer}.add-wishlist span,.add-wishlist-workpage span,.remove-wishlist-workpage span,.remove-wishlist span,.on-wishlist span,.create-account span{font-weight:normal;color:#3d4e53;text-transform:none;padding-left:20px}.add-wishlist span.on-wishlist,.add-wishlist-workpage span.on-wishlist,.remove-wishlist-workpage span.on-wishlist,.remove-wishlist span.on-wishlist,.on-wishlist span.on-wishlist,.create-account span.on-wishlist{background:url("/static/images/checkmark_small.png") left center no-repeat;cursor:default}.btn_wishlist .add-wishlist span,.add-wishlist-workpage span,.create-account span{background:url("/static/images/booklist/add-wishlist.png") left center no-repeat}.remove-wishlist-workpage span,.remove-wishlist span{background:url("/static/images/booklist/remove-wishlist-blue.png") left center no-repeat}div#content-block-content{padding-left:5px}div#content-block-content a{color:#6994a3}div#content-block-content #tabs-1 img{padding:5px;border:solid 5px #edf3f4}div#content-block-content #tabs-3{margin-left:-5px}.tabs-content{padding-right:5px}.tabs-content iframe{padding:5px;border:solid 5px #edf3f4}.tabs-content form{margin-left:-5px}.tabs-content .clearfix{margin-bottom:10px;border-bottom:2px solid #d6dde0}.work_supporter{height:auto;min-height:50px;margin-top:5px;vertical-align:middle}.work_supporter_avatar{float:left;margin-right:5px}.work_supporter_avatar img{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px}.work_supporter_name{height:50px;line-height:50px;float:left}.work_supporter_nocomment{height:50px;margin-top:5px;vertical-align:middle;min-width:235px;float:left}.show_supporter_contact_form{display:block;margin-left:5px;float:right}.supporter_contact_form{display:none;margin-left:5px}.contact_form_result{display:block;margin-left:5px}.work_supporter_wide{display:block;height:65px;margin-top:5px;float:none;margin-left:5px}.info_for_managers{display:block}.show_supporter_contact_form{cursor:pointer;opacity:.5}.show_supporter_contact_form:hover{cursor:pointer;opacity:1}.official{border:3px #8ac3d7 solid;padding:3px;margin-left:-5px}.editions div{float:left;padding-bottom:5px;margin-bottom:5px}.editions .image{width:60px;overflow:hidden}.editions .metadata{display:block;overflow:hidden;margin-left:5px}.editions a:hover{text-decoration:underline}.show_more_edition,.show_more_ebooks{cursor:pointer}.show_more_edition{text-align:right}.show_more_edition:hover{text-decoration:underline}.more_edition{display:none;clear:both;padding-bottom:10px;padding-left:60px}.more_ebooks{display:none}.show_more_ebooks:hover{text-decoration:underline}#js-rightcol .add-wishlist,#js-rightcol .on-wishlist,#js-rightcol .create-account{float:none}#js-rightcol .on-wishlist{margin-left:20px}#js-rightcol,#pledge-rightcol{float:right;width:235px;margin-bottom:20px}#js-rightcol h3.jsmod-title,#pledge-rightcol h3.jsmod-title{background:#a7c1ca;-moz-border-radius:10px;-webkit-border-radius:10px;border-radius:10px;padding:10px;height:auto;font-style:normal;font-size:15px;margin:0 0 10px 0;color:white}#js-rightcol h3.jsmod-title span,#pledge-rightcol h3.jsmod-title span{padding:0;color:#fff;font-style:normal;height:22px;line-height:22px}#js-rightcol .jsmodule,#pledge-rightcol .jsmodule{margin-bottom:10px}#js-rightcol .jsmodule a:hover,#pledge-rightcol .jsmodule a:hover{text-decoration:none}#pledge-rightcol{margin-top:7px}.js-rightcol-pad{border:1px solid #d6dde0;-moz-border-radius:10px;-webkit-border-radius:10px;border-radius:10px;padding:10px}#widgetcode{display:none;border:1px solid #d6dde0;-moz-border-radius:10px;-webkit-border-radius:10px;border-radius:10px;padding:10px}ul.support li{border-bottom:1px solid #d6dde0;padding:10px 5px 10px 10px;background:url("/static/images/icons/pledgearrow.png") 98% center no-repeat}ul.support li.no_link{background:0}ul.support li.last{border-bottom:0}ul.support li span{display:block;padding-right:10px}ul.support li span.menu-item-price{font-size:19px;float:left;display:inline;margin-bottom:3px}ul.support li span.menu-item-desc{float:none;clear:both;font-size:15px;font-weight:normal;line-height:19.5px}ul.support li:hover{color:#fff;background:#8dc63f url("/static/images/icons/pledgearrow-hover.png") 98% center no-repeat}ul.support li:hover a{color:#fff;text-decoration:none}ul.support li:hover.no_link{background:#fff;color:#8dc63f}.you_pledged{float:left;line-height:21px;font-weight:normal;color:#3d4e53;padding-left:20px;background:url("/static/images/checkmark_small.png") left center no-repeat}.thank-you{font-size:19px;font-weight:bold;margin:20px auto}div#libtools{border:1px solid #d6dde0;-moz-border-radius:10px;-webkit-border-radius:10px;border-radius:10px;margin-left:0;margin-top:1em;padding:10px}div#libtools p{margin-top:0;margin-bottom:0}div#libtools span{margin-top:0;margin-left:.5em;display:inline-block}div#libtools input[type="submit"]{margin-left:4em} \ No newline at end of file diff --git a/static/less/campaign2.less b/static/less/campaign2.less index 2c35e1b3..b8639525 100644 --- a/static/less/campaign2.less +++ b/static/less/campaign2.less @@ -63,10 +63,6 @@ width: 660px !important; } -a { - color:#3d4e53; -} - #js-search { margin: 0 15px 0 15px !important; }