From c97888df8222da3eb351510787c038430fb37d06 Mon Sep 17 00:00:00 2001 From: eric Date: Sat, 30 Jul 2016 02:35:32 -0400 Subject: [PATCH 01/31] 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/31] 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/31] 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/31] 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/31] 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.work }} @@ -130,9 +130,8 @@ ul.fancytree-container {

Google Books ID: {{ form.goog.errors }}{{ form.goog }}

GoodReads ID: {{ form.gdrd.errors }}{{ form.gdrd }}

LibraryThing ID: {{ form.thng.errors }}{{ form.thng }}

- {% if request.user.is_staff %} -

HTTP ID: {{ form.http.errors }}{{ form.http }}

- {% endif %} +

DOI: {{ form.doi.errors }}{{ form.doi }}

+

HTTP(S) ID: {{ form.http.errors }}{{ form.http }}

Description:
{{ form.description.errors }}{{ form.description }}
({% if work.last_campaign %} @@ -175,7 +174,7 @@ ul.fancytree-container { [ no cover specified for this edition ]
{% endif %} {{ form.cover_image.errors }}{{ form.cover_image }}{{ form.cover_image.help_text }} - (Enter a URL for an image, ideally 120px wide. )
+ (Enter a URL for an image, at least 300 px wide. The image will be scaled to the proportions of a 6x9 cover. )
OR...
{{ form.coverfile.errors }}{{ form.coverfile }}{{ form.coverfile.help_text }} diff --git a/frontend/views.py b/frontend/views.py index 03d0b2d0..adb12105 100755 --- a/frontend/views.py +++ b/frontend/views.py @@ -549,6 +549,7 @@ def new_edition(request, work_id, edition_id, by=None): 'gdrd': edition.goodreads_id, 'thng': edition.librarything_id, 'http': edition.http_id, + 'doi': edition.id_for('doi'), } if request.method == 'POST' : form = None @@ -572,6 +573,7 @@ def new_edition(request, work_id, edition_id, by=None): elif not form and admin: form = EditionForm(instance=edition, data=request.POST, files=request.FILES) if form.is_valid(): + print 'form is valid' form.save() if not work: work= models.Work(title=form.cleaned_data['title'],language=form.cleaned_data['language'],description=form.cleaned_data['description']) @@ -586,7 +588,7 @@ def new_edition(request, work_id, edition_id, by=None): work.save() id_msg="" - for id_type in ('isbn', 'oclc', 'goog', 'thng', 'gdrd', 'http'): + for id_type in ('isbn', 'oclc', 'goog', 'thng', 'gdrd', 'http', 'doi'): id_val = form.cleaned_data[id_type] if id_val=='delete': edition.identifiers.filter(type=id_type).delete() From 9b0c91a4397f7a1d2e2cf0680d7a5c964ca10eb6 Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 15 Aug 2016 13:47:00 -0400 Subject: [PATCH 06/31] implement age_level --- api/onix.py | 14 ++++++++++++++ api/opds.py | 10 ++++++++++ frontend/forms.py | 3 ++- frontend/templates/new_edition.html | 1 + frontend/templates/work.html | 2 ++ frontend/views.py | 8 ++++++++ 6 files changed, 37 insertions(+), 1 deletion(-) diff --git a/api/onix.py b/api/onix.py index a504cb9e..1aba6b76 100644 --- a/api/onix.py +++ b/api/onix.py @@ -1,5 +1,6 @@ import datetime import pytz +import re from lxml import etree from regluit.core import models from regluit.core.cc import ccinfo @@ -134,6 +135,19 @@ def product(edition, facet=None): subj_node.append(text_node("SubjectSchemeIdentifier", "20")) subj_node.append(text_node("SubjectHeadingText", subject.name)) + # audience range composite + if work.age_level: + range_match = re.search(r'(\d?\d?)-(\d?\d?)', work.age_level) + if range_match: + audience_range_node = etree.SubElement(descriptive_node, "AudienceRange") + audience_range_node.append(text_node("AudienceRangeQualifier", "17")) #Interest age, years + if range_match.group(1): + audience_range_node.append(text_node("AudienceRangePrecision", "03")) #from + audience_range_node.append(text_node("AudienceRangeValue", range_match.group(1))) + if range_match.group(2): + audience_range_node.append(text_node("AudienceRangePrecision", "04")) #from + audience_range_node.append(text_node("AudienceRangeValue", range_match.group(2))) + # Collateral Detail Block coll_node = etree.SubElement(product_node, "CollateralDetail") desc_node = etree.SubElement(coll_node, "TextContent") diff --git a/api/opds.py b/api/opds.py index 711b9f30..297d6a8b 100644 --- a/api/opds.py +++ b/api/opds.py @@ -163,6 +163,16 @@ def work_node(work, facet=None): # caused by control chars in subject.name logger.warning('Deleting subject: %s' % subject.name) subject.delete() + + # age level + # + if work.age_level: + category_node = etree.Element("category") + category_node.attrib["scheme"] = 'http://schema.org/typicalAgeRange' + category_node.attrib["term"] = work.age_level + category_node.attrib["label"] = work.get_age_level_display() + node.append(category_node) + # rating rating_node = etree.Element("{http://schema.org/}Rating") diff --git a/frontend/forms.py b/frontend/forms.py index 578cf4a0..0566bfe5 100644 --- a/frontend/forms.py +++ b/frontend/forms.py @@ -58,7 +58,7 @@ from regluit.core.models import ( UNGLUEITAR ) from regluit.libraryauth.models import Library -from regluit.core.parameters import LIBRARY, REWARDS, BUY2UNGLUE, THANKS +from regluit.core.parameters import LIBRARY, REWARDS, BUY2UNGLUE, THANKS, AGE_LEVEL_CHOICES from regluit.core.lookups import ( OwnerLookup, WorkLookup, @@ -199,6 +199,7 @@ class EditionForm(forms.ModelForm): } ) language = forms.ChoiceField(choices=LANGUAGES) + age_level = forms.ChoiceField(choices=AGE_LEVEL_CHOICES) description = forms.CharField( required=False, widget=CKEditorWidget()) coverfile = forms.ImageField(required=False) diff --git a/frontend/templates/new_edition.html b/frontend/templates/new_edition.html index 57adff02..ba2ae8a8 100644 --- a/frontend/templates/new_edition.html +++ b/frontend/templates/new_edition.html @@ -122,6 +122,7 @@ ul.fancytree-container {

Language: {{ form.language.errors }}{{ form.language }}

+

Age Level: {{ form.age_level.errors }}{{ form.age_level }}

Identifiers

{% if id_msg %} {{ id_msg }} {% endif %}

Enter 'delete' to remove the identifier.

diff --git a/frontend/templates/work.html b/frontend/templates/work.html index e8494a91..48d5ffa9 100644 --- a/frontend/templates/work.html +++ b/frontend/templates/work.html @@ -102,6 +102,8 @@ {{ work.last_campaign.publisher }} {% endif %} + + diff --git a/frontend/views.py b/frontend/views.py index adb12105..29d6534b 100755 --- a/frontend/views.py +++ b/frontend/views.py @@ -514,11 +514,15 @@ def new_edition(request, work_id, edition_id, by=None): language='en' description='' title='' + age_level = '' + description = '' + title = '' if work_id: work = safe_get_work(work_id) language=work.language description=work.description title=work.title + age_level = work.age_level else: work=None @@ -533,6 +537,7 @@ def new_edition(request, work_id, edition_id, by=None): edition.work = work language=edition.work.language description=edition.work.description + age_level = edition.work.age_level else: edition = models.Edition() if work: @@ -552,6 +557,7 @@ def new_edition(request, work_id, edition_id, by=None): 'doi': edition.id_for('doi'), } if request.method == 'POST' : + 'age_level': age_level, form = None edition.new_authors=zip(request.POST.getlist('new_author'),request.POST.getlist('new_author_relation')) edition.new_subjects=request.POST.getlist('new_subject') @@ -577,6 +583,7 @@ def new_edition(request, work_id, edition_id, by=None): form.save() if not work: work= models.Work(title=form.cleaned_data['title'],language=form.cleaned_data['language'],description=form.cleaned_data['description']) + age_level=form.cleaned_data['age_level'], work.save() edition.work=work edition.save() @@ -585,6 +592,7 @@ def new_edition(request, work_id, edition_id, by=None): work.title=form.cleaned_data['title'] work.publication_range = None # will reset on next access work.language = form.cleaned_data['language'] + work.age_level = form.cleaned_data['age_level'] work.save() id_msg="" From f6bbc5a548e16716d836cebcc38f4efc29bc6d5f Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 15 Aug 2016 15:22:32 -0400 Subject: [PATCH 07/31] pylint the views --- frontend/views.py | 1406 ++++++++++++++++++++++----------------------- 1 file changed, 697 insertions(+), 709 deletions(-) diff --git a/frontend/views.py b/frontend/views.py index 29d6534b..74458006 100755 --- a/frontend/views.py +++ b/frontend/views.py @@ -7,35 +7,27 @@ import json import logging import urllib import requests -import random -import oauth2 as oauth from datetime import timedelta, date from decimal import Decimal as D -from itertools import islice, chain +from itertools import chain from notification import models as notification from random import randint -from re import sub -from xml.etree import ElementTree as ET from tastypie.models import ApiKey -''' -django imports -''' -from django import forms +#django imports + from django.apps import apps from django.conf import settings from django.contrib import messages -from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User -from django.contrib.auth.views import login,password_reset, redirect_to_login +from django.contrib.auth.views import redirect_to_login from django_comments.models import Comment from django.contrib.sites.models import Site from django.core import signing -from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.exceptions import ValidationError from django.core.files.storage import default_storage -from django.core.files.temp import NamedTemporaryFile from django.core.mail import EmailMessage from django.core.urlresolvers import reverse, reverse_lazy from django.core.validators import validate_email @@ -48,7 +40,7 @@ from django.http import ( HttpResponse, HttpResponseNotFound ) -from django.shortcuts import render, render_to_response, get_object_or_404 +from django.shortcuts import render, get_object_or_404 from django.template import TemplateDoesNotExist from django.template.loader import render_to_string from django.utils.http import urlencode @@ -59,19 +51,16 @@ from django.views.generic.edit import FormView from django.views.generic.list import ListView from django.views.generic.base import ( TemplateView, - View ) -''' -regluit imports -''' +#regluit imports + from regluit.core import ( tasks, models, bookloader, librarything, userlists, - goodreads, ) import regluit.core.cc as cc from regluit.core.bookloader import merge_works, detach_edition @@ -79,7 +68,7 @@ from regluit.core.goodreads import GoodreadsClient from regluit.core.isbn import ISBN from regluit.core.search import gluejar_search from regluit.core.signals import supporter_message -from regluit.core.tasks import send_mail_task, emit_notifications, watermark_acq +from regluit.core.tasks import send_mail_task, watermark_acq from regluit.core.parameters import * from regluit.core.facets import get_facet_object, get_order_by @@ -126,24 +115,19 @@ from regluit.frontend.forms import ( from regluit.payment import baseprocessor, stripelib from regluit.payment.credit import credit_transaction from regluit.payment.manager import PaymentManager -from regluit.payment.models import Transaction, Account, Sent, CreditLog +from regluit.payment.models import Transaction, CreditLog from regluit.payment.parameters import ( TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE, TRANSACTION_STATUS_CANCELED, - TRANSACTION_STATUS_ERROR, - TRANSACTION_STATUS_FAILED, TRANSACTION_STATUS_INCOMPLETE, TRANSACTION_STATUS_NONE, TRANSACTION_STATUS_MODIFIED, PAYMENT_TYPE_AUTHORIZATION, - PAYMENT_TYPE_INSTANT, - PAYMENT_HOST_STRIPE, PAYMENT_HOST_NONE ) from regluit.utils.localdatetime import now, date_today -from regluit.pyepub import InvalidEpub from regluit.libraryauth.forms import UserNamePass from regluit.libraryauth.views import Authenticator, superlogin, login_user from regluit.libraryauth.models import Library @@ -160,7 +144,7 @@ def slideshow(): ending = models.Campaign.objects.filter(status='ACTIVE').order_by('deadline') count = ending.count() j = 0 - + worklist = [] if max > count: # add all the works with active campaigns @@ -172,13 +156,13 @@ def slideshow(): remainder_works = models.Work.objects.filter(campaigns__status="SUCCESSFUL").order_by('-campaigns__deadline')[:remainder] worklist.extend(remainder_works) else: - # if the active campaign list has more works than we can fit + # if the active campaign list has more works than we can fit # in our slideshow, it's the only source we need to draw from while j < max: worklist.append(ending[j].work) - j +=1 - - return (worklist[:4],worklist[4:8]) + j += 1 + + return (worklist[:4], worklist[4:8]) def process_kindle_email(request): """ @@ -198,7 +182,7 @@ def next(request): return response else: return HttpResponseRedirect('/') - + def safe_get_work(work_id): """ use this rather than querying the db directly for a work by id @@ -208,19 +192,19 @@ def safe_get_work(work_id): except models.Work.DoesNotExist: raise Http404 return work - + def cover_width(work): if work.percent_of_goal() < 100: cover_width = 100 - work.percent_of_goal() else: cover_width = 0 - + return cover_width def home(request, landing=False): faves = None if request.user.is_authenticated() : - next=request.GET.get('next', False) + next = request.GET.get('next', False) if next: # should happen only for new users return HttpResponseRedirect(next) @@ -236,14 +220,14 @@ def home(request, landing=False): except: #shouldn't occur except in tests featured = models.Work.objects.all()[0] - top_pledge = models.Campaign.objects.filter(status="ACTIVE",type=REWARDS).order_by('left')[:4] + top_pledge = models.Campaign.objects.filter(status="ACTIVE", type=REWARDS).order_by('left')[:4] top_b2u = models.Campaign.objects.filter(status="ACTIVE", type=BUY2UNGLUE).order_by('-work__num_wishes')[:4] - top_t4u = models.Campaign.objects.exclude(id = featured.id).filter(status="ACTIVE",type=THANKS).order_by('-work__num_wishes')[:4] + top_t4u = models.Campaign.objects.exclude(id = featured.id).filter(status="ACTIVE", type=THANKS).order_by('-work__num_wishes')[:4] most_wished = models.Work.objects.order_by('-num_wishes')[:4] - + unglued_books = models.Work.objects.filter(campaigns__status="SUCCESSFUL").order_by('-campaigns__deadline')[:4] - + cc_books = models.Work.objects.exclude(id = featured.id).filter( featured__isnull=False, ).distinct().order_by('-featured')[:4] @@ -275,40 +259,40 @@ def home(request, landing=False): lambda x: (x.submit_date, x, 'comment'), latest_comments ) - + latest_pledges_tuple = map( lambda x: (x.date_created, x, 'pledge'), latest_pledges ) - + latest_wishes_tuple = map( lambda x: (x.created, x, 'wish'), latest_wishes ) - + """ merge latest actions into a single list, sorted by date, to loop through in template """ latest_actions = sorted( - chain(latest_comments_tuple, latest_pledges_tuple, latest_wishes_tuple), + chain(latest_comments_tuple, latest_pledges_tuple, latest_wishes_tuple), key=lambda instance: instance[0], reverse=True ) - + if request.user.is_authenticated(): events = latest_actions[:12] else: events = latest_actions[:6] - + return render( request, - 'home.html', + 'home.html', { - 'events': events, - 'top_pledge': top_pledge, - 'top_b2u': top_b2u, - 'top_t4u': top_t4u, - 'unglued_books': unglued_books, + 'events': events, + 'top_pledge': top_pledge, + 'top_b2u': top_b2u, + 'top_t4u': top_t4u, + 'unglued_books': unglued_books, 'cc_books': cc_books, 'most_wished': most_wished, 'featured': featured, @@ -318,20 +302,20 @@ def home(request, landing=False): def stub(request): path = request.path[6:] # get rid of /stub/ - return render(request,'stub.html', {'path': path}) + return render(request, 'stub.html', {'path': path}) def acks(request, work): - return render(request,'front_matter.html', {'campaign': work.last_campaign()}) - + return render(request, 'front_matter.html', {'campaign': work.last_campaign()}) + def work(request, work_id, action='display'): work = safe_get_work(work_id) - alert='' + alert = '' formset = None if action == "acks": - return acks( request, work) + return acks(request, work) elif action == "editions": - EditionFormSet = inlineformset_factory(models.Work, models.Edition, fields=(), extra=0 ) + EditionFormSet = inlineformset_factory(models.Work, models.Edition, fields=(), extra=0) if request.method == "POST" and (request.user.is_staff or (work.last_campaign() and request.user in work.last_campaign().managers.all())): formset = EditionFormSet(data=request.POST, instance=work) if formset.is_valid(): @@ -339,10 +323,10 @@ def work(request, work_id, action='display'): detach_edition(form.instance) alert = 'editions have been split' if request.POST.has_key('select_edition'): - selected_id=request.POST['select_edition'] + selected_id = request.POST['select_edition'] try: - work.selected_edition= work.editions.get(id=selected_id) - work.title=work.selected_edition.title + work.selected_edition = work.editions.get(id=selected_id) + work.title = work.selected_edition.title work.save() alert = alert + 'edition selected' except models.Edition.DoesNotExist: @@ -355,9 +339,9 @@ def work(request, work_id, action='display'): if add_url == request.path: request.user.wishlist.add_work(work, "login", notify=True) request.session.pop("add_wishlist") - + process_kindle_email(request) - + if request.method == 'POST' and not request.user.is_anonymous(): activetab = '4' elif action == 'editions': @@ -366,40 +350,40 @@ def work(request, work_id, action='display'): try: activetab = request.GET['tab'] if activetab not in ['1', '2', '3', '4']: - activetab = '1'; + activetab = '1' except: - activetab = '1'; - + activetab = '1' + campaign = work.last_campaign() editions = work.editions.all().order_by('-publication_date')[:10] try: pledged = campaign.transactions().filter(user=request.user, status="ACTIVE") except: pledged = None - + cover_width_number = 0 - + if work.last_campaign_status() == 'ACTIVE': cover_width_number = cover_width(work) - + if action == 'preview': work.last_campaign_status = 'ACTIVE' - + if not request.user.is_anonymous(): - claimform = UserClaimForm( request.user, data={'claim-work':work.pk, 'claim-user': request.user.id}, prefix = 'claim') + claimform = UserClaimForm(request.user, data={'claim-work':work.pk, 'claim-user': request.user.id}, prefix = 'claim') else: claimform = None - + if campaign: # pull up premiums explicitly tied to the campaign # mandatory premiums are only displayed in pledge process premiums = campaign.custom_premiums() else: premiums = None - + wishers = work.num_wishes base_url = request.build_absolute_uri("/")[:-1] - + active_claims = work.claim.all().filter(status='active') if active_claims.count() == 1: claimstatus = 'one_active' @@ -409,17 +393,17 @@ def work(request, work_id, action='display'): pending_claims = work.claim.all().filter(status='pending') pending_claims_count = pending_claims.count() if pending_claims_count > 1: - claimstatus = 'disputed' + claimstatus = 'disputed' elif pending_claims_count == 1: - claimstatus = 'one_pending' - rights_holder_name = pending_claims[0].rights_holder.rights_holder_name + claimstatus = 'one_pending' + rights_holder_name = pending_claims[0].rights_holder.rights_holder_name else: - claimstatus = 'open' - + claimstatus = 'open' + return render(request, 'work.html', { - 'work': work, - 'premiums': premiums, - 'ungluers': userlists.supporting_users(work, 5), + 'work': work, + 'premiums': premiums, + 'ungluers': userlists.supporting_users(work, 5), 'claimform': claimform, 'wishers': wishers, 'base_url': base_url, @@ -433,7 +417,7 @@ def work(request, work_id, action='display'): 'action': action, 'formset': formset, 'kwform': SubjectSelectForm() - }) + }) def edition_uploads(request, edition_id): context = {} @@ -453,48 +437,48 @@ def edition_uploads(request, edition_id): logger.info("EbookFileForm is_valid") form.save() edition.work.last_campaign().save() - context['uploaded']=True + context['uploaded'] = True if campaign_type == BUY2UNGLUE: if edition.work.last_campaign().status == 'SUCCESSFUL': try: edition.work.last_campaign().watermark_success() except Exception as e: - context['upload_error']= e + context['upload_error'] = e form.instance.delete() else: # campaign mangager gets a copy - test_acq = models.Acq.objects.create(user=request.user,work=edition.work,license= TESTING) + test_acq = models.Acq.objects.create(user=request.user, work=edition.work, license= TESTING) try: test_acq.get_watermarked() - context['watermarked']= test_acq.watermarked + context['watermarked'] = test_acq.watermarked except Exception as e: - context['upload_error']= e + context['upload_error'] = e form.instance.delete() if campaign_type == THANKS: e = form.instance.check_file() if e != None: logger.error(e) - context['upload_error']= e + context['upload_error'] = e form.instance.delete() else: tasks.process_ebfs.delay(edition.work.last_campaign()) else: - context['upload_error']= form.errors - form = EbookFileForm(initial={'edition':edition,'format':'epub'}, campaign_type=campaign_type) + context['upload_error'] = form.errors + form = EbookFileForm(initial={'edition':edition, 'format':'epub'}, campaign_type=campaign_type) context.update({ - 'form': form, 'edition': edition, + 'form': form, 'edition': edition, 'ebook_files': models.EbookFile.objects.filter(edition = edition) }) - return render(request, 'edition_uploads.html', context ) + return render(request, 'edition_uploads.html', context) -def add_subject(subject_name,work, authority=''): +def add_subject(subject_name, work, authority=''): try: - subject= models.Subject.objects.get(name=subject_name) + subject = models.Subject.objects.get(name=subject_name) except models.Subject.DoesNotExist: - subject=models.Subject.objects.create(name=subject_name, authority=authority) + subject = models.Subject.objects.create(name=subject_name, authority=authority) subject.works.add(work) -def user_can_edit_work(user,work): +def user_can_edit_work(user, work): if user.is_staff : return True elif work and work.last_campaign(): @@ -503,30 +487,28 @@ def user_can_edit_work(user,work): return True else: return False - + @login_required def new_edition(request, work_id, edition_id, by=None): - if not request.user.is_authenticated() : + if not request.user.is_authenticated(): return render(request, "admins_only.html") - # if the work and edition are set, we save the edition and set the work - language='en' - description='' - title='' + # if the work and edition are set, we save the edition and set the work + language = 'en' age_level = '' description = '' title = '' if work_id: work = safe_get_work(work_id) - language=work.language - description=work.description - title=work.title + language = work.language age_level = work.age_level + description = work.description + title = work.title else: - work=None - - alert = '' + work = None + + alert = '' admin = user_can_edit_work(request.user, work) if edition_id: try: @@ -534,33 +516,33 @@ def new_edition(request, work_id, edition_id, by=None): except models.Edition.DoesNotExist: raise Http404 if work: - edition.work = work - language=edition.work.language - description=edition.work.description + edition.work = work + language = edition.work.language age_level = edition.work.age_level + description = edition.work.description else: edition = models.Edition() if work: - edition.work = work + edition.work = work - initial={ - 'language':language, - 'publisher_name':edition.publisher_name, - 'isbn':edition.isbn_13, - 'oclc':edition.oclc, - 'description':description, - 'title': title, - 'goog': edition.googlebooks_id, - 'gdrd': edition.goodreads_id, - 'thng': edition.librarything_id, - 'http': edition.http_id, - 'doi': edition.id_for('doi'), - } - if request.method == 'POST' : + initial = { + 'language': language, 'age_level': age_level, + 'publisher_name': edition.publisher_name, + 'isbn': edition.isbn_13, + 'oclc': edition.oclc, + 'description': description, + 'title': title, + 'goog': edition.googlebooks_id, + 'gdrd': edition.goodreads_id, + 'thng': edition.librarything_id, + 'http': edition.http_id, + 'doi': edition.id_for('doi'), + } + if request.method == 'POST': form = None - edition.new_authors=zip(request.POST.getlist('new_author'),request.POST.getlist('new_author_relation')) - edition.new_subjects=request.POST.getlist('new_subject') + edition.new_authors = zip(request.POST.getlist('new_author'), request.POST.getlist('new_author_relation')) + edition.new_subjects = request.POST.getlist('new_subject') if edition.id and admin: for author in edition.authors.all(): if request.POST.has_key('delete_author_%s' % author.id): @@ -571,42 +553,46 @@ def new_edition(request, work_id, edition_id, by=None): new_author_name = request.POST['add_author'].strip() new_author_relation = request.POST['add_author_relation'] try: - author= models.Author.objects.get(name=new_author_name) + author = models.Author.objects.get(name=new_author_name) except models.Author.DoesNotExist: - author=models.Author.objects.create(name=new_author_name) - edition.new_authors.append((new_author_name,new_author_relation)) + author = models.Author.objects.create(name=new_author_name) + edition.new_authors.append((new_author_name, new_author_relation)) form = EditionForm(instance=edition, data=request.POST, files=request.FILES) - elif not form and admin: + elif not form and admin: form = EditionForm(instance=edition, data=request.POST, files=request.FILES) if form.is_valid(): print 'form is valid' form.save() if not work: - work= models.Work(title=form.cleaned_data['title'],language=form.cleaned_data['language'],description=form.cleaned_data['description']) + work = models.Work( + title=form.cleaned_data['title'], + language=form.cleaned_data['language'], age_level=form.cleaned_data['age_level'], + description=form.cleaned_data['description'], + ) work.save() - edition.work=work + edition.work = work edition.save() else: - work.description=form.cleaned_data['description'] - work.title=form.cleaned_data['title'] + work.description = form.cleaned_data['description'] + work.title = form.cleaned_data['title'] work.publication_range = None # will reset on next access work.language = form.cleaned_data['language'] work.age_level = form.cleaned_data['age_level'] work.save() - - id_msg="" + + id_msg = "" for id_type in ('isbn', 'oclc', 'goog', 'thng', 'gdrd', 'http', 'doi'): id_val = form.cleaned_data[id_type] - if id_val=='delete': + if id_val == 'delete': edition.identifiers.filter(type=id_type).delete() elif id_val: - existing= models.Identifier.objects.filter(type=id_type, value=form.cleaned_data[id_type]) + existing = models.Identifier.objects.filter(type=id_type, value=form.cleaned_data[id_type]) if existing.count() and existing[0].edition != edition: - return render(request, 'new_edition.html', { - 'form': form, 'edition': edition, 'admin': admin, - 'id_msg': "%s = %s already exists"%( id_type, id_val ), - }) + return render(request, 'new_edition.html', { + 'form': form, 'edition': edition, 'admin': admin, + 'id_msg': "%s = %s already exists"%(id_type, id_val), + }) else: models.Identifier.set(type=id_type, value=id_val, edition=edition, work=work) for relator in edition.relators.all(): @@ -614,19 +600,19 @@ def new_edition(request, work_id, edition_id, by=None): new_relation = request.POST['change_relator_%s' % relator.id] relator.set(new_relation) for (author_name, author_relation) in edition.new_authors: - edition.add_author(author_name,author_relation) + edition.add_author(author_name, author_relation) if form.cleaned_data.has_key('bisac'): - bisacsh=form.cleaned_data['bisac'] + bisacsh = form.cleaned_data['bisac'] while bisacsh: add_subject(bisacsh.full_label, work, authority="bisacsh") bisacsh = bisacsh.parent for subject_name in edition.new_subjects: add_subject(subject_name, work) work_url = reverse('work', kwargs={'work_id': edition.work.id}) - cover_file=form.cleaned_data.get("coverfile",None) + cover_file = form.cleaned_data.get("coverfile", None) if cover_file: - # save it - cover_file_name= '/Users/%s/covers/%s/%s' % ( request.user.username, edition.pk, cover_file.name) + # save it + cover_file_name = '/Users/%s/covers/%s/%s' % (request.user.username, edition.pk, cover_file.name) file = default_storage.open(cover_file_name, 'w') file.write(cover_file.read()) file.close() @@ -638,7 +624,6 @@ def new_edition(request, work_id, edition_id, by=None): form = EditionForm(instance=edition, initial=initial) return render(request, 'new_edition.html', { 'form': form, 'edition': edition, 'admin':admin, 'alert':alert, - }) @@ -654,20 +639,20 @@ def manage_ebooks(request, edition_id, by=None): 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 = '' + # 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(): + elif work == None and request.user.rights_holder.count(): admin = True 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') + 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): @@ -675,35 +660,35 @@ def manage_ebooks(request, edition_id, by=None): 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) + edition.ebook_form = EbookForm(data = request.POST, prefix = 'ebook_%d'%edition.id) if edition.ebook_form.is_valid(): edition.ebook_form.save() alert = 'Thanks for adding an ebook to unglue.it!' - else: + 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) + edition.ebook_form = EbookForm(instance= models.Ebook(user = request.user, edition = edition, provider = 'x'), prefix = 'ebook_%d'%edition.id) try: show_ebook_form = 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, + 'show_ebook_form':show_ebook_form, }) def campaign_results(request, campaign): return render(request, 'campaign_results.html', { - 'campaign': campaign, + 'campaign': campaign, }) - - + + def manage_campaign(request, id, action='manage'): campaign = get_object_or_404(models.Campaign, id=id) - campaign.not_manager=False - campaign.problems=[] + campaign.not_manager = False + campaign.problems = [] if (not request.user.is_authenticated) or (not request.user in campaign.managers.all() and not request.user.is_staff): - campaign.not_manager=True + campaign.not_manager = True return render(request, 'manage_campaign.html', {'campaign': campaign}) if action == 'results': return campaign_results(request, campaign) @@ -711,7 +696,7 @@ def manage_campaign(request, id, action='manage'): activetab = '#1' offers = campaign.work.offers.all() for offer in offers: - offer.offer_form=OfferForm(instance=offer,prefix='offer_%d'%offer.id) + offer.offer_form = OfferForm(instance=offer, prefix='offer_%d'%offer.id) if request.method == 'POST' : if request.POST.has_key('add_premium') : @@ -721,20 +706,20 @@ def manage_campaign(request, id, action='manage'): alerts.append(_('New premium has been added')) new_premium_form = CustomPremiumForm(initial={'campaign': campaign}) else: - alerts.append(_('New premium has not been added')) + alerts.append(_('New premium has not been added')) form = getManageCampaignForm(instance=campaign) activetab = '#2' elif request.POST.has_key('save') or request.POST.has_key('launch') : - form= getManageCampaignForm(instance=campaign, data=request.POST) - if form.is_valid(): - form.save() + form = getManageCampaignForm(instance=campaign, data=request.POST) + if form.is_valid(): + form.save() campaign.dollar_per_day = None campaign.set_dollar_per_day() - campaign.work.selected_edition=campaign.edition - if campaign.type in {BUY2UNGLUE,THANKS} : - offers= campaign.work.create_offers() + campaign.work.selected_edition = campaign.edition + if campaign.type in {BUY2UNGLUE, THANKS} : + offers = campaign.work.create_offers() for offer in offers: - offer.offer_form=OfferForm(instance=offer,prefix='offer_%d'%offer.id) + offer.offer_form = OfferForm(instance=offer, prefix='offer_%d'%offer.id) campaign.update_left() if campaign.type is THANKS : campaign.work.description = form.cleaned_data['work_description'] @@ -761,20 +746,20 @@ def manage_campaign(request, id, action='manage'): if selected_premium.type == 'CU': selected_premium.type = 'XX' selected_premium.save() - alerts.append(_('Premium %s has been inactivated'% premium_to_stop)) + alerts.append(_('Premium %s has been inactivated'% premium_to_stop)) form = getManageCampaignForm(instance=campaign) new_premium_form = CustomPremiumForm(initial={'campaign': campaign}) elif request.POST.has_key('change_offer'): for offer in offers : if request.POST.has_key('offer_%d-work' % offer.id) : - offer.offer_form=OfferForm(instance=offer, data = request.POST, prefix='offer_%d'%offer.id) + offer.offer_form = OfferForm(instance=offer, data = request.POST, prefix='offer_%d'%offer.id) if offer.offer_form.is_valid(): offer.offer_form.save() offer.active = True offer.save() alerts.append(_('Offer has been changed')) else: - alerts.append(_('Offer has not been changed')) + alerts.append(_('Offer has not been changed')) form = getManageCampaignForm(instance=campaign) new_premium_form = CustomPremiumForm(data={'campaign': campaign}) activetab = '#2' @@ -782,16 +767,16 @@ def manage_campaign(request, id, action='manage'): if action == 'makemobi': tasks.make_mobi.delay(campaign) return HttpResponseRedirect(reverse('mademobi', args=[campaign.id])) - elif action =='mademobi': + elif action == 'mademobi': alerts.append('A MOBI file is being generated') form = getManageCampaignForm(instance=campaign, initial={'work_description':campaign.work.description}) new_premium_form = CustomPremiumForm(initial={'campaign': campaign}) - + return render(request, 'manage_campaign.html', { - 'campaign': campaign, - 'form':form, - 'problems': campaign.problems, - 'alerts': alerts, + 'campaign': campaign, + 'form':form, + 'problems': campaign.problems, + 'alerts': alerts, 'premiums' : campaign.custom_premiums(), 'premium_form' : new_premium_form, 'work': campaign.work, @@ -799,10 +784,10 @@ def manage_campaign(request, id, action='manage'): 'offers':offers, 'action':action, }) - + def googlebooks(request, googlebooks_id): - try: - edition = models.Identifier.objects.get(type='goog',value=googlebooks_id).edition + try: + edition = models.Identifier.objects.get(type='goog', value=googlebooks_id).edition except models.Identifier.DoesNotExist: try: edition = bookloader.add_by_googlebooks_id(googlebooks_id) @@ -849,29 +834,29 @@ class MapSubjectView(FormView): Allows a staffer to add given subject to all works with given the onto_subject keyword. e.g., subject = "Language" onto_subject="English language" """ - template_name="map_subject.html" + template_name = "map_subject.html" form_class = MapSubjectForm - + def dispatch(self, request, *args, **kwargs): if not request.user.is_staff: return render(request, "admins_only.html") else: return super(MapSubjectView, self).dispatch(request, *args, **kwargs) - + def form_valid(self, form): - context=self.get_context_data() - context['subject']=form.cleaned_data['subject'] - context['onto_subject']=form.cleaned_data['onto_subject'] + context = self.get_context_data() + context['subject'] = form.cleaned_data['subject'] + context['onto_subject'] = form.cleaned_data['onto_subject'] if self.request.POST.has_key('confirm_map_subject'): initial_count = context['onto_subject'].works.all().count() initial_free_count = context['onto_subject'].works.filter(is_free=True).count() context['onto_subject'].works.add(*list(context['subject'].works.all())) - context['map_complete']=True + context['map_complete'] = True context['form'] = MapSubjectForm(initial=form.cleaned_data) context['added'] = context['onto_subject'].works.all().count() - initial_count context['added_free'] = context['onto_subject'].works.filter(is_free=True).count() - initial_free_count else: - context['form']=MapSubjectForm(initial=form.cleaned_data) + context['form'] = MapSubjectForm(initial=form.cleaned_data) return render(self.request, self.template_name, context) class FilterableListView(ListView): @@ -884,30 +869,30 @@ class FilterableListView(ListView): return self.get_queryset_all().filter(language=self.request.GET['pub_lang']) else: return self.get_queryset_all() - + def get_context_data(self, **kwargs): context = super(FilterableListView, self).get_context_data(**kwargs) if self.request.GET.has_key('pub_lang'): - context['pub_lang']=self.request.GET['pub_lang'] + context['pub_lang'] = self.request.GET['pub_lang'] else: - context['pub_lang']='' - context['show_langs']=True - context['WISHED_LANGS']=settings.WISHED_LANGS + context['pub_lang'] = '' + context['show_langs'] = True + context['WISHED_LANGS'] = settings.WISHED_LANGS return context - + def render_to_response(self, context, **response_kwargs): if self.send_marc: return qs_marc_records(self.request, qs=self.object_list) else: - return super(FilterableListView,self).render_to_response(context, **response_kwargs) + return super(FilterableListView, self).render_to_response(context, **response_kwargs) -recommended_user = User.objects.filter( username=settings.UNGLUEIT_RECOMMENDED_USERNAME) +recommended_user = User.objects.filter(username=settings.UNGLUEIT_RECOMMENDED_USERNAME) class WorkListView(FilterableListView): template_name = "work_list.html" context_object_name = "work_list" - max_works=100000 - + max_works = 100000 + def get_queryset_all(self): facet = self.kwargs.get('facet', None) if (facet == 'popular'): @@ -919,31 +904,31 @@ class WorkListView(FilterableListView): return models.Work.objects.exclude(num_wishes=0).order_by('-created', '-num_wishes' ,'id') else: return models.Work.objects.all().order_by('-created', 'id') - + def get_context_data(self, **kwargs): - context = super(WorkListView, self).get_context_data(**kwargs) - qs=self.get_queryset() - context['ungluers'] = userlists.work_list_users(qs,5) - context['facet'] = self.kwargs.get('facet','') - works_unglued = qs.filter(is_free = True).distinct() | qs.filter(campaigns__status='SUCCESSFUL').distinct() - context['works_unglued'] = works_unglued[:self.max_works] - context['works_active'] = qs.filter(campaigns__status='ACTIVE').distinct()[:self.max_works] - context['works_wished'] = qs.filter(is_free=False).exclude(campaigns__status='ACTIVE').exclude(campaigns__status='SUCCESSFUL').distinct()[:self.max_works] - - counts={} - counts['unglued'] = context['works_unglued'].count() - counts['unglueing'] = context['works_active'].count() - counts['wished'] = context['works_wished'].count() - context['counts'] = counts - - if counts['unglueing']: - context['activetab'] = "#2" - elif counts['unglued']: - context['activetab'] = "#1" - else: - context['activetab'] = "#3" - - return context + context = super(WorkListView, self).get_context_data(**kwargs) + qs = self.get_queryset() + context['ungluers'] = userlists.work_list_users(qs, 5) + context['facet'] = self.kwargs.get('facet','') + works_unglued = qs.filter(is_free = True).distinct() | qs.filter(campaigns__status='SUCCESSFUL').distinct() + context['works_unglued'] = works_unglued[:self.max_works] + context['works_active'] = qs.filter(campaigns__status='ACTIVE').distinct()[:self.max_works] + context['works_wished'] = qs.filter(is_free=False).exclude(campaigns__status='ACTIVE').exclude(campaigns__status='SUCCESSFUL').distinct()[:self.max_works] + + counts = {} + counts['unglued'] = context['works_unglued'].count() + counts['unglueing'] = context['works_active'].count() + counts['wished'] = context['works_wished'].count() + context['counts'] = counts + + if counts['unglueing']: + context['activetab'] = "#2" + elif counts['unglued']: + context['activetab'] = "#1" + else: + context['activetab'] = "#3" + + return context class FacetedView(FilterableListView): template_name = "faceted_list.html" @@ -951,7 +936,7 @@ class FacetedView(FilterableListView): if not hasattr(self,'vertex'): facet_path = self.kwargs.get('path', '') self.vertex = get_facet_object(facet_path) - + order_by = self.request.GET.get('order_by', 'newest') #special cases if order_by == 'subjects': @@ -961,7 +946,7 @@ class FacetedView(FilterableListView): def get_context_data(self, **kwargs): context = super(FacetedView, self).get_context_data(**kwargs) facet = self.kwargs.get('facet','all') - qs=self.get_queryset() + qs = self.get_queryset() if self.request.GET.has_key('setkw') and self.request.user.is_staff: setkw = self.request.GET['setkw'] try: @@ -975,26 +960,26 @@ class FacetedView(FilterableListView): context['order_by'] = self.request.GET.get('order_by', 'newest') context['view_as'] = self.request.GET.get('view_as', None) return context - - + + class ByPubView(WorkListView): template_name = "bypub_list.html" context_object_name = "work_list" max_works = 100000 publisher_name = None publisher = None - + def get_publisher_name(self): self.publisher_name = get_object_or_404(models.PublisherName, id=self.kwargs['pubname']) self.set_publisher() - + def set_publisher(self): if self.publisher_name.key_publisher.count(): self.publisher = self.publisher_name.key_publisher.all()[0] elif self.publisher_name.publisher: self.publisher = self.publisher_name.publisher self.publisher_name = self.publisher.name - + def get_queryset_all(self): facet = self.kwargs.get('facet','') self.get_publisher_name() @@ -1009,12 +994,12 @@ class ByPubView(WorkListView): return objects.order_by('title', 'id') def get_context_data(self, **kwargs): - context = super(ByPubView, self).get_context_data(**kwargs) - context['pubname'] = self.publisher_name - context['publisher'] = self.publisher - context['facet'] = self.kwargs.get('facet','all') + context = super(ByPubView, self).get_context_data(**kwargs) + context['pubname'] = self.publisher_name + context['publisher'] = self.publisher + context['facet'] = self.kwargs.get('facet','all') - return context + return context class ByPubListView(ByPubView): def get_publisher_name(self): @@ -1025,7 +1010,7 @@ class ByPubListView(ByPubView): class UngluedListView(FilterableListView): template_name = "unglued_list.html" context_object_name = "work_list" - + def get_queryset_all(self): facet = self.kwargs['facet'] if (facet == 'popular'): @@ -1047,8 +1032,8 @@ class UngluedListView(FilterableListView): def get_context_data(self, **kwargs): context = super(UngluedListView, self).get_context_data(**kwargs) - qs=self.get_queryset() - context['ungluers'] = userlists.work_list_users(qs,5) + qs = self.get_queryset() + context['ungluers'] = userlists.work_list_users(qs, 5) facet = self.kwargs['facet'] context['facet'] = facet if facet == 'cc' or facet == 'creativecommons': @@ -1068,7 +1053,7 @@ class CampaignListView(FilterableListView): template_name = "campaign_list.html" context_object_name = "campaign_list" model = models.Campaign - + def get_queryset_all(self): facet = self.kwargs['facet'] if (facet == 'newest'): @@ -1078,7 +1063,7 @@ class CampaignListView(FilterableListView): elif (facet == 'pledges'): return models.Campaign.objects.filter(status='ACTIVE').annotate(pledges=Count('transaction')).order_by('-pledges') elif (facet == 'almost'): - return models.Campaign.objects.filter(status='ACTIVE').all() # STUB: will need to make db changes to make this work + return models.Campaign.objects.filter(status='ACTIVE').all() # STUB: will need to make db changes to make this work elif (facet == 'ending'): return models.Campaign.objects.filter(status='ACTIVE').order_by('deadline') elif (facet == 'soon'): @@ -1095,27 +1080,27 @@ class CampaignListView(FilterableListView): return models.Campaign.objects.all() def get_context_data(self, **kwargs): - context = super(CampaignListView, self).get_context_data(**kwargs) - qs=self.get_queryset() - context['ungluers'] = userlists.campaign_list_users(qs,5) - facet = self.kwargs['facet'] - context['facet'] =facet - context['facet_label'] = FACET_LABELS.get(facet, 'Active') - return context + context = super(CampaignListView, self).get_context_data(**kwargs) + qs = self.get_queryset() + context['ungluers'] = userlists.campaign_list_users(qs, 5) + facet = self.kwargs['facet'] + context['facet'] = facet + context['facet_label'] = FACET_LABELS.get(facet, 'Active') + return context class MergeView(FormView): - template_name="merge.html" - work=None - + template_name = "merge.html" + work = None + def dispatch(self, request, *args, **kwargs): if not request.user.is_staff: return render(request, "admins_only.html") else: return super(MergeView, self).dispatch(request, *args, **kwargs) - + def get_context_data(self, **kwargs): context = super(MergeView, self).get_context_data(**kwargs) - context['work']=self.work + context['work'] = self.work return context def get_form_class(self): @@ -1123,67 +1108,67 @@ class MergeView(FormView): return WorkForm else: return OtherWorkForm - + def get_form_kwargs(self): self.work = safe_get_work(self.kwargs["work_id"]) - form_kwargs= {'work':self.work} + form_kwargs = {'work':self.work} if self.request.method == 'POST': form_kwargs.update({'data':self.request.POST}) return form_kwargs def form_valid(self, form): - other_work=form.cleaned_data['other_work'] - context=self.get_context_data() + other_work = form.cleaned_data['other_work'] + context = self.get_context_data() if self.request.POST.has_key('confirm_merge_works'): - context['old_work_id']=other_work.id - merge_works(self.work,other_work,self.request.user) - context['merge_complete']=True + context['old_work_id'] = other_work.id + merge_works(self.work, other_work, self.request.user) + context['merge_complete'] = True else: - context['form']=WorkForm(initial={'other_work':other_work}) - context['other_work']=other_work + context['form'] = WorkForm(initial={'other_work':other_work}) + context['other_work'] = other_work return render(self.request, self.template_name, context) class GiftView(TemplateView): template_name = "gift.html" - - def get(self, request, *args, **kwargs): + + def get(self, request, *args, **kwargs): context = self.get_context_data() - context['transfer_form']=getTransferCreditForm(self.request.user.credit.available) + context['transfer_form'] = getTransferCreditForm(self.request.user.credit.available) return self.render_to_response(context) - - def post(self, request, *args, **kwargs): + + def post(self, request, *args, **kwargs): context = self.get_context_data() - transfer_form=getTransferCreditForm(self.request.user.credit.available, data=self.request.POST) + transfer_form = getTransferCreditForm(self.request.user.credit.available, data=self.request.POST) if transfer_form.is_valid(): if self.request.user.credit.transfer_to(transfer_form.cleaned_data['recipient'], transfer_form.cleaned_data['amount']): #successful transfer context['transfer_message'] = 'Your transfer has been successfully executed.' - context['recipient']= transfer_form.cleaned_data['recipient'] + context['recipient'] = transfer_form.cleaned_data['recipient'] context['transfer_amount'] = transfer_form.cleaned_data['amount'] - context['transfer_form']=getTransferCreditForm(self.request.user.credit.available) + context['transfer_form'] = getTransferCreditForm(self.request.user.credit.available) else: #unsuccessful transfer context['transfer_message'] = 'Your transfer was not successful.' - context['transfer_form']=transfer_form + context['transfer_form'] = transfer_form else: #not valid - context['transfer_form']=transfer_form + context['transfer_form'] = transfer_form return self.render_to_response(context) - + def get_context_data(self, *args, **kwargs): context = {'user' : self.request.user} return context - + class PledgeView(FormView): action = "pledge" - template_name="pledge.html" + template_name = "pledge.html" form_class = CampaignPledgeForm transaction = None campaign = None work = None premiums = None data = {} - + def get_preapproval_amount(self): preapproval_amount = self.request.GET.get('preapproval_amount', self.request.POST.get('preapproval_amount', None)) if preapproval_amount: @@ -1195,24 +1180,24 @@ class PledgeView(FormView): except: preapproval_amount = None if self.transaction: - if preapproval_amount: - preapproval_amount = preapproval_amount if preapproval_amount>self.transaction.amount else self.transaction.amount + if preapproval_amount: + preapproval_amount = preapproval_amount if preapproval_amount > self.transaction.amount else self.transaction.amount else: preapproval_amount = self.transaction.amount return preapproval_amount - + def get_form_kwargs(self): - + assert self.request.user.is_authenticated() self.work = safe_get_work(self.kwargs["work_id"]) - + # if there is no campaign or if campaign is not active, we should raise an error try: self.campaign = self.work.last_campaign() # TODO need to sort the premiums self.premiums = self.campaign.custom_premiums() | models.Premium.objects.filter(id=150) # Campaign must be ACTIVE - assert self.campaign.status == 'ACTIVE' + assert self.campaign.status == 'ACTIVE' except Exception, e: # this used to raise an exception, but that seemed pointless. This now has the effect of preventing any pledges. return {} @@ -1220,11 +1205,11 @@ class PledgeView(FormView): transactions = self.campaign.transactions().filter(user=self.request.user, status=TRANSACTION_STATUS_ACTIVE, type=PAYMENT_TYPE_AUTHORIZATION) premium_id = self.request.GET.get('premium_id', self.request.POST.get('premium_id', 150)) if transactions.count() == 0: - ack_name=self.request.user.profile.ack_name - ack_dedication='' - anonymous=self.request.user.profile.anon_pref + ack_name = self.request.user.profile.ack_name + ack_dedication = '' + anonymous = self.request.user.profile.anon_pref else: - self.transaction = transactions[0] + self.transaction = transactions[0] if premium_id == 150 and self.transaction.premium is not None: premium_id = self.transaction.premium.id if self.transaction.extra : @@ -1233,9 +1218,9 @@ class PledgeView(FormView): else: ack_name = self.request.user.profile.ack_name ack_dedication = '' - anonymous=self.transaction.anonymous + anonymous = self.transaction.anonymous - self.data = {'preapproval_amount':self.get_preapproval_amount(), 'premium_id':premium_id, + self.data = {'preapproval_amount':self.get_preapproval_amount(), 'premium_id':premium_id, 'ack_name':ack_name, 'ack_dedication':ack_dedication, 'anonymous':anonymous} if self.request.method == 'POST': self.data.update(self.request.POST.dict()) @@ -1248,38 +1233,38 @@ class PledgeView(FormView): return {'data':self.data} else: return {'initial':self.data} - + def get_context_data(self, **kwargs): """set up the pledge page""" - + context = super(PledgeView, self).get_context_data(**kwargs) - + context.update({ 'work':self.work, - 'campaign':self.campaign, - 'premiums':self.premiums, - 'premium_id':self.data.get('premium_id',None), - 'faqmenu': 'modify' if self.transaction else 'pledge', + 'campaign':self.campaign, + 'premiums':self.premiums, + 'premium_id':self.data.get('premium_id', None), + 'faqmenu': 'modify' if self.transaction else 'pledge', 'transaction': self.transaction, 'tid': self.transaction.id if self.transaction else None, 'cover_width': cover_width(self.work) }) - + return context - + def form_valid(self, form): # right now, if there is a non-zero pledge amount, go with that. otherwise, do the pre_approval - + p = PaymentManager() if self.transaction: # modifying the transaction... - assert self.transaction.type == PAYMENT_TYPE_AUTHORIZATION and self.transaction.status == TRANSACTION_STATUS_ACTIVE - status, url = p.modify_transaction(self.transaction, form.cleaned_data["preapproval_amount"], - paymentReason="Unglue.it %s for %s"% (self.action,self.campaign.name) , - pledge_extra=form.trans_extra + assert self.transaction.type == PAYMENT_TYPE_AUTHORIZATION and self.transaction.status == TRANSACTION_STATUS_ACTIVE + status, url = p.modify_transaction(self.transaction, form.cleaned_data["preapproval_amount"], + paymentReason="Unglue.it %s for %s"% (self.action, self.campaign.name) , + pledge_extra = form.trans_extra ) logger.info("status: {0}, url:{1}".format(status, url)) - + if status and url is not None: logger.info("PledgeView (Modify): " + url) return HttpResponseRedirect(url) @@ -1288,13 +1273,13 @@ class PledgeView(FormView): else: return HttpResponse("No modification made") else: - t, url = p.process_transaction('USD', form.amount(), - host = PAYMENT_HOST_NONE, - campaign=self.campaign, + t, url = p.process_transaction('USD', form.amount(), + host = PAYMENT_HOST_NONE, + campaign=self.campaign, user=self.request.user, - paymentReason="Unglue.it Pledge for {0}".format(self.campaign.name), + paymentReason="Unglue.it Pledge for {0}".format(self.campaign.name), pledge_extra=form.trans_extra - ) + ) if url: logger.info("PledgeView url: " + url) return HttpResponseRedirect(url) @@ -1302,8 +1287,8 @@ class PledgeView(FormView): logger.error("Attempt to produce transaction id {0} failed".format(t.id)) return HttpResponse("Our attempt to enable your transaction failed. We have logged this error.") -class PurchaseView(PledgeView): - template_name="purchase.html" +class PurchaseView(PledgeView): + template_name = "purchase.html" form_class = CampaignPurchaseForm action = "purchase" offer_id = None @@ -1312,8 +1297,8 @@ class PurchaseView(PledgeView): context = super(PledgeView, self).get_context_data(**kwargs) context.update({ 'work':self.work, - 'campaign':self.campaign, - 'faqmenu': 'purchase' , + 'campaign':self.campaign, + 'faqmenu': 'purchase' , 'transaction': self.transaction, 'tid': self.transaction.id if self.transaction else None, 'cover_width': cover_width(self.work), @@ -1321,18 +1306,18 @@ class PurchaseView(PledgeView): 'user_license': self.work.get_user_license(self.request.user), 'give': self.give }) - + return context def get_form_kwargs(self): assert self.request.user.is_authenticated() self.work = safe_get_work(self.kwargs["work_id"]) - + # if there is no campaign or if campaign is not active, we should raise an error try: self.campaign = self.work.last_campaign() # Campaign must be ACTIVE - assert self.campaign.status == 'ACTIVE' + assert self.campaign.status == 'ACTIVE' except Exception, e: # this used to raise an exception, but that seemed pointless. This now has the effect of preventing any pledges. return {} @@ -1369,24 +1354,24 @@ class PurchaseView(PledgeView): def form_valid(self, form): p = PaymentManager() - t, url = p.process_transaction('USD', form.amount(), - host = PAYMENT_HOST_NONE, - campaign=self.campaign, + t, url = p.process_transaction('USD', form.amount(), + host = PAYMENT_HOST_NONE, + campaign=self.campaign, user=self.request.user, - paymentReason="Unglue.it Purchase for {0}".format(self.campaign.name), + paymentReason="Unglue.it Purchase for {0}".format(self.campaign.name), pledge_extra=form.trans_extra - ) + ) if url: return HttpResponseRedirect(url) else: logger.error("Attempt to produce transaction id {0} failed".format(t.id)) return HttpResponse("Our attempt to enable your transaction failed. We have logged this error.") - + class FundView(FormView): - template_name="fund_the_pledge.html" + template_name = "fund_the_pledge.html" transaction = None action = None - + def get_form_class(self): if self.request.user.is_anonymous(): return AnonCCForm @@ -1397,7 +1382,7 @@ class FundView(FormView): def get_form_kwargs(self): kwargs = super(FundView, self).get_form_kwargs() - + #assert self.request.user.is_authenticated() if self.transaction is None: self.transaction = get_object_or_404(Transaction, id=self.kwargs["t_id"]) @@ -1415,48 +1400,48 @@ class FundView(FormView): else: data = {} kwargs['initial'] = data - + data.update( {'preapproval_amount':self.transaction.needed_amount, 'username':self.request.user.username if self.request.user.is_authenticated else None, 'work_id':self.transaction.campaign.work.id, 'title':self.transaction.campaign.work.title} - ) + ) return kwargs def get_context_data(self, **kwargs): context = super(FundView, self).get_context_data(**kwargs) - context['modified'] = self.transaction.status==TRANSACTION_STATUS_MODIFIED - context['preapproval_amount']=self.transaction.max_amount + context['modified'] = self.transaction.status == TRANSACTION_STATUS_MODIFIED + context['preapproval_amount'] = self.transaction.max_amount context['needed'] = self.transaction.needed_amount - context['transaction']=self.transaction + context['transaction'] = self.transaction context['STRIPE_PK'] = stripelib.STRIPE_PK context['action'] = self.action return context - + def form_valid(self, form): p = PaymentManager() stripe_token = form.cleaned_data.get("stripe_token", None) self.transaction.host = settings.PAYMENT_PROCESSOR - return_url = "%s?tid=%s" % (reverse('pledge_complete'),self.transaction.id) + return_url = "%s?tid=%s" % (reverse('pledge_complete'), self.transaction.id) if self.transaction.campaign.type == THANKS and self.transaction.user == None: #anonymous user, just charge the card! if self.request.user.is_authenticated(): self.transaction.user = self.request.user # if there's an email address, put it in the receipt column, so far unused. - self.transaction.receipt = form.cleaned_data.get("email",None) - t, url = p.charge(self.transaction, return_url = return_url, token=stripe_token ) + self.transaction.receipt = form.cleaned_data.get("email", None) + t, url = p.charge(self.transaction, return_url = return_url, token=stripe_token) elif self.request.user.is_anonymous(): #somehow the user lost their login return HttpResponseRedirect(reverse('superlogin')) elif self.transaction.user.id != self.request.user.id: # other sort of strange trouble! - return render(self.request, "pledge_user_error.html", {'transaction': self.transaction, 'action': self.action }) + return render(self.request, "pledge_user_error.html", {'transaction': self.transaction, 'action': self.action }) else: # if the user has active account, use it. Otherwise... if not self.request.user.profile.account: - + # if we get a stripe_token, create a new stripe account for the user if stripe_token: try: @@ -1468,106 +1453,106 @@ class FundView(FormView): return render(self.request, "pledge_card_error.html", {'transaction': self.transaction, 'exception':e }) # with the Account in hand, now do the transaction if self.action == 'pledge': - t, url = p.authorize(self.transaction, return_url = return_url ) + t, url = p.authorize(self.transaction, return_url = return_url) else: - t, url = p.charge(self.transaction, return_url = return_url ) - + t, url = p.charge(self.transaction, return_url = return_url) + # redirecting user to pledge_complete/payment_complete on successful preapproval (in the case of stripe) if url is not None: return HttpResponseRedirect(url) else: - return render(self.request, "pledge_card_error.html", {'transaction': self.transaction }) - + return render(self.request, "pledge_card_error.html", {'transaction': self.transaction }) + class GiftCredit(TemplateView): - template_name="gift_credit.html" + template_name = "gift_credit.html" def get_context_data(self, **kwargs): context = super(GiftCredit, self).get_context_data(**kwargs) - context['faqmenu']="gift" + context['faqmenu'] = "gift" try: - envelope=signing.loads(kwargs['token']) - context['envelope']=envelope + envelope = signing.loads(kwargs['token']) + context['envelope'] = envelope except signing.BadSignature: - self.template_name="gift_error.html" + self.template_name = "gift_error.html" return context try: work = models.Work.objects.get(id=envelope['work_id']) - campaign=work.last_campaign() + campaign = work.last_campaign() except models.Work.DoesNotExist: campaign = None - context['work']=work + context['work'] = work try: user = User.objects.get(username=envelope['username']) except User.DoesNotExist: - self.template_name="gift_user_error.html" - context['error']='user does not exist' + self.template_name = "gift_user_error.html" + context['error'] = 'user does not exist' return context if user != self.request.user: - self.template_name="gift_user_error.html" - context['error']='wrong user logged in' + self.template_name = "gift_user_error.html" + context['error'] = 'wrong user logged in' return context try: # check token not used CreditLog.objects.get(sent=envelope['sent']) - context['error']='credit already registered' + context['error'] = 'credit already registered' return context except CreditLog.DoesNotExist: #not used yet! - amount=envelope['amount']+envelope['cents']/D(100) - CreditLog.objects.create(user=user,amount=amount,action='deposit',sent=envelope['sent']) - ts=Transaction.objects.filter(user=user,campaign=campaign,status=TRANSACTION_STATUS_NONE).order_by('-pk') + amount = envelope['amount']+envelope['cents']/D(100) + CreditLog.objects.create(user=user, amount=amount, action='deposit', sent=envelope['sent']) + ts = Transaction.objects.filter(user=user, campaign=campaign, status=TRANSACTION_STATUS_NONE).order_by('-pk') if ts.count()==0: - ts=Transaction.objects.filter(user=user,campaign=campaign,status=TRANSACTION_STATUS_MODIFIED).order_by('-pk') + ts = Transaction.objects.filter(user=user, campaign=campaign, status=TRANSACTION_STATUS_MODIFIED).order_by('-pk') if ts.count()>0: - t=ts[0] - credit_transaction(t,user, amount) + t = ts[0] + credit_transaction(t, user, amount) for t in ts[1:]: - t.status=TRANSACTION_STATUS_CANCELED + t.status = TRANSACTION_STATUS_CANCELED t.save() - context['transaction']=t + context['transaction'] = t return context else: user.credit.add_to_balance(amount) return context - - + + class PledgeRechargeView(TemplateView): """ a view to allow for recharge of a transaction for failed transactions or ones with errors """ - template_name="pledge_recharge.html" + template_name = "pledge_recharge.html" def get_context_data(self, **kwargs): - + context = super(PledgeRechargeView, self).get_context_data(**kwargs) - + # the following should be true since PledgeView.as_view is wrapped in login_required assert self.request.user.is_authenticated() user = self.request.user - + work = safe_get_work(self.kwargs["work_id"]) campaign = work.last_campaign() - + if campaign is None: return Http404 - + transaction = campaign.transaction_to_recharge(user) - + # calculate a URL to do a preapproval -- in the future, we may want to do a straight up payment - + return_url = None nevermind_url = None - + if transaction is not None: - # the recipients of this authorization is not specified here but rather by the PaymentManager. + # the recipients of this authorization is not specified here but rather by the PaymentManager. paymentReason = "Unglue.it Recharge for {0}".format(campaign.name) - + p = PaymentManager() t, url = p.authorize(transaction, return_url=return_url, paymentReason=paymentReason) logger.info("Recharge url: {0}".format(url)) else: url = None - + context.update({ 'work':work, 'transaction':transaction, @@ -1575,69 +1560,69 @@ class PledgeRechargeView(TemplateView): 'recharge_url': url }) return context - + class FundCompleteView(TemplateView): """A callback for Payment to tell unglue.it that a payment transaction has completed successfully. - + Possible things to implement: - + after pledging, supporter receives email including thanks, work pledged, amount, expiry date, any next steps they should expect; others? study other confirmation emails for their contents should note that a confirmation email has been sent to $email from $sender - should briefly note next steps (e.g. if this campaign succeeds you will be emailed on date X) - + should briefly note next steps (e.g. if this campaign succeeds you will be emailed on date X) + """ - - template_name="pledge_complete.html" - + + template_name = "pledge_complete.html" + def get(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) - + if self.transaction: if self.transaction.campaign.type == THANKS: return DownloadView.as_view()(request, work=self.transaction.campaign.work) - + else: if request.user.is_authenticated: if self.user_is_ok(): return self.render_to_response(context) else: - return HttpResponseRedirect(reverse('work', kwargs={'work_id': self.transaction.campaign.work.id})) + return HttpResponseRedirect(reverse('work', kwargs={'work_id': self.transaction.campaign.work.id})) else: return redirect_to_login(request.get_full_path()) else: return HttpResponseRedirect(reverse('home')) - + def user_is_ok(self): if not self.transaction: - return False + return False if self.transaction.campaign.type == THANKS and self.transaction.user == None: # to handle anonymous donors- leakage not an issue return True - else: + else: return self.request.user.id == self.transaction.user.id - - + + def get_context_data(self): # pick up all get and post parameters and display context = super(FundCompleteView, self).get_context_data() self.transaction = None - + # pull out the transaction id and try to get the corresponding Transaction - transaction_id = self.request.POST.get("tid",self.request.GET.get("tid", None)) - + transaction_id = self.request.POST.get("tid", self.request.GET.get("tid", None)) + if not transaction_id: return context try: self.transaction = Transaction.objects.get(id=transaction_id) except (ValueError, Transaction.DoesNotExist): self.transaction = None - + if not self.transaction: return context - + # work and campaign in question try: campaign = self.transaction.campaign @@ -1645,12 +1630,12 @@ class FundCompleteView(TemplateView): except Exception, e: campaign = None work = None - + # # we need to check whether the user tied to the transaction is indeed the authenticated user. - + if not self.user_is_ok(): return context - + gift = self.transaction.extra.has_key('give_to') if not gift: # add the work corresponding to the Transaction on the user's wishlist if it's not already on the wishlist @@ -1658,52 +1643,51 @@ class FundCompleteView(TemplateView): self.transaction.user.wishlist.add_work(work, 'pledging', notify=True) #put info into session for download page to pick up. - self.request.session['amount']= int(self.transaction.amount * 100) + self.request.session['amount'] = int(self.transaction.amount * 100) if self.transaction.receipt: - self.request.session['receipt']= self.transaction.receipt - - + self.request.session['receipt'] = self.transaction.receipt + context["transaction"] = self.transaction context["work"] = work context["campaign"] = campaign context["faqmenu"] = "complete" context["site"] = Site.objects.get_current() - - return context + + return context class PledgeModifiedView(FundCompleteView): def get_context_data(self): context = super(PledgeModifiedView, self).get_context_data() - context['modified']=True + context['modified'] = True return context - + class PledgeCancelView(FormView): """A view for allowing a user to cancel the active transaction for specified campaign""" - template_name="pledge_cancel.html" + template_name = "pledge_cancel.html" form_class = PledgeCancelForm - + def get_context_data(self, **kwargs): context = super(PledgeCancelView, self).get_context_data(**kwargs) - + # initialize error to be None context["error"] = None - + # the following should be true since PledgeCancelView.as_view is wrapped in login_required - + if self.request.user.is_authenticated(): user = self.request.user else: context["error"] = "You are not logged in." return context - + campaign = get_object_or_404(models.Campaign, id=self.kwargs["campaign_id"]) if campaign.status != 'ACTIVE': context["error"] = "{0} is not an active campaign".format(campaign) return context - + work = campaign.work transactions = campaign.transactions().filter(user=user, status=TRANSACTION_STATUS_ACTIVE) - + if transactions.count() < 1: context["error"] = "You don't have an active transaction for this campaign." return context @@ -1717,29 +1701,29 @@ class PledgeCancelView(FormView): logger.error("Transaction id {0} transaction type, which should be {1}, is actually {2}".format(transaction.id, PAYMENT_TYPE_AUTHORIZATION, transaction.type)) context["error"] = "Your transaction type, which should be {0}, is actually {1}".format(PAYMENT_TYPE_AUTHORIZATION, transaction.type) return context - + # we've located the transaction, work, and campaign referenced in the view - + context["transaction"] = transaction context["work"] = work context["campaign"] = campaign context["faqmenu"] = "cancel" - + return context - + def form_valid(self, form): # check that user does, in fact, have an active transaction for specified campaign - + logger.info("arrived at pledge_cancel form_valid") # pull campaign_id from form, not from URI as we do from GET - campaign_id = self.request.POST.get('campaign_id',self.request.GET.get('campaign_id')) - + campaign_id = self.request.POST.get('campaign_id', self.request.GET.get('campaign_id')) + # this following logic should be extraneous. if self.request.user.is_authenticated(): user = self.request.user else: return HttpResponse("You need to be logged in.") - + try: # look up the specified campaign and attempt to pull up the appropriate transaction # i.e., the transaction actually belongs to user, that the transaction is active @@ -1769,10 +1753,10 @@ class PledgeCancelView(FormView): return HttpResponse("Our attempt to cancel your transaction failed. We have logged this error.") except Exception, e: logger.error("Exception from attempt to cancel pledge for campaign id {0} for username {1}: {2}".format(campaign_id, user.username, e)) - return HttpResponse("Sorry, something went wrong in canceling your campaign pledge. We have logged this error.") - + return HttpResponse("Sorry, something went wrong in canceling your campaign pledge. We have logged this error.") + def claim(request): - if request.method == 'GET': + if request.method == 'GET': data = request.GET else: data = request.POST @@ -1794,7 +1778,7 @@ def claim(request): active_claims = work.claim.exclude(status = 'release') context = {'form': form, 'work': work, 'rights_holder':rights_holder , 'active_claims':active_claims} return render(request, "claim.html", context) - + def new_survey(request, work_id): if not request.user.is_authenticated() : return HttpResponseRedirect(reverse('surveys')) @@ -1802,23 +1786,23 @@ def new_survey(request, work_id): Q(claim__user = request.user) | Q(claim__rights_holder__owner = request.user) ) if work_id: - work =safe_get_work(work_id) + work = safe_get_work(work_id) for my_work in my_works: - if my_work==work: - form=SurveyForm() + if my_work == work: + form = SurveyForm() break else: return HttpResponseRedirect(reverse('surveys')) else: work = None form = SurveyForm() - if request.method == 'POST': - form = SurveyForm( data=request.POST) + if request.method == 'POST': + form = SurveyForm(data=request.POST) if form.is_valid(): if not work and form.work: for my_work in my_works: - print '{} {}'.format(my_work.id,form.work.id) - if my_work==form.work: + print '{} {}'.format(my_work.id, form.work.id) + if my_work == form.work: work = form.work break else: @@ -1836,7 +1820,7 @@ def surveys(request): Q(claim__user = request.user) | Q(claim__rights_holder__owner = request.user) ) return render(request, "surveys.html", {"works":works}) - + def rh_tools(request): if not request.user.is_authenticated() : return render(request, "rh_tools.html") @@ -1848,15 +1832,15 @@ def rh_tools(request): if claim.can_open_new: if request.method == 'POST' and request.POST.has_key('cl_%s-work' % claim.id) and int(request.POST['cl_%s-work' % claim.id]) == claim.work.id : claim.campaign_form = OpenCampaignForm(data = request.POST, prefix = 'cl_'+str(claim.id),) - if claim.campaign_form.is_valid(): + if claim.campaign_form.is_valid(): new_campaign = claim.campaign_form.save(commit=False) - if new_campaign.type==BUY2UNGLUE: + if new_campaign.type == BUY2UNGLUE: new_campaign.target = D(settings.UNGLUEIT_MAXIMUM_TARGET) new_campaign.set_cc_date_initial() - elif new_campaign.type==REWARDS: + elif new_campaign.type == REWARDS: new_campaign.deadline = date_today() + timedelta(days=int(settings.UNGLUEIT_LONGEST_DEADLINE)) new_campaign.target = D(settings.UNGLUEIT_MINIMUM_TARGET) - elif new_campaign.type==THANKS: + elif new_campaign.type == THANKS: new_campaign.target = D(settings.UNGLUEIT_MINIMUM_TARGET) new_campaign.save() claim.campaign_form.save_m2m() @@ -1870,23 +1854,23 @@ def rh_tools(request): if claim.campaign: if claim.campaign.status in ['ACTIVE','INITIALIZED']: if request.method == 'POST' and request.POST.has_key('edit_managers_%s'% claim.campaign.id) : - claim.campaign.edit_managers_form=EditManagersForm( instance=claim.campaign, data=request.POST, prefix=claim.campaign.id) + claim.campaign.edit_managers_form = EditManagersForm(instance=claim.campaign, data=request.POST, prefix=claim.campaign.id) if claim.campaign.edit_managers_form.is_valid(): claim.campaign.edit_managers_form.save() claim.campaign.edit_managers_form = EditManagersForm(instance=claim.campaign, prefix=claim.campaign.id) else: - claim.campaign.edit_managers_form=EditManagersForm(instance=claim.campaign, prefix=claim.campaign.id) + claim.campaign.edit_managers_form = EditManagersForm(instance=claim.campaign, prefix=claim.campaign.id) campaigns = request.user.campaigns.all() new_campaign = None for campaign in campaigns: if campaign.clonable(): if request.method == 'POST' and request.POST.has_key('c%s-campaign_id'% campaign.id): - clone_form= CloneCampaignForm(data=request.POST, prefix = 'c%s' % campaign.id) + clone_form = CloneCampaignForm(data=request.POST, prefix = 'c%s' % campaign.id) if clone_form.is_valid(): campaign.clone() else: - campaign.clone_form= CloneCampaignForm(initial={'campaign_id':campaign.id}, prefix = 'c%s' % campaign.id) - return render(request, "rh_tools.html", {'claims': claims ,'campaigns': campaigns}) + campaign.clone_form = CloneCampaignForm(initial={'campaign_id':campaign.id}, prefix='c%s' % campaign.id) + return render(request, "rh_tools.html", {'claims': claims , 'campaigns': campaigns}) def rh_admin(request, facet='top'): if not request.user.is_authenticated() : @@ -1896,7 +1880,7 @@ def rh_admin(request, facet='top'): PendingFormSet = modelformset_factory(models.Claim, fields=['status'], extra=0) pending_data = models.Claim.objects.filter(status = 'pending') active_data = models.Claim.objects.filter(status = 'active') - if request.method == 'POST': + if request.method == 'POST': if 'create_rights_holder' in request.POST.keys(): form = RightsHolderForm(data=request.POST) pending_formset = PendingFormSet (queryset=pending_data) @@ -1913,12 +1897,12 @@ def rh_admin(request, facet='top'): form = RightsHolderForm() pending_formset = PendingFormSet(queryset=pending_data) rights_holders = models.RightsHolder.objects.all() - - context = { - 'request': request, - 'rights_holders': rights_holders, + + context = { + 'request': request, + 'rights_holders': rights_holders, 'form': form, - 'pending': zip(pending_data,pending_formset), + 'pending': zip(pending_data, pending_formset), 'pending_formset': pending_formset, 'active_data': active_data, 'facet': facet, @@ -1927,37 +1911,37 @@ def rh_admin(request, facet='top'): def campaign_admin(request): if not request.user.is_authenticated() : - return render(request, "admins_only.html") + return render(request, "admins_only.html") if not request.user.is_staff : return render(request, "admins_only.html") - + context = {} - + def campaigns_types(): # pull out Campaigns with Transactions that are ACTIVE -- and hence can be executed # Campaign.objects.filter(transaction__status='ACTIVE') - + campaigns_with_active_transactions = models.Campaign.objects.filter(transaction__status=TRANSACTION_STATUS_ACTIVE) - + # pull out Campaigns with Transactions that are INCOMPLETE - + campaigns_with_incomplete_transactions = models.Campaign.objects.filter(transaction__status=TRANSACTION_STATUS_INCOMPLETE) - + # show all Campaigns with Transactions that are COMPLETED - + campaigns_with_completed_transactions = models.Campaign.objects.filter(transaction__status=TRANSACTION_STATUS_COMPLETE) - + # show Campaigns with Transactions that are CANCELED - + campaigns_with_canceled_transactions = models.Campaign.objects.filter(transaction__status=TRANSACTION_STATUS_CANCELED) - + return (campaigns_with_active_transactions, campaigns_with_incomplete_transactions, campaigns_with_completed_transactions, campaigns_with_canceled_transactions) - + form = CampaignAdminForm() pm = PaymentManager() check_status_results = None command_status = None - + if request.method == 'GET': pass elif request.method == 'POST': @@ -1974,15 +1958,15 @@ def campaign_admin(request): check_status_results += "

No preapprovals needed updating

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

Payment updated: %s

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

No payments needed updating

" + check_status_results += "

No payments needed updating

" command_status = _("Transactions updated based on PaymentDetails and PreapprovalDetails") except Exception, e: check_status_results = e - elif 'execute_campaigns' in request.POST.keys(): + elif 'execute_campaigns' in request.POST.keys(): c_id = request.POST.get('active_campaign', None) if c_id is not None: try: @@ -1999,8 +1983,8 @@ def campaign_admin(request): results = pm.finish_campaign(campaign) command_status = str(results) except Exception, e: - command_status = "Error in finishing transactions for campaign %s " % (str(e)) - + command_status = "Error in finishing transactions for campaign %s " % (str(e)) + elif 'cancel_campaigns' in request.POST.keys(): c_id = request.POST.get('active_campaign', None) if c_id is not None: @@ -2009,11 +1993,11 @@ def campaign_admin(request): results = pm.cancel_campaign(campaign) command_status = str(results) except Exception, e: - command_status = "Error in canceling transactions for campaign %s " % (str(e)) - + command_status = "Error in canceling transactions for campaign %s " % (str(e)) + (campaigns_with_active_transactions, campaigns_with_incomplete_transactions, campaigns_with_completed_transactions, campaigns_with_canceled_transactions) = campaigns_types() - + context.update({ 'form': form, 'check_status_results':check_status_results, @@ -2033,7 +2017,7 @@ def supporter(request, supporter_username, template_name, extra_context={}): works_active = [] works_wished = [] works_on_wishlist = wishlist.works.all() - + if (works_on_wishlist): # querysets for tabs # unglued tab is anything with an existing ebook or successful campaign @@ -2044,17 +2028,17 @@ def supporter(request, supporter_username, template_name, extra_context={}): works_unglued = works_unglued.order_by('-campaigns__status', 'campaigns__deadline', '-num_wishes') works_active = works_on_wishlist.filter(campaigns__status='ACTIVE').order_by('campaigns__deadline').distinct() - + # everything else goes in tab 3 works_wished = works_on_wishlist.exclude(pk__in=works_active.values_list('pk', flat=True)).exclude(pk__in=works_unglued.values_list('pk', flat=True)).order_by('-num_wishes') - + slidelist = [] # badge counts backed = works_unglued.count() backing = works_active.count() wished = works_wished.count() - - else: + + else: backed = 0 backing = 0 wished = 0 @@ -2066,22 +2050,22 @@ def supporter(request, supporter_username, template_name, extra_context={}): activetab = "#1" else: activetab = "#3" - + # following block to support profile admin form in supporter page if request.user.is_authenticated() and request.user.username == supporter_username: - profile_obj=request.user.profile + profile_obj = request.user.profile - if request.method == 'POST': - profile_form = ProfileForm(data=request.POST,instance=profile_obj) + if request.method == 'POST': + profile_form = ProfileForm(data=request.POST, instance=profile_obj) if profile_form.is_valid(): if profile_form.cleaned_data['clear_facebook'] or profile_form.cleaned_data['clear_twitter'] or profile_form.cleaned_data['clear_goodreads'] : if profile_form.cleaned_data['clear_facebook']: - profile_obj.facebook_id=0 + profile_obj.facebook_id = 0 if profile_obj.avatar_source == models.FACEBOOK: profile_obj.avatar_source = models.UNGLUEITAR if profile_form.cleaned_data['clear_twitter']: - profile_obj.twitter_id="" + profile_obj.twitter_id = "" if profile_obj.avatar_source == models.TWITTER: profile_obj.avatar_source = models.UNGLUEITAR if profile_form.cleaned_data['clear_goodreads']: @@ -2095,8 +2079,8 @@ def supporter(request, supporter_username, template_name, extra_context={}): profile_form.save() else: - profile_form= ProfileForm(instance=profile_obj) - + profile_form = ProfileForm(instance=profile_obj) + else: profile_form = '' @@ -2112,39 +2096,39 @@ def supporter(request, supporter_username, template_name, extra_context={}): "backing": backing, "wished": wished, "profile_form": profile_form, - "ungluers": userlists.other_users(supporter, 5 ), + "ungluers": userlists.other_users(supporter, 5), "activetab": activetab, } context.update(extra_context) return render(request, template_name, context) -def library(request,library_name): - context={} +def library(request, library_name): + context = {} try: # determine if the supporter is a library - authenticator = Authenticator(request,library_name) + authenticator = Authenticator(request, library_name) context['authenticator'] = authenticator context['library'] = library = authenticator.library except Library.DoesNotExist: raise Http404 - works_active= models.Work.objects.filter(acqs__user=library.user,acqs__license=LIBRARY).distinct() - if works_active.count()>0: + works_active = models.Work.objects.filter(acqs__user=library.user, acqs__license=LIBRARY).distinct() + if works_active.count() > 0: context['works_active'] = works_active context['activetab'] = "#2" - context['ungluers'] = userlists.library_users(library, 5 ) - return supporter(request,library_name,template_name='libraryauth/library.html', extra_context=context) - - + context['ungluers'] = userlists.library_users(library, 5) + return supporter(request, library_name, template_name='libraryauth/library.html', extra_context=context) + + class ManageAccount(FormView): - template_name="manage_account.html" + template_name = "manage_account.html" form_class = PlainCCForm def get_context_data(self, **kwargs): context = super(ManageAccount, self).get_context_data(**kwargs) context['STRIPE_PK'] = stripelib.STRIPE_PK return context - + def form_valid(self, form): """ save the token, make an account""" @@ -2165,20 +2149,20 @@ class ManageAccount(FormView): def search(request): q = request.GET.get('q', '') ty = request.GET.get('ty', 'g') # ge= 'general, au= 'author' - request.session['q']=q + request.session['q'] = q try: page = int(request.GET.get('page', 1)) except ValueError: # garbage in page page = 1 gbo = request.GET.get('gbo', 'n') # gbo is flag for google books only - our_stuff = Q(is_free=True) | Q(campaigns__isnull=False ) - if q != '' and page==1 and not gbo=='y': + our_stuff = Q(is_free=True) | Q(campaigns__isnull=False) + if q != '' and page == 1 and not gbo == 'y': isbnq = ISBN(q) if isbnq.valid: work_query = Q(identifiers__value=str(isbnq), identifiers__type="isbn") - elif ty=='au': - work_query = Q(editions__authors__name=q) + elif ty == 'au': + work_query = Q(editions__authors__name=q) else: work_query = Q(title__icontains=q) | Q(editions__authors__name__icontains=q) | Q(subjects__name__iexact=q) campaign_works = models.Work.objects.filter(our_stuff).filter(work_query).distinct() @@ -2190,17 +2174,17 @@ def search(request): gbo = 'y' else: if gbo == 'n': - page=page-1 # because page=1 is the unglue.it results + page = page-1 # because page=1 is the unglue.it results results = gluejar_search(q, user_ip=request.META['REMOTE_ADDR'], page=page) campaign_works = None # flag search result as on wishlist as appropriate - works=[] + works = [] for result in results: try: - work = models.Identifier.objects.get(type='goog',value=result['googlebooks_id']).work + work = models.Identifier.objects.get(type='goog', value=result['googlebooks_id']).work works.append(work) - except models.Identifier.DoesNotExist: + except models.Identifier.DoesNotExist: works.append(result) context = { "q": q, @@ -2230,23 +2214,23 @@ def wishlist(request): work.subjects.remove(subject) return HttpResponse('removed work from '+setkw) elif add_work_id: - work =safe_get_work(add_work_id) + work = safe_get_work(add_work_id) work.subjects.add(subject) return HttpResponse('added work to '+setkw) - + if googlebooks_id: try: edition = bookloader.add_by_googlebooks_id(googlebooks_id) if edition.new: # add related editions asynchronously tasks.populate_edition.delay(edition.isbn_13) - request.user.wishlist.add_work(edition.work,'user', notify=True) + request.user.wishlist.add_work(edition.work, 'user', notify=True) return HttpResponse('added googlebooks id') except bookloader.LookupFailure: logger.warning("failed to load googlebooks_id %s" % googlebooks_id) return HttpResponse('error adding googlebooks id') except Exception, e: - logger.warning("Error in wishlist adding %s" % (e)) + logger.warning("Error in wishlist adding %s" % (e)) return HttpResponse('error adding googlebooks id') # TODO: redirect to work page, when it exists elif remove_work_id: @@ -2255,8 +2239,8 @@ def wishlist(request): return HttpResponse('removed work from wishlist') elif add_work_id: # if adding from work page, we have may work.id, not googlebooks_id - work =safe_get_work(add_work_id) - request.user.wishlist.add_work(work,'user', notify=True) + work = safe_get_work(add_work_id) + request.user.wishlist.add_work(work, 'user', notify=True) return HttpResponse('added work to wishlist') @require_POST @@ -2272,9 +2256,9 @@ def kw_edit(request, work_id): except models.Subject.DoesNotExist: return HttpResponse('invalid subject') work.subjects.remove(subject) - return HttpResponse('removed ' + remove_kw ) + return HttpResponse('removed ' + remove_kw) elif add_form: - form= SubjectSelectForm(data=request.POST) + form = SubjectSelectForm(data=request.POST) if form.is_valid(): add_kw = form.cleaned_data['add_kw'] try: @@ -2282,22 +2266,22 @@ def kw_edit(request, work_id): except models.Subject.DoesNotExist: return HttpResponse('invalid subject') work.subjects.add(subject) - return HttpResponse( add_kw.name ) + return HttpResponse(add_kw.name) else: - return HttpResponse('xxbadform' ) + return HttpResponse('xxbadform') else: return HttpResponse(str(add_form)) return HttpResponse(str(add_form)) - + class InfoPageView(TemplateView): - + def get_template_names(self, **kwargs): if self.kwargs['template_name']: return (self.kwargs['template_name']) else: return ('metrics.html') - + def get_context_data(self, **kwargs): users = User.objects users.today = users.filter(date_joined__range = (date_today(), now())) @@ -2339,7 +2323,7 @@ class InfoPageView(TemplateView): ebookfiles.year = ebookfiles.filter(created__year = date_today().year) ebookfiles.month = ebookfiles.year.filter(created__month = date_today().month) ebookfiles.yesterday = ebookfiles.filter(created__range = (date_today()-timedelta(days=1), date_today())) - wishlists= models.Wishlist.objects.exclude(wishes__isnull=True) + wishlists = models.Wishlist.objects.exclude(wishes__isnull=True) wishlists.today = wishlists.filter(created__range = (date_today(), now())) wishlists.days7 = wishlists.filter(created__range = (date_today()-timedelta(days=7), now())) wishlists.year = wishlists.filter(created__year = date_today().year) @@ -2348,7 +2332,7 @@ class InfoPageView(TemplateView): wishlists.yesterday = wishlists.filter(created__range = (date_today()-timedelta(days=1), date_today())) else: wishlists.yesterday = wishlists.month.filter(created__day = date_today().day-1) - + transactions = Transaction.objects.filter(status__in = [TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE]) transactions.sum = transactions.aggregate(Sum('amount'))['amount__sum'] transactions.today = transactions.filter(date_created__range = (date_today(), now())) @@ -2366,9 +2350,9 @@ class InfoPageView(TemplateView): marc.days7 = marc.filter(created__range = (date_today()-timedelta(days=7), now())) marc.year = marc.filter(created__year = date_today().year) marc.month = marc.year.filter(created__month = date_today().month) - marc.yesterday = marc.filter(created__range = (date_today()-timedelta(days=1),date_today())) + marc.yesterday = marc.filter(created__range = (date_today()-timedelta(days=1), date_today())) return { - 'users': users, + 'users': users, 'works': works, 'ebooks': ebooks, 'ebookfiles': ebookfiles, @@ -2378,38 +2362,38 @@ class InfoPageView(TemplateView): } class InfoLangView(TemplateView): - + def get_template_names(self, **kwargs): if self.kwargs['template_name']: return (self.kwargs['template_name']) else: return ('languages.html') - + def get_context_data(self, **kwargs): - languages=models.Work.objects.filter(num_wishes__gte = 1).values('language').annotate(lang_count=Count('language')).order_by('-lang_count') + languages = models.Work.objects.filter(num_wishes__gte = 1).values('language').annotate(lang_count=Count('language')).order_by('-lang_count') return { - 'wished_languages': languages, + 'wished_languages': languages, } - + class FAQView(FormView): template_name = "faq.html" form_class = DateCalculatorForm def form_valid(self, form): - form.instance.status='DEMO' - form.instance.type=BUY2UNGLUE + form.instance.status = 'DEMO' + form.instance.type = BUY2UNGLUE form.instance.set_dollar_per_day() form.instance.update_left() - form.instance._current_total=form.cleaned_data['revenue'] + form.instance._current_total = form.cleaned_data['revenue'] return self.render_to_response(self.get_context_data(form=form)) - + def get_initial(self): - return {'target':10000, 'cc_date_initial': date_today()+timedelta(days=1461),'revenue':0, 'type':BUY2UNGLUE, 'status':'DEMO'} + return {'target':10000, 'cc_date_initial': date_today()+timedelta(days=1461), 'revenue':0, 'type':BUY2UNGLUE, 'status':'DEMO'} def get_context_data(self, **kwargs): - cd = super(FAQView,self).get_context_data(**kwargs) + cd = super(FAQView, self).get_context_data(**kwargs) cd.update({ - 'location': self.kwargs["location"], + 'location': self.kwargs["location"], 'sublocation': self.kwargs["sublocation"], }) return cd @@ -2420,13 +2404,13 @@ class GoodreadsDisplayView(TemplateView): context = super(GoodreadsDisplayView, self).get_context_data(**kwargs) session = self.request.session gr_client = GoodreadsClient(key=settings.GOODREADS_API_KEY, secret=settings.GOODREADS_API_SECRET) - + user = self.request.user if user.is_authenticated(): api_key = ApiKey.objects.filter(user=user)[0].key context['api_key'] = api_key - if user.profile.goodreads_user_id is None: + if user.profile.goodreads_user_id is None: # calculate the Goodreads authorization URL (context["goodreads_auth_url"], request_token) = gr_client.begin_authorization(self.request.build_absolute_uri(reverse('goodreads_cb'))) logger.info("goodreads_auth_url: %s" %(context["goodreads_auth_url"])) @@ -2439,14 +2423,14 @@ class GoodreadsDisplayView(TemplateView): gr_shelf_load_form = GoodreadsShelfLoadingForm() # load the shelves into the form choices = [('all:%d' % (gr_shelves["total_book_count"]),'all (%d)' % (gr_shelves["total_book_count"]))] + \ - [("%s:%d" % (s["name"], s["book_count"]) ,"%s (%d)" % (s["name"],s["book_count"])) for s in gr_shelves["user_shelves"]] + [("%s:%d" % (s["name"], s["book_count"]) ,"%s (%d)" % (s["name"], s["book_count"])) for s in gr_shelves["user_shelves"]] gr_shelf_load_form.fields['goodreads_shelf_name_number'].widget = Select(choices=tuple(choices)) - + context["gr_shelf_load_form"] = gr_shelf_load_form - + # also load any CeleryTasks associated with the user context["celerytasks"] = models.CeleryTask.objects.filter(user=user) - + return context @login_required @@ -2459,13 +2443,13 @@ def goodreads_auth(request): # store request token in session so that we can redeem it for auth_token if authorization works request.session['goodreads_request_token'] = request_token['oauth_token'] request.session['goodreads_request_secret'] = request_token['oauth_token_secret'] - + return HttpResponseRedirect(goodreads_auth_url) -@login_required +@login_required def goodreads_cb(request): """handle callback from Goodreads""" - + session = request.session authorized_flag = request.GET['authorize'] # is it '1'? request_oauth_token = request.GET['oauth_token'] @@ -2474,21 +2458,21 @@ def goodreads_cb(request): request_token = {'oauth_token': session.get('goodreads_request_token'), 'oauth_token_secret': session.get('goodreads_request_secret')} gr_client = GoodreadsClient(key=settings.GOODREADS_API_KEY, secret=settings.GOODREADS_API_SECRET) - + access_token = gr_client.complete_authorization(request_token) - + # store the access token in the user profile profile = request.user.profile profile.goodreads_auth_token = access_token["oauth_token"] profile.goodreads_auth_secret = access_token["oauth_token_secret"] - + # let's get the userid, username user = gr_client.auth_user() - + profile.goodreads_user_id = user["userid"] profile.goodreads_user_name = user["name"] profile.goodreads_user_link = user["link"] - + profile.save() # is this needed? # redirect to the Goodreads display page -- should observe some next later @@ -2496,7 +2480,7 @@ def goodreads_cb(request): @require_POST @login_required -@csrf_exempt +@csrf_exempt def goodreads_flush_assoc(request): user = request.user if user.is_authenticated(): @@ -2509,9 +2493,9 @@ def goodreads_flush_assoc(request): profile.save() logger.info('Goodreads association flushed for user %s', user) return HttpResponseRedirect(reverse('goodreads_display')) - + @require_POST -@login_required +@login_required @csrf_exempt def goodreads_load_shelf(request): """ @@ -2528,16 +2512,16 @@ def goodreads_load_shelf(request): load_task_name = "load_goodreads_shelf_into_wishlist" load_task = getattr(tasks, load_task_name) task_id = load_task.delay(user.id, shelf_name, expected_number_of_books=expected_number_of_books) - + ct = models.CeleryTask() ct.task_id = task_id ct.function_name = load_task_name ct.user = user ct.description = "Loading Goodread shelf %s to user %s with %s books" % (shelf_name, user, expected_number_of_books) ct.save() - + return HttpResponse("We're on it! Reload the page to see the books we've snagged so far.") - except Exception,e: + except Exception, e: return HttpResponse("Error in loading shelf: %s " % (e)) logger.info("Error in loading shelf for user %s: %s ", user, e) @@ -2547,23 +2531,23 @@ def goodreads_calc_shelves(request): # we should move towards calculating this only if needed (perhaps with Ajax), caching previous results, etc to speed up # performance - + if request.user.profile.goodreads_user_id is not None: gr_client = GoodreadsClient(key=settings.GOODREADS_API_KEY, secret=settings.GOODREADS_API_SECRET) goodreads_shelves = gr_client.shelves_list(user_id=request.user.profile.goodreads_user_id) #goodreads_shelf_load_form = GoodreadsShelfLoadingForm() ## load the shelves into the form #choices = [('all:%d' % (goodreads_shelves["total_book_count"]),'all (%d)' % (goodreads_shelves["total_book_count"]))] + \ - # [("%s:%d" % (s["name"], s["book_count"]) ,"%s (%d)" % (s["name"],s["book_count"])) for s in goodreads_shelves["user_shelves"]] + # [("%s:%d" % (s["name"], s["book_count"]) ,"%s (%d)" % (s["name"], s["book_count"])) for s in goodreads_shelves["user_shelves"]] #goodreads_shelf_load_form.fields['goodreads_shelf_name_number'].widget = Select(choices=tuple(choices)) else: goodreads_shelf_load_form = None - + return HttpResponse(json.dumps(goodreads_shelves), content_type="application/json") - + @require_POST -@login_required +@login_required @csrf_exempt def librarything_load(request): """ @@ -2572,30 +2556,30 @@ def librarything_load(request): # Should be moved to the API user = request.user - - try: + + try: # figure out expected_number_of_books later - + lt_username = request.user.profile.librarything_id - logger.info('Adding task to load librarything %s to user %s', lt_username, user ) + logger.info('Adding task to load librarything %s to user %s', lt_username, user) load_task_name = "load_librarything_into_wishlist" load_task = getattr(tasks, load_task_name) task_id = load_task.delay(user.id, lt_username, None) - + ct = models.CeleryTask() ct.task_id = task_id ct.function_name = load_task_name ct.user = user ct.description = "Loading LibraryThing collection of %s to user %s." % (lt_username, user) ct.save() - + return HttpResponse("We're on it! Reload the page to see the books we've snagged so far.") - except Exception,e: + except Exception, e: return HttpResponse("Error in loading LibraryThing library: %s " % (e)) logger.info("Error in loading LibraryThing for user %s: %s ", user, e) @require_POST -@login_required +@login_required @csrf_exempt def clear_wishlist(request): try: @@ -2603,53 +2587,53 @@ def clear_wishlist(request): logger.info("Wishlist for user %s cleared", request.user) return HttpResponse('wishlist cleared') except Exception, e: - return HttpResponse("Error in clearing wishlist: %s " % (e)) logger.info("Error in clearing wishlist for user %s: %s ", request.user, e) + return HttpResponse("Error in clearing wishlist: %s " % (e)) @require_POST -@login_required +@login_required def msg(request): form = MsgForm(data=request.POST) if form.is_valid(): if not request.user.is_staff and request.user not in form.cleaned_data['work'].last_campaign().managers.all(): - logger.warning("unauthorized attempt to send message by %s for %s"% (request.user,form.cleaned_data['work'])) + logger.warning("unauthorized attempt to send message by %s for %s"% (request.user, form.cleaned_data['work'])) raise Http404 - supporter_message.send(sender=request.user,msg=form.cleaned_data["msg"], work=form.cleaned_data["work"],supporter=form.cleaned_data["supporter"]) + supporter_message.send(sender=request.user, msg=form.cleaned_data["msg"], work=form.cleaned_data["work"], supporter=form.cleaned_data["supporter"]) return HttpResponse("message sent") else: logger.info("Invalid form for user %s", request.user) raise Http404 - + class LibraryThingView(FormView): - template_name="librarything.html" + template_name = "librarything.html" form_class = LibraryThingForm - + def get_context_data(self, **kwargs): context = super(LibraryThingView, self).get_context_data(**kwargs) form = kwargs['form'] # get the books for the lt_username in the form - lt_username=self.request.GET.get("lt_username",None) + lt_username = self.request.GET.get("lt_username", None) if lt_username is not None: lt = librarything.LibraryThing(username=lt_username) context.update({'books':list(lt.parse_user_catalog(view_style=5))}) else: context.update({'books':None}) - + # try picking up the LibraryThing api key -- and set to None if not available. Not being used for # anything crucial at this moment, so a None is ok here try: context.update({'lt_api_key':settings.LIBRARYTHING_API_KEY}) except: pass - + return context - def form_valid(self,form): + def form_valid(self, form): return super(LibraryThingView, self).form_valid(form) - + @require_POST -@login_required +@login_required @csrf_exempt def clear_celery_tasks(request): try: @@ -2657,15 +2641,15 @@ def clear_celery_tasks(request): logger.info("Celery tasks for user %s cleared", request.user) return HttpResponse('Celery Tasks List cleared') except Exception, e: + logger.info("Error in clearing Celery Tasks for user %s: %s ", request.user, e) return HttpResponse("Error in clearing Celery Tasks: %s " % (e)) - logger.info("Error in clearing Celery Tasks for user %s: %s ", request.user, e) def celery_test(request): return HttpResponse("celery_test") # routing views that try to redirect to the works page on a 3rd party site # -# TODO: need to queue up a task to look up IDs if we have to fallback to +# TODO: need to queue up a task to look up IDs if we have to fallback to # routing based on ISBN or search def work_librarything(request, work_id): @@ -2697,10 +2681,10 @@ def work_openlibrary(request, work_id): # as long as there were some matches get the first one and route to it if len(j.keys()) > 0: first = j.keys()[0] - url = "http://openlibrary.org" + j[first]['key'] + url = "http://openlibrary.org" + j[first]['key'] except ValueError: # fail at openlibrary - logger.warning("failed to get OpenLibrary json at %s" % u) + logger.warning("failed to get OpenLibrary json at %s" % u) # fall back to doing a search on openlibrary if not url: q = urlencode({'q': work.title + " " + work.author()}) @@ -2722,11 +2706,11 @@ def work_goodreads(request, work_id): @login_required def emailshare(request, action): if request.method == 'POST': - form=EmailShareForm(request.POST) + form = EmailShareForm(request.POST) if form.is_valid(): subject = form.cleaned_data['subject'] message = form.cleaned_data['message'] - sender = '%s via Unglue.it <%s>'%(request.user.username, request.user.email) + sender = '%s via Unglue.it <%s>' % (request.user.username, request.user.email) recipient = form.cleaned_data['recipient'] send_mail_task.delay(subject, message, sender, [recipient]) try: @@ -2735,11 +2719,11 @@ def emailshare(request, action): # if we totally failed to have a next value, we should still redirect somewhere useful next = 'https://unglue.it' return HttpResponseRedirect(next) - - else: + + else: work = None status = None - + try: next = request.GET['next'] work_id = next.split('/')[-2] @@ -2749,11 +2733,11 @@ def emailshare(request, action): status = work.last_campaign().status except: pass - context = {'request':request,'work':work,'site': Site.objects.get_current(), 'action': action} + context = {'request':request, 'work':work, 'site': Site.objects.get_current(), 'action': action} if work and action : message = render_to_string('emails/i_just_pledged.txt', context) subject = "Help me unglue "+work.title - else: + else: # customize the call to action depending on campaign status if status == 'ACTIVE': message = render_to_string('emails/pledge_this.txt', context) @@ -2768,42 +2752,42 @@ def emailshare(request, action): form = EmailShareForm(initial={ 'next':next, 'subject': subject, 'message': message}) - return render(request, "emailshare.html", {'form':form}) - + return render(request, "emailshare.html", {'form':form}) + def ask_rh(request, campaign_id): campaign = get_object_or_404(models.Campaign, id=campaign_id) - return feedback(request, recipient=campaign.email, template="ask_rh.html", - message_template="ask_rh.txt", + return feedback(request, recipient=campaign.email, template="ask_rh.html", + message_template="ask_rh.txt", redirect_url = reverse('work', args=[campaign.work.id]), - extra_context={'campaign':campaign, 'subject':campaign }) - + extra_context={'campaign':campaign, 'subject':campaign }) + def feedback(request, recipient='support@gluejar.com', template='feedback.html', message_template='feedback.txt', extra_context=None, redirect_url=None): context = extra_context or {} - context['num1'] = randint(0,10) - context['num2'] = randint(0,10) + context['num1'] = randint(0, 10) + context['num2'] = randint(0, 10) context['answer'] = context['num1'] + context['num2'] - + if request.method == 'POST': - form=FeedbackForm(request.POST) + form = FeedbackForm(request.POST) if form.is_valid(): context.update(form.cleaned_data) - context['request']=request + context['request'] = request if extra_context: context.update(extra_context) - message = render_to_string(message_template,context) + message = render_to_string(message_template, context) send_mail_task.delay(context['subject'], message, context['sender'], [recipient]) if redirect_url: return HttpResponseRedirect(redirect_url) else: - return render(request, "thanks.html", context) - + return render(request, "thanks.html", context) + else: context['num1'] = request.POST['num1'] context['num2'] = request.POST['num2'] - + else: if request.user.is_authenticated(): - context['sender']=request.user.email; + context['sender'] = request.user.email try: context['page'] = request.GET['page'] except: @@ -2812,8 +2796,8 @@ def feedback(request, recipient='support@gluejar.com', template='feedback.html', context['subject'] = "Feedback on page "+context['page'] form = FeedbackForm(initial=context) context['form'] = form - return render(request, template, context) - + return render(request, template, context) + def comment(request): latest_comments = Comment.objects.all().order_by('-submit_date')[:20] return render(request, "comments.html", {'latest_comments': latest_comments}) @@ -2837,13 +2821,13 @@ def lockss(request, work_id): except: ebooks = None authors = work.authors.all() - + return render(request, "lockss.html", {'work':work, 'ebooks':ebooks, 'authors':authors}) - + def lockss_manifest(request, year): """ manifest pages for lockss harvester -- yearly indices - (lockss needs pages listing all books unglued by year, with + (lockss needs pages listing all books unglued by year, with programmatically determinable URLs) """ year = int(year) @@ -2853,12 +2837,12 @@ def lockss_manifest(request, year): ebooks = models.Edition.objects.filter(unglued=True).filter(created__range=(start_date, end_date)) except: ebooks = None - + return render(request, "lockss_manifest.html", {'ebooks':ebooks, 'year': year}) class DownloadView(PurchaseView): - template_name="download.html" - form_class = CampaignThanksForm + template_name = "download.html" + form_class = CampaignThanksForm def show_beg(self): if not self.campaign or self.campaign.type != THANKS: return False @@ -2868,18 +2852,18 @@ class DownloadView(PurchaseView): return False elif self.campaign.status != 'ACTIVE': return self.request.GET.has_key('testmode') or self.request.POST.has_key('testmode') - else: + else: return True - + def form_valid(self, form): p = PaymentManager() - t, url = p.process_transaction('USD', form.cleaned_data["preapproval_amount"], - host = PAYMENT_HOST_NONE, - campaign=self.campaign, - user=self.request.user, - paymentReason="Unglue.it Contribution for {0}".format(self.campaign.name), - pledge_extra=form.trans_extra, - ) + t, url = p.process_transaction('USD', form.cleaned_data["preapproval_amount"], + host = PAYMENT_HOST_NONE, + campaign = self.campaign, + user = self.request.user, + paymentReason="Unglue.it Contribution for {0}".format(self.campaign.name), + pledge_extra = form.trans_extra, + ) if url: return HttpResponseRedirect(url) else: @@ -2889,7 +2873,7 @@ class DownloadView(PurchaseView): def get_form_kwargs(self): if self.kwargs.has_key('work'): self.work = self.kwargs["work"] - self.show_beg= lambda: False + self.show_beg = lambda: False else: self.work = safe_get_work(self.kwargs["work_id"]) self.campaign = self.work.last_campaign() @@ -2910,23 +2894,23 @@ class DownloadView(PurchaseView): def get_context_data(self, **kwargs): context = super(FormView, self).get_context_data(**kwargs) # adapt funtion view to class view - work = self.work + work = self.work request = self.request site = Site.objects.get_current() unglued_ebooks = work.ebooks().filter(edition__unglued=True) other_ebooks = work.ebooks().filter(edition__unglued=False) xfer_url = kindle_url = None - acq=None + acq = None formats = {} # a dict of format name and url for ebook in work.ebooks().all(): - formats[ebook.format] = reverse('download_ebook', args=[ebook.id] ) - - if request.user.is_authenticated(): + formats[ebook.format] = reverse('download_ebook', args=[ebook.id]) + + if request.user.is_authenticated(): #add a fave request.user.wishlist.add_work(work,'download') - - all_acqs=request.user.acqs.filter(work=work).order_by('-created') + + all_acqs = request.user.acqs.filter(work=work).order_by('-created') for an_acq in all_acqs: if not an_acq.expired: # skip for THANKS @@ -2938,20 +2922,20 @@ class DownloadView(PurchaseView): if not an_acq.on_reserve: watermark_acq.delay(an_acq) acq = an_acq - formats['epub']= reverse('download_acq', kwargs={'nonce':acq.nonce, 'format':'epub'}) - formats['mobi']= reverse('download_acq', kwargs={'nonce':acq.nonce, 'format':'mobi'}) + formats['epub'] = reverse('download_acq', kwargs={'nonce':acq.nonce, 'format':'epub'}) + formats['mobi'] = reverse('download_acq', kwargs={'nonce':acq.nonce, 'format':'mobi'}) xfer_url = settings.BASE_URL_SECURE + formats['epub'] kindle_url = settings.BASE_URL_SECURE + formats['mobi'] can_kindle = True break - - + + if not acq: # google ebooks have a captcha which breaks some of our services non_google_ebooks = work.ebooks().exclude(provider='Google Books') - - #send to kindle - + + #send to kindle + try: kindle_ebook = non_google_ebooks.filter(format='mobi')[0] can_kindle = kindle_ebook.kindle_sendable() @@ -2964,10 +2948,10 @@ class DownloadView(PurchaseView): # configure the xfer url try: xfer_epub_ebook = non_google_ebooks.filter(format='epub')[0] - xfer_url = settings.BASE_URL_SECURE + reverse('download_ebook',args=[xfer_epub_ebook.id]) + xfer_url = settings.BASE_URL_SECURE + reverse('download_ebook', args=[xfer_epub_ebook.id]) except: xfer_url = None - agent = request.META.get('HTTP_USER_AGENT','') + agent = request.META.get('HTTP_USER_AGENT','') iOS = 'iPad' in agent or 'iPhone' in agent or 'iPod' in agent iOS_app = iOS and not 'Safari' in agent android = 'Android' in agent @@ -2990,7 +2974,7 @@ class DownloadView(PurchaseView): 'acq':acq, 'show_beg': self.show_beg, 'preapproval_amount': self.get_preapproval_amount(), - 'work': work, + 'work': work, 'site': site, 'action': "Contribution", 'user_license': self.user_license, @@ -3011,7 +2995,7 @@ def feature(request, work_id): if work.is_free: work.featured = now() work.save() - return HttpResponseRedirect(reverse('landing', args=[] )) + return HttpResponseRedirect(reverse('landing', args=[])) else: return HttpResponse('can\'t feature an work without an ebook') @@ -3029,7 +3013,7 @@ def borrow(request, work_id): if libuser: acq = work.get_user_license(libuser).borrowable_acq if not libuser or not acq: - acq=work.get_lib_license(request.user).borrowable_acq + acq = work.get_lib_license(request.user).borrowable_acq if acq: borrowed = acq.borrow(request.user) return DownloadView.as_view()(request, work=work) @@ -3049,12 +3033,12 @@ def reserve(request, work_id): library = work.get_lib_license(request.user).next_acq.library except: library = None - - models.Hold.objects.get_or_create(library=library,work=work,user=request.user) - return PurchaseView.as_view()(request,work_id=work_id) - + + models.Hold.objects.get_or_create(library=library, work=work, user=request.user) + return PurchaseView.as_view()(request, work_id=work_id) + def download_ebook(request, ebook_id): - ebook = get_object_or_404(models.Ebook,id=ebook_id) + ebook = get_object_or_404(models.Ebook, id=ebook_id) ebook.increment() logger.info("ebook: {0}, user_ip: {1}".format(ebook_id, request.META['REMOTE_ADDR'])) return HttpResponseRedirect(ebook.url) @@ -3067,15 +3051,15 @@ def download_purchased(request, work_id): def download_campaign(request, work_id, format): work = safe_get_work(work_id) - # Raise 404 unless there is a SUCCESSFUL BUY2UNGLUE campaign associated with work + # Raise 404 unless there is a SUCCESSFUL BUY2UNGLUE campaign associated with work try: campaign = work.campaigns.get(status='SUCCESSFUL', type=BUY2UNGLUE) except Campaign.DoesNotExist as e: raise Http404 - ebfs= models.EbookFile.objects.filter(edition__work=campaign.work, format=format).exclude(file='').order_by('-created') + ebfs = models.EbookFile.objects.filter(edition__work=campaign.work, format=format).exclude(file='').order_by('-created') logger.info(ebfs.count()) - # return the link to the most recently created EbookFile (if any) with specified format for the campaign + # return the link to the most recently created EbookFile (if any) with specified format for the campaign for ebf in ebfs: logger.info(ebf.file.url) return HttpResponseRedirect(ebf.file.url) @@ -3084,14 +3068,14 @@ def download_campaign(request, work_id, format): raise Http404 def download_acq(request, nonce, format): - acq = get_object_or_404(models.Acq,nonce=nonce) + acq = get_object_or_404(models.Acq, nonce=nonce) if acq.on_reserve: acq.borrow() if format == 'epub': - return HttpResponseRedirect( acq.get_epub_url() ) + return HttpResponseRedirect(acq.get_epub_url()) else: - return HttpResponseRedirect( acq.get_mobi_url() ) - + return HttpResponseRedirect(acq.get_mobi_url()) + def about(request, facet): template = "about_" + facet + ".html" try: @@ -3103,7 +3087,7 @@ def receive_gift(request, nonce): try: gift = models.Gift.objects.get(acq__nonce=nonce) except models.Gift.DoesNotExist: - return render(request, 'gift_error.html', ) + return render(request, 'gift_error.html',) context = {'gift': gift, "site": Site.objects.get_current() } work = gift.acq.work context['work'] = work @@ -3113,8 +3097,8 @@ def receive_gift(request, nonce): if request.user.is_authenticated(): #check that user hasn't redeemed the gift themselves if (gift.acq.user.id == request.user.id) and not gift.acq.expired: - return HttpResponseRedirect( reverse('display_gift', args=[gift.id,'existing'] )) - return render(request, 'gift_error.html', context ) + return HttpResponseRedirect(reverse('display_gift', args=[gift.id,'existing'])) + return render(request, 'gift_error.html', context) if request.user.is_authenticated(): user_license = work.get_user_license(request.user) if user_license and user_license.purchased: @@ -3122,7 +3106,7 @@ def receive_gift(request, nonce): if user_license.is_duplicate or request.user.id == gift.giver.id: # regift if request.method == 'POST': - form=RegiftForm( data=request.POST) + form = RegiftForm(data=request.POST) if form.is_valid(): giftee = models.Gift.giftee(form.cleaned_data['give_to'], request.user.username) new_acq = models.Acq.objects.create(user=giftee, work=gift.acq.work, license= gift.acq.license) @@ -3132,20 +3116,20 @@ def receive_gift(request, nonce): gift.use() notification.send([giftee], "purchase_gift", context, True) return render(request, 'gift_duplicate.html', context) - context['form']= RegiftForm() + context['form'] = RegiftForm() return render(request, 'gift_duplicate.html', context) else: # new book! - gift.use() + gift.use() request.user.wishlist.add_work(gift.acq.work, 'gift') - return HttpResponseRedirect( reverse('display_gift', args=[gift.id,'existing'] )) + return HttpResponseRedirect(reverse('display_gift', args=[gift.id,'existing'])) else: # we'll just leave the old user inactive. gift.acq.user = request.user gift.acq.save() - gift.use() + gift.use() request.user.wishlist.add_work(gift.acq.work, 'gift') - return HttpResponseRedirect( reverse('display_gift', args=[gift.id,'existing'] )) + return HttpResponseRedirect(reverse('display_gift', args=[gift.id,'existing'])) if (gift.acq.created - gift.acq.user.date_joined) > timedelta(minutes=1) or gift.used: # giftee is established user (or gift has been used), ask them to log in return superlogin(request, extra_context=context, template_name='gift_login.html') @@ -3154,21 +3138,21 @@ def receive_gift(request, nonce): gift.use() gift.acq.user.wishlist.add_work(gift.acq.work, 'gift') login_user(request, gift.acq.user) - - return HttpResponseRedirect( reverse('display_gift', args=[gift.id, 'newuser'] )) -@login_required -def display_gift(request, gift_id, message): + return HttpResponseRedirect(reverse('display_gift', args=[gift.id, 'newuser'])) + +@login_required +def display_gift(request, gift_id, message): try: gift = models.Gift.objects.get(id=gift_id) except models.Gift.DoesNotExist: - return render(request, 'gift_error.html', ) + return render(request, 'gift_error.html',) if request.user.id != gift.acq.user.id : return HttpResponse("this is not your gift") redeemed_gift = request.session.get('gift_nonce', None) == gift.acq.nonce context = {'gift': gift, 'work': gift.acq.work , 'message':message } if request.method == 'POST' and redeemed_gift: - form=UserNamePass(data=request.POST) + form = UserNamePass(data=request.POST) form.oldusername = request.user.username context['form'] = form if form.is_valid(): @@ -3180,28 +3164,32 @@ def display_gift(request, gift_id, message): return render(request, 'gift_welcome.html', context) else: if redeemed_gift: - form = UserNamePass(initial={'username':request.user.username}) + form = UserNamePass(initial={'username':request.user.username}) form.oldusername = request.user.username context['form'] = form return render(request, 'gift_welcome.html', context) - -@login_required -@csrf_exempt + +@login_required +@csrf_exempt def ml_status(request): return render(request, "ml_status.html") @require_POST -@login_required +@login_required def ml_subscribe(request): - request.user.profile.ml_subscribe(double_optin=False,send_welcome=True, merge_vars = {"OPTIN_IP":request.META['REMOTE_ADDR'],"OPTIN_TIME":now().isoformat()}) + request.user.profile.ml_subscribe( + double_optin=False, + send_welcome=True, + merge_vars = {"OPTIN_IP":request.META['REMOTE_ADDR'], "OPTIN_TIME":now().isoformat()} + ) return HttpResponseRedirect(reverse("notification_notice_settings")) @require_POST -@login_required +@login_required def ml_unsubscribe(request): request.user.profile.ml_unsubscribe() return HttpResponseRedirect(reverse("notification_notice_settings")) - + def press(request): latest_items = models.Press.objects.order_by('-date')[:3] highlighted_items = models.Press.objects.filter(highlight=True).order_by('-date') @@ -3211,7 +3199,7 @@ def press(request): 'highlighted_items': highlighted_items, 'all_items': all_items }) - + def press_submitterator(request): if not request.user.is_staff: return render(request, "admins_only.html") @@ -3224,7 +3212,7 @@ def press_submitterator(request): title = form.cleaned_data['title'] else: form = PressForm() - + return render(request, 'press_submitterator.html', { 'form':form, 'title':title @@ -3244,10 +3232,10 @@ def kindle_config(request, work_id=None): request.user.profile.save() template = "kindle_change_successful.html" else: - form = KindleEmailForm() + form = KindleEmailForm() return render(request, template, { - 'form': form, - 'work': work, + 'form': form, + 'work': work, 'ok_email': request.user.profile.kindle_email and ('kindle' in request.user.profile.kindle_email), }) @@ -3259,15 +3247,15 @@ def send_to_kindle(request, work_id, javascript='0'): def local_response(request, javascript, context, message): context['message'] = message if javascript == '1': - return render(request,'kindle_response_message.html',context ) + return render(request, 'kindle_response_message.html', context) else: return render(request, 'kindle_response_graceful_degradation.html', context) - - work=safe_get_work(work_id) - context= {'work':work} + + work = safe_get_work(work_id) + context = {'work':work} acq = None - if request.user.is_authenticated(): - all_acqs=request.user.acqs.filter(work=work).order_by('-created') + if request.user.is_authenticated(): + all_acqs = request.user.acqs.filter(work=work).order_by('-created') for an_acq in all_acqs: if not an_acq.expired: # skip for THANKS @@ -3280,7 +3268,7 @@ def send_to_kindle(request, work_id, javascript='0'): watermark_acq.delay(an_acq) acq = an_acq break - + if acq: ebook = acq.ebook() title = acq.work.kindle_safe_title() @@ -3308,10 +3296,10 @@ def send_to_kindle(request, work_id, javascript='0'): return local_response(request, javascript, context, 3) request.session['kindle_email'] = kindle_email elif request.user.is_authenticated(): - kindle_email = request.user.profile.kindle_email + kindle_email = request.user.profile.kindle_email context['kindle_email'] = kindle_email - + """ Amazon SES has a 10 MB size limit (http://aws.amazon.com/ses/faqs/#49) in messages sent to determine whether the file will meet this limit, we probably need to compare the @@ -3335,7 +3323,7 @@ def send_to_kindle(request, work_id, javascript='0'): if ebook.filesize > models.send_to_kindle_limit: logger.info('ebook %s is too large to be emailed' % work.id) return local_response(request, javascript, context, 0) - + try: email = EmailMessage(from_email='notices@gluejar.com', to=[kindle_email]) @@ -3348,29 +3336,29 @@ def send_to_kindle(request, work_id, javascript='0'): if request.POST.has_key('kindle_email') and not request.user.is_authenticated(): return HttpResponseRedirect(reverse('superlogin')) return local_response(request, javascript, context, 2) - - + + def userlist_marc(request, userlist=None): if userlist: - user = get_object_or_404(User,username=userlist) + user = get_object_or_404(User, username=userlist) return qs_marc_records(request, qs=user.wishlist.works.all()) else: return qs_marc_records(request, qs=request.user.wishlist.works.all()) - return render( request,'marc.html',{'userlist' : [] }) + return render(request, 'marc.html', {'userlist' : [] }) def work_marc(request, work_id): work = safe_get_work(work_id) return qs_marc_records(request, qs=[ work ]) - + class LibModeView(FormView): template_name = 'marc_config.html' form_class = LibModeForm success_url = reverse_lazy('marc') - + def form_valid(self, form): - enable= form.data.has_key('enable') + enable = form.data.has_key('enable') if enable: try: libpref = self.request.user.libpref @@ -3383,11 +3371,11 @@ class LibModeView(FormView): self.request.user.libpref.delete() except: pass - messages.success(self.request,"Tools are disabled." ) + messages.success(self.request,"Tools are disabled.") if reverse('marc_config', args=[]) in self.request.META['HTTP_REFERER']: return HttpResponseRedirect(reverse('marc_config', args=[])) else: return super(LibModeView, self).form_valid(form) - - + + From 95fd91e283d331666569eb9f9cd3d3b78a640ae4 Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 15 Aug 2016 16:12:27 -0400 Subject: [PATCH 08/31] pylint the forms --- frontend/forms.py | 378 +++++++++++++++++++++++++--------------------- 1 file changed, 209 insertions(+), 169 deletions(-) diff --git a/frontend/forms.py b/frontend/forms.py index 0566bfe5..f71fd671 100644 --- a/frontend/forms.py +++ b/frontend/forms.py @@ -1,22 +1,17 @@ -""" -external library imports -""" +#external library imports + import logging import re -import zipfile -from datetime import timedelta, datetime, date +from datetime import timedelta, date from decimal import Decimal as D -""" -django imports -""" +#django imports + from django import forms from django.conf import settings from django.conf.global_settings import LANGUAGES from django.contrib.auth.models import User -from django.core.validators import validate_email -from django.db import models from django.forms.widgets import RadioSelect from django.forms.extras.widgets import SelectDateWidget from django.utils.translation import ugettext_lazy as _ @@ -32,10 +27,8 @@ from selectable.forms import ( from PyPDF2 import PdfFileReader +#regluit imports -""" -regluit imports -""" from regluit.core.models import ( UserProfile, RightsHolder, @@ -51,10 +44,8 @@ from regluit.core.models import ( Work, Press, Libpref, - Subject, TWITTER, FACEBOOK, - GRAVATAR, UNGLUEITAR ) from regluit.libraryauth.models import Library @@ -63,11 +54,10 @@ from regluit.core.lookups import ( OwnerLookup, WorkLookup, PublisherNameLookup, - EditionLookup, SubjectLookup, ) from regluit.utils.localdatetime import now -from regluit.utils.fields import EpubFileField, ISBNField +from regluit.utils.fields import ISBNField from regluit.mobi import Mobi from regluit.pyepub import EPUB from regluit.bisac.models import BisacHeading @@ -90,15 +80,15 @@ class SurveyForm(forms.Form): label = forms.CharField(max_length=64, required=True) survey = forms.ModelChoiceField(Questionnaire.objects.all(), widget=RadioSelect(), empty_label=None, required = True,) isbn = ISBNField( - label=_("ISBN"), - max_length=17, + label=_("ISBN"), + max_length=17, required = False, help_text = _("13 digits, no dash."), error_messages = { 'invalid': _("This must be a valid ISBN-13."), } ) - + def clean_isbn(self): isbn = self.cleaned_data['isbn'] if not isbn: @@ -121,7 +111,7 @@ class EditionForm(forms.ModelForm): required =False ) bisac = forms.ModelChoiceField( bisac_headings, required=False ) - + publisher_name = AutoCompleteSelectField( PublisherNameLookup, label='Publisher Name', @@ -131,8 +121,8 @@ class EditionForm(forms.ModelForm): ) isbn = ISBNField( - label=_("ISBN"), - max_length=17, + label=_("ISBN"), + max_length=17, required = False, help_text = _("13 digits, no dash."), error_messages = { @@ -140,8 +130,8 @@ class EditionForm(forms.ModelForm): } ) goog = forms.RegexField( - label=_("Google Books ID"), - max_length=12, + label=_("Google Books ID"), + max_length=12, regex=r'^([a-zA-Z0-9\-_]{12}|delete)$', required = False, help_text = _("12 alphanumeric characters, dash or underscore, case sensitive."), @@ -150,8 +140,8 @@ class EditionForm(forms.ModelForm): } ) gdrd = forms.RegexField( - label=_("GoodReads ID"), - max_length=8, + label=_("GoodReads ID"), + max_length=8, regex=r'^(\d+|delete)$', required = False, help_text = _("1-8 digits."), @@ -160,8 +150,8 @@ class EditionForm(forms.ModelForm): } ) thng = forms.RegexField( - label=_("LibraryThing ID"), - max_length=8, + label=_("LibraryThing ID"), + max_length=8, regex=r'(^\d+|delete)$', required = False, help_text = _("1-8 digits."), @@ -170,7 +160,7 @@ class EditionForm(forms.ModelForm): } ) oclc = forms.RegexField( - label=_("OCLCnum"), + label=_("OCLCnum"), regex=r'^(\d\d\d\d\d\d\d\d\d*|delete)$', required = False, help_text = _("8 or more digits."), @@ -182,7 +172,7 @@ class EditionForm(forms.ModelForm): label=_("HTTP URL"), # https://mathiasbynens.be/demo/url-regex regex=re.compile(r"(https?|ftp)://(-\.)?([^\s/?\.#]+\.?)+(/[^\s]*)?$", - flags=re.IGNORECASE|re.S ), + flags=re.IGNORECASE|re.S ), required = False, help_text = _("no spaces of funny stuff."), error_messages = { @@ -190,7 +180,7 @@ class EditionForm(forms.ModelForm): } ) doi = forms.RegexField( - label=_("DOI"), + 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'"), @@ -202,15 +192,15 @@ class EditionForm(forms.ModelForm): age_level = forms.ChoiceField(choices=AGE_LEVEL_CHOICES) description = forms.CharField( required=False, widget=CKEditorWidget()) coverfile = forms.ImageField(required=False) - + def __init__(self, *args, **kwargs): super(EditionForm, self).__init__(*args, **kwargs) self.relators = [] if self.instance: for relator in self.instance.relators.all(): select = forms.Select(choices=CREATOR_RELATIONS).render('change_relator_%s' % relator.id , relator.relation.code ) - self.relators.append({'relator':relator,'select':select}) - + self.relators.append({'relator':relator, 'select':select}) + def clean_doi(self): doi = self.cleaned_data["doi"] if doi: @@ -219,7 +209,7 @@ class EditionForm(forms.ModelForm): 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 @@ -229,39 +219,41 @@ class EditionForm(forms.ModelForm): 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: model = Edition exclude = ('created', 'work') - widgets = { + widgets = { 'title': forms.TextInput(attrs={'size': 40}), 'add_author': forms.TextInput(attrs={'size': 30}), 'add_subject': forms.TextInput(attrs={'size': 30}), 'unglued': forms.CheckboxInput(), 'cover_image': forms.TextInput(attrs={'size': 60}), } - + class EbookFileForm(forms.ModelForm): file = forms.FileField(max_length=16777216) - + def __init__(self, campaign_type=BUY2UNGLUE, *args, **kwargs): super(EbookFileForm, self).__init__(*args, **kwargs) self.campaign_type = campaign_type if campaign_type == BUY2UNGLUE: - self.fields['format'].widget=forms.HiddenInput() + self.fields['format'].widget = forms.HiddenInput() if campaign_type == THANKS: - self.fields['format'].widget=forms.Select(choices=(('pdf','PDF'),( 'epub','EPUB'), ('mobi','MOBI'))) - + self.fields['format'].widget = forms.Select( + choices = (('pdf', 'PDF'), ('epub', 'EPUB'), ('mobi', 'MOBI')) + ) + def clean_format(self): if self.campaign_type is BUY2UNGLUE: return 'epub' else: logger.info("EbookFileForm "+self.cleaned_data.get('format','')) return self.cleaned_data.get('format','') - + def clean(self): format = self.cleaned_data['format'] - the_file = self.cleaned_data.get('file',None) + the_file = self.cleaned_data.get('file', None) if the_file and the_file.name: if format == 'epub': try: @@ -289,19 +281,19 @@ class EbookFileForm(forms.ModelForm): class EbookForm(forms.ModelForm): class Meta: model = Ebook - exclude =( 'created', 'download_count', 'active', 'filesize') - widgets = { - 'edition': forms.HiddenInput, - 'user': forms.HiddenInput, - 'provider': forms.HiddenInput, + exclude = ('created', 'download_count', 'active', 'filesize') + widgets = { + 'edition': forms.HiddenInput, + 'user': forms.HiddenInput, + 'provider': forms.HiddenInput, 'url': forms.TextInput(attrs={'size' : 60}), } def clean_provider(self): - new_provider= Ebook.infer_provider(self.data[self.prefix + '-url']) + new_provider = Ebook.infer_provider(self.data[self.prefix + '-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'] try: @@ -309,25 +301,25 @@ class EbookForm(forms.ModelForm): except Ebook.DoesNotExist: return url raise forms.ValidationError(_("There's already an ebook with that url.")) - + def UserClaimForm ( user_instance, *args, **kwargs ): class ClaimForm(forms.ModelForm): - i_agree=forms.BooleanField(error_messages={'required': 'You must agree to the Terms in order to claim a work.'}) - rights_holder=forms.ModelChoiceField(queryset=user_instance.rights_holder.all(), empty_label=None) - + i_agree = forms.BooleanField(error_messages={'required': 'You must agree to the Terms in order to claim a work.'}) + rights_holder = forms.ModelChoiceField(queryset=user_instance.rights_holder.all(), empty_label=None) + class Meta: model = Claim exclude = ('status',) - widgets = { - 'user': forms.HiddenInput, - 'work': forms.HiddenInput, + widgets = { + 'user': forms.HiddenInput, + 'work': forms.HiddenInput, } def __init__(self): super(ClaimForm, self).__init__(*args, **kwargs) return ClaimForm() - + class RightsHolderForm(forms.ModelForm): owner = AutoCompleteSelectField( OwnerLookup, @@ -337,7 +329,7 @@ class RightsHolderForm(forms.ModelForm): error_messages={'required': 'Please ensure the owner is a valid Unglue.it account.'}, ) email = forms.EmailField( - label=_("notification email address for rights holder"), + label=_("notification email address for rights holder"), max_length=100, error_messages={'required': 'Please enter an email address for the rights holder.'}, ) @@ -353,12 +345,12 @@ class RightsHolderForm(forms.ModelForm): return rights_holder_name raise forms.ValidationError(_("Another rights holder with that name already exists.")) - + class ProfileForm(forms.ModelForm): - clear_facebook=forms.BooleanField(required=False) - clear_twitter=forms.BooleanField(required=False) - clear_goodreads=forms.BooleanField(required=False) - + clear_facebook = forms.BooleanField(required=False) + clear_twitter = forms.BooleanField(required=False) + clear_goodreads = forms.BooleanField(required=False) + class Meta: model = UserProfile fields = 'tagline', 'librarything_id', 'home_url', 'clear_facebook', 'clear_twitter', 'clear_goodreads', 'avatar_source' @@ -381,10 +373,10 @@ class ProfileForm(forms.ModelForm): def clean(self): # check that if a social net is cleared, we're not using it a avatar source - if self.cleaned_data.get("clear_facebook", False) and self.cleaned_data.get("avatar_source", None)==FACEBOOK: - self.cleaned_data["avatar_source"]==UNGLUEITAR - if self.cleaned_data.get("clear_twitter", False) and self.cleaned_data.get("avatar_source", None)==TWITTER: - self.cleaned_data["avatar_source"]==UNGLUEITAR + if self.cleaned_data.get("clear_facebook", False) and self.cleaned_data.get("avatar_source", None) == FACEBOOK: + self.cleaned_data["avatar_source"] == UNGLUEITAR + if self.cleaned_data.get("clear_twitter", False) and self.cleaned_data.get("avatar_source", None) == TWITTER: + self.cleaned_data["avatar_source"] == UNGLUEITAR return self.cleaned_data class CloneCampaignForm(forms.Form): @@ -415,30 +407,33 @@ def getTransferCreditForm(maximum, data=None, *args, **kwargs ): ) amount = forms.IntegerField( required=True, - min_value=1, + min_value=1, max_value=maximum, label="Transfer Amount", - error_messages={'min_value': 'Transfer amount must be positive', 'max_value': 'You only have %(limit_value)s available for transfer'}, + error_messages={ + 'min_value': 'Transfer amount must be positive', + 'max_value': 'You only have %(limit_value)s available for transfer' + }, ) return TransferCreditForm( data=data ) class WorkForm(forms.Form): - other_work = forms.ModelChoiceField(queryset=Work.objects.all(), - widget=forms.HiddenInput(), - required=True, + other_work = forms.ModelChoiceField(queryset=Work.objects.all(), + widget=forms.HiddenInput(), + required=True, error_messages={'required': 'Missing work to merge with.'}, ) - work=None + work = None def clean_other_work(self): - if self.cleaned_data["other_work"].id== self.work.id: + if self.cleaned_data["other_work"].id == self.work.id: raise forms.ValidationError(_("You can't merge a work into itself")) return self.cleaned_data["other_work"] def __init__(self, work=None, *args, **kwargs): super(WorkForm, self).__init__(*args, **kwargs) - self.work=work + self.work = work class OtherWorkForm(WorkForm): other_work = AutoCompleteSelectField( @@ -447,12 +442,12 @@ class OtherWorkForm(WorkForm): widget=AutoCompleteSelectWidget(WorkLookup), required=True, error_messages={'required': 'Missing work to merge with.'}, - ) + ) def __init__(self, *args, **kwargs): super(OtherWorkForm, self).__init__(*args, **kwargs) self.fields['other_work'].widget.update_query_parameters({'language':self.work.language}) - + class EditManagersForm(forms.ModelForm): managers = AutoCompleteSelectMultipleField( OwnerLookup, @@ -471,7 +466,7 @@ class CustomPremiumForm(forms.ModelForm): class Meta: model = Premium fields = 'campaign', 'amount', 'description', 'type', 'limit' - widgets = { + widgets = { 'description': forms.Textarea(attrs={'cols': 80, 'rows': 4}), 'campaign': forms.HiddenInput, 'type': forms.HiddenInput(attrs={'value':'XX'}), @@ -479,21 +474,24 @@ class CustomPremiumForm(forms.ModelForm): } def clean_type(self): return 'CU' - + class OfferForm(forms.ModelForm): class Meta: model = Offer fields = 'work', 'price', 'license' - widgets = { + widgets = { 'work': forms.HiddenInput, 'license': forms.HiddenInput, } - -date_selector=range(date.today().year, settings.MAX_CC_DATE.year+1) + +date_selector = range(date.today().year, settings.MAX_CC_DATE.year+1) class CCDateForm(object): - target = forms.DecimalField( min_value= D(settings.UNGLUEIT_MINIMUM_TARGET), error_messages={'required': 'Please specify a Revenue Target.'} ) + target = forms.DecimalField( + min_value= D(settings.UNGLUEIT_MINIMUM_TARGET), + error_messages={'required': 'Please specify a Revenue Target.'} + ) minimum_target = settings.UNGLUEIT_MINIMUM_TARGET maximum_target = settings.UNGLUEIT_MAXIMUM_TARGET max_cc_date = settings.MAX_CC_DATE @@ -510,26 +508,26 @@ class CCDateForm(object): new_cc_date_initial = self.cleaned_data['cc_date_initial'] if new_cc_date_initial.date() > settings.MAX_CC_DATE: raise forms.ValidationError('The initial Ungluing Date cannot be after %s'%settings.MAX_CC_DATE) - elif new_cc_date_initial - now() < timedelta(days=0): + elif new_cc_date_initial - now() < timedelta(days=0): raise forms.ValidationError('The initial Ungluing date must be in the future!') return new_cc_date_initial class DateCalculatorForm(CCDateForm, forms.ModelForm): revenue = forms.DecimalField() cc_date_initial = forms.DateTimeField( - widget = SelectDateWidget(years=date_selector) + widget = SelectDateWidget(years=date_selector) ) class Meta: model = Campaign fields = 'target', 'cc_date_initial', 'revenue', def getManageCampaignForm ( instance, data=None, initial=None, *args, **kwargs ): - + def get_queryset(): - work=instance.work + work = instance.work return Edition.objects.filter(work = work) - - class ManageCampaignForm(CCDateForm,forms.ModelForm): + + class ManageCampaignForm(CCDateForm, forms.ModelForm): target = forms.DecimalField( required= (instance.type in {REWARDS, BUY2UNGLUE})) deadline = forms.DateTimeField( required = (instance.type==REWARDS), @@ -540,91 +538,102 @@ def getManageCampaignForm ( instance, data=None, initial=None, *args, **kwargs ) widget = SelectDateWidget(years=date_selector) if instance.status=='INITIALIZED' else forms.HiddenInput ) paypal_receiver = forms.EmailField( - label=_("contact email address for this campaign"), - max_length=100, + label=_("contact email address for this campaign"), + max_length=100, error_messages={'required': 'You must enter the email we should contact you at for this campaign.'}, ) - edition = forms.ModelChoiceField(get_queryset(), widget=RadioSelect(),empty_label='no edition selected',required = False,) - publisher = forms.ModelChoiceField(instance.work.publishers(), empty_label='no publisher selected', required = False,) + edition = forms.ModelChoiceField( + get_queryset(), + widget=RadioSelect(), + empty_label='no edition selected', + required=False, + ) + publisher = forms.ModelChoiceField( + instance.work.publishers(), + empty_label='no publisher selected', + required=False, + ) work_description = forms.CharField( required=False , widget=CKEditorWidget()) - + class Meta: model = Campaign - fields = 'description', 'details', 'license', 'target', 'deadline', 'paypal_receiver', 'edition', 'email', 'publisher', 'cc_date_initial', "do_watermark", "use_add_ask", + fields = ('description', 'details', 'license', 'target', 'deadline', 'paypal_receiver', + 'edition', 'email', 'publisher', 'cc_date_initial', "do_watermark", "use_add_ask", + ) widgets = { 'deadline': SelectDateWidget } - + def clean_target(self): if self.instance.type == THANKS: return None - new_target = super(ManageCampaignForm,self).clean_target() + new_target = super(ManageCampaignForm, self).clean_target() if self.instance: if self.instance.status == 'ACTIVE' and self.instance.target < new_target: raise forms.ValidationError(_('The fundraising target for an ACTIVE campaign cannot be increased.')) return new_target def clean_cc_date_initial(self): - if self.instance.type in {REWARDS,THANKS} : + if self.instance.type in {REWARDS, THANKS} : return None if self.instance: if self.instance.status != 'INITIALIZED': # can't change this once launched return self.instance.cc_date_initial - return super(ManageCampaignForm,self).clean_cc_date_initial() - + return super(ManageCampaignForm, self).clean_cc_date_initial() + def clean_deadline(self): if self.instance.type in {BUY2UNGLUE, THANKS} : return None new_deadline_date = self.cleaned_data['deadline'] - new_deadline= new_deadline_date + timedelta(hours=23,minutes=59) + new_deadline = new_deadline_date + timedelta(hours=23, minutes=59) if self.instance: if self.instance.status == 'ACTIVE': return self.instance.deadline if new_deadline_date - now() > timedelta(days=int(settings.UNGLUEIT_LONGEST_DEADLINE)): raise forms.ValidationError(_('The chosen closing date is more than %s days from now' % settings.UNGLUEIT_LONGEST_DEADLINE)) - elif new_deadline - now() < timedelta(days=0): + elif new_deadline - now() < timedelta(days=0): raise forms.ValidationError(_('The chosen closing date is in the past')) return new_deadline - + def clean_license(self): new_license = self.cleaned_data['license'] if self.instance: if self.instance.status == 'ACTIVE' and self.instance.license != new_license: # should only allow change to a less restrictive license - if self.instance.license == 'CC BY-ND' and new_license in ['CC BY-NC-ND','CC BY-NC-SA','CC BY-NC']: + if self.instance.license == 'CC BY-ND' and new_license in ['CC BY-NC-ND', 'CC BY-NC-SA', 'CC BY-NC']: raise forms.ValidationError(_('The proposed license for an ACTIVE campaign may not add restrictions.')) elif self.instance.license == 'CC BY' and new_license != 'CC0': raise forms.ValidationError(_('The proposed license for an ACTIVE campaign may not add restrictions.')) - elif self.instance.license == 'CC BY-NC' and new_license in ['CC BY-NC-ND','CC BY-NC-SA','CC BY-SA','CC BY-ND']: + elif self.instance.license == 'CC BY-NC' and new_license in ['CC BY-NC-ND', 'CC BY-NC-SA', 'CC BY-SA', 'CC BY-ND']: raise forms.ValidationError(_('The proposed license for an ACTIVE campaign may not add restrictions.')) - elif self.instance.license == 'CC BY-ND' and new_license in ['CC BY-NC-ND','CC BY-NC-SA','CC BY-SA','CC BY-NC']: + elif self.instance.license == 'CC BY-ND' and new_license in ['CC BY-NC-ND', 'CC BY-NC-SA', 'CC BY-SA', 'CC BY-NC']: raise forms.ValidationError(_('The proposed license for an ACTIVE campaign may not add restrictions.')) - elif self.instance.license == 'CC BY-SA' and new_license in ['CC BY-NC-ND','CC BY-NC-SA','CC BY-ND','CC BY-NC']: + elif self.instance.license == 'CC BY-SA' and new_license in ['CC BY-NC-ND', 'CC BY-NC-SA', 'CC BY-ND', 'CC BY-NC']: raise forms.ValidationError(_('The proposed license for an ACTIVE campaign may not add restrictions.')) - elif self.instance.license == 'CC BY-NC-SA' and new_license in ['CC BY-NC-ND','CC BY-ND']: + elif self.instance.license == 'CC BY-NC-SA' and new_license in ['CC BY-NC-ND', 'CC BY-ND']: raise forms.ValidationError(_('The proposed license for an ACTIVE campaign may not add restrictions.')) elif self.instance.license == 'CC0' : raise forms.ValidationError(_('The proposed license for an ACTIVE campaign may not add restrictions.')) - elif self.instance.license in ['GDFL' , 'LAL']: + elif self.instance.license in ['GDFL', 'LAL']: raise forms.ValidationError(_('Once you start a campaign with GDFL or LAL, you can\'t use any other license.')) return new_license if initial and not initial.get('edition', None) and not instance.edition: - initial['edition']= instance.work.editions.all()[0] - return ManageCampaignForm(instance = instance, data=data, initial=initial) + initial['edition'] = instance.work.editions.all()[0] + return ManageCampaignForm(instance=instance, data=data, initial=initial) class CampaignPurchaseForm(forms.Form): anonymous = forms.BooleanField(required=False, label=_("Make this purchase anonymous, please")) offer_id = forms.IntegerField(required=False) - offer=None + offer = None library_id = forms.IntegerField(required=False) library = None - copies = forms.IntegerField(required=False,min_value=1) + copies = forms.IntegerField(required=False, min_value=1) give_to = forms.EmailField(required = False) give_message = forms.CharField(required = False, max_length=512, ) - + def clean_offer_id(self): offer_id = self.cleaned_data['offer_id'] try: - self.offer= Offer.objects.get(id=offer_id) + self.offer = Offer.objects.get(id=offer_id) except Offer.DoesNotExist: raise forms.ValidationError(_("Sorry, that offer is not valid.")) @@ -635,23 +644,23 @@ class CampaignPurchaseForm(forms.Form): self.library = Library.objects.get(id=library_id) except Library.DoesNotExist: raise forms.ValidationError(_("Sorry, that Library is not valid.")) - + def clean_copies(self): - copies = self.cleaned_data.get('copies',1) + copies = self.cleaned_data.get('copies', 1) return copies if copies else 1 - + def clean_anonymous(self): if self.data.get('give', False): return True else: return self.cleaned_data['anonymous'] - + def clean(self): if self.offer.license == LIBRARY: if not self.library: raise forms.ValidationError(_("No library specified." )) if self.data.get('give', False): - if not self.cleaned_data.get('give_to',None): + if not self.cleaned_data.get('give_to', None): raise forms.ValidationError(_("Gift recipient email is needed." )) else: if 'give_to' in self._errors: @@ -659,28 +668,31 @@ class CampaignPurchaseForm(forms.Form): return self.cleaned_data def amount(self): - - return self.offer.price * self.cleaned_data.get('copies',1) if self.offer else None - + + return self.offer.price * self.cleaned_data.get('copies', 1) if self.offer else None + @property def trans_extra(self): pe = PledgeExtra( anonymous=self.cleaned_data['anonymous'], offer = self.offer ) if self.library: - pe.extra['library_id']=self.library.id - pe.extra['copies']=self.cleaned_data.get('copies',1) + pe.extra['library_id'] = self.library.id + pe.extra['copies'] = self.cleaned_data.get('copies', 1) if self.data.get('give', False): - pe.extra['give_to']=self.cleaned_data['give_to'] - pe.extra['give_message']=self.cleaned_data['give_message'] + pe.extra['give_to'] = self.cleaned_data['give_to'] + pe.extra['give_message'] = self.cleaned_data['give_message'] return pe class CampaignThanksForm(forms.Form): - anonymous = forms.BooleanField(required=False, label=_("Make this contribution anonymous, please")) + anonymous = forms.BooleanField( + required=False, + label=_("Make this contribution anonymous, please") + ) preapproval_amount = forms.DecimalField( required = True, min_value=D('1.00'), - max_value=D('2000.00'), - decimal_places=2, + max_value=D('2000.00'), + decimal_places=2, label="Pledge Amount", ) @property @@ -692,43 +704,47 @@ class CampaignPledgeForm(forms.Form): preapproval_amount = forms.DecimalField( required = False, min_value=D('1.00'), - max_value=D('2000.00'), - decimal_places=2, + max_value=D('2000.00'), + decimal_places=2, label="Pledge Amount", ) def amount(self): return self.cleaned_data["preapproval_amount"] if self.cleaned_data else None - + anonymous = forms.BooleanField(required=False, label=_("Make this pledge anonymous, please")) - ack_name = forms.CharField(required=False, max_length=64, label=_("What name should we display?")) + ack_name = forms.CharField( + required=False, + max_length=64, + label=_("What name should we display?") + ) ack_dedication = forms.CharField(required=False, max_length=140, label=_("Your dedication:")) premium_id = forms.IntegerField(required=False) - premium=None - + premium = None + @property def trans_extra(self): return PledgeExtra( anonymous=self.cleaned_data['anonymous'], ack_name=self.cleaned_data['ack_name'], ack_dedication=self.cleaned_data['ack_dedication'], premium=self.premium) - + def clean_preapproval_amount(self): preapproval_amount = self.cleaned_data['preapproval_amount'] if preapproval_amount is None: raise forms.ValidationError(_("Please enter a pledge amount.")) return preapproval_amount - + def clean_premium_id(self): premium_id = self.cleaned_data['premium_id'] try: - self.premium= Premium.objects.get(id=premium_id) - if self.premium.limit>0: - if self.premium.limit<=self.premium.premium_count: + self.premium = Premium.objects.get(id=premium_id) + if self.premium.limit > 0: + if self.premium.limit <= self.premium.premium_count: raise forms.ValidationError(_("Sorry, that premium is fully subscribed.")) except Premium.DoesNotExist: raise forms.ValidationError(_("Sorry, that premium is not valid.")) - + def clean(self): # check on whether the preapproval amount is < amount for premium tier. If so, put an error message preapproval_amount = self.cleaned_data.get("preapproval_amount") @@ -736,7 +752,7 @@ class CampaignPledgeForm(forms.Form): # preapproval_amount failed validation, that error is the relevant one return self.cleaned_data elif self.premium is None: - raise forms.ValidationError(_("Please select a premium." )) + raise forms.ValidationError(_("Please select a premium." )) elif preapproval_amount < self.premium.amount: logger.info("raising form validating error") raise forms.ValidationError(_("Sorry, you must pledge at least $%s to select that premium." % (self.premium.amount))) @@ -747,11 +763,11 @@ class TokenCCMixin(forms.Form): class BaseCCMixin(forms.Form): work_id = forms.IntegerField(required=False, widget=forms.HiddenInput()) - preapproval_amount= forms.DecimalField( + preapproval_amount = forms.DecimalField( required=False, - min_value=D('1.00'), - max_value=D('100000.00'), - decimal_places=2, + min_value=D('1.00'), + max_value=D('100000.00'), + decimal_places=2, label="Amount", ) class UserCCMixin(forms.Form): @@ -771,7 +787,7 @@ class CCForm(UserCCMixin, BaseCCForm): class AccountCCForm( BaseCCMixin, UserCCMixin, forms.Form): pass - + class GoodreadsShelfLoadingForm(forms.Form): goodreads_shelf_name_number = forms.CharField(widget=forms.Select(choices=( ('all','all'), @@ -787,40 +803,60 @@ class PledgeCancelForm(forms.Form): class CampaignAdminForm(forms.Form): campaign_id = forms.IntegerField() - + class EmailShareForm(forms.Form): recipient = forms.EmailField(error_messages={'required': 'Please specify a recipient.'}) subject = forms.CharField(max_length=100, error_messages={'required': 'Please specify a subject.'}) - message = forms.CharField(widget=forms.Textarea(), error_messages={'required': 'Please include a message.'}) + message = forms.CharField( + widget=forms.Textarea(), + error_messages={'required': 'Please include a message.'} + ) # allows us to return user to original page by passing it as hidden form input # we can't rely on POST or GET since the emailshare view handles both # and may iterate several times as it catches user errors, losing URL info next = forms.CharField(widget=forms.HiddenInput()) - + class FeedbackForm(forms.Form): - sender = forms.EmailField(widget=forms.TextInput(attrs={'size':50}), label="Your email", error_messages={'required': 'Please specify your email address.'}) - subject = forms.CharField(max_length=500, widget=forms.TextInput(attrs={'size':50}), error_messages={'required': 'Please specify a subject.'}) - message = forms.CharField(widget=forms.Textarea(), error_messages={'required': 'Please specify a message.'}) + sender = forms.EmailField( + widget=forms.TextInput(attrs={'size':50}), + label="Your email", + error_messages={'required': 'Please specify your email address.'} + ) + subject = forms.CharField( + max_length=500, + widget=forms.TextInput(attrs={'size':50}), + error_messages={'required': 'Please specify a subject.'} + ) + message = forms.CharField( + widget=forms.Textarea(), + error_messages={'required': 'Please specify a message.'} + ) page = forms.CharField(widget=forms.HiddenInput()) - notarobot = forms.IntegerField(label="Please prove you're not a robot", error_messages={'required': "You must do the sum to prove you're not a robot."}) + notarobot = forms.IntegerField( + label="Please prove you're not a robot", + error_messages={'required': "You must do the sum to prove you're not a robot."} + ) answer = forms.IntegerField(widget=forms.HiddenInput()) num1 = forms.IntegerField(widget=forms.HiddenInput()) num2 = forms.IntegerField(widget=forms.HiddenInput()) - + def clean(self): cleaned_data = self.cleaned_data notarobot = str(cleaned_data.get("notarobot")) answer = str(cleaned_data.get("answer")) if notarobot != answer: raise forms.ValidationError(_("Whoops, try that sum again.")) - + return cleaned_data class MsgForm(forms.Form): - msg = forms.CharField(widget=forms.Textarea(), error_messages={'required': 'Please specify a message.'}) + msg = forms.CharField( + widget=forms.Textarea(), + error_messages={'required': 'Please specify a message.'} + ) def full_clean(self): - super(MsgForm,self).full_clean() + super(MsgForm, self).full_clean() if self.data.has_key("supporter"): try: self.cleaned_data['supporter'] = User.objects.get(id=self.data["supporter"]) @@ -835,20 +871,20 @@ class MsgForm(forms.Form): raise ValidationError("Work does not exist") else: raise ValidationError("Work is not specified") - + class PressForm(forms.ModelForm): class Meta: model = Press exclude = () - widgets = { + widgets = { 'date': SelectDateWidget(years=range(2010,2025)), } - + class KindleEmailForm(forms.Form): kindle_email = forms.EmailField() - - + + class LibModeForm(forms.ModelForm): class Meta: model = Libpref @@ -856,7 +892,11 @@ class LibModeForm(forms.ModelForm): class RegiftForm(forms.Form): give_to = forms.EmailField(label="email address of recipient") - give_message = forms.CharField( max_length=512, label="your gift message", initial="Here's an ebook from unglue.it, I hope you like it! - me") + give_message = forms.CharField( + max_length=512, + label="your gift message", + initial="Here's an ebook from unglue.it, I hope you like it! - me", + ) class SubjectSelectForm(forms.Form): add_kw = AutoCompleteSelectField( @@ -875,4 +915,4 @@ class MapSubjectForm(forms.Form): widget=AutoCompleteSelectWidget(SubjectLookup,allow_new=False), label='Target Subject', ) - + From b3cefc2d3c68687c06e7fbe0f37c8a48d3f34bff Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 15 Aug 2016 18:28:39 -0400 Subject: [PATCH 09/31] implement related works --- core/models/__init__.py | 19 +++++++------ core/parameters.py | 7 ++++- frontend/forms.py | 43 +++++++++++++++++++++-------- frontend/templates/new_edition.html | 18 ++++++++++++ frontend/templates/work.html | 12 ++++++++ frontend/views.py | 15 +++++++++- 6 files changed, 91 insertions(+), 23 deletions(-) diff --git a/core/models/__init__.py b/core/models/__init__.py index 2b64eb7d..a7f39d5b 100755 --- a/core/models/__init__.py +++ b/core/models/__init__.py @@ -73,20 +73,21 @@ from regluit.core.signals import ( watermarker = BooXtream() from .bibmodels import ( - Work, + Author, + Ebook, + EbookFile, Edition, Identifier, - Author, - Relation, - Relator, - Subject, + path_for_file, Publisher, PublisherName, - WasWork, - EbookFile, - Ebook, - path_for_file, + Relation, + Relator, safe_get_work, + Subject, + WasWork, + Work, + WorkRelation, ) pm = PostMonkey(settings.MAILCHIMP_API_KEY) diff --git a/core/parameters.py b/core/parameters.py index c3383426..cfbf44ba 100644 --- a/core/parameters.py +++ b/core/parameters.py @@ -15,7 +15,12 @@ AGE_LEVEL_CHOICES = ( ('15-18', 'Teen - Grade 10-12, Age 15-18'), ('18-', 'Adult/Advanced Reader') ) -TEXT_RELATION_CHOICES = (('translation', ''), ('revision', ''), ('sequel', ''), ('compilation', '')) +TEXT_RELATION_CHOICES = ( + ('translation', 'translation'), + ('revision', 'revision'), + ('sequel', 'sequel'), + ('compilation', 'compilation') +) diff --git a/frontend/forms.py b/frontend/forms.py index f71fd671..60025ef5 100644 --- a/frontend/forms.py +++ b/frontend/forms.py @@ -49,7 +49,14 @@ from regluit.core.models import ( UNGLUEITAR ) from regluit.libraryauth.models import Library -from regluit.core.parameters import LIBRARY, REWARDS, BUY2UNGLUE, THANKS, AGE_LEVEL_CHOICES +from regluit.core.parameters import ( + LIBRARY, + REWARDS, + BUY2UNGLUE, + THANKS, + AGE_LEVEL_CHOICES, + TEXT_RELATION_CHOICES, +) from regluit.core.lookups import ( OwnerLookup, WorkLookup, @@ -105,20 +112,32 @@ class EditionForm(forms.ModelForm): add_author = forms.CharField(max_length=500, required=False) add_author_relation = forms.ChoiceField(choices=CREATOR_RELATIONS, initial=('aut', 'Author')) add_subject = AutoCompleteSelectField( - SubjectLookup, - widget=AutoCompleteSelectWidget(SubjectLookup,allow_new=True), - label='Keyword', - required =False + SubjectLookup, + widget=AutoCompleteSelectWidget(SubjectLookup, allow_new=True), + label='Keyword', + required=False, ) + add_related_work = AutoCompleteSelectField( + WorkLookup, + widget=AutoCompleteSelectWidget(WorkLookup, allow_new=False, attrs={'size': 40}), + label='Related Work', + required=False, + ) + add_work_relation = forms.ChoiceField( + choices=TEXT_RELATION_CHOICES, + initial=('translation', 'translation'), + required=False, + ) + bisac = forms.ModelChoiceField( bisac_headings, required=False ) publisher_name = AutoCompleteSelectField( - PublisherNameLookup, - label='Publisher Name', - widget=AutoCompleteSelectWidget(PublisherNameLookup,allow_new=True), - required=False, - allow_new=True, - ) + PublisherNameLookup, + label='Publisher Name', + widget=AutoCompleteSelectWidget(PublisherNameLookup,allow_new=True), + required=False, + allow_new=True, + ) isbn = ISBNField( label=_("ISBN"), @@ -189,7 +208,7 @@ class EditionForm(forms.ModelForm): } ) language = forms.ChoiceField(choices=LANGUAGES) - age_level = forms.ChoiceField(choices=AGE_LEVEL_CHOICES) + age_level = forms.ChoiceField(choices=AGE_LEVEL_CHOICES, required=False) description = forms.CharField( required=False, widget=CKEditorWidget()) coverfile = forms.ImageField(required=False) diff --git a/frontend/templates/new_edition.html b/frontend/templates/new_edition.html index ba2ae8a8..34b6a1d1 100644 --- a/frontend/templates/new_edition.html +++ b/frontend/templates/new_edition.html @@ -98,6 +98,7 @@ ul.fancytree-container { {% csrf_token %} {{ form.work }} {{ form.non_field_errors }} +

Title: {{ form.title.errors }}{{ form.title }}

Publisher Name : {{ form.publisher_name.errors }}{{ form.publisher_name }}
(If you change this, click another form element before submitting)

@@ -122,6 +123,23 @@ ul.fancytree-container {

Language: {{ form.language.errors }}{{ form.language }}

+ {% if edition.pk %} +

Add a Related Work: {{ form.add_work_relation.errors }}{{ form.add_work_relation }} of {{ form.add_related_work.errors }}{{ form.add_related_work }}

+
    {% for work_rel in edition.work.works_related_to.all %} +
  • + This work is a {{ work_rel.relation }} of {{ work_rel.from_work }}. + +
  • + {% endfor %} + {% for work_rel in edition.work.works_related_from.all %} +
  • + {{ work_rel.to_work }} is a {{ work_rel.relation }} of this work. + +
  • + {% endfor %}
+ + + {% endif %}

Age Level: {{ form.age_level.errors }}{{ form.age_level }}

Identifiers

{% if id_msg %} {{ id_msg }} {% endif %} diff --git a/frontend/templates/work.html b/frontend/templates/work.html index 48d5ffa9..c9af509b 100644 --- a/frontend/templates/work.html +++ b/frontend/templates/work.html @@ -278,6 +278,18 @@ {{ work.last_campaign.description|safe }} {% endif %}
+
+ {% for work_rel in work.works_related_to.all %} +

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

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

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

+ {% endfor %} +
diff --git a/frontend/views.py b/frontend/views.py index 74458006..7a7499b7 100755 --- a/frontend/views.py +++ b/frontend/views.py @@ -549,6 +549,13 @@ def new_edition(request, work_id, edition_id, by=None): edition.remove_author(author) form = EditionForm(instance=edition, data=request.POST, files=request.FILES) break + work_rels = models.WorkRelation.objects.filter(Q(to_work=work) | Q(from_work=work)) + for work_rel in work_rels: + if request.POST.has_key('delete_work_rel_%s' % work_rel.id): + work_rel.delete() + form = EditionForm(instance=edition, data=request.POST, files=request.FILES) + break + if request.POST.has_key('add_author_submit') and admin: new_author_name = request.POST['add_author'].strip() new_author_relation = request.POST['add_author_relation'] @@ -561,7 +568,6 @@ def new_edition(request, work_id, edition_id, by=None): elif not form and admin: form = EditionForm(instance=edition, data=request.POST, files=request.FILES) if form.is_valid(): - print 'form is valid' form.save() if not work: work = models.Work( @@ -599,6 +605,13 @@ def new_edition(request, work_id, edition_id, by=None): if request.POST.has_key('change_relator_%s' % relator.id): new_relation = request.POST['change_relator_%s' % relator.id] relator.set(new_relation) + related_work = form.cleaned_data['add_related_work'] + if related_work: + models.WorkRelation.objects.get_or_create( + to_work=work, + from_work=related_work, + relation=form.cleaned_data['add_work_relation'], + ) for (author_name, author_relation) in edition.new_authors: edition.add_author(author_name, author_relation) if form.cleaned_data.has_key('bisac'): From 8f71b24b1b1d27406118edcc34217695863a1f81 Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 16 Aug 2016 11:04:37 -0400 Subject: [PATCH 10/31] make adding editions easier --- frontend/views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/views.py b/frontend/views.py index 7a7499b7..ef28a2cc 100755 --- a/frontend/views.py +++ b/frontend/views.py @@ -524,6 +524,10 @@ def new_edition(request, work_id, edition_id, by=None): edition = models.Edition() if work: edition.work = work + edition.publication_date = work.earliest_publication_date + edition.new_authors = [] + for relator in work.relators(): + edition.new_authors.append((relator.author.name, relator.relation.code)) initial = { 'language': language, From aafbd7c70b314b042f2ea04ab3cf9c0c2da8c18f Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 16 Aug 2016 11:42:58 -0400 Subject: [PATCH 11/31] set translation relation in add_related --- core/bookloader.py | 1 + core/models/bibmodels.py | 2 +- core/tests.py | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/bookloader.py b/core/bookloader.py index a64fbf7a..a02b21f8 100755 --- a/core/bookloader.py +++ b/core/bookloader.py @@ -449,6 +449,7 @@ def add_related(isbn): for w in works_to_merge: logger.debug("merge_works path 2 %s %s", lang_edition.work.id, w.id ) merge_works(lang_edition.work, w) + models.WorkRelation.objects.get_or_create(to_work=lang_edition.work, from_work=work, relation='translation') return new_editions diff --git a/core/models/bibmodels.py b/core/models/bibmodels.py index 03a45eb6..eb1587a2 100644 --- a/core/models/bibmodels.py +++ b/core/models/bibmodels.py @@ -96,7 +96,7 @@ 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') + 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='') class Meta: diff --git a/core/tests.py b/core/tests.py index 3fd7b8a8..96038ca1 100755 --- a/core/tests.py +++ b/core/tests.py @@ -192,6 +192,8 @@ class BookLoaderTests(TestCase): self.assertTrue(models.Edition.objects.count() > 15) self.assertEqual(models.Work.objects.filter(language=lang).count(), 1) self.assertTrue(edition.work.editions.count() > 9) + self.assertTrue(edition.work.reverse_related.count() > 0) + self.assertTrue(edition.work.works_related_from.all()[0].to_work.works_related_to.all()[0].id == edition.work.id) def test_populate_edition(self): From 363c86fd94d9079d8afd1f52de5a619d7765e5a4 Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 16 Aug 2016 17:16:44 -0400 Subject: [PATCH 12/31] implement EditionNote --- core/lookups.py | 13 ++++++++++-- ...805_1550.py => 0003_auto_20160816_1645.py} | 21 ++++++++++++------- core/migrations/0004_auto_20160808_1548.py | 2 +- core/models/__init__.py | 1 + core/models/bibmodels.py | 6 +++++- frontend/forms.py | 10 +++++++-- frontend/templates/edition_display.html | 3 +++ frontend/templates/new_edition.html | 1 + marc/load.py | 13 +++++++++++- marc/models.py | 1 + 10 files changed, 57 insertions(+), 14 deletions(-) rename core/migrations/{0003_auto_20160805_1550.py => 0003_auto_20160816_1645.py} (73%) diff --git a/core/lookups.py b/core/lookups.py index 452c505c..df8764b3 100644 --- a/core/lookups.py +++ b/core/lookups.py @@ -3,7 +3,7 @@ from selectable.registry import registry from django.contrib.auth.models import User from django.db.models import Count -from regluit.core.models import Work, PublisherName, Edition, Subject +from regluit.core.models import Work, PublisherName, Edition, Subject, EditionNote class OwnerLookup(ModelLookup): model = User @@ -54,8 +54,17 @@ class SubjectLookup(ModelLookup): def get_query(self, request, term): return super(SubjectLookup, self).get_query( request, term).annotate(Count('works')).order_by('-works__count') +class EditionNoteLookup(ModelLookup): + model = EditionNote + search_fields = ('note__icontains',) + def create_item(self, value): + new_note, created = EditionNote.objects.get_or_create(note=value) + new_note.save() + return new_note + registry.register(OwnerLookup) registry.register(WorkLookup) registry.register(PublisherNameLookup) registry.register(EditionLookup) -registry.register(SubjectLookup) \ No newline at end of file +registry.register(SubjectLookup) +registry.register(EditionNoteLookup) \ No newline at end of file diff --git a/core/migrations/0003_auto_20160805_1550.py b/core/migrations/0003_auto_20160816_1645.py similarity index 73% rename from core/migrations/0003_auto_20160805_1550.py rename to core/migrations/0003_auto_20160816_1645.py index 97b0829f..1ae7aa68 100644 --- a/core/migrations/0003_auto_20160805_1550.py +++ b/core/migrations/0003_auto_20160816_1645.py @@ -11,11 +11,18 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='EditionNote', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('note', models.CharField(max_length=64, unique=True, null=True, blank=True)), + ], + ), migrations.CreateModel( name='WorkRelation', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('relation', models.CharField(max_length=15, choices=[(b'translation', b''), (b'revision', b''), (b'sequel', b''), (b'compilation', b'')])), + ('relation', models.CharField(max_length=15, choices=[(b'translation', b'translation'), (b'revision', b'revision'), (b'sequel', b'sequel'), (b'compilation', b'compilation')])), ], ), migrations.AddField( @@ -23,11 +30,6 @@ class Migration(migrations.Migration): 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', @@ -43,9 +45,14 @@ class Migration(migrations.Migration): name='to_work', field=models.ForeignKey(related_name='works_related_to', to='core.Work'), ), + migrations.AddField( + model_name='edition', + name='note', + field=models.ForeignKey(to='core.EditionNote', null=True), + ), migrations.AddField( model_name='work', name='related', - field=models.ManyToManyField(to='core.Work', null=True, through='core.WorkRelation'), + field=models.ManyToManyField(related_name='reverse_related', null=True, through='core.WorkRelation', to='core.Work'), ), ] diff --git a/core/migrations/0004_auto_20160808_1548.py b/core/migrations/0004_auto_20160808_1548.py index ae514814..52c7d0d7 100644 --- a/core/migrations/0004_auto_20160808_1548.py +++ b/core/migrations/0004_auto_20160808_1548.py @@ -36,7 +36,7 @@ class Migration(migrations.Migration): dependencies = [ - ('core', '0003_auto_20160805_1550'), + ('core', '0003_auto_20160816_1645'), ] operations = [ diff --git a/core/models/__init__.py b/core/models/__init__.py index a7f39d5b..dd7e4df0 100755 --- a/core/models/__init__.py +++ b/core/models/__init__.py @@ -77,6 +77,7 @@ from .bibmodels import ( Ebook, EbookFile, Edition, + EditionNote, Identifier, path_for_file, Publisher, diff --git a/core/models/bibmodels.py b/core/models/bibmodels.py index eb1587a2..fa0554d8 100644 --- a/core/models/bibmodels.py +++ b/core/models/bibmodels.py @@ -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, blank=True) + note = models.ForeignKey("EditionNote", null=True) def __unicode__(self): if self.isbn_13: @@ -942,6 +942,10 @@ class Edition(models.Model): def description(self): return self.work.description +class EditionNote(models.Model): + note = models.CharField(max_length=64, null=True, blank=True, unique=True) + def __unicode__(self): + return self.note class Publisher(models.Model): created = models.DateTimeField(auto_now_add=True) diff --git a/frontend/forms.py b/frontend/forms.py index 60025ef5..0c10f46a 100644 --- a/frontend/forms.py +++ b/frontend/forms.py @@ -62,6 +62,7 @@ from regluit.core.lookups import ( WorkLookup, PublisherNameLookup, SubjectLookup, + EditionNoteLookup, ) from regluit.utils.localdatetime import now from regluit.utils.fields import ISBNField @@ -211,7 +212,13 @@ class EditionForm(forms.ModelForm): age_level = forms.ChoiceField(choices=AGE_LEVEL_CHOICES, required=False) description = forms.CharField( required=False, widget=CKEditorWidget()) coverfile = forms.ImageField(required=False) - + note = AutoCompleteSelectField( + EditionNoteLookup, + widget=AutoCompleteSelectWidget(EditionNoteLookup, allow_new=True), + label='Edition Note', + required=False, + allow_new=True, + ) def __init__(self, *args, **kwargs): super(EditionForm, self).__init__(*args, **kwargs) self.relators = [] @@ -238,7 +245,6 @@ class EditionForm(forms.ModelForm): 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: model = Edition exclude = ('created', 'work') diff --git a/frontend/templates/edition_display.html b/frontend/templates/edition_display.html index a21f367a..86de5126 100644 --- a/frontend/templates/edition_display.html +++ b/frontend/templates/edition_display.html @@ -16,6 +16,9 @@ {% endfor %}
{% endif %} + {% if edition.note %} + {{ edition.note }}.
+ {% endif %} {% if edition.publisher %} Publisher: {{edition.publisher}}
{% endif %} diff --git a/frontend/templates/new_edition.html b/frontend/templates/new_edition.html index 34b6a1d1..b6cfff3b 100644 --- a/frontend/templates/new_edition.html +++ b/frontend/templates/new_edition.html @@ -141,6 +141,7 @@ ul.fancytree-container { {% endif %}

Age Level: {{ form.age_level.errors }}{{ form.age_level }}

+

Edition Note: {{ form.note.errors }}{{ form.note }}

Identifiers

{% if id_msg %} {{ id_msg }} {% endif %}

Enter 'delete' to remove the identifier.

diff --git a/marc/load.py b/marc/load.py index 8ab5d05d..7482d364 100644 --- a/marc/load.py +++ b/marc/load.py @@ -89,6 +89,17 @@ def stub(edition): ) record.add_ordered_field(field245) + #edition statement + if edition.note: + field250 = pymarc.Field( + tag='250', + indicators = [' ', ' '], + subfields = [ + 'a', unicode(edition.note), + ] + ) + record.add_ordered_field(field250) + # publisher, date if edition.publisher: field260 = pymarc.Field( @@ -101,7 +112,7 @@ def stub(edition): if edition.publication_date: field260.add_subfield('c', unicode(edition.publication_date)) record.add_ordered_field(field260) - + if edition.description: #add 520 field (description) field520 = pymarc.Field( diff --git a/marc/models.py b/marc/models.py index d7402f26..65bf14a6 100644 --- a/marc/models.py +++ b/marc/models.py @@ -56,6 +56,7 @@ class AbstractEdition: publisher = '' title = '' publication_date = '' + note = '' # the edition should be able to report ebook downloads, with should have format and url attributes def downloads(self): From 221efdec6cd51b331f2512f8114e3f0f150d160a Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 16 Aug 2016 17:17:24 -0400 Subject: [PATCH 13/31] make it easier to find and edit ebook editions --- frontend/forms.py | 6 +++++- frontend/templates/edition_display.html | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/forms.py b/frontend/forms.py index 0c10f46a..b432c2a2 100644 --- a/frontend/forms.py +++ b/frontend/forms.py @@ -242,7 +242,11 @@ class EditionForm(forms.ModelForm): has_goog = self.cleaned_data.get("goog", False) not in nulls has_http = self.cleaned_data.get("http", False) not in nulls has_doi = self.cleaned_data.get("doi", False) not in nulls - if not has_isbn and not has_oclc and not has_goog and not has_http and not has_doi: + try: + has_id = self.instance.work.identifiers.all().count() > 0 + except AttributeError: + has_id = False + if not has_id and not has_isbn and not has_oclc and not has_goog and not has_http and not has_doi: raise forms.ValidationError(_("There must be either an ISBN, a DOI, a URL or an OCLC number.")) return self.cleaned_data class Meta: diff --git a/frontend/templates/edition_display.html b/frontend/templates/edition_display.html index 86de5126..0c9583c4 100644 --- a/frontend/templates/edition_display.html +++ b/frontend/templates/edition_display.html @@ -19,6 +19,9 @@ {% if edition.note %} {{ edition.note }}.
{% endif %} + {% if edition.ebooks.all %} + {{ edition.ebooks.all.count }} ebooks
+ {% endif %} {% if edition.publisher %} Publisher: {{edition.publisher}}
{% endif %} From 651f45f6242b30664c70cb43eddb652cd18dac87 Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 18 Aug 2016 15:56:25 -0400 Subject: [PATCH 14/31] fix short authors ordering by relation_code didn't pick up the author when ther was an "author of introduction" --- core/models/bibmodels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/models/bibmodels.py b/core/models/bibmodels.py index fa0554d8..7c781dc6 100644 --- a/core/models/bibmodels.py +++ b/core/models/bibmodels.py @@ -243,7 +243,7 @@ class Work(models.Model): 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") + auths = self.relators() if auths[0].relation.code == 'aut': return "%s et al." % auths[0].author.name else: From 7ad0d7f49439f3eb72ba993dc79c36dd10db2efa Mon Sep 17 00:00:00 2001 From: eric Date: Wed, 24 Aug 2016 15:41:29 -0400 Subject: [PATCH 15/31] implemented ebook versions It turned out that ebookfile handling needed revamping with the addition of ebook versions; so this change turned out to be rather big. --- core/migrations/0005_ebookfile_ebook.py | 19 +++ core/migrations/0006_auto_20160818_1809.py | 42 +++++ core/models/__init__.py | 180 ++++++++++++--------- core/models/bibmodels.py | 166 ++++++++++--------- core/tasks.py | 9 +- core/tests.py | 24 ++- distro/push.py | 4 +- frontend/forms.py | 5 +- frontend/templates/download.html | 4 +- frontend/templates/edition_display.html | 4 +- frontend/templates/edition_upload.html | 15 +- frontend/templates/edition_uploads.html | 28 +++- frontend/templates/manage_campaign.html | 9 +- frontend/templates/work.html | 2 +- frontend/urls.py | 2 +- frontend/views.py | 35 ++-- static/css/campaign2.css | 2 +- static/less/campaign2.less | 4 - 18 files changed, 357 insertions(+), 197 deletions(-) create mode 100644 core/migrations/0005_ebookfile_ebook.py create mode 100644 core/migrations/0006_auto_20160818_1809.py diff --git a/core/migrations/0005_ebookfile_ebook.py b/core/migrations/0005_ebookfile_ebook.py new file mode 100644 index 00000000..c738940b --- /dev/null +++ b/core/migrations/0005_ebookfile_ebook.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_auto_20160808_1548'), + ] + + operations = [ + migrations.AddField( + model_name='ebookfile', + name='ebook', + field=models.ForeignKey(related_name='ebook_files', to='core.Ebook', null=True), + ), + ] diff --git a/core/migrations/0006_auto_20160818_1809.py b/core/migrations/0006_auto_20160818_1809.py new file mode 100644 index 00000000..5ea66109 --- /dev/null +++ b/core/migrations/0006_auto_20160818_1809.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from django.db import migrations, models + +class Migration(migrations.Migration): + + def add_ebooks_to_ebfs(apps, schema_editor): + EbookFile = apps.get_model('core', 'EbookFile') + Ebook = apps.get_model('core', 'Ebook') + for ebf in EbookFile.objects.all(): + 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 not ebf.ebook: + if ebf.edition.work.campaigns.filter(type=3): + ebf.ebook = Ebook.objects.create( + edition=ebf.edition, + active=False, + url=ebf.file.url, + provider='Unglue.it', + format=ebf.format, + rights=ebf.edition.work.campaigns.order_by('-created')[0].license + ) + ebf.save() + elif ebf.edition.work.campaigns.filter(type=2): + pass + else: + print 'ebf {} is dangling'.format(ebf.id) + + def noop(apps, schema_editor): + pass + + dependencies = [ + ('core', '0005_ebookfile_ebook'), + ] + + operations = [ + migrations.RunPython(add_ebooks_to_ebfs, reverse_code=noop, hints={'core': 'EbookFile'}), + ] diff --git a/core/models/__init__.py b/core/models/__init__.py index dd7e4df0..d31b8232 100755 --- a/core/models/__init__.py +++ b/core/models/__init__.py @@ -78,6 +78,7 @@ from .bibmodels import ( EbookFile, Edition, EditionNote, + good_providers, Identifier, path_for_file, Publisher, @@ -675,7 +676,11 @@ class Campaign(models.Model): self.activated = datetime.today() if self.type == THANKS: # make ebooks from ebookfiles - self.work.make_ebooks_from_ebfs() + if self.use_add_ask: + self.add_ask_to_ebfs() + else: + self.revert_asks() + self.work.remove_old_ebooks() self.save() action = CampaignAction(campaign=self, type='activated', comment=self.get_type_display()) ungluers = self.work.wished_by() @@ -934,82 +939,112 @@ class Campaign(models.Model): 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 make_mobis(self): + # make archive files for ebooks, make mobi files for epubs + versions = set() + for ebook in self.work.ebooks().filter(provider__in=good_providers, format='mobi'): + versions.add(ebook.version_label) + for ebook in self.work.ebooks_all().exclude(provider='Unglue.it').filter(provider__in=good_providers, format='epub'): + if not ebook.version_label in versions: + # now make the mobi file + ebf = ebook.get_archive_ebf() + ebf.make_mobi() def add_ask_to_ebfs(self, position=0): if not self.use_add_ask or self.type != THANKS: return - 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: + format_versions = [] + to_dos = [] + for ebf in self.work.ebookfiles().filter(asking=False, ebook__provider='Unglue.it').order_by('-created'): + format_version = '{}_{}'.format(ebf.ebook.format, ebf.ebook.version_label) + if ebf.format in ('pdf', 'epub') and not format_version in format_versions: ebf.file.open() - 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) + to_dos.append({'content': ebf.file.read(), 'ebook': ebf.ebook}) + format_versions.append(format_version) + for ebook in self.work.ebooks_all().exclude(provider='Unglue.it').filter(provider__in=good_providers): + format_version = '{}_{}'.format(ebook.format, ebook.version_label) + if ebook.format in ('pdf', 'epub') and not format_version in format_versions: + to_dos.append({'content': ebook.get_archive().read(), 'ebook': ebook}) + format_versions.append(format_version) + new_ebfs = [] + for to_do in to_dos: + edition = to_do['ebook'].edition + version = to_do['ebook'].version + if to_do['ebook'].format == 'pdf': + try: + added = ask_pdf({'campaign':self, 'work':self.work, 'site':Site.objects.get_current()}) + new_file = SpooledTemporaryFile() + old_file = SpooledTemporaryFile() + old_file.write(to_do['content']) + if position == 0: + pdf_append(added, old_file, new_file) + else: + pdf_append(old_file, added, new_file) + new_file.seek(0) + new_pdf_ebf = EbookFile.objects.create(edition=edition, format='pdf', asking=True) + new_pdf_ebf.version = version + new_pdf_ebf.file.save(path_for_file('ebf', None), ContentFile(new_file.read())) + new_pdf_ebf.save() + new_ebfs.append(new_pdf_ebf) + except Exception as e: + logger.error("error appending pdf ask %s" % (e)) + elif to_do['ebook'].format == 'epub': + try: + old_file = SpooledTemporaryFile() + old_file.write(to_do['content']) + new_file = ask_epub(old_file, {'campaign':self, 'work':self.work, 'site':Site.objects.get_current()}) + new_file.seek(0) + new_epub_ebf = EbookFile.objects.create(edition=edition, format='epub', asking=True) + new_epub_ebf.file.save(path_for_file(new_epub_ebf, None), ContentFile(new_file.read())) + new_epub_ebf.save() + new_epub_ebf.version = version + new_ebfs.append(new_epub_ebf) + + # now make the mobi file + new_mobi_ebf = EbookFile.objects.create(edition=edition, format='mobi', asking=True) + new_mobi_ebf.file.save(path_for_file('ebf', None), ContentFile(mobi.convert_to_mobi(new_epub_ebf.file.url))) + new_mobi_ebf.save() + new_mobi_ebf.version = version + new_ebfs.append(new_mobi_ebf) + except Exception as e: + logger.error("error making epub ask or mobi %s" % (e)) + for ebf in new_ebfs: + ebook = Ebook.objects.create( + edition=ebf.edition, + format=ebf.format, + rights=self.license, + provider="Unglue.it", + url=ebf.file.url, + version=ebf.version + ) + ebf.ebook = ebook + ebf.save() + new_ebf_pks = [ebf.pk for ebf in new_ebfs] + + for old_ebf in self.work.ebookfiles().filter(asking=True).exclude(pk__in=new_ebf_pks): + obsolete = Ebook.objects.filter(url=old_ebf.file.url) + old_ebf.ebook.deactivate() + old_ebf.file.delete() + old_ebf.delete() + + for non_asking in self.work.ebookfiles().filter(asking=False, ebook__active=True): + non_asking.ebook.deactivate() + + def revert_asks(self): + # there should be a deactivated non-asking ebook for every asking ebook + if self.type != THANKS: # just to make sure that ebf's can be unglued by mistake + return + format_versions = [] + for ebf in EbookFile.objects.filter(edition__work=self.work).exclude(file='').exclude(ebook=None).order_by('-created'): + format_version = '{}_{}'.format(ebf.format, ebf.ebook.version_label) + if ebf.asking: + ebf.ebook.deactivate() + elif format_version in format_versions: + # this ebook file has the wrong "asking" + ebf.ebook.deactivate() + else: + ebf.ebook.activate() + format_versions.append(format_version) def make_unglued_ebf(self, format, watermarked): r = urllib2.urlopen(watermarked.download_link(format)) @@ -1023,6 +1058,7 @@ class Campaign(models.Model): rights=self.license, provider="Unglue.it", url=settings.BASE_URL_SECURE + reverse('download_campaign', args=[self.work.id, format]), + version='unglued', ) old_ebooks = Ebook.objects.exclude(pk=ebook.pk).filter( edition=self.work.preferred_edition, diff --git a/core/models/bibmodels.py b/core/models/bibmodels.py index 7c781dc6..025ecbcb 100644 --- a/core/models/bibmodels.py +++ b/core/models/bibmodels.py @@ -25,8 +25,10 @@ from regluit.marc.models import MARCRecord as NewMARC from regluit.utils.localdatetime import now from regluit.questionnaire.models import Landing +from regluit.core import mobi import regluit.core.cc as cc from regluit.core.epub import test_epub + from regluit.core.parameters import ( AGE_LEVEL_CHOICES, BORROWED, @@ -41,6 +43,7 @@ from regluit.core.parameters import ( ) logger = logging.getLogger(__name__) +good_providers = ('Internet Archive', 'Unglue.it', 'Github', 'OAPEN Library') class Identifier(models.Model): @@ -360,7 +363,15 @@ class Work(models.Model): def pdffiles(self): return EbookFile.objects.filter(edition__work=self, format='pdf').exclude(file='').order_by('-created') - + + def versions(self): + version_labels = [''] + for ebook in self.ebooks(): + if not ebook.version_label in version_labels: + version_labels.append(ebook.version_label) + version_labels.remove('') + return version_labels + def formats(self): fmts = [] for fmt in ['pdf', 'epub', 'mobi', 'html']: @@ -369,48 +380,20 @@ class Work(models.Model): 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): + # this method is triggered after an file upload or new ebook saved old = Ebook.objects.filter(edition__work=self, active=True).order_by('-created') - done_formats = [] + + # keep most recent ebook for each format and version label + done_format_versions = [] for eb in old: - if eb.format in done_formats: + format_version = '{}_{}'.format(eb.format, eb.version_label) + if format_version in done_format_versions: eb.deactivate() else: - done_formats.append(eb.format) + done_format_versions.append(format_version) + + # check for failed uploads. null_files = EbookFile.objects.filter(edition__work=self, file='') for ebf in null_files: ebf.file.delete() @@ -1005,7 +988,8 @@ class EbookFile(models.Model): edition = models.ForeignKey('Edition', related_name='ebook_files') created = models.DateTimeField(auto_now_add=True) asking = models.BooleanField(default=False) - + ebook = models.ForeignKey('Ebook', related_name='ebook_files', null=True) + version = None def check_file(self): if self.format == 'epub': return test_epub(self.file) @@ -1018,6 +1002,24 @@ class EbookFile(models.Model): except: return False + def make_mobi(self): + if not self.format == 'epub': + return False + new_mobi_ebf = EbookFile.objects.create(edition=self.edition, format='mobi', asking=self.asking) + new_mobi_ebf.file.save(path_for_file('ebf', None), ContentFile(mobi.convert_to_mobi(self.file.url))) + new_mobi_ebf.save() + if self.ebook: + new_ebook = Ebook.objects.create( + edition=self.edition, + format='mobi', + url=new_mobi_ebf.file.url, + rights=self.ebook.rights, + version=self.ebook.version, + ) + new_mobi_ebf.ebook = new_ebook + new_mobi_ebf.save() + return True + send_to_kindle_limit = 7492232 class Ebook(models.Model): @@ -1041,49 +1043,63 @@ class Ebook(models.Model): 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] + def get_archive(self): # returns an open file + ebf = self.get_archive_ebf() + if not ebf: + return None + try: + ebf.file.open() + except ValueError: + logger.error(u'couldn\'t open EbookFile {}'.format(ebf.id)) + return None + except IOError: + logger.error(u'EbookFile {} does not exist'.format(ebf.id)) + return None + return ebf.file + + def get_archive_ebf(self): # returns an ebf + if not self.ebook_files.filter(asking=False): + if not self.provider in good_providers: + return None try: - ebf.file.open() - except ValueError: - logger.error(u'couldn\'t open EbookFile {}'.format(ebf.id)) - return None + r = urllib2.urlopen(self.url) + try: + self.filesize = int(r.info().getheaders("Content-Length")[0]) + if self.save: + self.filesize = self.filesize if self.filesize < 2147483647 else 2147483647 # largest safe positive integer + self.save() + ebf = EbookFile.objects.create(edition=self.edition, ebook=self, format=self.format) + ebf.file.save(path_for_file(ebf, None), ContentFile(r.read())) + ebf.file.close() + ebf.save() + return ebf + except IndexError: + # response has no Content-Length header probably a bad link + logging.error('Bad link error: {}'.format(self.url)) except IOError: - logger.error(u'EbookFile {} does not exist'.format(ebf.id)) - return None - return ebf.file + logger.error(u'could not open {}'.format(self.url)) + else: + ebf = self.ebook_files.filter(asking=False).order_by('-created')[0] + return ebf def set_provider(self): self.provider = Ebook.infer_provider(self.url) return self.provider + @property + def version_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 + @property def rights_badge(self): if self.rights is None: diff --git a/core/tasks.py b/core/tasks.py index 37e43449..42a22c50 100644 --- a/core/tasks.py +++ b/core/tasks.py @@ -134,12 +134,13 @@ def process_ebfs(campaign): if campaign.use_add_ask: campaign.add_ask_to_ebfs() else: - campaign.work.make_ebooks_from_ebfs(add_ask=False) - campaign.work.remove_old_ebooks() + campaign.revert_asks() + campaign.make_mobis() + @task -def make_mobi(campaign): - return campaign.make_mobi() +def make_mobi(ebookfile): + return ebookfile.make_mobi() @task def refresh_acqs(): diff --git a/core/tests.py b/core/tests.py index 96038ca1..b30cc926 100755 --- a/core/tests.py +++ b/core/tests.py @@ -1011,6 +1011,11 @@ class EbookFileTests(TestCase): dj_file = DjangoFile(temp_file) ebf = EbookFile( format='pdf', edition=e, file=dj_file) ebf.save() + eb = Ebook( format='pdf', edition=e, url=ebf.file.url, provider='Unglue.it') + eb.save() + ebf.ebook = eb + ebf.save() + temp_file.close() finally: @@ -1018,7 +1023,7 @@ class EbookFileTests(TestCase): os.remove(temp.name) #test the ask-appender c.add_ask_to_ebfs() - asking_pdf = c.work.ebookfiles().filter(asking = True)[0].file.url + asking_pdf = c.work.ebookfiles().filter(asking=True)[0].file.url assert test_pdf(asking_pdf) #Now do the same with epub @@ -1034,16 +1039,25 @@ class EbookFileTests(TestCase): dj_file = DjangoFile(temp_file) ebf = EbookFile( format='epub', edition=e, file=dj_file) ebf.save() - + eb = Ebook( format='epub', edition=e, url=ebf.file.url, provider='Unglue.it') + eb.save() + ebf.ebook = eb + ebf.save() temp_file.close() + ebf.make_mobi() finally: # make sure we get rid of temp file os.remove(temp.name) #test the ask-appender c.add_ask_to_ebfs() - self.assertTrue( c.work.ebookfiles().filter(asking = True, format='epub').count >0) - self.assertTrue( c.work.ebookfiles().filter(asking = True, format='mobi').count >0) - + self.assertTrue( c.work.ebookfiles().filter(asking = True, format='epub').count() > 0) + self.assertTrue( c.work.ebookfiles().filter(asking = True, format='mobi').count() > 0) + self.assertTrue( c.work.ebookfiles().filter(asking = True, ebook__active=True).count() > 0) + self.assertTrue( c.work.ebookfiles().filter(asking = False, ebook__active=True).count() == 0) + #test the unasker + c.revert_asks() + self.assertTrue( c.work.ebookfiles().filter(asking = True, ebook__active=True).count() == 0) + self.assertTrue( c.work.ebookfiles().filter(asking = False, ebook__active=True).count() > 0) class MobigenTests(TestCase): def test_convert_to_mobi(self): diff --git a/distro/push.py b/distro/push.py index 6a64feb6..30c4fb4b 100644 --- a/distro/push.py +++ b/distro/push.py @@ -4,7 +4,7 @@ from StringIO import StringIO from regluit.core.facets import BaseFacet -from regluit.core.models import Work +from regluit.core.models import Work, good_providers from regluit.api.onix import onix_feed from .models import Target @@ -45,7 +45,7 @@ def get_target_facet(target, start=datetime(1900,1,1), new=False): editions__ebooks__created__gt = start, identifiers__type="isbn", editions__ebooks__format__in = formats, - editions__ebooks__provider__in = ('Internet Archive', 'Unglue.it', 'Github', 'OAPEN Library'), + editions__ebooks__provider__in = good_providers, ).distinct().order_by('-featured') model_filters = {"Ebook": format_filter, "Edition": edition_format_filter} diff --git a/frontend/forms.py b/frontend/forms.py index b432c2a2..8721dfa6 100644 --- a/frontend/forms.py +++ b/frontend/forms.py @@ -261,7 +261,8 @@ class EditionForm(forms.ModelForm): } class EbookFileForm(forms.ModelForm): - file = forms.FileField(max_length=16777216) + version = forms.CharField(max_length=512, required=False) + file = forms.FileField(max_length=16777216) def __init__(self, campaign_type=BUY2UNGLUE, *args, **kwargs): super(EbookFileForm, self).__init__(*args, **kwargs) @@ -305,7 +306,7 @@ class EbookFileForm(forms.ModelForm): class Meta: model = EbookFile widgets = { 'edition': forms.HiddenInput} - exclude = { 'created', 'asking' } + exclude = { 'created', 'asking', 'ebook' } class EbookForm(forms.ModelForm): class Meta: diff --git a/frontend/templates/download.html b/frontend/templates/download.html index adb5fa1e..a651df00 100644 --- a/frontend/templates/download.html +++ b/frontend/templates/download.html @@ -149,7 +149,7 @@ $j(document).ready(function() { {{ ebook.rights}} {{ ebook.format }} - {{ ebook.format }} + {{ ebook.format }} {% if ebook.version_label %} ({{ ebook.version_label }}) {% endif %} {% if ebook.is_direct %}{% endif %} @@ -168,7 +168,7 @@ $j(document).ready(function() { {{ ebook.rights}} {{ ebook.format }} at {{ebook.provider}} - {{ ebook.format }} at {{ ebook.provider }} + {{ ebook.format }} {% if ebook.version_label %} ({{ ebook.version_label }}) {% endif %} at {{ ebook.provider }} {% if ebook.is_direct %}{% endif %} {% if not forloop.last %}

{% endif %} diff --git a/frontend/templates/edition_display.html b/frontend/templates/edition_display.html index 0c9583c4..703011e1 100644 --- a/frontend/templates/edition_display.html +++ b/frontend/templates/edition_display.html @@ -19,8 +19,8 @@ {% if edition.note %} {{ edition.note }}.
{% endif %} - {% if edition.ebooks.all %} - {{ edition.ebooks.all.count }} ebooks
+ {% if edition.downloads.count %} + {{ edition.downloads.count }} ebooks
{% endif %} {% if edition.publisher %} Publisher: {{edition.publisher}}
diff --git a/frontend/templates/edition_upload.html b/frontend/templates/edition_upload.html index 721d6091..104a6b4f 100644 --- a/frontend/templates/edition_upload.html +++ b/frontend/templates/edition_upload.html @@ -7,21 +7,32 @@

eBooks for this Edition

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

Add an eBook for this Edition:

- If you know that this edition is available as a public domain or Creative Commons ebook, you can enter the link here and "unglue" it. Right now, we're only accepting URLs that point to Internet Archive, Wikisources, Wikibooks, Hathitrust, Project Gutenberg, raw files at Github, or Google Books. + 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.
diff --git a/frontend/templates/edition_uploads.html b/frontend/templates/edition_uploads.html index 317af0e5..44238419 100644 --- a/frontend/templates/edition_uploads.html +++ b/frontend/templates/edition_uploads.html @@ -20,12 +20,20 @@ LibraryThing ID: {{ edition.librarything_id }}
+{% if edition.ebooks.all.0 %} +

Active eBooks for this Work

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

Ebook Files for this Edition

    {% for ebook_file in edition.ebook_files.all %} {% if ebook_file.file %} -
  • {% if ebook_file.active %}ACTIVE {% endif %}{{ebook_file.file}} created {{ebook_file.created}} {% if ebook_file.asking %}(This file has had the campaign 'ask' added.){% endif %}
  • +
  • {% if ebook_file.active %}ACTIVE {% elif ebook_file.ebook.active %} MIRROR {% endif %}{{ebook_file.file}} created {{ebook_file.created}} {% if ebook_file.asking %}(This file has had the campaign 'ask' added.){% endif %}
  • {% endif %} {% endfor %}
@@ -66,15 +74,27 @@ For ePUB files, use the epubcheck tool to make sure everything will work properly.

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

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

+{% if edition.work.versions %} +

There are named versions for this ebook. Specify the version you want to replace.
{{form.version.errors}}Version:

+{% 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:

Edition {{ edition.id }}:

    -
  • There are {{ edition.downloads.all.count }} downloadable ebook for this edition
  • Edit this edition
  • {% ifnotequal campaign.type 1 %} {% if campaign.rh.can_sell %} {% if edition.ebook_files.all.0 %} -
  • You have uploaded ebook files for this edition. Upload another
  • +
  • You have uploaded ebook files for this edition. You can manage its ebooks or upload another
  • {% else %} -
  • You can Load a file for this edition.
  • +
  • You can Manage ebooks for this edition.
  • {% endif %} {% endif %} {% endifnotequal %} @@ -156,11 +155,11 @@ Please fix the following before launching your campaign:

    If you don't see an edition that matches what you want to release, you can create a new edition.

    {% if campaign.work.ebookfiles.0 %} -

    Uploaded Files

    +

    All ebook files for this book

    {% endif %} {% if campaign.work.epubfiles.0 %} {% for ebf in campaign.work.epubfiles %} -

    {% if ebf.active %}ACTIVE {% endif %}EPUB file: {{ebf.file}}
    created {{ebf.created}} for edition {{ebf.edition.id}} {% if ebf.asking %}(This file has had the campaign 'ask' added.){% endif %}
    {% if ebf.active %}{% ifequal action 'mademobi' %}A MOBI file is being generated. (Takes a minute or two.) {% else %}You can generate a MOBI file. {% endifequal %}{% endif %}

    +

    {% if ebf.active %}ACTIVE {% elif ebf.ebook.active %} MIRROR {% endif %}EPUB file: {{ebf.file}}
    created {{ebf.created}} for edition {{ebf.edition.id}} {% if ebf.asking %}(This file has had the campaign 'ask' added.){% endif %}
    {% if ebf.active %}{% ifequal action 'mademobi' %}A MOBI file is being generated. (Takes a minute or two.) {% else %}You can generate a MOBI file. {% endifequal %}{% endif %}

    {% endfor %} {% if campaign.work.test_acqs.0 %}
      diff --git a/frontend/templates/work.html b/frontend/templates/work.html index c9af509b..ade5721d 100644 --- a/frontend/templates/work.html +++ b/frontend/templates/work.html @@ -431,7 +431,7 @@ This work has been downloaded {{ work.download_count }} times via unglue.it ebook links.
        {% for ebook in work.ebooks.all %} -
      1. {{ ebook.download_count }} - {{ ebook.format }} ({{ ebook.rights }}) at {{ ebook.provider }}
      2. +
      3. {{ ebook.download_count }} - {{ ebook.format }} {% if ebook.version %} {{ ebook.version }} {% endif %}({{ ebook.rights }}) at {{ ebook.provider }}.
      4. {% endfor %}
diff --git a/frontend/urls.py b/frontend/urls.py index d5d49da4..a059a965 100644 --- a/frontend/urls.py +++ b/frontend/urls.py @@ -27,7 +27,7 @@ urlpatterns = [ url(r"^rightsholders/$", views.rh_tools, name="rightsholders"), url(r"^rightsholders/campaign/(?P\d+)/$", views.manage_campaign, name="manage_campaign"), url(r"^rightsholders/campaign/(?P\d+)/results/$", views.manage_campaign, {'action': 'results'}, name="campaign_results"), - url(r"^rightsholders/campaign/(?P\d+)/makemobi/$", views.manage_campaign, {'action': 'makemobi'}, name="makemobi"), + url(r"^rightsholders/campaign/(?P\d+)/(?P\d+)/makemobi/$", views.manage_campaign, {'action': 'makemobi'}, name="makemobi"), url(r"^rightsholders/campaign/(?P\d+)/mademobi/$", views.manage_campaign, {'action': 'mademobi'}, name="mademobi"), url(r"^rightsholders/edition/(?P\d*)/(?P\d*)$", views.new_edition, {'by': 'rh'}, name="rh_edition"), url(r"^rightsholders/edition/(?P\d*)/upload/$", views.edition_uploads, name="edition_uploads"), diff --git a/frontend/views.py b/frontend/views.py index 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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 %} + 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: -