Skip to content

Commit

Permalink
Adding base project and barebone models structures
Browse files Browse the repository at this point in the history
  • Loading branch information
tobiaghiraldini committed Jan 28, 2019
1 parent 2018cff commit a78b546
Show file tree
Hide file tree
Showing 23 changed files with 321 additions and 19 deletions.
29 changes: 27 additions & 2 deletions docs/code.rst
Expand Up @@ -4,13 +4,38 @@ Documentation for the Code
.. automodule:: flexible_plans


Models
Plan Models
=========================

This module is responsible to define two main models, and a few specific subsclasses
The Plan Model is a concrete, swappable subclass of the abstract BasePlan.

* BasePlan
* Plan


.. automodule:: flexible_plans.models.plans
:members:


Feature Models
=========================

There are a few Feature concrete, swappable subsclasses of the abstract BaseFeature to represent different types of features
with different logic:

* BaseFeature
* Feature
* MeteredFeature
* CumulativeFeature
* PermissionFeature


.. automodule:: flexible_plans.models.features
:members:


Views
=========================

All the CRUD views are defined and available.
Plus, specific views for the Plan management are available
1 change: 0 additions & 1 deletion docs/index.rst
Expand Up @@ -15,7 +15,6 @@ Contents:
installation
usage
code
documentation/modules
contributing
authors
history
1 change: 1 addition & 0 deletions flexible_plans/__init__.py
@@ -1 +1,2 @@
__version__ = '0.1.0'
default_app_config = 'flexible_plans.apps.FlexiblePlansConfig'
Empty file.
Empty file added flexible_plans/api/urls.py
Empty file.
Empty file added flexible_plans/api/views.py
Empty file.
8 changes: 8 additions & 0 deletions flexible_plans/apps.py
Expand Up @@ -3,4 +3,12 @@


class FlexiblePlansConfig(AppConfig):
"""
Default App Config
Usage:
* add 'flexible_plans' to INSTALLED_APPS settings and this config will be loaded
* create and add another config to INSTALLED_APPS settings to load this app with custom AppConfig
"""
name = 'flexible_plans'
30 changes: 30 additions & 0 deletions flexible_plans/context_processors.py
@@ -0,0 +1,30 @@
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse


def subscription_status(request):
"""
User Subscription Context Processor, to keep the Subscription Status available in the templates
* ``ACCOUNT_EXPIRED = boolean``, account was expired state,
* ``ACCOUNT_NOT_ACTIVE = boolean``, set when account is not expired, but it is over quotas so it is
not active
* ``EXPIRE_IN_DAYS = integer``, number of days to account expiration,
* ``EXTEND_URL = string``, URL to account extend page.
* ``ACTIVATE_URL = string``, URL to account activation needed if account is not active
:param request: HttpRequest object
:return: the current user susbscription status object
"""
if request.user.is_authenticated:
try:
return {
'ACCOUNT_EXPIRED': request.user.subscription.is_expired(),
'ACCOUNT_NOT_ACTIVE': (
not request.user.subscription.is_active() and not request.user.subscription.is_expired()),
'EXPIRE_IN_DAYS': request.user.subscription.days_left(),
'EXTEND_URL': reverse('current_plan'),
'ACTIVATE_URL': reverse('account_activation'),
}
except ObjectDoesNotExist:
pass
return {}
Empty file added flexible_plans/listeners.py
Empty file.
23 changes: 23 additions & 0 deletions flexible_plans/management/commands/import_plans.py
@@ -0,0 +1,23 @@
from django.core.management import BaseCommand


class Command(BaseCommand):
"""
import_plans Management Command
Usage: from the project shell invoke the admin command
.. code-block:: bash
$ python manage.py import_plans <provider_name>
where <provider_name> must match with the providers' name configured in settings (all lowercase)
"""
help = ''

def handle(self, *args, **options):
pass



86 changes: 86 additions & 0 deletions flexible_plans/migrations/0001_initial.py
@@ -0,0 +1,86 @@
# Generated by Django 2.1.5 on 2019-01-28 14:58

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields


class Migration(migrations.Migration):

initial = True

dependencies = [
('auth', '0009_alter_user_last_name_max_length'),
]

operations = [
migrations.CreateModel(
name='Feature',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='name')),
('codename', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='codename')),
],
options={
'swappable': 'FLEXIBLE_PLANS_FEATURE_MODEL',
},
),
migrations.CreateModel(
name='Plan',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('is_removed', models.BooleanField(default=False)),
('name', models.CharField(max_length=255, unique=True, verbose_name='name')),
('description', models.TextField(blank=True, verbose_name='description')),
('default', models.NullBooleanField(db_index=True, default=None, help_text='Both "Unknown" and "No" means that the plan is not default', unique=True)),
('available', models.BooleanField(db_index=True, default=False, help_text='Is still available for purchase', verbose_name='available')),
('provider', models.CharField(max_length=100)),
],
options={
'swappable': 'FLEXIBLE_PLANS_PLAN_MODEL',
},
),
migrations.CreateModel(
name='CumulativeFeature',
fields=[
('feature_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.FLEXIBLE_PLANS_FEATURE_MODEL)),
('usage', models.PositiveIntegerField(default=0)),
],
options={
'abstract': False,
},
bases=('flexible_plans.feature',),
),
migrations.CreateModel(
name='MeteredFeature',
fields=[
('feature_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.FLEXIBLE_PLANS_FEATURE_MODEL)),
('units', models.PositiveIntegerField(default=0)),
('usage', models.PositiveIntegerField(default=0)),
],
options={
'abstract': False,
},
bases=('flexible_plans.feature',),
),
migrations.CreateModel(
name='PermissionFeature',
fields=[
('feature_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.FLEXIBLE_PLANS_FEATURE_MODEL)),
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='auth.Permission')),
],
options={
'abstract': False,
},
bases=('flexible_plans.feature',),
),
migrations.AddField(
model_name='plan',
name='features',
field=models.ManyToManyField(to=settings.FLEXIBLE_PLANS_FEATURE_MODEL),
),
]
Empty file.
2 changes: 1 addition & 1 deletion flexible_plans/models/__init__.py
@@ -1 +1 @@
__all__ = ['features', 'plans']
__all__ = ['features', 'plans', 'customers', 'subscriptions']
12 changes: 12 additions & 0 deletions flexible_plans/models/customers.py
@@ -0,0 +1,12 @@
from django.db import models
from django.conf import settings
import swapper


