From 5f926cc9d9fc88ecbad6a275de9546ce71698368 Mon Sep 17 00:00:00 2001
From: Brent Charbonneau <accounts+github@brentc.com>
Date: Sun, 30 Mar 2014 11:43:24 -0400
Subject: [PATCH] Support free plans.

Fixes #24

Plans without pricings will be `plan.is_free: true`. Switching to these
plans causes the userplan have it's expiry cleared.

Since there's no expiry set, the standard plan change process doesn't
make sense, so the plan table is adjusted to present the standard plan
order links rather than plan change links.
---
 plans/fixtures/test_django-plans_plans.json | 12 ++++++
 plans/locale/en/LC_MESSAGES/django.po       | 23 +++++++++--
 plans/models.py                             | 18 +++++---
 plans/plan_change.py                        |  3 ++
 plans/templates/mail/change_plan_body.txt   |  6 ++-
 plans/templates/plans/plan_table.html       | 46 ++++++++++++++-------
 plans/tests/tests.py                        | 20 +++++++++
 plans/views.py                              | 17 ++++++--
 8 files changed, 116 insertions(+), 29 deletions(-)

diff --git a/plans/fixtures/test_django-plans_plans.json b/plans/fixtures/test_django-plans_plans.json
index d28c60ce..26095318 100644
--- a/plans/fixtures/test_django-plans_plans.json
+++ b/plans/fixtures/test_django-plans_plans.json
@@ -70,6 +70,18 @@
             "order": 5
         }
     },
