From c97888df8222da3eb351510787c038430fb37d06 Mon Sep 17 00:00:00 2001
From: eric
Date: Sat, 30 Jul 2016 02:35:32 -0400
Subject: [PATCH 01/29] pylint the models
---
core/models.py | 1196 ++++++++++++++++++++++++------------------------
1 file changed, 598 insertions(+), 598 deletions(-)
mode change 100755 => 100644 core/models.py
diff --git a/core/models.py b/core/models.py
old mode 100755
new mode 100644
index 865784d6..f25a40af
--- a/core/models.py
+++ b/core/models.py
@@ -94,69 +94,68 @@ 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)
+ 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
+ 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)
+ 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)
+ 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)
+ 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)
+ 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
-
+ 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 )
+ 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']:
+ 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
@@ -164,7 +163,7 @@ class Claim(models.Model):
def __unicode__(self):
return self.work.title
-
+
@property
def campaign(self):
return self.work.last_campaign()
@@ -174,59 +173,59 @@ class Claim(models.Model):
return self.work.campaigns.all()
def notify_claim(sender, created, instance, **kwargs):
- if 'example.org' in instance.user.email or hasattr(instance,'dont_notify'):
+ 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'})
+ (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)
+ ul = (instance.user, rights)
else:
- ul=(instance.user, instance.rights_holder.owner, rights)
+ ul = (instance.user, instance.rights_holder.owner, rights)
notification.send(ul, "rights_holder_claim", {'claim': instance,})
-post_save.connect(notify_claim,sender=Claim)
-
+post_save.connect(notify_claim, sender=Claim)
+
class RightsHolder(models.Model):
- created = models.DateTimeField(auto_now_add=True)
+ 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 )
+ 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'))
+ 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)
+ 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)
+ description = models.TextField(null=True, blank=False)
+ limit = models.IntegerField(default=0)
@property
def premium_count(self):
- t_model=apps.get_model('payment','Transaction')
+ 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')
+ 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):
+ 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
+ self.extra['ack_name'] = ack_name
if ack_dedication:
- self.extra['ack_dedication']=ack_dedication
+ self.extra['ack_dedication'] = ack_dedication
class CampaignAction(models.Model):
timestamp = models.DateTimeField(auto_now_add=True)
@@ -234,86 +233,85 @@ class CampaignAction(models.Model):
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)
+ 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
+ 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'),)
+ 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=CHOICES)
- watermarked = models.ForeignKey("booxtream.Boox", null=True)
+ 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
+
+ # 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 __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:
+ 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.watermarked is None or self.watermarked.expired:
if self.on_reserve:
self.borrow(self.user)
- do_watermark= self.work.last_campaign().do_watermark
- params={
+ 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',
@@ -328,13 +326,13 @@ class Acq(models.Model):
}
personalized = personalize(self.work.epubfiles()[0].file, self)
personalized.seek(0)
- self.watermarked = watermarker.platform(epubfile= personalized, **params)
+ 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()
-
+ 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()
@@ -342,50 +340,50 @@ class Acq(models.Model):
self.lib_acq.refreshes = now() + delta
self.lib_acq.refreshed = False
self.lib_acq.save()
-
+
@property
def on_reserve(self):
- return self.license==RESERVE
-
+ return self.license == RESERVE
+
def borrow(self, user=None):
if self.on_reserve:
- self.license=BORROWED
+ self.license = BORROWED
self.expire_in(timedelta(days=14))
- self.user.wishlist.add_work( self.work, "borrow")
+ 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)
+ 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):
+ 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')
-
+ return Hold.objects.filter(library__user=self.user, work=self.work).order_by('created')
-def config_acq(sender, instance, created, **kwargs):
+
+def config_acq(sender, instance, created, **kwargs):
if created:
- instance.nonce=instance._hash()
+ 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)
+post_save.connect(config_acq, sender=Acq)
class Hold(models.Model):
created = models.DateTimeField(auto_now_add=True)
@@ -394,9 +392,9 @@ class Hold(models.Model):
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)
+ 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()
+ return Hold.objects.filter(work=self.work, library=self.library, created__lt=self.created).count()
class Campaign(models.Model):
LICENSE_CHOICES = cc.FREECHOICES
@@ -405,7 +403,7 @@ class Campaign(models.Model):
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')
+ 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)
@@ -417,53 +415,57 @@ class Campaign(models.Model):
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')))
+ 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)
+ 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)
-
+ 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()
-
+ old_managers = self.managers.all()
+
# copy custom premiums
- new_premiums= self.premiums.filter(type='CU')
-
+ 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
+
+ # 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
-
+ self.managers = old_managers
+
# clone associated premiums
for premium in new_premiums:
- premium.pk=None
+ premium.pk = None
premium.created = None
premium.campaign = self
premium.save()
@@ -473,30 +475,30 @@ class Campaign(models.Model):
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:
+
+ if self.status == 'UNSUCCESSFUL' and self.work.last_campaign().id == self.id:
return True
else:
return False
@property
def launchable(self):
- may_launch=True
+ may_launch = True
try:
if self.status != 'INITIALIZED':
if self.status == 'ACTIVE':
- self.problems.append(_('The campaign is already launched'))
+ 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.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
+ may_launch = False
else:
self.problems.append(_('A pledge campaign must have a closing date'))
may_launch = False
@@ -507,16 +509,16 @@ class Campaign(models.Model):
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.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))
@@ -524,17 +526,17 @@ class Campaign(models.Model):
else:
self.problems.append(_('A buy-to-unglue campaign must have a target'))
may_launch = False
- if self.type==THANKS:
+ 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 :
+ 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:
@@ -544,112 +546,112 @@ class Campaign(models.Model):
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':
+ if not self.status == 'ACTIVE':
return False
- elif self.type==REWARDS:
+ 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 = 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)
-
+ 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 = CampaignAction(campaign=self, type='failed', comment=self.current_total)
action.save()
if process_transactions:
p = PaymentManager()
- results = p.cancel_campaign(self)
-
+ results = p.cancel_campaign(self)
+
if send_notice:
- regluit.core.signals.unsuccessful_campaign.send(sender=None,campaign=self)
+ 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:
+ 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()
+ 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)
-
+ 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)
+ 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:
+ 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()
-
+
+ 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':
+ 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())
-
+ self.cc_date_initial = datetime.combine(a_date, datetime.min.time())
+
@property
def cc_date(self):
- if self.type in { REWARDS, THANKS }:
+ if self.type in {REWARDS, THANKS}:
return None
- if self.dollar_per_day == 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
+ self._current_total = None
if self.type == THANKS:
- self.left == Decimal(0.00)
+ 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):
+
+ 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':
@@ -660,15 +662,15 @@ class Campaign(models.Model):
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.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()
+ 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
@@ -677,19 +679,19 @@ class Campaign(models.Model):
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 = CampaignAction(campaign=self, type='suspended', comment=reason)
action.save()
- self.status='SUSPENDED'
+ 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 = CampaignAction(campaign=self, type='withdrawn', comment=reason)
action.save()
- self.status='WITHDRAWN'
+ self.status = 'WITHDRAWN'
self.save()
return self
@@ -699,43 +701,43 @@ class Campaign(models.Model):
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)
+ reason = ''
+ action = CampaignAction(campaign=self, type='restarted', comment=reason)
action.save()
- self.status= 'ACTIVE'
+ 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()
+ 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
+ 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
@@ -744,14 +746,14 @@ class Campaign(models.Model):
else:
return None
else:
- return None
-
+ 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":[]}
+ ungluers = {"all":[], "supporters":[], "patrons":[], "bibliophiles":[]}
if self.status == "ACTIVE":
translist = p.query_campaign(self, summary=False, pledged=True, authorized=True)
elif self.status == "SUCCESSFUL":
@@ -767,19 +769,19 @@ class Campaign(models.Model):
ungluers['patrons'].append(transaction.user)
elif transaction.amount >= Premium.TIERS["supporter"]:
ungluers['supporters'].append(transaction.user)
-
- self._ungluers_= ungluers
+
+ 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
- """
+ """
+ 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":[]}
+ 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":
@@ -789,18 +791,18 @@ class Campaign(models.Model):
for transaction in translist:
ungluers['all'].append(transaction)
if transaction.amount >= Premium.TIERS["bibliophile"]:
- ungluers['bibliophiles'].append(transaction)
+ ungluers['bibliophiles'].append(transaction)
elif transaction.amount >= Premium.TIERS["patron"]:
if transaction.anonymous:
ungluers['anon_patrons'] += 1
else:
- ungluers['patrons'].append(transaction)
+ ungluers['patrons'].append(transaction)
elif transaction.amount >= Premium.TIERS["supporter"]:
if transaction.anonymous:
ungluers['anon_supporters'] += 1
else:
- ungluers['supporters'].append(transaction)
-
+ ungluers['supporters'].append(transaction)
+
return ungluers
def effective_premiums(self):
@@ -815,15 +817,15 @@ class Campaign(models.Model):
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
@@ -844,14 +846,14 @@ class Campaign(models.Model):
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(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"""
@@ -868,7 +870,7 @@ class Campaign(models.Model):
return self.rh.rights_holder_name
except:
return ''
-
+
@property
def license_url(self):
return cc.CCLicense.url(self.license)
@@ -876,7 +878,7 @@ class Campaign(models.Model):
@property
def license_badge(self):
return cc.CCLicense.badge(self.license)
-
+
@property
def success_date(self):
if self.status == 'SUCCESSFUL':
@@ -885,18 +887,18 @@ class Campaign(models.Model):
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.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
@@ -904,7 +906,7 @@ class Campaign(models.Model):
return ''
time_remaining = self.deadline - now()
countdown = ""
-
+
if time_remaining.days:
countdown = "%s days" % str(time_remaining.days + 1)
elif time_remaining.seconds > 3600:
@@ -913,49 +915,49 @@ class Campaign(models.Model):
countdown = "%s minutes" % str(time_remaining.seconds/60 + 1)
else:
countdown = "Seconds"
-
- return countdown
-
+
+ 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())
-
+ 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.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 :
+ 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:
+ 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:
+ 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)
+ 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)
+ 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:
@@ -964,70 +966,70 @@ class Campaign(models.Model):
new_file = SpooledTemporaryFile()
old_file = SpooledTemporaryFile()
old_file.write(pdf_to_do)
- if position==0:
+ 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.file.save(path_for_file('ebf', None), ContentFile(new_file.read()))
new_pdf_ebf.save()
- new_ebfs['pdf']=new_pdf_ebf
+ 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 = 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.file.save(path_for_file(new_epub_ebf, None), ContentFile(new_file.read()))
new_epub_ebf.save()
- new_ebfs['epub']=new_epub_ebf
+ 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.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
+ 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):
+ 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()))
+ 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]),
- )
+ 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",
- )
+ 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={
+ params = {
'customeremailaddress': self.license,
'customername': 'The Public',
'languagecode':'1033',
@@ -1039,22 +1041,22 @@ class Campaign(models.Model):
'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)
+ 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
+ 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()
@@ -1062,40 +1064,40 @@ class Campaign(models.Model):
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)
+ 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 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)
+ identifier = Identifier.get_or_add(type=type, value=value, edition=edition, work=work)
if identifier.work.id != work.id:
- identifier.work=work
+ 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:
+ 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):
+ 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 = 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)
@@ -1106,9 +1108,9 @@ class Work(models.Model):
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)
+ 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)
+ 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)
@@ -1125,7 +1127,7 @@ class Work(models.Model):
@property
def googlebooks_id(self):
try:
- preferred_id=self.preferred_edition.googlebooks_id
+ preferred_id = self.preferred_edition.googlebooks_id
# note that there should always be a preferred edition
except AttributeError:
# this work has no edition.
@@ -1144,9 +1146,9 @@ class Work(models.Model):
else:
return ''
- @property
+ @property
def goodreads_id(self):
- preferred_id=self.preferred_edition.goodreads_id
+ preferred_id = self.preferred_edition.goodreads_id
if preferred_id:
return preferred_id
try:
@@ -1158,7 +1160,7 @@ class Work(models.Model):
def goodreads_url(self):
return "http://www.goodreads.com/book/show/%s" % self.goodreads_id
- @property
+ @property
def librarything_id(self):
try:
return self.identifiers.filter(type='ltwk')[0].value
@@ -1169,17 +1171,17 @@ class Work(models.Model):
def librarything_url(self):
return "http://www.librarything.com/work/%s" % self.librarything_id
- @property
+ @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'
@@ -1195,13 +1197,13 @@ class Work(models.Model):
return 'jpeg'
else:
return 'image'
-
+
def uses_google_cover(self):
if self.preferred_edition and self.preferred_edition.cover_image:
return False
- else:
+ 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()
@@ -1219,36 +1221,36 @@ class Work(models.Model):
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:
+ 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:
+ 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:
+ 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:
+ 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:
+ 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().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)
@@ -1256,14 +1258,14 @@ class Work(models.Model):
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:
+ 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 "%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 "_"
@@ -1291,7 +1293,7 @@ class Work(models.Model):
except IndexError:
self._last_campaign_ = None
return self._last_campaign_
-
+
@property
def preferred_edition(self):
if self.selected_edition:
@@ -1304,9 +1306,9 @@ class Work(models.Model):
try:
self.selected_edition = self.editions.all().order_by('-cover_image', '-created')[0] # prefer editions with covers
self.save()
- return self.selected_edition
+ return self.selected_edition
except IndexError:
- #should only happen if there are no editions for the work,
+ #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
@@ -1314,7 +1316,7 @@ class Work(models.Model):
#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:
@@ -1330,9 +1332,9 @@ class Work(models.Model):
status = 0
campaign = self.last_campaign()
if campaign is not None:
- if(campaign.status == 'SUCCESSFUL'):
+ if campaign.status == 'SUCCESSFUL':
status = 6
- elif(campaign.status == 'ACTIVE'):
+ elif campaign.status == 'ACTIVE':
if campaign.target is not None:
target = float(campaign.target)
else:
@@ -1342,7 +1344,7 @@ class Work(models.Model):
status = 6
else:
if campaign.type == BUY2UNGLUE:
- status = int( 6 - 6*campaign.left/campaign.target)
+ status = int(6 - 6*campaign.left/campaign.target)
else:
status = int(float(campaign.current_total)*6/target)
if status >= 6:
@@ -1352,21 +1354,21 @@ class Work(models.Model):
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')
+ 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
+ # 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):
@@ -1376,67 +1378,67 @@ class Work(models.Model):
return EbookFile.objects.filter(edition__work=self, format='pdf').exclude(file='').order_by('-created')
def formats(self):
- fmts=[]
+ 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= []
+ 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,)
+ previous_ebooks = Ebook.objects.filter(url=ebf.file.url,)
try:
previous_ebook = previous_ebooks[0]
for eb in previous_ebooks[1:]: #housekeeping
- eb.deactivate()
+ eb.deactivate()
except IndexError:
previous_ebook = None
-
+
if ebf.format not in done_formats:
- if ebf.asking==add_ask or ebf.format=='mobi':
+ 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,
- )
+ 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
-
+ return
+
def remove_old_ebooks(self):
- old=Ebook.objects.filter(edition__work=self, active=True).order_by('-created')
- done_formats= []
+ 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='')
+ 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
+ dlc = 0
for ebook in self.ebooks(all=True):
dlc += ebook.download_count
return dlc
-
+
def first_pdf(self):
return self.first_ebook('pdf')
@@ -1467,22 +1469,22 @@ class Work(models.Model):
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.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 )
+ 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:
+ if self.preferred_edition is None:
return ''
- preferred_id=self.preferred_edition.oclc
+ preferred_id = self.preferred_edition.oclc
if preferred_id:
return preferred_id
try:
@@ -1491,22 +1493,22 @@ class Work(models.Model):
return ''
def first_isbn_13(self):
- if self.preferred_edition == None:
+ if self.preferred_edition is None:
return ''
- preferred_id=self.preferred_edition.isbn_13
+ 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:
+ if edition.publication_date and len(edition.publication_date) >= 4:
return edition.publication_date
-
+
@property
def publication_date(self):
if self.publication_range:
@@ -1521,7 +1523,7 @@ class Work(models.Model):
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]
+ latest_publication = edition.publication_date[:4]
except IndexError:
continue
break
@@ -1529,11 +1531,11 @@ class Work(models.Model):
publication_range = earliest_publication
else:
publication_range = earliest_publication + "-" + latest_publication
- self.publication_range = publication_range
+ self.publication_range = publication_range
self.save()
return publication_range
return ''
-
+
@property
def has_unglued_edition(self):
"""
@@ -1542,7 +1544,7 @@ class Work(models.Model):
if self.ebooks().filter(edition__unglued=True):
return True
return False
-
+
@property
def user_with_rights(self):
"""
@@ -1557,41 +1559,41 @@ class Work(models.Model):
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:
+ for choice in OFFER_CHOICES:
if not self.offers.filter(license=choice[0]):
- self.offers.create(license=choice[0],active=True,price=Decimal(10))
+ 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)
+
+ 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)
+ 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)
+ lib_license = self.get_lib_license(user)
if lib_license and lib_license.thanked:
return True
return False
-
- def in_library(self,user):
+
+ def in_library(self, user):
if user.is_anonymous():
return False
- lib_license=self.get_lib_license(user)
+ lib_license = self.get_lib_license(user)
if lib_license and lib_license.acqs.count():
return True
return False
@@ -1605,34 +1607,26 @@ class Work(models.Model):
return self.acqs.filter(license=TESTING).order_by('-created')
class user_license:
- acqs=Acq.objects.none()
- def __init__(self,acqs):
- self.acqs=acqs
-
+ 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
-
+ 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:
+ 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:
+ def purchased(self):
+ purchases = self.acqs.filter(license=INDIVIDUAL, expires__isnull=True)
+ if purchases.count() == 0:
return None
else:
return purchases[0]
@@ -1640,41 +1634,39 @@ class Work(models.Model):
@property
def lib_acqs(self):
return self.acqs.filter(license=LIBRARY)
-
+
@property
- def next_acq(self):
+ 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:
+ 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
-
+ return self.acqs.filter(license=LIBRARY, refreshes__lt=now()).count() > 0
+
@property
def thanked(self):
- return self.acqs.filter(license=THANKED).count()>0
-
+ 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
+
+ @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
-
-
+ 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:
+ if user is None:
return None
if hasattr(user, 'is_anonymous'):
if user.is_anonymous():
@@ -1683,13 +1675,13 @@ class Work(models.Model):
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 = []
@@ -1701,9 +1693,9 @@ class Work(models.Model):
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)
@@ -1711,7 +1703,7 @@ class Author(models.Model):
def __unicode__(self):
return self.name
-
+
@property
def last_name_first(self):
names = self.name.rsplit()
@@ -1722,43 +1714,43 @@ class Author(models.Model):
elif len(names) == 2:
return names[1] + ", " + names[0]
else:
- reversed_name= names[-1]+","
+ reversed_name = names[-1]+","
for name in names[0:-1]:
- reversed_name+=" "
- reversed_name+=name
+ 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')
+ 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):
+
+ def set(self, relation_code):
if self.relation.code != relation_code:
try:
- self.relation = Relation.objects.get(code = relation_code)
+ 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)
+ is_visible = models.BooleanField(default=True)
authority = models.CharField(max_length=10, blank=False, default="")
class Meta:
@@ -1766,15 +1758,15 @@ class Subject(models.Model):
def __unicode__(self):
return self.name
-
-
- @property
+
+
+ @property
def kw(self):
return 'kw.%s' % self.name
-
+
def free_works(self):
- return self.works.filter( is_free = True )
-
+ return self.works.filter(is_free=True)
+
class Edition(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=1000)
@@ -1796,64 +1788,64 @@ class Edition(models.Model):
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():
+ 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
+ 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)
+ 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 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 self.cover_image:
+ im = get_thumbnail(self.cover_image, '128', crop='noop', quality=95)
if im.exists():
- return im.url
+ 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
+ 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):
+
+ def id_for(self, type):
if not self.pk:
return ''
try:
@@ -1864,7 +1856,7 @@ class Edition(models.Model):
@property
def isbn_13(self):
return self.id_for('isbn')
-
+
@property
def googlebooks_id(self):
return self.id_for('goog')
@@ -1881,24 +1873,24 @@ class Edition(models.Model):
def goodreads_id(self):
return self.id_for('gdrd')
- @property
+ @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)
+ 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
+ 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)
+ (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()
@@ -1911,7 +1903,7 @@ class Edition(models.Model):
except Relator.DoesNotExist:
pass
- def set_publisher(self,publisher_name):
+ def set_publisher(self, publisher_name):
if publisher_name and publisher_name != '':
try:
pub_name = PublisherName.objects.get(name=publisher_name)
@@ -1920,7 +1912,7 @@ class Edition(models.Model):
except PublisherName.DoesNotExist:
pub_name = PublisherName.objects.create(name=publisher_name)
pub_name.save()
-
+
self.publisher_name = pub_name
self.save()
@@ -1930,21 +1922,21 @@ class Edition(models.Model):
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 ''
+ 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:
@@ -1952,9 +1944,9 @@ class Edition(models.Model):
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):
+ def description(self):
return self.work.description
@@ -1969,13 +1961,13 @@ class Publisher(models.Model):
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)
+ 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:
@@ -1983,11 +1975,11 @@ class PublisherName(models.Model):
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)
+ was = models.IntegerField(unique=True)
moved = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True)
@@ -1996,10 +1988,10 @@ 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)
+ work = Work.objects.get(id=work_id)
except Work.DoesNotExist:
try:
- work = WasWork.objects.get(was = work_id).work
+ work = WasWork.objects.get(was=work_id).work
except WasWork.DoesNotExist:
raise Work.DoesNotExist()
except ValueError:
@@ -2007,23 +1999,23 @@ def safe_get_work(work_id):
raise Work.DoesNotExist()
return work
-FORMAT_CHOICES = (('pdf','PDF'),( 'epub','EPUB'), ('html','HTML'), ('text','TEXT'), ('mobi','MOBI'))
+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)
+ format = models.CharField(max_length=25, choices=FORMAT_CHOICES)
edition = models.ForeignKey('Edition', related_name='ebook_files')
- created = models.DateTimeField(auto_now_add=True)
+ 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:
@@ -2031,22 +2023,22 @@ class EbookFile(models.Model):
except:
return False
-send_to_kindle_limit=7492232
+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)
+ 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)
+ 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)
@@ -2055,31 +2047,31 @@ class Ebook(models.Model):
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.edition.ebook_files.filter(format=self.format).count() == 0:
if self.provider is not 'Unglue.it':
try:
- r=urllib2.urlopen(self.url)
+ 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.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 = 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) )
+ logging.error('Bad link error: {}'.format(self.url))
except IOError:
- logger.error(u'could not open {}'.format(self.url) )
+ 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) )
+ logger.error(u'couldn\'t find ebookfile for {}'.format(self.url))
# try the url instead
f = urllib.urlopen(self.url)
return f
@@ -2088,89 +2080,90 @@ class Ebook(models.Model):
try:
ebf.file.open()
except ValueError:
- logger.error(u'couldn\'t open EbookFile {}'.format(ebf.id) )
+ logger.error(u'couldn\'t open EbookFile {}'.format(ebf.id))
return None
except IOError:
- logger.error(u'EbookFile {} does not exist'.format(ebf.id) )
+ 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)
+ self.provider = Ebook.infer_provider(self.url)
return self.provider
-
+
@property
def rights_badge(self):
- if self.rights is None :
+ if self.rights is None:
return cc.CCLicense.badge('PD-US')
return cc.CCLicense.badge(self.rights)
-
+
@staticmethod
- def infer_provider( url):
+ 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'
+ 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('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'
+ 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
+ provider = None
return provider
-
+
def increment(self):
- Ebook.objects.filter(id=self.id).update(download_count = F('download_count') +1)
-
+ 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])
+ 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.active = False
self.save()
-def set_free_flag(sender, instance, created, **kwargs):
+ 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:
+ 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 instance.edition.work.is_free==False and instance.edition.work.ebooks().count()>0:
+ 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)
+
+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:
+ # 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)
+pre_delete.connect(reset_free_flag, sender=Ebook)
+
class Wishlist(models.Model):
created = models.DateTimeField(auto_now_add=True)
@@ -2179,35 +2172,35 @@ class Wishlist(models.Model):
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)
+ w = Wishes.objects.get(wishlist=self, work=work)
except:
- Wishes.objects.create(source=source,wishlist=self,work=work)
+ 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')
+ wishlist = models.ForeignKey('Wishlist')
work = models.ForeignKey('Work', related_name='wishes')
class Meta:
db_table = 'core_wishlist_works'
@@ -2215,24 +2208,24 @@ class Wishes(models.Model):
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
+pledger.instance = None
def pledger2():
if not pledger2.instance:
pledger2.instance = Badge.objects.get(name='pledger2')
return pledger2.instance
-pledger2.instance=None
+pledger2.instance = None
ANONYMOUS_AVATAR = '/static/images/header/avatar.png'
(NO_AVATAR, GRAVATAR, TWITTER, FACEBOOK, UNGLUEITAR) = AVATARS
@@ -2241,17 +2234,16 @@ 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"
+ 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)
+ 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)
@@ -2263,16 +2255,25 @@ class UserProfile(models.Model):
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')))
-
+ 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
+ def reset_pledge_badge(self):
+ #count user pledges
n_pledges = self.pledge_count
if self.badges.exists():
self.badges.remove(pledger())
@@ -2281,28 +2282,28 @@ class UserProfile(models.Model):
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()
+ 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:
+ 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:
+ 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)
@@ -2314,7 +2315,7 @@ class UserProfile(models.Model):
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
@@ -2331,8 +2332,8 @@ class UserProfile(models.Model):
if last:
return last.anonymous
else:
- return None
-
+ return None
+
@property
def on_ml(self):
if "@example.org" in self.user.email:
@@ -2341,7 +2342,7 @@ class UserProfile(models.Model):
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
+ 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))
@@ -2363,19 +2364,19 @@ class UserProfile(models.Model):
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(self.user.username + '@unglue.it').hexdigest() + "?"
gravatar_url += urllib.urlencode({'d':'wavatar', 's':'50'})
return gravatar_url
-
+
@property
def avatar_url(self):
@@ -2392,31 +2393,31 @@ class UserProfile(models.Model):
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={}
+ socials = self.user.social_auth.all()
+ auths = {}
for social in socials:
- auths[social.provider]=True
+ auths[social.provider] = True
return auths
@property
def libraries(self):
- libs=[]
+ 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()
+ url = models.URLField()
title = models.CharField(max_length=140)
source = models.CharField(max_length=140)
- date = models.DateField( db_index=True,)
+ 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)
@@ -2424,7 +2425,7 @@ class Press(models.Model):
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
+ 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)
@@ -2432,18 +2433,17 @@ class Gift(models.Model):
@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) = 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)
-
+ 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
-
From 24c1cf8a037db462f99fd30aa02c209fea2849ea Mon Sep 17 00:00:00 2001
From: eric
Date: Sat, 30 Jul 2016 02:36:01 -0400
Subject: [PATCH 02/29] refactor the models
---
core/models/__init__.py | 1352 ++++++++++++++++++++++++++++++++++++++
core/models/bibmodels.py | 1145 ++++++++++++++++++++++++++++++++
core/parameters.py | 2 +
3 files changed, 2499 insertions(+)
create mode 100755 core/models/__init__.py
create mode 100644 core/models/bibmodels.py
diff --git a/core/models/__init__.py b/core/models/__init__.py
new file mode 100755
index 00000000..2b64eb7d
--- /dev/null
+++ b/core/models/__init__.py
@@ -0,0 +1,1352 @@
+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 (
+ Work,
+ Edition,
+ Identifier,
+ Author,
+ Relation,
+ Relator,
+ Subject,
+ Publisher,
+ PublisherName,
+ WasWork,
+ EbookFile,
+ Ebook,
+ path_for_file,
+ safe_get_work,
+)
+
+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
+ 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 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(self.user.username + '@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..a0ddbd57
--- /dev/null
+++ b/core/models/bibmodels.py
@@ -0,0 +1,1145 @@
+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
+
+import regluit.core.cc as cc
+from regluit.core.epub import test_epub
+from regluit.core.parameters import (
+ BORROWED,
+ BUY2UNGLUE,
+ INDIVIDUAL,
+ LIBRARY,
+ OFFER_CHOICES,
+ TESTING,
+ THANKED,
+ THANKS,
+)
+
+logger = logging.getLogger(__name__)
+
+
+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 = 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 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(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..b62377c1 100644
--- a/core/parameters.py
+++ b/core/parameters.py
@@ -1,3 +1,5 @@
(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'),)
\ No newline at end of file
From 1e7ea4b43c4247a0e4ee0b983a3ea9e0b432b315 Mon Sep 17 00:00:00 2001
From: eric
Date: Sat, 30 Jul 2016 14:25:49 -0400
Subject: [PATCH 03/29] finish the refactor
---
core/models.py | 2449 ------------------------------------------------
1 file changed, 2449 deletions(-)
delete mode 100644 core/models.py
diff --git a/core/models.py b/core/models.py
deleted file mode 100644
index f25a40af..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):
- 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
- 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 = 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 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(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)
-
-
-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(self.user.username + '@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
From c650c672c22ee061279127139947d92fdcdbdb23 Mon Sep 17 00:00:00 2001
From: eric
Date: Fri, 5 Aug 2016 15:53:29 -0400
Subject: [PATCH 04/29] change the models
---
core/migrations/0003_auto_20160805_1550.py | 51 ++++++++++++++++++++++
core/models/bibmodels.py | 23 ++++++----
core/parameters.py | 22 +++++++++-
3 files changed, 86 insertions(+), 10 deletions(-)
create mode 100644 core/migrations/0003_auto_20160805_1550.py
diff --git a/core/migrations/0003_auto_20160805_1550.py b/core/migrations/0003_auto_20160805_1550.py
new file mode 100644
index 00000000..97b0829f
--- /dev/null
+++ b/core/migrations/0003_auto_20160805_1550.py
@@ -0,0 +1,51 @@
+# -*- 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='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''), (b'revision', b''), (b'sequel', b''), (b'compilation', b'')])),
+ ],
+ ),
+ migrations.AddField(
+ model_name='ebook',
+ name='version',
+ field=models.CharField(max_length=255, null=True),
+ ),
+ migrations.AddField(
+ model_name='edition',
+ name='note',
+ field=models.CharField(max_length=64, 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='work',
+ name='related',
+ field=models.ManyToManyField(to='core.Work', null=True, through='core.WorkRelation'),
+ ),
+ ]
diff --git a/core/models/bibmodels.py b/core/models/bibmodels.py
index a0ddbd57..8f22b761 100644
--- a/core/models/bibmodels.py
+++ b/core/models/bibmodels.py
@@ -28,12 +28,14 @@ from regluit.questionnaire.models import Landing
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,
)
@@ -42,7 +44,7 @@ logger = logging.getLogger(__name__)
class Identifier(models.Model):
- # olib, ltwk, goog, gdrd, thng, isbn, oclc, olwk, olib, gute, glue
+ # olib, ltwk, goog, gdrd, thng, isbn, oclc, olwk, olib, gute, glue, buy, 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)
@@ -94,6 +96,8 @@ class Work(models.Model):
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')
+ age_level = models.CharField(max_length=5, choices=AGE_LEVEL_CHOICES, default='')
class Meta:
ordering = ['title']
@@ -674,6 +678,10 @@ class Work(models.Model):
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):
@@ -755,6 +763,7 @@ class Edition(models.Model):
work = models.ForeignKey("Work", related_name="editions", null=True)
cover_image = models.URLField(null=True, blank=True)
unglued = models.BooleanField(default=False)
+ note = models.CharField(max_length=64, null=True)
def __unicode__(self):
if self.isbn_13:
@@ -979,14 +988,12 @@ def safe_get_work(work_id):
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)
+ 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)
@@ -1006,19 +1013,17 @@ class EbookFile(models.Model):
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)
+ 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 = None #placeholder
+ version = models.CharField(max_length=255, null=True)
# use 'PD-US', 'CC BY', 'CC BY-NC-SA', 'CC BY-NC-ND', 'CC BY-NC', 'CC BY-ND', 'CC BY-SA', 'CC0'
- rights = models.CharField(max_length=255, null=True, choices=RIGHTS_CHOICES, db_index=True)
+ 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)
diff --git a/core/parameters.py b/core/parameters.py
index b62377c1..c3383426 100644
--- a/core/parameters.py
+++ b/core/parameters.py
@@ -2,4 +2,24 @@
(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'),)
\ No newline at end of file
+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', ''), ('revision', ''), ('sequel', ''), ('compilation', ''))
+
+
+
+
+
+
+
From abedff089c24a57d55cb0b40cca726e31e37d6c0 Mon Sep 17 00:00:00 2001
From: eric
Date: Mon, 8 Aug 2016 16:27:12 -0400
Subject: [PATCH 05/29] add direct support for doi
---
core/migrations/0004_auto_20160808_1548.py | 44 ++++++++++++++++++++++
core/models/bibmodels.py | 8 +++-
frontend/forms.py | 23 ++++++++++-
frontend/templates/edition_display.html | 3 ++
frontend/templates/new_edition.html | 9 ++---
frontend/views.py | 4 +-
6 files changed, 81 insertions(+), 10 deletions(-)
create mode 100644 core/migrations/0004_auto_20160808_1548.py
diff --git a/core/migrations/0004_auto_20160808_1548.py b/core/migrations/0004_auto_20160808_1548.py
new file mode 100644
index 00000000..ae514814
--- /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_20160805_1550'),
+ ]
+
+ operations = [
+ migrations.RunPython(url_to_doi, reverse_code=doi_to_url, hints={'core': 'Identifier'}),
+ ]
diff --git a/core/models/bibmodels.py b/core/models/bibmodels.py
index 8f22b761..03a45eb6 100644
--- a/core/models/bibmodels.py
+++ b/core/models/bibmodels.py
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
class Identifier(models.Model):
- # olib, ltwk, goog, gdrd, thng, isbn, oclc, olwk, olib, gute, glue, buy, doi
+ # 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)
@@ -763,7 +763,7 @@ class Edition(models.Model):
work = models.ForeignKey("Work", related_name="editions", null=True)
cover_image = models.URLField(null=True, blank=True)
unglued = models.BooleanField(default=False)
- note = models.CharField(max_length=64, null=True)
+ note = models.CharField(max_length=64, null=True, blank=True)
def __unicode__(self):
if self.isbn_13:
@@ -858,6 +858,10 @@ class Edition(models.Model):
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')
diff --git a/frontend/forms.py b/frontend/forms.py
index 5d054ca2..578cf4a0 100644
--- a/frontend/forms.py
+++ b/frontend/forms.py
@@ -189,6 +189,15 @@ class EditionForm(forms.ModelForm):
'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)
description = forms.CharField( required=False, widget=CKEditorWidget())
coverfile = forms.ImageField(required=False)
@@ -201,13 +210,23 @@ class EditionForm(forms.ModelForm):
select = forms.Select(choices=CREATOR_RELATIONS).render('change_relator_%s' % relator.id , relator.relation.code )
self.relators.append({'relator':relator,'select':select})
+ def clean_doi(self):
+ doi = self.cleaned_data["doi"]
+ if doi:
+ 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
+ if 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:
diff --git a/frontend/templates/edition_display.html b/frontend/templates/edition_display.html
index a05bd15e..a21f367a 100644
--- a/frontend/templates/edition_display.html
+++ b/frontend/templates/edition_display.html
@@ -28,6 +28,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/new_edition.html b/frontend/templates/new_edition.html
index ffb7d616..57adff02 100644
--- a/frontend/templates/new_edition.html
+++ b/frontend/templates/new_edition.html
@@ -93,7 +93,7 @@ ul.fancytree-container {
Create New Edition
{% endif %}
-Title and ISBN 13 are required; the rest is optional, though a cover image is strongly recommended.
+Title and ISBN 13 (or DOI, OCLCNum or URL) are required; the rest is optional, though a cover image is strongly recommended.
{% csrf_token %}
-{{form.as_p}}
+{{form.edition.errors}}{{form.edition}}
+{{form.format.errors}}Format: {{form.format}}
+{% if edition.work.versions %}
+There are named versions for this ebook. Specify the version you want to replace. {{form.version.errors}}Version:
+---
+{% for version in edition.work.versions %}
+{{ version }}
+{% endfor %}
+
+{% else %}
+
+{% endif %}
+{{form.file.errors}}Upload File: {{form.file}}
-{% if edition.work %}
+
More Edition Management
{% if edition.work.last_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:
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 ef28a2cc..117e3175 100755
--- a/frontend/views.py
+++ b/frontend/views.py
@@ -428,9 +428,8 @@ 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() :
@@ -462,6 +461,19 @@ def edition_uploads(request, edition_id):
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=form.cleaned_data['version'],
+ active=False,
+ provider="Unglue.it",
+ )
+ form.instance.ebook = new_ebook
+ form.instance.save()
+
else:
context['upload_error'] = form.errors
form = EbookFileForm(initial={'edition':edition, 'format':'epub'}, campaign_type=campaign_type)
@@ -654,19 +666,10 @@ 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
+ 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')
@@ -680,6 +683,7 @@ def manage_ebooks(request, edition_id, by=None):
edition.ebook_form = EbookForm(data = request.POST, prefix = 'ebook_%d'%edition.id)
if edition.ebook_form.is_valid():
edition.ebook_form.save()
+ edition.work.remove_old_ebooks()
alert = 'Thanks for adding an ebook to unglue.it!'
else:
alert = 'your submitted ebook had errors'
@@ -700,7 +704,7 @@ def campaign_results(request, 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 = []
@@ -782,7 +786,8 @@ def manage_campaign(request, id, action='manage'):
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':
alerts.append('A MOBI file is being generated')
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;
}
From 5eabbbb4d2a70bdcc6e4c366bb061ad9ca1ec948 Mon Sep 17 00:00:00 2001
From: eric
Date: Wed, 24 Aug 2016 15:43:28 -0400
Subject: [PATCH 16/29] implement versions in api
---
api/opds.py | 41 ++++++++++++++++++++++++-----------------
core/bookloader.py | 2 +-
2 files changed, 25 insertions(+), 18 deletions(-)
diff --git a/api/opds.py b/api/opds.py
index 297d6a8b..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?
@@ -237,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/bookloader.py b/core/bookloader.py
index a02b21f8..bb5468b6 100755
--- a/core/bookloader.py
+++ b/core/bookloader.py
@@ -902,7 +902,7 @@ def load_from_yaml(yaml_url, test_mode=False):
rights = cc.match_license(metadata.rights),
format = ebook_format,
edition = edition,
- # version = metadata._version
+ version = metadata._version
)
return work.id
From c0efbf86ea9a5942f1ea91433e530d67d08294a5 Mon Sep 17 00:00:00 2001
From: eric
Date: Thu, 25 Aug 2016 17:55:29 -0400
Subject: [PATCH 17/29] add unglueitar fix
---
core/models/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/core/models/__init__.py b/core/models/__init__.py
index d31b8232..be11dcba 100755
--- a/core/models/__init__.py
+++ b/core/models/__init__.py
@@ -1314,7 +1314,7 @@ class UserProfile(models.Model):
def unglueitar(self):
# construct the url
- gravatar_url = "https://www.gravatar.com/avatar/" + hashlib.md5(self.user.username + '@unglue.it').hexdigest() + "?"
+ 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
From f2e6afc3afc39138259cb4fe8f865836b7912d49 Mon Sep 17 00:00:00 2001
From: eric
Date: Thu, 25 Aug 2016 17:56:16 -0400
Subject: [PATCH 18/29] clean up admin
---
core/admin.py | 85 ++++++++++++++++++++++++++++++++--------
core/models/__init__.py | 4 +-
core/models/bibmodels.py | 12 +++---
3 files changed, 77 insertions(+), 24 deletions(-)
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/models/__init__.py b/core/models/__init__.py
index be11dcba..d78da01c 100755
--- a/core/models/__init__.py
+++ b/core/models/__init__.py
@@ -1187,9 +1187,9 @@ class UserProfile(models.Model):
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)
+ facebook_id = models.BigIntegerField(null=True, blank=True)
librarything_id = models.CharField(max_length=31, blank=True)
- badges = models.ManyToManyField('Badge', related_name='holders')
+ 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)
diff --git a/core/models/bibmodels.py b/core/models/bibmodels.py
index 025ecbcb..9748be75 100644
--- a/core/models/bibmodels.py
+++ b/core/models/bibmodels.py
@@ -95,12 +95,12 @@ class Work(models.Model):
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)
+ 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='')
+ age_level = models.CharField(max_length=5, choices=AGE_LEVEL_CHOICES, default='', blank=True)
class Meta:
ordering = ['title']
@@ -741,12 +741,12 @@ class Subject(models.Model):
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,)
+ 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)
+ note = models.ForeignKey("EditionNote", null=True, blank=True)
def __unicode__(self):
if self.isbn_13:
@@ -1030,7 +1030,7 @@ class Ebook(models.Model):
download_count = models.IntegerField(default=0)
active = models.BooleanField(default=True)
filesize = models.PositiveIntegerField(null=True)
- version = models.CharField(max_length=255, null=True)
+ version = models.CharField(max_length=255, null=True, blank=True)
# use 'PD-US', 'CC BY', 'CC BY-NC-SA', 'CC BY-NC-ND', 'CC BY-NC', 'CC BY-ND', 'CC BY-SA', 'CC0'
rights = models.CharField(max_length=255, null=True, choices=cc.CHOICES, db_index=True)
From d1951bab75ef93878e11bcdcc57cfac62580098c Mon Sep 17 00:00:00 2001
From: eric
Date: Fri, 26 Aug 2016 10:28:23 -0400
Subject: [PATCH 19/29] add file upload to ebook submission form
---
frontend/forms.py | 55 ++++++++++-------
frontend/templates/edition_upload.html | 82 +++++++++++++++-----------
frontend/views.py | 41 +++++++------
3 files changed, 107 insertions(+), 71 deletions(-)
diff --git a/frontend/forms.py b/frontend/forms.py
index 8721dfa6..eb6e26d0 100644
--- a/frontend/forms.py
+++ b/frontend/forms.py
@@ -260,6 +260,25 @@ class EditionForm(forms.ModelForm):
'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):
version = forms.CharField(max_length=512, required=False)
file = forms.FileField(max_length=16777216)
@@ -284,23 +303,7 @@ class EbookFileForm(forms.ModelForm):
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) )
+ test_file(the_file)
return self.cleaned_data
class Meta:
@@ -309,6 +312,8 @@ class EbookFileForm(forms.ModelForm):
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},))
class Meta:
model = Ebook
exclude = ('created', 'download_count', 'active', 'filesize')
@@ -316,22 +321,32 @@ class EbookForm(forms.ModelForm):
'edition': forms.HiddenInput,
'user': forms.HiddenInput,
'provider': forms.HiddenInput,
- 'url': forms.TextInput(attrs={'size' : 60}),
}
def clean_provider(self):
- new_provider = Ebook.infer_provider(self.data[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.'})
diff --git a/frontend/templates/edition_upload.html b/frontend/templates/edition_upload.html
index 104a6b4f..1a9324ad 100644
--- a/frontend/templates/edition_upload.html
+++ b/frontend/templates/edition_upload.html
@@ -1,42 +1,58 @@
- {% 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 %}
+ {% 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 %} {{ ebook.version }}. {% endif %}
Downloaded {{ ebook.download_count }} times since {{ ebook.created }}
- {% endfor %}
- {% endif %}
+ {% endif %}
+ {% endfor %}
-
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 %}{{ 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 }}
- {{ edition.ebook_form.version.errors }}Version: {{ edition.ebook_form.version }}
-
-
-
Note on versions
-
- Unglue.it's version strings have two components, a label and a iteration.
- The iteration is denoted by a dot and a number at the end of version string, and is assumed to be 0 if not given explicitly.
- Unglue.it will show the user just the label and will suppress display of all but the highest iteration for a given label.
- so if the ebooks have versions "", ".1", "1.0.0", "1.0.2", "Open Access" and "Open Access.1", Unglue.it will display 3 ebooks labelled "", "1.0" and "Open Access".
- If you want ebooks from two editions with the same format to display, give them different version labels.
-
-
- {% else %}
-
Adding ebook links is disabled for this work.
- {% endif %}
+ {% 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 }}
+ {% if admin %}
+ {{ ebook_form.version.errors }}Version: {{ ebook_form.version }}
+ {% else %}
+
+ {% endif %}
+
+
+ {% if admin %}
+
Note on versions
+
+ Unglue.it's version strings have two components, a label and a iteration.
+ The iteration is denoted by a dot and a number at the end of version string, and is assumed to be 0 if not given explicitly.
+ Unglue.it will show the user just the label and will suppress display of all but the highest iteration for a given label.
+ so if the ebooks have versions "", ".1", "1.0.0", "1.0.2", "Open Access" and "Open Access.1", Unglue.it will display 3 ebooks labelled "", "1.0" and "Open Access".
+ If you want ebooks from two editions with the same format to display, give them different version labels.
+
+
The existing version labels for this work are:
+
+ {% for vers in edition.work.versions %}
+ {% if vers %}"{{ vers }}"{% else %}"" [no version specified]{% endif %}
+
+ {% endfor %}
+ {% endif %}
+
+ {% else %}
+
Adding ebook links is disabled for this work.
{% endif %}
\ No newline at end of file
diff --git a/frontend/views.py b/frontend/views.py
index 117e3175..dcc209fe 100755
--- a/frontend/views.py
+++ b/frontend/views.py
@@ -671,31 +671,36 @@ def manage_ebooks(request, edition_id, by=None):
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()
- edition.work.remove_old_ebooks()
- alert = 'Thanks for adding an ebook to unglue.it!'
+ 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:
- alert = 'your submitted ebook had errors'
+ ebook_form.save()
+ 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):
From dcda9f8f893231d5688892ac7f3c93ea65bf22c9 Mon Sep 17 00:00:00 2001
From: eric
Date: Fri, 26 Aug 2016 12:27:48 -0400
Subject: [PATCH 20/29] add format setting javascript
---
frontend/templates/manage_ebooks.html | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/frontend/templates/manage_ebooks.html b/frontend/templates/manage_ebooks.html
index 0537d270..cffab3f1 100644
--- a/frontend/templates/manage_ebooks.html
+++ b/frontend/templates/manage_ebooks.html
@@ -6,7 +6,27 @@
+
{% endblock %}
{% block doccontent %}
From 64de760d630bb38ca89fb13db7c47f19cf5eb6e8 Mon Sep 17 00:00:00 2001
From: eric
Date: Fri, 26 Aug 2016 13:10:43 -0400
Subject: [PATCH 21/29] test was non-deterministic
---
core/tests.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/core/tests.py b/core/tests.py
index b30cc926..8522345f 100755
--- a/core/tests.py
+++ b/core/tests.py
@@ -193,7 +193,8 @@ class BookLoaderTests(TestCase):
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)
- self.assertTrue(edition.work.works_related_from.all()[0].to_work.works_related_to.all()[0].id == edition.work.id)
+ back_set = {back.id for back in edition.work.works_related_from.all()[0].to_work.works_related_to.all() }
+ self.assertTrue(edition.work.id in back_set)
def test_populate_edition(self):
From 98d4943e2bcf91acbe865fdf61a6bb6e441732e5 Mon Sep 17 00:00:00 2001
From: eric
Date: Fri, 26 Aug 2016 13:49:48 -0400
Subject: [PATCH 22/29] Merge branch 'postdj18' into versions-relations-ednotes
# Conflicts:
---
requirements_versioned.pip | 1 -
1 file changed, 1 deletion(-)
diff --git a/requirements_versioned.pip b/requirements_versioned.pip
index 5a9391ab..e2ee4b7b 100644
--- a/requirements_versioned.pip
+++ b/requirements_versioned.pip
@@ -60,7 +60,6 @@ oauth2==1.5.211
oauthlib==1.1.2
paramiko==1.14.1
postmonkey==1.0b
-pyasn1==0.1.4
pycrypto==2.6
pymarc==3.0.2
pyparsing==2.0.3
From 243e7d8029d613035a49367594073ef9bdf3f07b Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Mon, 12 Sep 2016 10:42:12 -0700
Subject: [PATCH 23/29] fix error:
======================================================================
ERROR: test_nix (regluit.api.tests.FeedTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/raymondyee/D/Document/Gluejar/Gluejar.github/regluit/api/tests.py", line 173, in test_nix
r = self.client.get('/api/onix/by/')
File "/Users/raymondyee/anaconda/envs/regluit613/lib/python2.7/site-packages/django/test/client.py", line 500, in get
**extra)
File "/Users/raymondyee/anaconda/envs/regluit613/lib/python2.7/site-packages/django/test/client.py", line 303, in get
return self.generic('GET', path, secure=secure, **r)
File "/Users/raymondyee/anaconda/envs/regluit613/lib/python2.7/site-packages/django/test/client.py", line 379, in generic
return self.request(**r)
File "/Users/raymondyee/anaconda/envs/regluit613/lib/python2.7/site-packages/django/test/client.py", line 466, in request
six.reraise(*exc_info)
File "/Users/raymondyee/anaconda/envs/regluit613/lib/python2.7/site-packages/django/core/handlers/base.py", line 132, in get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/Users/raymondyee/anaconda/envs/regluit613/lib/python2.7/site-packages/django/views/generic/base.py", line 71, in view
return self.dispatch(request, *args, **kwargs)
File "/Users/raymondyee/anaconda/envs/regluit613/lib/python2.7/site-packages/django/views/generic/base.py", line 89, in dispatch
return handler(request, *args, **kwargs)
File "/Users/raymondyee/D/Document/Gluejar/Gluejar.github/regluit/api/views.py", line 208, in get
return HttpResponse(onix.onix_feed(facet_class, max),
File "/Users/raymondyee/D/Document/Gluejar/Gluejar.github/regluit/api/onix.py", line 25, in onix_feed
editions = facet.facet_object.filter_model("Edition",editions).distinct()
File "/Users/raymondyee/D/Document/Gluejar/Gluejar.github/regluit/core/facets.py", line 44, in filter_model
return model_filter( self._filter_model(model, query_set))
File "/Users/raymondyee/D/Document/Gluejar/Gluejar.github/regluit/core/facets.py", line 147, in edition_license_filter
return query_set.filter(ebooks__rights=cc.ccinfo(facet_name))
File "/Users/raymondyee/anaconda/envs/regluit613/lib/python2.7/site-packages/django/db/models/query.py", line 679, in filter
return self._filter_or_exclude(False, *args, **kwargs)
File "/Users/raymondyee/anaconda/envs/regluit613/lib/python2.7/site-packages/django/db/models/query.py", line 697, in _filter_or_exclude
clone.query.add_q(Q(*args, **kwargs))
File "/Users/raymondyee/anaconda/envs/regluit613/lib/python2.7/site-packages/django/db/models/sql/query.py", line 1310, in add_q
clause, require_inner = self._add_q(where_part, self.used_aliases)
File "/Users/raymondyee/anaconda/envs/regluit613/lib/python2.7/site-packages/django/db/models/sql/query.py", line 1338, in _add_q
allow_joins=allow_joins, split_subq=split_subq,
File "/Users/raymondyee/anaconda/envs/regluit613/lib/python2.7/site-packages/django/db/models/sql/query.py", line 1177, in build_filter
if isinstance(value, Iterator):
File "/Users/raymondyee/anaconda/envs/regluit613/lib/python2.7/abc.py", line 144, in __instancecheck__
return cls.__subclasscheck__(subtype)
File "/Users/raymondyee/anaconda/envs/regluit613/lib/python2.7/abc.py", line 180, in __subclasscheck__
if issubclass(subclass, scls):
File "/Users/raymondyee/anaconda/envs/regluit613/lib/python2.7/abc.py", line 161, in __subclasscheck__
ok = cls.__subclasshook__(subclass)
File "/Users/raymondyee/anaconda/envs/regluit613/lib/python2.7/site-packages/backports_abc.py", line 66, in __subclasshook__
mro = C.__mro__
AttributeError: class ccinfo has no attribute '__mro__'
---
core/cc.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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
From 298dca48b30321e7ad924528b2ac44cea16aefbc Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Mon, 12 Sep 2016 11:30:25 -0700
Subject: [PATCH 24/29] clean up .pyc and empty directories with software
update
---
deploy/update-just | 3 +++
deploy/update-prod | 3 +++
deploy/update-regluit | 3 +++
3 files changed, 9 insertions(+)
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
From 0e75a750ab741f7cff42d655fae4cb7e5df74de6 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Thu, 15 Sep 2016 11:50:31 -0700
Subject: [PATCH 25/29] an extra migration produced by `makemigrations`
---
core/migrations/0007_auto_20160913_2037.py | 49 ++++++++++++++++++++++
1 file changed, 49 insertions(+)
create mode 100644 core/migrations/0007_auto_20160913_2037.py
diff --git a/core/migrations/0007_auto_20160913_2037.py b/core/migrations/0007_auto_20160913_2037.py
new file mode 100644
index 00000000..c468c9e6
--- /dev/null
+++ b/core/migrations/0007_auto_20160913_2037.py
@@ -0,0 +1,49 @@
+# -*- 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.AlterField(
+ model_name='ebook',
+ name='version',
+ field=models.CharField(max_length=255, null=True, 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='publication_range',
+ field=models.CharField(max_length=50, null=True, blank=True),
+ ),
+ ]
From e8d4ab82be642395bb06c7d71016124387e3f488 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Thu, 22 Sep 2016 14:28:49 -0700
Subject: [PATCH 26/29] add comments to this migration
---
core/migrations/0006_auto_20160818_1809.py | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/core/migrations/0006_auto_20160818_1809.py b/core/migrations/0006_auto_20160818_1809.py
index 5ea66109..5b3cbe4c 100644
--- a/core/migrations/0006_auto_20160818_1809.py
+++ b/core/migrations/0006_auto_20160818_1809.py
@@ -5,16 +5,25 @@ 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,
@@ -25,11 +34,13 @@ class Migration(migrations.Migration):
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
From 5fc4d631ff15016d6264c6aca0a996839c15c08f Mon Sep 17 00:00:00 2001
From: eric
Date: Fri, 23 Sep 2016 14:53:54 -0400
Subject: [PATCH 27/29] split version into label and iter
---
core/bookloader.py | 2 +-
...913_2037.py => 0007_auto_20160923_1314.py} | 18 +++++-
core/models/__init__.py | 5 +-
core/models/bibmodels.py | 59 ++++++++++++-------
frontend/forms.py | 18 +++++-
frontend/templates/edition_upload.html | 28 +++------
frontend/templates/edition_uploads.html | 23 ++++----
frontend/templates/work.html | 2 +-
frontend/views.py | 5 +-
9 files changed, 98 insertions(+), 62 deletions(-)
rename core/migrations/{0007_auto_20160913_2037.py => 0007_auto_20160923_1314.py} (78%)
diff --git a/core/bookloader.py b/core/bookloader.py
index bb5468b6..d20b4b22 100755
--- a/core/bookloader.py
+++ b/core/bookloader.py
@@ -902,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/migrations/0007_auto_20160913_2037.py b/core/migrations/0007_auto_20160923_1314.py
similarity index 78%
rename from core/migrations/0007_auto_20160913_2037.py
rename to core/migrations/0007_auto_20160923_1314.py
index c468c9e6..3f1ee8b2 100644
--- a/core/migrations/0007_auto_20160913_2037.py
+++ b/core/migrations/0007_auto_20160923_1314.py
@@ -11,10 +11,19 @@ class Migration(migrations.Migration):
]
operations = [
- migrations.AlterField(
+ migrations.RemoveField(
model_name='ebook',
name='version',
- field=models.CharField(max_length=255, null=True, blank=True),
+ ),
+ 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',
@@ -41,6 +50,11 @@ class Migration(migrations.Migration):
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',
diff --git a/core/models/__init__.py b/core/models/__init__.py
index d78da01c..a00e475d 100755
--- a/core/models/__init__.py
+++ b/core/models/__init__.py
@@ -969,7 +969,7 @@ class Campaign(models.Model):
new_ebfs = []
for to_do in to_dos:
edition = to_do['ebook'].edition
- version = to_do['ebook'].version
+ 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()})
@@ -1015,7 +1015,8 @@ class Campaign(models.Model):
rights=self.license,
provider="Unglue.it",
url=ebf.file.url,
- version=ebf.version
+ version_label=ebf.version['label'],
+ version_iter=ebf.version['iter'],
)
ebf.ebook = ebook
ebf.save()
diff --git a/core/models/bibmodels.py b/core/models/bibmodels.py
index 9748be75..695e484d 100644
--- a/core/models/bibmodels.py
+++ b/core/models/bibmodels.py
@@ -90,7 +90,7 @@ 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)
+ 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)
@@ -365,11 +365,10 @@ class Work(models.Model):
return EbookFile.objects.filter(edition__work=self, format='pdf').exclude(file='').order_by('-created')
def versions(self):
- version_labels = ['']
- for ebook in self.ebooks():
- if not ebook.version_label in version_labels:
+ 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)
- version_labels.remove('')
return version_labels
def formats(self):
@@ -382,9 +381,9 @@ class Work(models.Model):
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('-created')
+ old = Ebook.objects.filter(edition__work=self, active=True).order_by('-version_iter', '-created')
- # keep most recent ebook for each format and version label
+ # 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)
@@ -1014,7 +1013,8 @@ class EbookFile(models.Model):
format='mobi',
url=new_mobi_ebf.file.url,
rights=self.ebook.rights,
- version=self.ebook.version,
+ version_label=self.ebook.version_label,
+ version_iter=self.ebook.version_iter,
)
new_mobi_ebf.ebook = new_ebook
new_mobi_ebf.save()
@@ -1030,7 +1030,8 @@ class Ebook(models.Model):
download_count = models.IntegerField(default=0)
active = models.BooleanField(default=True)
filesize = models.PositiveIntegerField(null=True)
- version = models.CharField(max_length=255, null=True, blank=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)
@@ -1087,19 +1088,35 @@ class Ebook(models.Model):
return self.provider
@property
- def version_label(self):
- if self.version is None:
- return ''
- version_match = re.search(r'(.*)\.(\d+)$',self.version)
- return version_match.group(1) if version_match else self.version
-
- @property
- def version_iter(self):
- if self.version is None:
- return 0
- version_match = re.search(r'(.*)\.(\d+)$',self.version)
- return int(version_match.group(2)) if version_match else 0
+ 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:
diff --git a/frontend/forms.py b/frontend/forms.py
index eb6e26d0..a3faf19a 100644
--- a/frontend/forms.py
+++ b/frontend/forms.py
@@ -280,8 +280,9 @@ def test_file(the_file):
raise forms.ValidationError(_('%s is not a valid PDF file' % the_file.name) )
class EbookFileForm(forms.ModelForm):
- version = forms.CharField(max_length=512, required=False)
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)
@@ -293,6 +294,10 @@ class EbookFileForm(forms.ModelForm):
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'
@@ -310,18 +315,25 @@ class EbookFileForm(forms.ModelForm):
model = EbookFile
widgets = { 'edition': forms.HiddenInput}
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')
+ 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.cleaned_data['url'])
if not new_provider:
diff --git a/frontend/templates/edition_upload.html b/frontend/templates/edition_upload.html
index 1a9324ad..812accc3 100644
--- a/frontend/templates/edition_upload.html
+++ b/frontend/templates/edition_upload.html
@@ -8,7 +8,7 @@
{% for ebook in edition.ebooks.all %}
{% if ebook.active %}
{{ ebook.format }} {{ebook.rights}} at {{ebook.provider}}.
- {% if ebook.version %} {{ ebook.version }}. {% endif %}
+ {% if ebook.version_label %} {{ ebook.version_label }} (v{{ ebook.version_iter }}). {% endif %}
Downloaded {{ ebook.download_count }} times since {{ ebook.created }}
{% endif %}
{% endfor %}
@@ -27,30 +27,20 @@
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 }}
- {% if admin %}
- {{ ebook_form.version.errors }}Version: {{ ebook_form.version }}
- {% else %}
-
+ {{ ebook_form.rights.errors }}License: {{ ebook_form.rights }}
+ Version Label (optional): {% if edition.work.versions %}
+
+ (no label)
+ {% for vers in edition.work.versions %}{{ vers }} {% endfor %}
+ or add a new version label:
{% endif %}
+ {{ ebook_form.new_version_label.errors }} {{ ebook_form.new_version_label }}
- {% if admin %}
Note on versions
- Unglue.it's version strings have two components, a label and a iteration.
- The iteration is denoted by a dot and a number at the end of version string, and is assumed to be 0 if not given explicitly.
- Unglue.it will show the user just the label and will suppress display of all but the highest iteration for a given label.
- so if the ebooks have versions "", ".1", "1.0.0", "1.0.2", "Open Access" and "Open Access.1", Unglue.it will display 3 ebooks labelled "", "1.0" and "Open Access".
- If you want ebooks from two editions with the same format to display, give them different version labels.
+ If you want ebooks from two editions with the same format and provider to display, give them different version labels.
- The existing version labels for this work are:
-