class Customer(models.Model):
user = models.OneToOneField(settings.AUTH_USER, on_delete=models.CASCADE)

class Meta:
# Setting model as swappable
swappable = swapper.swappable_setting('flexible_plans', 'Customer')

3 changes: 3 additions & 0 deletions flexible_plans/models/features.py
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-

from django.db import models
from model_utils.managers import InheritanceManager
from django.utils.translation import gettext_lazy as _
Expand Down Expand Up @@ -46,6 +48,7 @@ class Feature(BaseFeature):
objects = InheritanceManager()

class Meta:
# Setting model as swappable
swappable = swapper.swappable_setting('flexible_plans', 'Feature')


Expand Down
56 changes: 41 additions & 15 deletions flexible_plans/models/plans.py
Expand Up @@ -9,31 +9,23 @@
class BasePlan(TimeStampedModel, SoftDeletableModel):
"""
Abstract BasePlan
-----------------
BasePlan is TimeStamped, which means it holds a reference to dates of creation and modification.
BasePlan is SoftDeletable, which means it becomes deactivated and not removed entirely (to keep a reference to
subscribed plans which become soft-deleted and thus no more available).
"""
name = models.CharField(_('name'), max_length=255, unique=True)
description = models.TextField(_('description'), blank=True)
class Meta:
abstract = True


class Plan(BasePlan):
"""
Single plan defined in the system. A plan can be customized (for specific users), which means
that only those users can purchase this plan and have it selected.
Plan also can be visible and available. Plan is displayed on the list of currently available plans
BasePlan also can be visible and available. Plan is displayed on the list of currently available plans
for user if it is visible. User cannot change plan to a plan that is not visible. Available means
that user can buy a plan. If plan is not visible but still available it means that user which
is using this plan already will be able to extend this plan again. If plan is not visible and not
available, he will be forced then to change plan next time he extends an account.
Plan is defined by its Features, which can be quotas, permissions on objects and features
"""
BasePlan is defined by its Features, which can be quotas, permissions on objects and features
"""
name = models.CharField(_('name'), max_length=255, unique=True)
description = models.TextField(_('description'), blank=True)
default = models.NullBooleanField(
help_text=_('Both "Unknown" and "No" means that the plan is not default'),
default=None,
Expand All @@ -45,3 +37,37 @@ class Plan(BasePlan):
help_text=_('Is still available for purchase')
)
features = models.ManyToManyField(swapper.get_model_name('flexible_plans', 'Feature'))

@staticmethod
def get_provider_choices():
from django.conf import settings
choices = []
if getattr(settings, 'PAYMENT_PROVIDERS', None):
choices = list(settings.PAYMENT_PROVIDERS)
return choices

@staticmethod
def get_default_plan(cls):
try:
default_plan = cls.objects.get(default=True)
except cls.DoesNotExists():
default_plan = None
return default_plan

class Meta:
abstract = True


class Plan(BasePlan):
"""
Single plan defined in the system. A plan can be customized (for specific users), which means
that only those users can purchase this plan and have it selected.
"""
provider = models.CharField(max_length=100, choices=BasePlan.get_provider_choices())

class Meta:
# Setting model as swappable
swappable = swapper.swappable_setting('flexible_plans', 'Plan')


50 changes: 50 additions & 0 deletions flexible_plans/models/subscriptions.py
@@ -0,0 +1,50 @@
import swapper
from django.db import models
from model_utils.models import TimeStampedModel
from datetime import date, timedelta, datetime


class BaseSubscription(TimeStampedModel):
"""
Base class for user subscriptions to plans
"""
plan = models.ForeignKey(swapper.get_model_name('flexible_plans', 'Plan'))
customer = models.ForeignKey(swapper.get_model_name('flexible_plans', 'Customer'))

expire = models.DateField(
_('expire'), default=None, blank=True, null=True, db_index=True)
active = models.BooleanField(_('active'), default=True, db_index=True)

def is_active(self):
return self.active

def is_expired(self):
if self.expire is None:
return False
else:
return self.expire < date.today()

def days_left(self):
if self.expire is None:
return None
else:
return (self.expire - date.today()).days

def clean_activation(self):
errors = plan_validation(self.user)
if not errors['required_to_activate']:
plan_validation(self.user, on_activation=True)
self.activate()
else:
self.deactivate()
return errors

class Meta:
abstract = True


class Subscription(BaseSubscription):
"""
Concrete swappable class for user subscriptions to plans
"""
pass
6 changes: 6 additions & 0 deletions flexible_plans/policies/base.py
@@ -0,0 +1,6 @@


class PlanChangePolicy(object):
"""
Base Class to handle the plan change policies
"""
Empty file.
12 changes: 12 additions & 0 deletions flexible_plans/signals.py
@@ -0,0 +1,12 @@
from django.dispatch import Signal

"""
Signals to emit to coordinate with other applications depending on flexible_plans
"""

customer_created = Signal()
customer_created.__doc__ = """
Sent after the creation of the Customer associated to the User
"""


0 comments on commit a78b546

Please sign in to comment.