Source code for spreedly.models

import warnings
import logging

from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.db import models
from django.http import Http404
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
from django.db import IntegrityError

from pyspreedly import api
import spreedly.settings as spreedly_settings

from requests import HTTPError
from urlparse import urljoin

logger = logging.getLogger(__name__)

try:
    from django.utils.timezone import datetime
except ImportError:
    from datetime import datetime
from datetime import timedelta


class HttpUnprocessableEntity(HTTPError):
    pass


[docs]class PlanManager(models.Manager): """ Manager that handles syncing plans and finding enabled plans """
[docs] def enabled(self): """ :returns: Returns all enabled :py:class:`Plans` """ return self.model.objects.filter(enabled=True)
def get_by_natural_key(self, name): return self.get(name=name)
[docs] def sync_plans(self): """ Gets a full list of plans from spreedly, and updates the local db to match it """ client = api.Client(settings.SPREEDLY_AUTH_TOKEN, settings.SPREEDLY_SITE_NAME) for plan in client.get_plans(): plan = plan['subscription_plan'] p, created = Plan.objects.get_or_create(pk=plan['id']) changed = False for k, v in plan.items(): if hasattr(p, k) and not getattr(p, k) == v: setattr(p, k, v) changed = True if changed: p.save() # Figure out what spreedly calls these in XML to get the lookup correct.
PLAN_TYPES = ( ('regular', _('Regular')), ('metered', _('Metered')), ('free_trial', _('Trial'),) )
[docs]class Plan(models.Model): ''' Subscription plan ''' id = models.IntegerField(db_index=True, primary_key=True, verbose_name="Spreedly ID", help_text="Spreedly plan ID") name = models.CharField(max_length=64, null=True) description = models.TextField(null=True, blank=True) terms = models.CharField(max_length=100, blank=True) plan_type = models.CharField(max_length=10, choices=PLAN_TYPES, blank=True) price = models.DecimalField(max_digits=6, decimal_places=2, default='0', help_text=u'USD', null=True) enabled = models.BooleanField(default=False) force_recurring = models.BooleanField(default=False) needs_to_be_renewed = models.BooleanField(default=False) duration_quantity = models.IntegerField(blank=True, default=0) duration_units = models.CharField(max_length=10, blank=True) feature_level = models.CharField(max_length=100, blank=True) return_url = models.URLField(blank=True) created_at = models.DateTimeField(editable=False, null=True) date_changed = models.DateTimeField(editable=False, null=True) version = models.IntegerField(blank=True, default=1) spreedly_site_id = models.IntegerField(db_index=True, null=True) objects = PlanManager() class NotEligibile(Exception): pass def natural_key(self): return (self.name, ) def get_absolute_url(self): return reverse('plan_details', kwargs={'plan_pk': self.id}) class Meta: ordering = ['name'] def __init__(self, *args, **kwargs): self._client = api.Client(settings.SPREEDLY_AUTH_TOKEN, settings.SPREEDLY_SITE_NAME) super(Plan, self).__init__(*args, **kwargs) def __unicode__(self): return self.name
[docs] def trial_eligible(self, user): """ Is a customer/user eligibile for a trial? :param user: :py:class:`auth.User` """ try: subscription = user.subscription return (subscription.plan == self and subscription.eligible_for_free_trial) except: return self.is_free_trial_plan
[docs] def start_trial(self, user): """ Check if a user is eligibile for a trial on this plan, and if so, start a plan. :param user: user object to check :returns: py:class:`Subscription` :raises: py:exc:`Plan.NotEligibile` if the user is not eligibile """ if self.trial_eligible(user): # user needs to exist on spreedly side before it can be signed up for trial try: self._client.get_info(user.id) except HTTPError: self._client.create_subscriber(customer_id=user.id, screen_name=user.username) response = self._client.subscribe(subscriber_id=user.id, plan_id=self.id) return Subscription.objects.get_or_create(user, self, response) else: raise self.NotEligibile()
@property def plan_type_display(self): warnings.warn("Deprecated due to switiching to choices", DeprecationWarning) return self.plan_type.replace('_', ' ').title() @property def is_gift_plan(self): return self.plan_type == "gift" @property def is_free_trial_plan(self): return self.plan_type == "free_trial" def get_return_url(self, user, namespace=None): site = Site.objects.get(pk=settings.SITE_ID) base_url = 'https://{site.domain}/'.format(site=site) reverse_urlname = "{0}:spreedly_return".format(namespace) if \ namespace else 'spreedly_return' url = urljoin(base_url, reverse(reverse_urlname, kwargs={ 'user_id': user.id, 'plan_pk': self.id})) return url def subscription_url(self, user, namespace=None): try: token = user.subscription.token except (AttributeError, Subscription.DoesNotExist): token = None subscription_url = self._client.get_signup_url( subscriber_id=user.id, plan_id=self.id, screen_name=user.username, token=token) return_url = self.get_return_url(user, namespace) return "{subscription_url}?return_url={return_url}".format( subscription_url=subscription_url, return_url=return_url)
[docs]class FeeGroup(models.Model): name = models.CharField(max_length=100, primary_key=True) def __unicode__(self): return unicode(self.name)
[docs]class Fee(models.Model): """ .. py:class::Fee A Fee for a given Plan. :attr plan: ForeignKey(Plan) :attr name: CharField(max_length=100) :attr group: ForeignKey(FeeGroup) :attr default_amount: DecimalField(default=0) """ plan = models.ForeignKey(Plan) name = models.CharField(max_length=100) group = models.ForeignKey(FeeGroup) default_amount = models.DecimalField(max_digits=6, decimal_places=2, default='0', help_text=u'USD') def __unicode__(self): return u"{self.plan.name}: {self.name}".format(self=self)
[docs] def add_fee(self, user, description, amount=None): """ .. py:method::add_fee(user, description[, amount]) add a fee to the given user, with description and amount. if amount is not passed, then it will use `default_amount` if it is greater than 0. if 404 or 422 are returned, the default action is not to save the line item to the db, this can be overriden with the setting SPREEDLY_SAVE_ON_FAIL, but it is not recomended as who knows what will happen. :param user: the user to bill for the fee. they must be subscribed to `self.plan` :param description: The description of the fee to appear on the invoice :param amount: The amount to bill or `None` :raises: :py:exc:`ValueError` if the user is not subscribed to the plan or is subscribed to a different plan. :raises: :py:exc:`Http404` if spreedly can't find the plan, user, etc. :raises: :py:exc:`HttpUnprocessableEntity` if spreedly raised 422 for some reason. """ if not amount: amount = self.default_amount if amount <= 0: raise ValueError("Amount must be greater than 0") try: if user.subscription.plan != self.plan: raise ValueError("This fee is not for the user's plan") except Subscription.DoesNotExist: raise ValueError("This user is not signed up to a plan") line_item = LineItem( fee=self, user=user, amount=amount, description=description) response = self.plan._client.add_fee(user.id, self.name, description, self.group.name, amount) response_code = response.status_code msg_template = 'fee failed to process due to {reason}: {fee}, ' \ '{user}, {description}, {amount}. response: ' \ '{response}' if response_code == 404: logger.error(msg_template.format(reason="not found", fee=self, user=user, description=description, amount=amount, response=response)) raise Http404() elif response_code == 422: logger.error(msg_template.format(reason="unprocesable", fee=self, user=user, description=description, amount=amount, response=response)) raise HttpUnprocessableEntity() try: if response_code == 201: line_item.successfull = True line_item.save() elif spreedly_settings.SPREEDLY_SAVE_ON_FAIL: # This is probably a terrible idea line_item.successfull = False return line_item.save() except Exception as e: logger.critical( 'line_item failed to save: {fee}, {user}' ', {description}, {amount}. response: {response} ' 'line_item: {line_item}, error: {e}'.format( fee=self, user=user, description=description, amount=amount, response=response, line_item=line_item, e=e)) e.response = response raise e
[docs]class LineItem(models.Model): """This is an instance of a fee""" fee = models.ForeignKey(Fee) user = models.ForeignKey('auth.User') amount = models.DecimalField(max_digits=6, decimal_places=2, default='0', help_text=u'USD') issued_at = models.DateTimeField(auto_now_add=True) started = models.BooleanField(default=False) successfull = models.BooleanField(default=False) description = models.TextField() reference = models.CharField(max_length=100, null=True) error_code = models.TextField(null=True)
class SubscriptionManager(models.Manager): def create_local(self, user, plan=None): """ Get a subscriber from spreedly and create the local model for it :param user: py:class:`auth.User` :param plan: py:class:`Plan` :returns: py:class:`Subscription` """ try: subscription = self.get_query_set().get(user=user, plan=plan) raise IntegrityError("Subscriber already exists") except Subscription.DoesNotExist: subscription = Subscription() try: data = subscription._client.get_info(user.id) except HTTPError: logger.exception("Coudln't get subscriber from spreedly") raise for k in data: try: if data[k] is not None: if getattr(subscription, k, None) != data[k]: setattr(subscription, k, data[k]) except AttributeError: pass subscription.user = user subscription.plan = plan subscription.active = getattr(subscription, 'active', bool(plan)) subscription.save() return subscription def get_or_create(self, user, plan=None, data=None): """ get or create a subscription based on a user, plan and data passed :param user: py:class:`auth.User` :param plan: py:class:`Plan` :param data: python dict containing the data as returned from spreedly :returns: py:class:`Subscription` """ try: subscription = self.get_query_set().get(user=user, plan=plan) except Subscription.DoesNotExist: subscription = Subscription() if not data: # new client, no plan. try: data = subscription._client.get_info(user.id) except HTTPError: data = subscription._client.create_subscriber(user.id, user.username) for k in data: try: if data[k] is not None: if getattr(subscription, k, None) != data[k]: setattr(subscription, k, data[k]) except AttributeError: pass subscription.user = user subscription.plan = plan subscription.active = getattr(subscription, 'active', bool(plan)) subscription.save() return subscription
[docs]class Subscription(models.Model): """ Class that manages the details for a specific :py:class:`auth.User`'s subscription to a plan. Since a user can only have one subscription, this is sometimes treated as a user profile class. """ name = models.CharField(max_length=100, blank=True) user = models.OneToOneField('auth.User', primary_key=True) first_name = models.CharField(blank=True, max_length=100) last_name = models.CharField(blank=True, max_length=100) feature_level = models.CharField(max_length=100, blank=True) active_until = models.DateTimeField(blank=True, null=True) token = models.CharField(max_length=100, blank=True) eligible_for_free_trial = models.BooleanField(default=False) lifetime = models.BooleanField(default=False) recurring = models.BooleanField(default=False) active = models.BooleanField(default=False) plan = models.ForeignKey(Plan, null=True, default=None, on_delete=models.PROTECT) url = models.URLField(editable=False) card_expires_before_next_auto_renew = models.BooleanField(default=False) store_credit = models.DecimalField(max_digits=6, decimal_places=2, default='0', help_text=u'USD') objects = SubscriptionManager() def __init__(self, *args, **kwargs): self._client = api.Client(settings.SPREEDLY_AUTH_TOKEN, settings.SPREEDLY_SITE_NAME) super(Subscription, self).__init__(*args, **kwargs) def __unicode__(self): return u'Subscription for %s' % self.user def save(self, *args, **kwargs): if self.active and not self.user.is_active: self.user.is_active = True self.user.save() self.url = urljoin(self._client.base_url, 'subscriber_accounts/{token}'.format(token=self.token)) return super(Subscription, self).save(*args, **kwargs) def get_absolute_url(self): return self.url @property
[docs] def ending_this_month(self): """ Will this plan end within the next 30 days """ return (datetime.today() <= self.active_until <= datetime.today() + timedelta(days=30))
@property
[docs] def subscription_active(self): ''' gets the status based on current active status and active_until ''' return self.active and (self.active_until > datetime.today() or self.active_until is None)
[docs] def allow_free_trial(self): """ Allow a free Trial :returns: :py:class:`Subscription` :raises: :py:class:`Exception` (of some kind) if bad juju """ response = self._client.allow_free_trial(self.user.id) for k in response: try: if response[k] is not None: if getattr(self, k) != response[k]: setattr(self, k, response[k]) except AttributeError: pass self.save() return self
[docs] def update_subscription(self, data=None): """update a subscription with supplied data""" #TODO calculate surchargs/credits caused by changes. if data is None: data = self._client.get_info(self.user.id) plan = Plan.objects.get(pk=data['subscription_plan_version']['subscription_plan_id']) for k in data: try: if data[k] is not None: if getattr(self, k) != data[k]: setattr(self, k, data[k]) except AttributeError: pass self.plan = plan self.save()
[docs] def create_complimentary_subscription(self, time, unit, feature_level): """ :raises: :py:exc:`NotImplementedError` cause it isn't implemented """ raise NotImplementedError()
[docs] def add_fee(self, fee, units, description): """ Add a fee to the subscription :param fee: :py:class:`Fee` to add to the linked user :param units: the number of units the charge is for (100kb, 4 nights, etc.) :param description: a description of the charge :returns: None :raises: Http404 if incorrect subscriber, HttpUnprocessableEntity for any other 422 error """ amount = fee.default_amount * units fee.add_fee(self.user, description, amount)

Project Versions

This Page