+    {
+        "pk": 9,
+        "model": "plans.plan",
+        "fields": {
+            "available": true,
+            "visible": true,
+            "created": "2014-03-30T10:41:52.428Z",
+            "description": "Free Test",
+            "name": "Free",
+            "order": 6
+        }
+    },
     {
         "pk": 6,
         "model": "plans.billinginfo",
diff --git a/plans/locale/en/LC_MESSAGES/django.po b/plans/locale/en/LC_MESSAGES/django.po
index 533fbd6c..410b6f00 100644
--- a/plans/locale/en/LC_MESSAGES/django.po
+++ b/plans/locale/en/LC_MESSAGES/django.po
@@ -370,12 +370,17 @@ msgstr ""
 msgid "Hi"
 msgstr ""
 
-#: templates/mail/change_plan_body.txt:4
+#: templates/mail/change_plan_body.txt:5
 #, python-format
 msgid "Your current plan is %(plan_name)s and it will expire on %(expire)s. "
 msgstr ""
 
-#: templates/mail/change_plan_body.txt:6
+#: templates/mail/change_plan_body.txt:7
+#, python-format
+msgid "Your current plan is %(plan_name)s. "
+msgstr ""
+
+#: templates/mail/change_plan_body.txt:10
 #: templates/mail/expired_account_body.txt:11
 #: templates/mail/extend_account_body.txt:8
 #: templates/mail/invoice_created_body.txt:11
@@ -721,7 +726,19 @@ msgstr ""
 msgid "Buy"
 msgstr ""
 
-#: templates/plans/plan_table.html:93
+#: templates/plans/plan_table.html:90
+msgid "Free"
+msgstr ""
+
+#: templates/plans/plan_table.html:91
+msgid "no expiry"
+msgstr ""
+
+#: templates/plans/plan_table.html:96
+msgid "Select"
+msgstr ""
+
+#: templates/plans/plan_table.html:109
 #, python-format
 msgid ""
 "\n"
diff --git a/plans/models.py b/plans/models.py
index e5de54f7..782d1dc9 100644
--- a/plans/models.py
+++ b/plans/models.py
@@ -83,6 +83,8 @@ def get_quota_dict(self):
             quota_dic[plan_quota.quota.codename] = plan_quota.value
         return quota_dic
 
+    def is_free(self):
+        return self.planpricing_set.count() == 0
 
 class BillingInfo(models.Model):
     """
@@ -191,7 +193,8 @@ def initialize(self):
         Set up user plan for first use
         """
         if not self.is_active():
-            if self.expire is None:
+            # Plans without pricings don't need to expire
+            if self.expire is None and self.plan.planpricing_set.count():
                 self.expire = now() + timedelta(
                     days=getattr(settings, 'PLAN_DEFAULT_GRACE_PERIOD', 30))
             self.activate()  # this will call self.save()
@@ -209,6 +212,11 @@ def extend_account(self, plan, pricing):
             # Process a plan change request (downgrade or upgrade)
             # No account activation or extending at this point
             self.plan = plan
+
+            if self.expire is not None and not plan.planpricing_set.count():
+                # Assume no expiry date for plans without pricing.
+                self.expire = None
+
             self.save()
             account_change_plan.send(sender=self, user=self.user)
             mail_context = Context({'user': self.user, 'userplan': self, 'plan': plan})
@@ -221,9 +229,7 @@ def extend_account(self, plan, pricing):
             # Processing standard account extending procedure
             if self.plan == plan:
                 status = True
-                if self.expire is None:
-                    pass
-                elif self.expire > date.today():
+                if self.expire is not None and self.expire > date.today():
                     self.expire += timedelta(days=pricing.period)
                 else:
                     self.expire = date.today() + timedelta(days=pricing.period)
@@ -231,9 +237,9 @@ def extend_account(self, plan, pricing):
             else:
                 # This should not ever happen (as this case should be managed by plan change request)
                 # but just in case we consider a case when user has a different plan
-                if self.expire is None:
+                if not self.plan.is_free and self.expire is None:
                     status = True
-                elif self.expire > date.today():
+                elif not self.plan.is_free and self.expire > date.today():
                     status = False
                     accounts_logger.warning("Account '%s' [id=%d] plan NOT changed to '%s' [id=%d]" % (
                         self.user, self.user.pk, plan, plan.pk))
diff --git a/plans/plan_change.py b/plans/plan_change.py
index 29d14e7d..2a5e3c6c 100644
--- a/plans/plan_change.py
+++ b/plans/plan_change.py
@@ -7,6 +7,9 @@ def _calculate_day_cost(self, plan, period):
         """
         Finds most fitted plan pricing for a given period, and calculate day cost
         """
+        if plan.is_free():
+            # If plan is free then cost is always 0
+            return 0
 
         plan_pricings = plan.planpricing_set.order_by('-pricing__period').select_related('pricing')
         selected_pricing = None
diff --git a/plans/templates/mail/change_plan_body.txt b/plans/templates/mail/change_plan_body.txt
index 6ed8d98c..489b5100 100644
--- a/plans/templates/mail/change_plan_body.txt
+++ b/plans/templates/mail/change_plan_body.txt
@@ -1,7 +1,11 @@
 {% load i18n %}{% autoescape off %}
 {% trans "Hi" %} {% firstof user.get_full_name user.username %},
 
-{% blocktrans with plan_name=plan.name expire=userplan.expire %}Your current plan is {{ plan_name }} and it will expire on {{ expire }}. {% endblocktrans %}
+{% if userplan.expire != None %}
+    {% blocktrans with plan_name=plan.name expire=userplan.expire %}Your current plan is {{ plan_name }} and it will expire on {{ expire }}. {% endblocktrans %}
+{% else %}
+    {% blocktrans with plan_name=plan.name %}Your current plan is {{ plan_name }}. {% endblocktrans %}
+{% endif %}
 
 {% trans "Thank you" %}
 --
diff --git a/plans/templates/plans/plan_table.html b/plans/templates/plans/plan_table.html
index dd882857..5ab351a1 100644
--- a/plans/templates/plans/plan_table.html
+++ b/plans/templates/plans/plan_table.html
@@ -58,7 +58,7 @@
         <th></th>
         {% for plan in plan_list %}
         <th class="planpricing_footer {% ifequal forloop.counter0 current_userplan_index %}current{% endifequal %}">
-            {% if plan != userplan.plan and not userplan.is_expired %}
+            {% if plan != userplan.plan and not userplan.is_expired and not userplan.plan.is_free %}
                 <a href="{% url 'create_order_plan_change' pk=plan.id %}" class="change_plan">{% trans "Change" %}</a>{% endif %}
         </th>
         {% endfor %}
@@ -70,22 +70,38 @@
         <th></th>
         {% for plan in plan_list %}
             <th class="planpricing_footer {% ifequal forloop.counter0 current_userplan_index %}current{% endifequal %}">
-
-
                 {% if plan.available %}
-                <ul>
-                    {% for plan_pricing in plan.planpricing_set.all %}
+                    <ul>
+                    {% if not plan.is_free %}
+                        {% for plan_pricing in plan.planpricing_set.all %}
+                            <li>
+                            {% if plan_pricing.pricing.url %}<a href="{{ plan_pricing.pricing.url }}" class="info_link pricing">{% endif %}
+                            <span class="plan_pricing_name">{{ plan_pricing.pricing.name }}</span>
+                            <span class="plan_pricing_period">({{ plan_pricing.pricing.period }} {% trans "days" %})</span>
+                            {% if plan_pricing.pricing.url %}</a>{% endif %}
+                            <span class="plan_pricing_price">{{ plan_pricing.price }}&nbsp;{{ CURRENCY }}</span>
+                            {% if plan_pricing.plan == userplan.plan or userplan.is_expired or userplan.plan.is_free %}
+                            <a href="{% url 'create_order_plan' pk=plan_pricing.pk %}" class="buy">{% trans "Buy" %}</a>
+                            {% endif %}
+                        {% endfor %}
+                    {% else %}
+                        {# Allow selecting plans with no pricings #}
                         <li>
-                        {% if plan_pricing.pricing.url %}<a href="{{ plan_pricing.pricing.url }}" class="info_link pricing">{% endif %}
-                        <span class="plan_pricing_name">{{ plan_pricing.pricing.name }}</span>
-                        <span class="plan_pricing_period">({{ plan_pricing.pricing.period }} {% trans "days" %})</span>
-                        {% if plan_pricing.pricing.url %}</a>{% endif %}
-                        <span class="plan_pricing_price">{{ plan_pricing.price }}&nbsp;{{ CURRENCY }}</span>
-                        {% if plan_pricing.plan == userplan.plan or userplan.is_expired %}
-                        <a href="{% url 'create_order_plan' pk=plan_pricing.pk %}" class="buy">{% trans "Buy" %}</a>
-                        {% endif %}
-                    {% endfor %}
-                </ul>
+                            <span class="plan_pricing_name">{% trans "Free" %}</span>
+                            <span class="plan_pricing_period">({% trans "no expiry" %})</span>
+                            <span class="plan_pricing_price">0&nbsp;{{ CURRENCY }}</span>
+                            {% if plan != userplan.plan or userplan.is_expired %}
+                                <a href="{% url 'create_order_plan_change' pk=plan.id %}" class="change_plan">
+                                    {% if userplan.is_expired %}
+                                        {% trans "Select" %}
+                                    {% else %}
+                                        {% trans "Change" %}
+                                    {% endif %}
+                                </a>
+                            {% endif %}
+                        </li>
+                    {% endif %}
+                    </ul>
 
                    {% else %}
                      <span class="plan_not_available">
diff --git a/plans/tests/tests.py b/plans/tests/tests.py
index d2d6f22a..44809dd7 100644
--- a/plans/tests/tests.py
+++ b/plans/tests/tests.py
@@ -113,6 +113,26 @@ def test_disable_emails(self):
             self.assertEqual(u.userplan.active, True)
             self.assertEqual(len(mail.outbox), 0)
 
+    def test_switch_to_free_no_expiry(self):
+        """
+        Tests switching to a free Plan and checks that their expiry is cleared
+        Tests if expire date is set correctly
+        Tests if mail has been send
+        Tests if account has been activated
+        """
+        u = User.objects.get(username='test1')
+        self.assertIsNotNone(u.userplan.expire)
+
+        plan = Plan.objects.get(name="Free")
+        self.assertTrue(plan.is_free())
+        self.assertNotEqual(u.userplan.plan, plan)
+
+        # Switch to Free Plan
+        u.userplan.extend_account(plan, None)
+        self.assertEquals(u.userplan.plan, plan)
+        self.assertIsNone(u.userplan.expire)
+        self.assertEqual(u.userplan.active, True)
+
 
 class TestInvoice(TestCase):
     fixtures = ['initial_plan', 'test_django-plans_auth', 'test_django-plans_plans']
diff --git a/plans/views.py b/plans/views.py
index 9192578e..a73d837f 100644
--- a/plans/views.py
+++ b/plans/views.py
@@ -230,10 +230,12 @@ def get_all_context(self):
                                                   Q(plan__customized=self.request.user) | Q(
                                                       plan__customized__isnull=True)))
 
-
         # User is not allowed to create new order for Plan when he has different Plan
-        # He should use Plan Change View for this kind of action
-        if not self.request.user.userplan.is_expired() and self.request.user.userplan.plan != self.plan_pricing.plan:
+        # unless it's a free plan. Otherwise, the should use Plan Change View for this
+        # kind of action
+        if not self.request.user.userplan.is_expired() \
+                and not self.request.user.userplan.plan.is_free() \
+                and self.request.user.userplan.plan != self.plan_pricing.plan:
             raise Http404
 
         self.plan = self.plan_pricing.plan
@@ -300,7 +302,14 @@ def get_policy(self):
 
     def get_price(self):
         policy = self.get_policy()
-        period = self.request.user.userplan.days_left()
+        userplan = self.request.user.userplan
+
+        if userplan.expire is not None:
+            period = self.request.user.userplan.days_left()
+        else:
+            # Use the default period of the new plan
+            period = 30
+
         return policy.get_change_price(self.request.user.userplan.plan, self.plan, period)
 
     def get_context_data(self, **kwargs):