Remove funding.offer.due.
[wolnelektury.git] / apps / funding / models.py
1 # -*- coding: utf-8 -*-
2 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
3 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
4 #
5 from datetime import date, datetime
6 from urllib import urlencode
7 from django.core.urlresolvers import reverse
8 from django.core.mail import send_mail
9 from django.conf import settings
10 from django.template.loader import render_to_string
11 from django.db import models
12 from django.utils.translation import ugettext_lazy as _, ugettext, override
13 import getpaid
14 from catalogue.models import Book
15 from catalogue.utils import get_random_hash
16 from polls.models import Poll
17 from django.contrib.sites.models import Site
18
19
20 class Offer(models.Model):
21     """ A fundraiser for a particular book. """
22     author = models.CharField(_('author'), max_length=255)
23     title = models.CharField(_('title'), max_length=255)
24     slug = models.SlugField(_('slug'))
25     description = models.TextField(_('description'), blank=True)
26     target = models.DecimalField(_('target'), decimal_places=2, max_digits=10)
27     start = models.DateField(_('start'), db_index=True)
28     end = models.DateField(_('end'), db_index=True)
29     redakcja_url = models.URLField(_('redakcja URL'), blank=True)
30     book = models.ForeignKey(Book, null=True, blank=True,
31         help_text=_('Published book.'))
32     cover = models.ImageField(_('Cover'), upload_to = 'funding/covers')
33     poll = models.ForeignKey(Poll, help_text = _('Poll'),  null = True, blank = True, on_delete = models.SET_NULL)
34         
35     def cover_img_tag(self):
36         return u'<img src="%s" />' % self.cover.url
37     cover_img_tag.short_description = _('Cover preview')
38     cover_img_tag.allow_tags = True
39         
40     class Meta:
41         verbose_name = _('offer')
42         verbose_name_plural = _('offers')
43         ordering = ['-end']
44
45     def __unicode__(self):
46         return u"%s - %s" % (self.author, self.title)
47
48     def get_absolute_url(self):
49         return reverse('funding_offer', args=[self.slug])
50
51     def save(self, *args, **kw):
52         published_now = (self.book_id is not None and 
53             self.pk is not None and
54             type(self).objects.values('book').get(pk=self.pk)['book'] != self.book_id)
55         retval = super(Offer, self).save(*args, **kw)
56         if published_now:
57             self.notify_published()
58         return retval
59
60     def is_current(self):
61         return self.start <= date.today() <= self.end
62
63     def is_win(self):
64         return self.sum() >= self.target
65
66     def remaining(self):
67         if self.is_current():
68             return None
69         if self.is_win():
70             return self.sum() - self.target
71         else:
72             return self.sum()
73
74     @classmethod
75     def current(cls):
76         """ Returns current fundraiser or None. """
77         today = date.today()
78         objects = cls.objects.filter(start__lte=today, end__gte=today)
79         try:
80             return objects[0]
81         except IndexError:
82             return None
83
84     @classmethod
85     def past(cls):
86         """ QuerySet for all current and past fundraisers. """
87         today = date.today()
88         return cls.objects.filter(end__lt=today)
89
90     @classmethod
91     def public(cls):
92         """ QuerySet for all current and past fundraisers. """
93         today = date.today()
94         return cls.objects.filter(start__lte=today)
95
96     def get_perks(self, amount=None):
97         """ Finds all the perks for the offer.
98         
99         If amount is provided, returns the perks you get for it.
100
101         """
102         perks = Perk.objects.filter(
103                 models.Q(offer=self) | models.Q(offer=None)
104             ).exclude(end_date__lt=date.today())
105         if amount is not None:
106             perks = perks.filter(price__lte=amount)
107         return perks
108
109     def funding_payed(self):
110         """ QuerySet for all completed payments for the offer. """
111         return Funding.payed().filter(offer=self)
112
113     def sum(self):
114         """ The money gathered. """
115         return self.funding_payed().aggregate(s=models.Sum('amount'))['s'] or 0
116
117     def notify_all(self, subject, template_name, extra_context=None):
118         Funding.notify_funders(
119             subject, template_name, extra_context,
120             query_filter=models.Q(offer=self)
121         )
122
123     def notify_end(self):
124         assert not self.is_current()
125         self.notify_all(
126             _('The fundraiser has ended!'),
127             'funding/email/end.txt', {
128                 'offer': self,
129                 'is_win': self.is_win(),
130                 'remaining': self.remaining(),
131                 'current': self.current(),
132             })
133
134     def notify_near(self):
135         assert self.is_current()
136         sum_ = self.sum()
137         need = self.target - sum_
138         self.notify_all(
139             _('The fundraiser will end soon!'),
140             'funding/email/near.txt', {
141                 'days': (self.end - date.today()).days + 1,
142                 'offer': self,
143                 'is_win': self.is_win(),
144                 'sum': sum_,
145                 'need': need,
146             })
147
148     def notify_published(self):
149         assert self.book is not None
150         self.notify_all(
151             _('The book you helped fund has been published.'),
152             'funding/email/published.txt', {
153                 'offer': self,
154                 'book': self.book,
155                 'author': ", ".join(a[0] for a in self.book.related_info()['tags']['author']),
156                 'current': self.current(),
157             })
158
159
160 class Perk(models.Model):
161     """ A perk offer.
162     
163     If no attached to a particular Offer, applies to all.
164
165     """
166     offer = models.ForeignKey(Offer, verbose_name=_('offer'), null=True, blank=True)
167     price = models.DecimalField(_('price'), decimal_places=2, max_digits=10)
168     name = models.CharField(_('name'), max_length=255)
169     long_name = models.CharField(_('long name'), max_length=255)
170     end_date = models.DateField(_('end date'), null=True, blank=True)
171
172     class Meta:
173         verbose_name = _('perk')
174         verbose_name_plural = _('perks')
175         ordering = ['-price']
176
177     def __unicode__(self):
178         return "%s (%s%s)" % (self.name, self.price, u" for %s" % self.offer if self.offer else "")
179
180
181 class Funding(models.Model):
182     """ A person paying in a fundraiser.
183
184     The payment was completed if and only if payed_at is set.
185
186     """
187     offer = models.ForeignKey(Offer, verbose_name=_('offer'))
188     name = models.CharField(_('name'), max_length=127, blank=True)
189     email = models.EmailField(_('email'), blank=True, db_index=True)
190     amount = models.DecimalField(_('amount'), decimal_places=2, max_digits=10)
191     payed_at = models.DateTimeField(_('payed at'), null=True, blank=True, db_index=True)
192     perks = models.ManyToManyField(Perk, verbose_name=_('perks'), blank=True)
193     language_code = models.CharField(max_length = 2, null = True, blank = True)
194     notifications = models.BooleanField(_('notifications'), default=True, db_index=True)
195     notify_key = models.CharField(max_length=32)
196
197     class Meta:
198         verbose_name = _('funding')
199         verbose_name_plural = _('fundings')
200         ordering = ['-payed_at']
201
202     @classmethod
203     def payed(cls):
204         """ QuerySet for all completed payments. """
205         return cls.objects.exclude(payed_at=None)
206
207     def __unicode__(self):
208         return unicode(self.offer)
209
210     def get_absolute_url(self):
211         return reverse('funding_funding', args=[self.pk])
212
213     def get_disable_notifications_url(self):
214         return "%s?%s" % (reverse("funding_disable_notifications"),
215             urlencode({
216                 'email': self.email,
217                 'key': self.notify_key,
218             }))
219
220     def save(self, *args, **kwargs):
221         if self.email and not self.notify_key:
222             self.notify_key = get_random_hash(self.email)
223         return super(Funding, self).save(*args, **kwargs)
224
225     @classmethod
226     def notify_funders(cls, subject, template_name, extra_context=None,
227                 query_filter=None, payed_only=True):
228         funders = cls.objects.exclude(email="").filter(notifications=True)
229         if payed_only:
230             funders = funders.exclude(payed_at=None)
231         if query_filter is not None:
232             funders = funders.filter(query_filter)
233         emails = set()
234         for funder in funders:
235             if funder.email in emails:
236                 continue
237             emails.add(funder.email)
238             funder.notify(subject, template_name, extra_context)
239
240     def notify(self, subject, template_name, extra_context=None):
241         context = {
242             'funding': self,
243             'site': Site.objects.get_current(),
244         }
245         context.update(extra_context)
246         with override(self.language_code or 'pl'):
247             send_mail(subject,
248                 render_to_string(template_name, context),
249                 getattr(settings, 'CONTACT_EMAIL', 'wolnelektury@nowoczesnapolska.org.pl'),
250                 [self.email],
251                 fail_silently=False
252             )
253
254     def disable_notifications(self):
255         """Disables all notifications for this e-mail address."""
256         type(self).objects.filter(email=self.email).update(notifications=False)
257
258
259 # Register the Funding model with django-getpaid for payments.
260 getpaid.register_to_payment(Funding, unique=False, related_name='payment')
261
262
263 class Spent(models.Model):
264     """ Some of the remaining money spent on a book. """
265     book = models.ForeignKey(Book)
266     amount = models.DecimalField(_('amount'), decimal_places=2, max_digits=10)
267     timestamp = models.DateField(_('when'))
268
269     class Meta:
270         verbose_name = _('money spent on a book')
271         verbose_name_plural = _('money spent on books')
272         ordering = ['-timestamp']
273
274     def __unicode__(self):
275         return u"Spent: %s" % unicode(self.book)
276
277
278 def new_payment_query_listener(sender, order=None, payment=None, **kwargs):
279     """ Set payment details for getpaid. """
280     payment.amount = order.amount
281     payment.currency = 'PLN'
282 getpaid.signals.new_payment_query.connect(new_payment_query_listener)
283
284
285 def user_data_query_listener(sender, order, user_data, **kwargs):
286     """ Set user data for payment. """
287     user_data['email'] = order.email
288 getpaid.signals.user_data_query.connect(user_data_query_listener)
289
290 def payment_status_changed_listener(sender, instance, old_status, new_status, **kwargs):
291     """ React to status changes from getpaid. """
292     if old_status != 'paid' and new_status == 'paid':
293         instance.order.payed_at = datetime.now()
294         instance.order.save()
295         if instance.order.email:
296             instance.order.notify(
297                 _('Thank you for your support!'),
298                 'funding/email/thanks.txt'
299             )
300 getpaid.signals.payment_status_changed.connect(payment_status_changed_listener)