├── .gitignore ├── CONTRIBUTORS ├── GPL-LICENSE.txt ├── MANIFEST.in ├── README ├── docs ├── usersubscription-states.dot └── usersubscription-states.dot.png ├── setup.py └── subscription ├── __init__.py ├── admin.py ├── locale └── pl │ └── LC_MESSAGES │ └── django.po ├── migrations ├── 0001_initial.py ├── 0002_trial_period.py ├── 0003_auto__chg_field_subscription_group__del_unique_subscription_group.py └── __init__.py ├── models.py ├── providers.py ├── signals.py ├── tests.py ├── urls.py ├── utils.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.egg-info 4 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Many thanks to all the contributors! 2 | 3 | CONTRIBUTORS include: 4 | ironfroggy (http://github.com/ironfroggy) 5 | Igor Gumenyuk (http://github.com/exslim) 6 | gearheart (http://github.com/gearheart) 7 | leftpudding (http://github.com/leftpudding) 8 | -------------------------------------------------------------------------------- /GPL-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2010 saaskit.org 2 | ----------------------------------- 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | 17 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft * 2 | prune .hg 3 | prune .git 4 | prune .ropeproject 5 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Django Subscription 2 | =================== 3 | 4 | Django-subscription is an application for handling PayPal-based pay 5 | subscriptions. Module does not handle explicit permissions; instead, 6 | subscribed user are automatically added to predefined groups using 7 | `django.contrib.auth' application. It needs django-paypal application 8 | available at [http://github.com/johnboxall/django-paypal/] for handling 9 | payments. 10 | 11 | Table of Contents 12 | ================= 13 | 1 Installation 14 | 2 Settings 15 | 3 Models 16 | 3.1 Subscription 17 | 3.1.1 methods 18 | 3.2 UserSubscription 19 | 3.2.1 methods 20 | 3.3 Transaction 21 | 4 Signals 22 | 5 Views 23 | 6 URLs 24 | 7 Templates 25 | 8 Subscription change 26 | 9 Example code 27 | 10 Bugs and omissions 28 | 10.1 Plans 29 | 11 License 30 | 31 | 32 | 1 Installation 33 | ~~~~~~~~~~~~~~ 34 | Copy or symlink `subscription/' directory on Python path (`setup.py' 35 | script for automated installation will be supplied later on). Module 36 | contents are available in the `subscription' module. 37 | 38 | In order to use application, add `subscription' to INSTALLED_APPS in 39 | Django project `settings.py' file. 40 | 41 | 2 Settings 42 | ~~~~~~~~~~ 43 | In project's `settings.py' file `SUBSCRIPTION_PAYPAL_SETTINGS' 44 | should be set to a dictionary with default PayPal button settings, 45 | as described in django-paypal documentation. At least the `business' 46 | key should be set to your PayPal business e-mail. 47 | 48 | `SUBSCRIPTION_PAYPAL_FORM' can be set to a form class pathname as 49 | string to use custom PayPal payment button class. Default is 50 | 'paypal.standard.forms.PayPalPaymentsForm'. To use PayPal encrypted 51 | buttons or shared secrets, specify needed django-paypal settings and 52 | set an appropriate class here. 53 | 54 | `SUBSCRIPTION_GRACE_PERIOD' is an integer and it specifies number of 55 | days after individual subscription expiry on which account is 56 | actually treated as expired. Default is 2 days. Intent of this 57 | setting is that recurring payments take place e.g. monthly, so 58 | either on last day of subscription period, or even on first day 59 | after it; this way we avoind unintentionally locking out user 60 | account. 61 | 62 | 3 Models 63 | ~~~~~~~~ 64 | Two models defined by the application are available in the 65 | `subscription.models' module. 66 | 67 | 3.1 Subscription 68 | ================ 69 | Main model used by the application is `Subscription'. It 70 | represents a single subscription available for users. Subscription 71 | has following fields: 72 | - `name' - short name 73 | - `description' - longer description 74 | - `price' - subscription price 75 | - `recurrence_period' - PayPal subscription recurrence period (used 76 | only if `recurrence_unit' is not `None') 77 | - `recurrence_unit' - in what units is recurrence period expressed: 78 | - D for days 79 | - W for weeks 80 | - M for months 81 | - Y for years 82 | - None (NULL) for one-time (non-recurring) payment 83 | - `group' - one to one relation to 84 | `django.contrib.auth.models.Group'. Subscription is identified 85 | by the group. 86 | 87 | 3.1.1 methods 88 | ------------- 89 | - `price_per_day()' - returns estimate subscription price per day, 90 | as a float. This value is used to give user that upgrades 91 | subscription a rebate for unused part of month. Value is only 92 | an estimate: average length of month (30.4368 days) and year 93 | (365.2425 days) are used. 94 | - `get_pricing_display()' - return pretty pricing info for display 95 | as a string. 96 | 97 | 3.2 UserSubscription 98 | ==================== 99 | This model instances define a user's subscription. Model has 100 | following fields: 101 | - `user' - one-to-one relation to `auth.User' model, primary key; 102 | - `subscription' - foreign key relation to `Subscription' model, 103 | specifies kind of subscription `user' is subscribed to; 104 | - `expires' - expiry date (if null, subscription never expires) 105 | - `active' - boolean, True if subscription is active 106 | - `cancelled' - boolean, True if subscription was cancelled 107 | 108 | Fields `active' and `cancelled' are used for implementing the 109 | subscription change flow (see later). Every UserSubscription 110 | starts with both `active' and `cancelled' set to `False'. When 111 | PayPal subscription is confirmed, `active' is set to True. When 112 | any other PayPal subscription for the same user is confirmed, 113 | `active' is set to `False' (because `active' is set to `True' for 114 | this other subscription, in other UserSubscription instance). When 115 | subscription is cancelled at PayPal, `cancelled' is set to `True'. 116 | When UserSubscription is cancelled and not active, it is deleted. 117 | When UserSubscription has expired and is cancelled, it is deleted. 118 | Transition graph of these state bits can be found in 119 | [file:docs/usersubscription-states.dot.png] (GraphViz source in 120 | [file:docs/usersubscription-states.dot]). 121 | 122 | Class field `grace_timedelta' is provided (read-only) and contains 123 | effective value of `SUBSCRIPTION_GRACE_PERIOD' setting as 124 | `datetime.timedelta' object. 125 | 126 | 3.2.1 methods 127 | ------------- 128 | - `user_is_group_member()' - returns true if `user' is member of 129 | `subscription.group'; 130 | - `expired()' - returns true if there is more than 131 | `SUBSCRIPTION_GRACE_PERIOD' days after `expires' date; 132 | - `valid()' - returns true if: 133 | + `expired()' is false and `user_is_group_member()' is false, or 134 | + `expired()' is true and `user_is_group_member()' is true; 135 | - `unsubscribe()' - remove `subscription.group' from `user''s groups 136 | - `subscribe()' - add `subscription.group' to `user''s groups 137 | (called automatically on PayPal one-time payment and 138 | subscription start); 139 | - `fix()' - if not `valid()', call `unsubscribe()' or `subscribe()'; 140 | - `extend(timedelta=None)' - extend `expires' field by provided 141 | `datetime.timedelta', or by `subscription''s recurrence period 142 | (called automatically on PayPal subscription payments); 143 | - `try_change(subscription)' - sends `change_check' signal to test 144 | whether change from `self.subscription' to Subscription object 145 | supplied in `subscription' parameter is possible. Returns list 146 | of reasons why upgrade is denied; if list is empty, upgrade is 147 | allowed. 148 | 149 | Convenience function `subscription.models.unsubscribe_expired()' 150 | is also provided. It loops over all expired `UserSubscription' 151 | instances and calls `unsubscribe()' method. It is intended to be 152 | called automatically from cron, django-cron, or on some event. 153 | Alternatively, `fix()' can be called on events related to 154 | user, e.g. on user login. 155 | 156 | 3.3 Transaction 157 | =============== 158 | `Transaction' model is mostly read-only and is used to view 159 | subscription-related events in the admin panel. It has following 160 | fields: 161 | - `timestamp' - date and time of event 162 | - `subscription' - foreign key of `Subscription' model that event 163 | was related to 164 | - `user' - foreign key of `django.contrib.auth.models.User' model 165 | that event was related to 166 | - `ipn' - foreign key of `paypal.standard.ipn.models.PayPalIPN' 167 | model identifying payment callback related to event 168 | - `event' - type of event, one of: 169 | - new usersubscription 170 | - one-time payment 171 | - subscription payment 172 | - unexpected payment 173 | - payment flagged 174 | - deactivated 175 | - activated 176 | - unexpected subscription 177 | - remove subscription 178 | - cancel subscription 179 | - unexpected cancel 180 | - modify subscription 181 | - subscription expired 182 | The "unexpected" events are ones that could not be related to any 183 | specific user/subscription pair. 184 | - `amount' - amount (`mc_gross') of `ipn' 185 | - `comment' - site admin's comment, only field intended to be 186 | modified. 187 | In admin panel's `Transaction' object list, fields `subscription', 188 | `user', `ipn' are links to related modes instance's admin forms. 189 | 190 | 4 Signals 191 | ~~~~~~~~~ 192 | On subscription-related events, the application sends signals that 193 | project code can connect to and do some site-specific things (e.g. 194 | send a nice e-mail to user). Signals are available in 195 | `subscription.signals' package. All signals have `Subscription' 196 | instance (or, in extreme cases with `event' signal, `None') as 197 | sender, and have arguments `ipn' 198 | (`paypal.standard.ipn.models.PayPalIPN' model instance), `user' 199 | (`django.contrib.auth.models.User' instance), `subscription' 200 | (`Subscription' instance or None, same as sender), 201 | `usersubscription' (`UserSubscription' instance). Signals are: 202 | - `signed_up' - user signed up for one-time payment, 203 | - `subscribed' - user subscribed 204 | - `unsubscribed' - user unsubscribed from PayPal (`usersubscription' 205 | is a deleted object if `usersubscription.active' is True) 206 | - `paid' - payment received from a subscription 207 | - `event' - other strange event, does not receive `usersubscription' 208 | argument (there is no meaningful `UserSubscription' object) and 209 | receives additional `event' argument, which may be 210 | - `unexpected_payment' 211 | - `flagged' 212 | - `unexpected_subscription' 213 | - `unexpected_cancel' 214 | - `subscription_modify' 215 | 216 | Signal `change_check' is a hook for verification of subscription 217 | change. Sender is `UserSubscription' object with user's current 218 | subscription, additional parameter `subscription' provides 219 | subscription to change to. If subscription change is possible, 220 | listener should return `None', otherwise it should return a string 221 | describing reason that will be displayed to user. 222 | 223 | 5 Views 224 | ~~~~~~~ 225 | Views are available in `subscription.views' module 226 | - `subscription_list' lists available subscription using 227 | `subscription/subscription_list.html' template 228 | - `subscription_detail' presents details of the selected 229 | subscription (login is required for this view) along with PayPal 230 | button for subscription or upgrade. 231 | 232 | 6 URLs 233 | ~~~~~~ 234 | Module `subscription.urls' configures default urls for module. This 235 | are: 236 | - root URL displays `subscription_list' view 237 | - /id/ (numeric ID) displays `subscription_detail' view for 238 | Subscription with ID /id/ 239 | - `paypal/' is PayPal IPN URL 240 | - `done/' displays `subscription/subscription_done.html' template 241 | and is where successful PayPal transactions for initial 242 | subscription are redirected 243 | - `change-done/' displays 244 | `subscription/subscription_change_done.html' template and is 245 | where successful PayPal transactions for subscription change are 246 | redirected 247 | - `cancel/' displays `subscription/subscription_cancel.html' 248 | template and is where cancelled PayPal transactions are redirected 249 | 250 | 7 Templates 251 | ~~~~~~~~~~~ 252 | Templates `subscription/subscription_done.html' and 253 | `subscription/subscription_cancel.html' receive no context. 254 | 255 | Template `subscription/subscription_change_dane.html' receives 256 | `cancel_url' parameter, which is URL to PayPal list of transactions 257 | with site's merchant account, making it easier to cancel the old 258 | subscription. 259 | 260 | Template `subscription/subscription_list.html' receives 261 | `object_list' variable which is a list of `Subscription' objects. 262 | 263 | Template `subscription/subscription_detail.html' receives: 264 | - `object' variable which is a `Subscription' object, 265 | - `usersubscription' variable, which is current user's active 266 | `UserSubscription' instance (may be used to tell apart initial 267 | subscription from subscription change/upgrade, or to display 268 | current subscription's expiry date), 269 | - `change_denied_reasons', which is a list of reasons that 270 | subscription change/upgrade is denied; if false (empty list or 271 | `None' if user is not subscribed), change or signup is allowed, 272 | - `form' variable which is a PayPal form for the `object', if 273 | `change_denied_reasons' is false, 274 | - `cancel_url', which is URL to PayPal list of transactions with 275 | site's merchant account, making it easier to cancel the old 276 | subscription. 277 | 278 | 8 Subscription change 279 | ~~~~~~~~~~~~~~~~~~~~~ 280 | Most complex flow in this app is when user wants to change (upgrade) 281 | current subscription. For subscriptions we are using PayPal 282 | standard subscriptions API. This means, we get three kinds of 283 | asynchronous IPN notifications: 284 | - subscr_signup when user signs up for new subscription, 285 | - subscr_payment on every single payment, 286 | - subscr_cancel when user or merchant cancels subscription (or 287 | subscr_eot when time-limited subscription runs out; we treat 288 | subscr_eot exactly as subscr_cancel). 289 | When user signs up, we get subscr_signup and subscr_payment for 290 | first payment, in random order. There is no support for changing 291 | running subscription, so user needs to sign up for new subscription 292 | and cancel old one. 293 | 294 | Events for subscriptions are handled this way: 295 | - subscr_payment finds UserSubscription object for User and 296 | Subscription ID specified in the IPN. If UserSubscription is not 297 | found, new one is created, which becomes inactive. Found or new 298 | UserSubscription object is extended for the next billing period. 299 | - subscr_signup finds UserSubscription object for User and 300 | Subscription ID specified in the IPN. If UserSubscription is not 301 | found, new one is created. Found or created UserSubscription is 302 | set to active, User is added to subscription's group; if user has 303 | another UserSubscription, they are made inactive and user is 304 | removed from these Subscription groups. In effect, on signup the 305 | new subscription becomes user's only active one, and its group 306 | only subscription-related group to which user belongs. 307 | - subscr_cancel finds relevant UserSubscription object. If it is 308 | inactive (which means subscription change), removes user from its 309 | subscription's group, and deletes the UserSubscription. If it is 310 | active, does nothing, so user can use up rest of current billing 311 | period. 312 | 313 | So, signup flow is: 314 | - user clicks in PayPal subscribe button displayed on subscription 315 | detail page and subscribes at PayPal, 316 | - subscr_payment extends the UserSubscription, 317 | - subscr_signup makes the UserSubscription active and uncancelled 318 | and adds user to group, 319 | - whichever of those got called first, creates the UserSubscription. 320 | 321 | Cancel flow is: 322 | - user cancels subscription at PayPal, 323 | - UserSubscription is active, so it is marked cancelled, kept and 324 | stays valid until expiry. 325 | 326 | Subscription change flow is: 327 | - If user is allowed to change subscription, subscription detail page 328 | displays PayPal subscribe button, 329 | - user clicks subscribe button and signs up for new subscription at 330 | PayPal, 331 | - landing page after PayPal transaction displays link to PayPal 332 | transaction list which user can use to cancel old subscription at 333 | PayPal, 334 | - user cancels old subscription at PayPal; 335 | - whichever of subscr_payment or subscr_signup gets called first, 336 | creates new, inactive, uncancelled UserSubscription instance, 337 | - subscr_payment extends new UserSubscription instance for next 338 | billing period, 339 | - subscr_signup deactivates all active UserSubscriptions and removes 340 | user from group; then, activates and uncancels new 341 | UserSubscription and adds user to its subscription's group, 342 | - subscr_cancel (which gets called after previous two, because user 343 | needs some time to click through the PayPal forms) finds inactive 344 | UserSubscription, ensures that user is really not member of group, 345 | and deletes the UserSubscription object. 346 | 347 | If user makes a mistake and cancels new subscription instead of the 348 | old one, new subscription goes through "Cancel flow" above, does not 349 | get deleted, so user has chance to fix things at PayPal. Project 350 | should add `signals.unsubscribed' handler that would detect such 351 | situation (if `usersubscription' parameter is active, and user has 352 | inactive UserSubscription objects, cancel was probably a mistake) 353 | and notify user of his mistake. 354 | 355 | 9 Example code 356 | ~~~~~~~~~~~~~~ 357 | Example usage and templates are available as `django-saas-kit' 358 | project at [http://github.com/saas-kit/django-saas-kit/] 359 | 360 | 10 Bugs and omissions 361 | ~~~~~~~~~~~~~~~~~~~~~ 362 | - There is no `setup.py' script for automated installation. 363 | - No support for PayPal PDT; PDT has only presentational value (IPN 364 | needs to be received anyway, and PDT should be used only to 365 | display transaction details to user on after transaction landing 366 | page), so support for it has been intentionally omitted. 367 | 368 | 10.1 Plans 369 | ========== 370 | - Single payments for subscription, including possibility of 371 | pay-as-you-go scheme 372 | 373 | 11 License 374 | ~~~~~~~~~~ 375 | This project is licensed on terms of GPL (GPL-LICENSE.txt) licenses. 376 | -------------------------------------------------------------------------------- /docs/usersubscription-states.dot: -------------------------------------------------------------------------------- 1 | digraph usersubscription { 2 | 00[label="-active\n-cancelled"]; 3 | 01[label="-active\n+cancelled"]; 4 | 10[label="+active\n-cancelled"]; 5 | 11[label="+active\n+cancelled"]; 6 | CREATE[shape="box",style="bold"]; 7 | DELETE[shape="box",style="bold"]; 8 | 9 | CREATE->00[penwidth=3,arrowType="vee"]; 10 | 00->10[label="signup self"]; 11 | 10->00[label="signup other"]; 12 | 10->11[label="cancel"]; 13 | 11->10[label="signup self"]; 14 | 00->01[label="cancel"]; 15 | 11->01[label="signup other"]; 16 | 01->DELETE[penwidth=3,arrowType="vee"]; 17 | } 18 | -------------------------------------------------------------------------------- /docs/usersubscription-states.dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaque/django-subscription/be38c513dc6adb4e5080487ebe9e744704f3f4d8/docs/usersubscription-states.dot.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup(name="django-subscription", 4 | version="0.1", 5 | description="Subscriptions based web app using django-paypal", 6 | author="SaaS kit", 7 | author_email="admin@saaskit.org", 8 | packages=find_packages(), 9 | include_package_data=True, 10 | ) 11 | 12 | -------------------------------------------------------------------------------- /subscription/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaque/django-subscription/be38c513dc6adb4e5080487ebe9e744704f3f4d8/subscription/__init__.py -------------------------------------------------------------------------------- /subscription/admin.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib import admin 3 | from django.utils.html import conditional_escape as esc 4 | 5 | from models import Subscription, UserSubscription, Transaction 6 | 7 | 8 | def _pricing(sub): 9 | return sub.get_pricing_display() 10 | 11 | 12 | def _trial(sub): 13 | return sub.get_trial_display() 14 | 15 | 16 | class SubscriptionAdmin(admin.ModelAdmin): 17 | list_display = ('name', _pricing, _trial) 18 | admin.site.register(Subscription, SubscriptionAdmin) 19 | 20 | 21 | def _subscription(trans): 22 | if trans.subscription != None: 23 | return u'%s' % ( 24 | trans.subscription.pk, esc(trans.subscription)) 25 | _subscription.allow_tags = True 26 | 27 | 28 | def _user(trans): 29 | if trans.user != None: 30 | return u'%s' % ( 31 | trans.user.pk, esc(trans.user)) 32 | _user.allow_tags = True 33 | 34 | 35 | def _ipn(trans): 36 | if trans.ipn != None: 37 | return u'#%s' % ( 38 | trans.ipn.pk, trans.ipn.pk) 39 | _ipn.allow_tags = True 40 | 41 | 42 | class UserSubscriptionAdminForm(forms.ModelForm): 43 | class Meta: 44 | model = UserSubscription 45 | fix_group_membership = forms.fields.BooleanField(required=False) 46 | extend_subscription = forms.fields.BooleanField(required=False) 47 | 48 | 49 | class UserSubscriptionAdmin(admin.ModelAdmin): 50 | list_display = ('__unicode__', _user, _subscription, 'active', 'expires', 'valid') 51 | list_display_links = ('__unicode__',) 52 | list_filter = ('active', 'subscription', ) 53 | date_hierarchy = 'expires' 54 | form = UserSubscriptionAdminForm 55 | fieldsets = ( 56 | (None, {'fields': ('user', 'subscription', 'expires', 'active')}), 57 | ('Actions', {'fields': ('fix_group_membership', 'extend_subscription'), 58 | 'classes': ('collapse',)}), 59 | ) 60 | 61 | def save_model(self, request, obj, form, change): 62 | if form.cleaned_data['extend_subscription']: 63 | obj.extend() 64 | if form.cleaned_data['fix_group_membership']: 65 | obj.fix() 66 | obj.save() 67 | 68 | # action for Django-SVN or django-batch-admin app 69 | actions = ('fix', 'extend',) 70 | 71 | def fix(self, request, queryset): 72 | for us in queryset.all(): 73 | us.fix() 74 | fix.short_description = 'Fix group membership' 75 | 76 | def extend(self, request, queryset): 77 | for us in queryset.all(): 78 | us.extend() 79 | extend.short_description = 'Extend subscription' 80 | 81 | admin.site.register(UserSubscription, UserSubscriptionAdmin) 82 | 83 | 84 | class TransactionAdmin(admin.ModelAdmin): 85 | date_hierarchy = 'timestamp' 86 | list_display = ('timestamp', 'id', 'event', _subscription, _user, _ipn, 'amount', 'comment') 87 | list_display_links = ('timestamp', 'id') 88 | list_filter = ('subscription', 'user') 89 | admin.site.register(Transaction, TransactionAdmin) 90 | -------------------------------------------------------------------------------- /subscription/locale/pl/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2009-06-29 17:50+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" 19 | 20 | #: models.py:42 21 | msgid "No recurrence" 22 | msgstr "" 23 | 24 | #: models.py:43 25 | msgid "Day" 26 | msgstr "" 27 | 28 | #: models.py:44 29 | msgid "Week" 30 | msgstr "" 31 | 32 | #: models.py:45 33 | msgid "Month" 34 | msgstr "" 35 | 36 | #: models.py:46 37 | msgid "Year" 38 | msgstr "" 39 | 40 | #: models.py:82 41 | #, python-format 42 | msgid "%(price).02f / %(unit)s" 43 | msgid_plural "%(price).02f / %(period)d %(unit_plural)s" 44 | msgstr[0] "" 45 | msgstr[1] "" 46 | 47 | #: models.py:90 48 | #, python-format 49 | msgid "%(price).02f one-time fee" 50 | msgstr "" 51 | 52 | #: models.py:188 53 | msgid "This is your current subscription." 54 | msgstr "" 55 | -------------------------------------------------------------------------------- /subscription/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | 2 | from south.db import db 3 | from django.db import models 4 | from subscription.models import * 5 | 6 | class Migration: 7 | 8 | def forwards(self, orm): 9 | 10 | # Adding model 'UserSubscription' 11 | db.create_table('subscription_usersubscription', ( 12 | ('id', orm['subscription.UserSubscription:id']), 13 | ('user', orm['subscription.UserSubscription:user']), 14 | ('subscription', orm['subscription.UserSubscription:subscription']), 15 | ('expires', orm['subscription.UserSubscription:expires']), 16 | ('active', orm['subscription.UserSubscription:active']), 17 | ('cancelled', orm['subscription.UserSubscription:cancelled']), 18 | )) 19 | db.send_create_signal('subscription', ['UserSubscription']) 20 | 21 | # Adding model 'Transaction' 22 | db.create_table('subscription_transaction', ( 23 | ('id', orm['subscription.Transaction:id']), 24 | ('timestamp', orm['subscription.Transaction:timestamp']), 25 | ('subscription', orm['subscription.Transaction:subscription']), 26 | ('user', orm['subscription.Transaction:user']), 27 | ('ipn', orm['subscription.Transaction:ipn']), 28 | ('event', orm['subscription.Transaction:event']), 29 | ('amount', orm['subscription.Transaction:amount']), 30 | ('comment', orm['subscription.Transaction:comment']), 31 | )) 32 | db.send_create_signal('subscription', ['Transaction']) 33 | 34 | # Adding model 'Subscription' 35 | db.create_table('subscription_subscription', ( 36 | ('id', orm['subscription.Subscription:id']), 37 | ('name', orm['subscription.Subscription:name']), 38 | ('description', orm['subscription.Subscription:description']), 39 | ('price', orm['subscription.Subscription:price']), 40 | ('recurrence_period', orm['subscription.Subscription:recurrence_period']), 41 | ('recurrence_unit', orm['subscription.Subscription:recurrence_unit']), 42 | ('group', orm['subscription.Subscription:group']), 43 | )) 44 | db.send_create_signal('subscription', ['Subscription']) 45 | 46 | # Creating unique_together for [user, subscription] on UserSubscription. 47 | db.create_unique('subscription_usersubscription', ['user_id', 'subscription_id']) 48 | 49 | 50 | 51 | def backwards(self, orm): 52 | 53 | # Deleting model 'UserSubscription' 54 | db.delete_table('subscription_usersubscription') 55 | 56 | # Deleting model 'Transaction' 57 | db.delete_table('subscription_transaction') 58 | 59 | # Deleting model 'Subscription' 60 | db.delete_table('subscription_subscription') 61 | 62 | # Deleting unique_together for [user, subscription] on UserSubscription. 63 | db.delete_unique('subscription_usersubscription', ['user_id', 'subscription_id']) 64 | 65 | 66 | 67 | models = { 68 | 'auth.group': { 69 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 70 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 71 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}) 72 | }, 73 | 'auth.permission': { 74 | 'Meta': {'unique_together': "(('content_type', 'codename'),)"}, 75 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 76 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 77 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 78 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 79 | }, 80 | 'auth.user': { 81 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 82 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 83 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 84 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True'}), 85 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 86 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 87 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 88 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 89 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 90 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 91 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 92 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}), 93 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 94 | }, 95 | 'contenttypes.contenttype': { 96 | 'Meta': {'unique_together': "(('app_label', 'model'),)", 'db_table': "'django_content_type'"}, 97 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 98 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 99 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 100 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 101 | }, 102 | 'ipn.paypalipn': { 103 | 'Meta': {'db_table': "'paypal_ipn'"}, 104 | 'address_city': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), 105 | 'address_country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 106 | 'address_country_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 107 | 'address_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 108 | 'address_state': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), 109 | 'address_status': ('django.db.models.fields.CharField', [], {'max_length': '11', 'blank': 'True'}), 110 | 'address_street': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 111 | 'address_zip': ('django.db.models.fields.CharField', [], {'max_length': '20', 'blank': 'True'}), 112 | 'amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 113 | 'amount1': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 114 | 'amount2': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 115 | 'amount3': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 116 | 'amount_per_cycle': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 117 | 'auction_buyer_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 118 | 'auction_closing_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 119 | 'auction_multi_item': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), 120 | 'auth_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 121 | 'auth_exp': ('django.db.models.fields.CharField', [], {'max_length': '28', 'blank': 'True'}), 122 | 'auth_id': ('django.db.models.fields.CharField', [], {'max_length': '19', 'blank': 'True'}), 123 | 'auth_status': ('django.db.models.fields.CharField', [], {'max_length': '9', 'blank': 'True'}), 124 | 'business': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 125 | 'case_creation_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 126 | 'case_id': ('django.db.models.fields.CharField', [], {'max_length': '14', 'blank': 'True'}), 127 | 'case_type': ('django.db.models.fields.CharField', [], {'max_length': '24', 'blank': 'True'}), 128 | 'charset': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 129 | 'contact_phone': ('django.db.models.fields.CharField', [], {'max_length': '20', 'blank': 'True'}), 130 | 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 131 | 'currency_code': ('django.db.models.fields.CharField', [], {'default': "'USD'", 'max_length': '32', 'blank': 'True'}), 132 | 'custom': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 133 | 'exchange_rate': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '16', 'blank': 'True'}), 134 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 135 | 'flag': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 136 | 'flag_code': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), 137 | 'flag_info': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 138 | 'for_auction': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 139 | 'handling_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 140 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 141 | 'initial_payment_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 142 | 'invoice': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 143 | 'ipaddress': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'blank': 'True'}), 144 | 'item_name': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 145 | 'item_number': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 146 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 147 | 'mc_amount1': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 148 | 'mc_amount2': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 149 | 'mc_amount3': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 150 | 'mc_currency': ('django.db.models.fields.CharField', [], {'default': "'USD'", 'max_length': '32', 'blank': 'True'}), 151 | 'mc_fee': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 152 | 'mc_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 153 | 'mc_handling': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 154 | 'mc_shipping': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 155 | 'memo': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 156 | 'next_payment_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 157 | 'notify_version': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 158 | 'num_cart_items': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), 159 | 'option_name1': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 160 | 'option_name2': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 161 | 'outstanding_balance': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 162 | 'parent_txn_id': ('django.db.models.fields.CharField', [], {'max_length': '19', 'blank': 'True'}), 163 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '24', 'blank': 'True'}), 164 | 'payer_business_name': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 165 | 'payer_email': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 166 | 'payer_id': ('django.db.models.fields.CharField', [], {'max_length': '13', 'blank': 'True'}), 167 | 'payer_status': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'}), 168 | 'payment_cycle': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 169 | 'payment_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 170 | 'payment_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 171 | 'payment_status': ('django.db.models.fields.CharField', [], {'max_length': '9', 'blank': 'True'}), 172 | 'payment_type': ('django.db.models.fields.CharField', [], {'max_length': '7', 'blank': 'True'}), 173 | 'pending_reason': ('django.db.models.fields.CharField', [], {'max_length': '14', 'blank': 'True'}), 174 | 'period1': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 175 | 'period2': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 176 | 'period3': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 177 | 'period_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 178 | 'product_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 179 | 'product_type': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 180 | 'profile_status': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 181 | 'protection_eligibility': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 182 | 'quantity': ('django.db.models.fields.IntegerField', [], {'default': '1', 'null': 'True', 'blank': 'True'}), 183 | 'query': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 184 | 'reason_code': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 185 | 'reattempt': ('django.db.models.fields.CharField', [], {'max_length': '1', 'blank': 'True'}), 186 | 'receipt_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 187 | 'receiver_email': ('django.db.models.fields.EmailField', [], {'max_length': '127', 'blank': 'True'}), 188 | 'receiver_id': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 189 | 'recur_times': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), 190 | 'recurring': ('django.db.models.fields.CharField', [], {'max_length': '1', 'blank': 'True'}), 191 | 'recurring_payment_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 192 | 'remaining_settle': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 193 | 'residence_country': ('django.db.models.fields.CharField', [], {'max_length': '2', 'blank': 'True'}), 194 | 'response': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 195 | 'retry_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 196 | 'rp_invoice_id': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 197 | 'settle_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 198 | 'settle_currency': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 199 | 'shipping': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 200 | 'shipping_method': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 201 | 'subscr_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 202 | 'subscr_effective': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 203 | 'subscr_id': ('django.db.models.fields.CharField', [], {'max_length': '19', 'blank': 'True'}), 204 | 'tax': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 205 | 'test_ipn': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 206 | 'time_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 207 | 'transaction_entity': ('django.db.models.fields.CharField', [], {'max_length': '7', 'blank': 'True'}), 208 | 'transaction_subject': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 209 | 'txn_id': ('django.db.models.fields.CharField', [], {'max_length': '19', 'blank': 'True'}), 210 | 'txn_type': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 211 | 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 212 | 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 213 | 'verify_sign': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}) 214 | }, 215 | 'subscription.subscription': { 216 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 217 | 'group': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True'}), 218 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 219 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), 220 | 'price': ('django.db.models.fields.DecimalField', [], {'max_digits': '64', 'decimal_places': '2'}), 221 | 'recurrence_period': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 222 | 'recurrence_unit': ('django.db.models.fields.CharField', [], {'max_length': '1', 'null': 'True'}) 223 | }, 224 | 'subscription.transaction': { 225 | 'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 226 | 'comment': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), 227 | 'event': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 228 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 229 | 'ipn': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['ipn.PayPalIPN']", 'null': 'True', 'blank': 'True'}), 230 | 'subscription': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['subscription.Subscription']", 'null': 'True', 'blank': 'True'}), 231 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 232 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}) 233 | }, 234 | 'subscription.usersubscription': { 235 | 'Meta': {'unique_together': "(('user', 'subscription'),)"}, 236 | 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 237 | 'cancelled': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 238 | 'expires': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today', 'null': 'True'}), 239 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 240 | 'subscription': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['subscription.Subscription']"}), 241 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 242 | } 243 | } 244 | 245 | complete_apps = ['subscription'] 246 | -------------------------------------------------------------------------------- /subscription/migrations/0002_trial_period.py: -------------------------------------------------------------------------------- 1 | 2 | from south.db import db 3 | from django.db import models 4 | from subscription.models import * 5 | 6 | class Migration: 7 | 8 | def forwards(self, orm): 9 | 10 | # Adding field 'Subscription.trial_period' 11 | db.add_column('subscription_subscription', 'trial_period', orm['subscription.subscription:trial_period']) 12 | 13 | # Adding field 'Subscription.trial_unit' 14 | db.add_column('subscription_subscription', 'trial_unit', orm['subscription.subscription:trial_unit']) 15 | 16 | 17 | 18 | def backwards(self, orm): 19 | 20 | # Deleting field 'Subscription.trial_period' 21 | db.delete_column('subscription_subscription', 'trial_period') 22 | 23 | # Deleting field 'Subscription.trial_unit' 24 | db.delete_column('subscription_subscription', 'trial_unit') 25 | 26 | 27 | 28 | models = { 29 | 'auth.group': { 30 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 31 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 32 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}) 33 | }, 34 | 'auth.permission': { 35 | 'Meta': {'unique_together': "(('content_type', 'codename'),)"}, 36 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 37 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 38 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 39 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 40 | }, 41 | 'auth.user': { 42 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 43 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 44 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 45 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True'}), 46 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 47 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 48 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 49 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 50 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 51 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 52 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 53 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}), 54 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 55 | }, 56 | 'contenttypes.contenttype': { 57 | 'Meta': {'unique_together': "(('app_label', 'model'),)", 'db_table': "'django_content_type'"}, 58 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 59 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 60 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 61 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 62 | }, 63 | 'ipn.paypalipn': { 64 | 'Meta': {'db_table': "'paypal_ipn'"}, 65 | 'address_city': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), 66 | 'address_country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 67 | 'address_country_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 68 | 'address_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 69 | 'address_state': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), 70 | 'address_status': ('django.db.models.fields.CharField', [], {'max_length': '11', 'blank': 'True'}), 71 | 'address_street': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 72 | 'address_zip': ('django.db.models.fields.CharField', [], {'max_length': '20', 'blank': 'True'}), 73 | 'amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 74 | 'amount1': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 75 | 'amount2': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 76 | 'amount3': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 77 | 'amount_per_cycle': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 78 | 'auction_buyer_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 79 | 'auction_closing_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 80 | 'auction_multi_item': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), 81 | 'auth_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 82 | 'auth_exp': ('django.db.models.fields.CharField', [], {'max_length': '28', 'blank': 'True'}), 83 | 'auth_id': ('django.db.models.fields.CharField', [], {'max_length': '19', 'blank': 'True'}), 84 | 'auth_status': ('django.db.models.fields.CharField', [], {'max_length': '9', 'blank': 'True'}), 85 | 'business': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 86 | 'case_creation_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 87 | 'case_id': ('django.db.models.fields.CharField', [], {'max_length': '14', 'blank': 'True'}), 88 | 'case_type': ('django.db.models.fields.CharField', [], {'max_length': '24', 'blank': 'True'}), 89 | 'charset': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 90 | 'contact_phone': ('django.db.models.fields.CharField', [], {'max_length': '20', 'blank': 'True'}), 91 | 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 92 | 'currency_code': ('django.db.models.fields.CharField', [], {'default': "'USD'", 'max_length': '32', 'blank': 'True'}), 93 | 'custom': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 94 | 'exchange_rate': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '16', 'blank': 'True'}), 95 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 96 | 'flag': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 97 | 'flag_code': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), 98 | 'flag_info': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 99 | 'for_auction': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 100 | 'handling_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 101 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 102 | 'initial_payment_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 103 | 'invoice': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 104 | 'ipaddress': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'blank': 'True'}), 105 | 'item_name': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 106 | 'item_number': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 107 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 108 | 'mc_amount1': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 109 | 'mc_amount2': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 110 | 'mc_amount3': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 111 | 'mc_currency': ('django.db.models.fields.CharField', [], {'default': "'USD'", 'max_length': '32', 'blank': 'True'}), 112 | 'mc_fee': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 113 | 'mc_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 114 | 'mc_handling': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 115 | 'mc_shipping': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 116 | 'memo': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 117 | 'next_payment_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 118 | 'notify_version': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 119 | 'num_cart_items': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), 120 | 'option_name1': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 121 | 'option_name2': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 122 | 'outstanding_balance': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 123 | 'parent_txn_id': ('django.db.models.fields.CharField', [], {'max_length': '19', 'blank': 'True'}), 124 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '24', 'blank': 'True'}), 125 | 'payer_business_name': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 126 | 'payer_email': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 127 | 'payer_id': ('django.db.models.fields.CharField', [], {'max_length': '13', 'blank': 'True'}), 128 | 'payer_status': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'}), 129 | 'payment_cycle': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 130 | 'payment_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 131 | 'payment_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 132 | 'payment_status': ('django.db.models.fields.CharField', [], {'max_length': '9', 'blank': 'True'}), 133 | 'payment_type': ('django.db.models.fields.CharField', [], {'max_length': '7', 'blank': 'True'}), 134 | 'pending_reason': ('django.db.models.fields.CharField', [], {'max_length': '14', 'blank': 'True'}), 135 | 'period1': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 136 | 'period2': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 137 | 'period3': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 138 | 'period_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 139 | 'product_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 140 | 'product_type': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 141 | 'profile_status': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 142 | 'protection_eligibility': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 143 | 'quantity': ('django.db.models.fields.IntegerField', [], {'default': '1', 'null': 'True', 'blank': 'True'}), 144 | 'query': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 145 | 'reason_code': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 146 | 'reattempt': ('django.db.models.fields.CharField', [], {'max_length': '1', 'blank': 'True'}), 147 | 'receipt_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 148 | 'receiver_email': ('django.db.models.fields.EmailField', [], {'max_length': '127', 'blank': 'True'}), 149 | 'receiver_id': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 150 | 'recur_times': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), 151 | 'recurring': ('django.db.models.fields.CharField', [], {'max_length': '1', 'blank': 'True'}), 152 | 'recurring_payment_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 153 | 'remaining_settle': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 154 | 'residence_country': ('django.db.models.fields.CharField', [], {'max_length': '2', 'blank': 'True'}), 155 | 'response': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 156 | 'retry_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 157 | 'rp_invoice_id': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 158 | 'settle_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 159 | 'settle_currency': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 160 | 'shipping': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 161 | 'shipping_method': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 162 | 'subscr_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 163 | 'subscr_effective': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 164 | 'subscr_id': ('django.db.models.fields.CharField', [], {'max_length': '19', 'blank': 'True'}), 165 | 'tax': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 166 | 'test_ipn': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 167 | 'time_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 168 | 'transaction_entity': ('django.db.models.fields.CharField', [], {'max_length': '7', 'blank': 'True'}), 169 | 'transaction_subject': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 170 | 'txn_id': ('django.db.models.fields.CharField', [], {'max_length': '19', 'blank': 'True'}), 171 | 'txn_type': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 172 | 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 173 | 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 174 | 'verify_sign': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}) 175 | }, 176 | 'subscription.subscription': { 177 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 178 | 'group': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True'}), 179 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 180 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), 181 | 'price': ('django.db.models.fields.DecimalField', [], {'max_digits': '64', 'decimal_places': '2'}), 182 | 'recurrence_period': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 183 | 'recurrence_unit': ('django.db.models.fields.CharField', [], {'max_length': '1', 'null': 'True'}), 184 | 'trial_period': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 185 | 'trial_unit': ('django.db.models.fields.CharField', [], {'max_length': '1', 'null': 'True'}) 186 | }, 187 | 'subscription.transaction': { 188 | 'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 189 | 'comment': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), 190 | 'event': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 191 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 192 | 'ipn': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['ipn.PayPalIPN']", 'null': 'True', 'blank': 'True'}), 193 | 'subscription': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['subscription.Subscription']", 'null': 'True', 'blank': 'True'}), 194 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 195 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}) 196 | }, 197 | 'subscription.usersubscription': { 198 | 'Meta': {'unique_together': "(('user', 'subscription'),)"}, 199 | 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 200 | 'cancelled': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), 201 | 'expires': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today', 'null': 'True'}), 202 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 203 | 'subscription': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['subscription.Subscription']"}), 204 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 205 | } 206 | } 207 | 208 | complete_apps = ['subscription'] 209 | -------------------------------------------------------------------------------- /subscription/migrations/0003_auto__chg_field_subscription_group__del_unique_subscription_group.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Removing unique constraint on 'subscription', fields ['group'] 12 | db.delete_unique(u'subscription_subscription', ['group_id']) 13 | 14 | 15 | # Changing field 'Subscription.group' 16 | db.alter_column(u'subscription_subscription', 'group_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group'])) 17 | 18 | def backwards(self, orm): 19 | 20 | # Changing field 'Subscription.group' 21 | db.alter_column(u'subscription_subscription', 'group_id', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['auth.Group'], unique=True)) 22 | # Adding unique constraint on 'subscription', fields ['group'] 23 | db.create_unique(u'subscription_subscription', ['group_id']) 24 | 25 | 26 | models = { 27 | u'auth.group': { 28 | 'Meta': {'object_name': 'Group'}, 29 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 30 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 31 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 32 | }, 33 | u'auth.permission': { 34 | 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, 35 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 36 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), 37 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 38 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 39 | }, 40 | u'auth.user': { 41 | 'Meta': {'object_name': 'User'}, 42 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 43 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 44 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 45 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 46 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 47 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 48 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 49 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 50 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 51 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 52 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 53 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 54 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 55 | }, 56 | u'contenttypes.contenttype': { 57 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 58 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 59 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 60 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 61 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 62 | }, 63 | u'ipn.paypalipn': { 64 | 'Meta': {'object_name': 'PayPalIPN', 'db_table': "'paypal_ipn'"}, 65 | 'address_city': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), 66 | 'address_country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 67 | 'address_country_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 68 | 'address_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 69 | 'address_state': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), 70 | 'address_status': ('django.db.models.fields.CharField', [], {'max_length': '11', 'blank': 'True'}), 71 | 'address_street': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), 72 | 'address_zip': ('django.db.models.fields.CharField', [], {'max_length': '20', 'blank': 'True'}), 73 | 'amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 74 | 'amount1': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 75 | 'amount2': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 76 | 'amount3': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 77 | 'amount_per_cycle': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 78 | 'auction_buyer_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 79 | 'auction_closing_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 80 | 'auction_multi_item': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), 81 | 'auth_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 82 | 'auth_exp': ('django.db.models.fields.CharField', [], {'max_length': '28', 'blank': 'True'}), 83 | 'auth_id': ('django.db.models.fields.CharField', [], {'max_length': '19', 'blank': 'True'}), 84 | 'auth_status': ('django.db.models.fields.CharField', [], {'max_length': '9', 'blank': 'True'}), 85 | 'business': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 86 | 'case_creation_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 87 | 'case_id': ('django.db.models.fields.CharField', [], {'max_length': '14', 'blank': 'True'}), 88 | 'case_type': ('django.db.models.fields.CharField', [], {'max_length': '24', 'blank': 'True'}), 89 | 'charset': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 90 | 'contact_phone': ('django.db.models.fields.CharField', [], {'max_length': '20', 'blank': 'True'}), 91 | 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 92 | 'currency_code': ('django.db.models.fields.CharField', [], {'default': "'USD'", 'max_length': '32', 'blank': 'True'}), 93 | 'custom': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 94 | 'exchange_rate': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '16', 'blank': 'True'}), 95 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 96 | 'flag': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 97 | 'flag_code': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), 98 | 'flag_info': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 99 | 'for_auction': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 100 | 'from_view': ('django.db.models.fields.CharField', [], {'max_length': '6', 'null': 'True', 'blank': 'True'}), 101 | 'handling_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 102 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 103 | 'initial_payment_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 104 | 'invoice': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 105 | 'ipaddress': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'blank': 'True'}), 106 | 'item_name': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 107 | 'item_number': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 108 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 109 | 'mc_amount1': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 110 | 'mc_amount2': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 111 | 'mc_amount3': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 112 | 'mc_currency': ('django.db.models.fields.CharField', [], {'default': "'USD'", 'max_length': '32', 'blank': 'True'}), 113 | 'mc_fee': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 114 | 'mc_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 115 | 'mc_handling': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 116 | 'mc_shipping': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 117 | 'memo': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 118 | 'next_payment_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 119 | 'notify_version': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 120 | 'num_cart_items': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), 121 | 'option_name1': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 122 | 'option_name2': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 123 | 'outstanding_balance': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 124 | 'parent_txn_id': ('django.db.models.fields.CharField', [], {'max_length': '19', 'blank': 'True'}), 125 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '24', 'blank': 'True'}), 126 | 'payer_business_name': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 127 | 'payer_email': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 128 | 'payer_id': ('django.db.models.fields.CharField', [], {'max_length': '13', 'blank': 'True'}), 129 | 'payer_status': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'}), 130 | 'payment_cycle': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 131 | 'payment_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 132 | 'payment_gross': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 133 | 'payment_status': ('django.db.models.fields.CharField', [], {'max_length': '9', 'blank': 'True'}), 134 | 'payment_type': ('django.db.models.fields.CharField', [], {'max_length': '7', 'blank': 'True'}), 135 | 'pending_reason': ('django.db.models.fields.CharField', [], {'max_length': '14', 'blank': 'True'}), 136 | 'period1': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 137 | 'period2': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 138 | 'period3': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 139 | 'period_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 140 | 'product_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 141 | 'product_type': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 142 | 'profile_status': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 143 | 'protection_eligibility': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 144 | 'quantity': ('django.db.models.fields.IntegerField', [], {'default': '1', 'null': 'True', 'blank': 'True'}), 145 | 'query': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 146 | 'reason_code': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}), 147 | 'reattempt': ('django.db.models.fields.CharField', [], {'max_length': '1', 'blank': 'True'}), 148 | 'receipt_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 149 | 'receiver_email': ('django.db.models.fields.EmailField', [], {'max_length': '127', 'blank': 'True'}), 150 | 'receiver_id': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 151 | 'recur_times': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), 152 | 'recurring': ('django.db.models.fields.CharField', [], {'max_length': '1', 'blank': 'True'}), 153 | 'recurring_payment_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 154 | 'remaining_settle': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 155 | 'residence_country': ('django.db.models.fields.CharField', [], {'max_length': '2', 'blank': 'True'}), 156 | 'response': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 157 | 'retry_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 158 | 'rp_invoice_id': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}), 159 | 'settle_amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 160 | 'settle_currency': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), 161 | 'shipping': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 162 | 'shipping_method': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 163 | 'subscr_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 164 | 'subscr_effective': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 165 | 'subscr_id': ('django.db.models.fields.CharField', [], {'max_length': '19', 'blank': 'True'}), 166 | 'tax': ('django.db.models.fields.DecimalField', [], {'default': '0', 'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 167 | 'test_ipn': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 168 | 'time_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 169 | 'transaction_entity': ('django.db.models.fields.CharField', [], {'max_length': '7', 'blank': 'True'}), 170 | 'transaction_subject': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 171 | 'txn_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '19', 'blank': 'True'}), 172 | 'txn_type': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), 173 | 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 174 | 'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), 175 | 'verify_sign': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}) 176 | }, 177 | u'subscription.subscription': { 178 | 'Meta': {'ordering': "('price', '-recurrence_period')", 'object_name': 'Subscription'}, 179 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 180 | 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.Group']"}), 181 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 182 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), 183 | 'price': ('django.db.models.fields.DecimalField', [], {'max_digits': '64', 'decimal_places': '2'}), 184 | 'recurrence_period': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 185 | 'recurrence_unit': ('django.db.models.fields.CharField', [], {'max_length': '1', 'null': 'True'}), 186 | 'trial_period': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), 187 | 'trial_unit': ('django.db.models.fields.CharField', [], {'max_length': '1', 'null': 'True'}) 188 | }, 189 | u'subscription.transaction': { 190 | 'Meta': {'ordering': "('-timestamp',)", 'object_name': 'Transaction'}, 191 | 'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '64', 'decimal_places': '2', 'blank': 'True'}), 192 | 'comment': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), 193 | 'event': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 194 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 195 | 'ipn': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['ipn.PayPalIPN']", 'null': 'True', 'blank': 'True'}), 196 | 'subscription': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['subscription.Subscription']", 'null': 'True', 'blank': 'True'}), 197 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 198 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}) 199 | }, 200 | u'subscription.usersubscription': { 201 | 'Meta': {'unique_together': "(('user', 'subscription'),)", 'object_name': 'UserSubscription'}, 202 | 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 203 | 'cancelled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 204 | 'expires': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today', 'null': 'True'}), 205 | u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 206 | 'subscription': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['subscription.Subscription']"}), 207 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}) 208 | } 209 | } 210 | 211 | complete_apps = ['subscription'] -------------------------------------------------------------------------------- /subscription/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaque/django-subscription/be38c513dc6adb4e5080487ebe9e744704f3f4d8/subscription/migrations/__init__.py -------------------------------------------------------------------------------- /subscription/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.conf import settings 4 | from django.db import models 5 | from django.contrib import auth 6 | from django.utils.translation import ugettext as _, ungettext, ugettext_lazy 7 | 8 | from paypal.standard import ipn 9 | 10 | import signals 11 | import utils 12 | 13 | 14 | class Transaction(models.Model): 15 | timestamp = models.DateTimeField(auto_now_add=True, editable=False) 16 | subscription = models.ForeignKey('subscription.Subscription', 17 | null=True, blank=True, editable=False) 18 | user = models.ForeignKey(auth.models.User, 19 | null=True, blank=True, editable=False) 20 | ipn = models.ForeignKey(ipn.models.PayPalIPN, 21 | null=True, blank=True, editable=False) 22 | event = models.CharField(max_length=100, editable=False) 23 | amount = models.DecimalField(max_digits=64, decimal_places=2, 24 | null=True, blank=True, editable=False) 25 | comment = models.TextField(blank=True, default='') 26 | 27 | class Meta: 28 | ordering = ('-timestamp',) 29 | 30 | 31 | _recurrence_unit_days = { 32 | 'D': 1., 33 | 'W': 7., 34 | 'M': 30.4368, # http://en.wikipedia.org/wiki/Month#Julian_and_Gregorian_calendars 35 | 'Y': 365.2425, # http://en.wikipedia.org/wiki/Year#Calendar_year 36 | } 37 | 38 | _TIME_UNIT_CHOICES = ( 39 | ('0', ugettext_lazy('No trial')), 40 | ('D', ugettext_lazy('Day')), 41 | ('W', ugettext_lazy('Week')), 42 | ('M', ugettext_lazy('Month')), 43 | ('Y', ugettext_lazy('Year')), 44 | ) 45 | 46 | 47 | class Subscription(models.Model): 48 | name = models.CharField(max_length=100, unique=True, null=False) 49 | description = models.TextField(blank=True) 50 | price = models.DecimalField(max_digits=64, decimal_places=2) 51 | trial_period = models.PositiveIntegerField(null=True, blank=True) 52 | trial_unit = models.CharField(max_length=1, null=True, choices=_TIME_UNIT_CHOICES) 53 | recurrence_period = models.PositiveIntegerField(null=True, blank=True) 54 | recurrence_unit = models.CharField(max_length=1, null=True, 55 | choices=((None, ugettext_lazy("No recurrence")),) 56 | + _TIME_UNIT_CHOICES) 57 | group = models.ForeignKey(auth.models.Group, null=False, blank=False, unique=False) 58 | 59 | _PLURAL_UNITS = { 60 | '0': ugettext_lazy('No trial'), 61 | 'D': 'days', 62 | 'W': 'weeks', 63 | 'M': 'months', 64 | 'Y': 'years', 65 | } 66 | 67 | class Meta: 68 | ordering = ('price', '-recurrence_period') 69 | 70 | def __unicode__(self): 71 | return self.name 72 | 73 | def price_per_day(self): 74 | """Return estimate subscription price per day, as a float. 75 | 76 | This is used to charge difference when user changes 77 | subscription. Price returned is an estimate; month length 78 | used is 30.4368 days, year length is 365.2425 days (averages 79 | including leap years). One-time payments return 0. 80 | """ 81 | if self.recurrence_unit is None: 82 | return 0 83 | return float(self.price) / ( 84 | self.recurrence_period * _recurrence_unit_days[self.recurrence_unit] 85 | ) 86 | 87 | @models.permalink 88 | def get_absolute_url(self): 89 | return ('subscription_detail', (), dict(object_id=str(self.id))) 90 | 91 | def get_pricing_display(self): 92 | if not self.price: 93 | return u'Free' 94 | elif self.recurrence_period: 95 | return ungettext('%(price).02f / %(unit)s', 96 | '%(price).02f / %(period)d %(unit_plural)s', 97 | self.recurrence_period) % { 98 | 'price': self.price, 99 | 'unit': self.get_recurrence_unit_display(), 100 | 'unit_plural': _(self._PLURAL_UNITS[self.recurrence_unit],), 101 | 'period': self.recurrence_period, 102 | } 103 | else: 104 | return _('%(price).02f one-time fee') % {'price': self.price} 105 | 106 | def get_trial_display(self): 107 | if self.trial_period: 108 | return ungettext('One %(unit)s', 109 | '%(period)d %(unit_plural)s', 110 | self.trial_period) % { 111 | 'unit': self.get_trial_unit_display().lower(), 112 | 'unit_plural': _(self._PLURAL_UNITS[self.trial_unit],), 113 | 'period': self.trial_period, 114 | } 115 | else: 116 | return _("No trial") 117 | 118 | def save(self, *args, **kwargs): 119 | """ 120 | Set trial period to 0 if the trial unit is 0 121 | """ 122 | if self.trial_unit == "0": 123 | self.trial_period = 0 124 | 125 | super(Subscription, self).save(*args, **kwargs) 126 | 127 | 128 | # add User.get_subscription() method 129 | def __user_get_subscription(user): 130 | if not hasattr(user, '_subscription_cache'): 131 | sl = Subscription.objects.filter(group__in=user.groups.all())[:1] 132 | if sl: 133 | user._subscription_cache = sl[0] 134 | else: 135 | user._subscription_cache = None 136 | return user._subscription_cache 137 | auth.models.User.add_to_class('get_subscription', __user_get_subscription) 138 | 139 | 140 | class ActiveUSManager(models.Manager): 141 | """Custom Manager for UserSubscription that returns only live US objects.""" 142 | def get_query_set(self): 143 | return super(ActiveUSManager, self).get_query_set().filter(active=True) 144 | 145 | 146 | class UserSubscription(models.Model): 147 | user = models.ForeignKey(auth.models.User) 148 | subscription = models.ForeignKey(Subscription) 149 | expires = models.DateField(null=True, default=datetime.date.today) 150 | active = models.BooleanField(default=True) 151 | cancelled = models.BooleanField(default=True) 152 | 153 | objects = models.Manager() 154 | active_objects = ActiveUSManager() 155 | 156 | grace_timedelta = datetime.timedelta( 157 | getattr(settings, 'SUBSCRIPTION_GRACE_PERIOD', 2)) 158 | 159 | class Meta: 160 | unique_together = (('user', 'subscription'), ) 161 | 162 | def user_is_group_member(self): 163 | "Returns True is user is member of subscription's group" 164 | return self.subscription.group in self.user.groups.all() 165 | user_is_group_member.boolean = True 166 | 167 | def expired(self): 168 | """Returns true if there is more than SUBSCRIPTION_GRACE_PERIOD 169 | days after expiration date.""" 170 | return self.expires is not None and ( 171 | self.expires + self.grace_timedelta < datetime.date.today()) 172 | expired.boolean = True 173 | 174 | def valid(self): 175 | """Validate group membership. 176 | 177 | Returns True if not expired and user is in group, or expired 178 | and user is not in group.""" 179 | if self.expired() or not self.active: 180 | return not self.user_is_group_member() 181 | else: 182 | return self.user_is_group_member() 183 | valid.boolean = True 184 | 185 | def unsubscribe(self): 186 | """Unsubscribe user.""" 187 | self.user.groups.remove(self.subscription.group) 188 | self.user.save() 189 | 190 | def subscribe(self): 191 | """Subscribe user.""" 192 | self.user.groups.add(self.subscription.group) 193 | self.user.save() 194 | 195 | def fix(self): 196 | """Fix group membership if not valid().""" 197 | if not self.valid(): 198 | if self.expired() or not self.active: 199 | self.unsubscribe() 200 | Transaction(user=self.user, subscription=self.subscription, ipn=None, 201 | event='subscription expired' 202 | ).save() 203 | if self.cancelled: 204 | self.delete() 205 | Transaction(user=self.user, subscription=self.subscription, ipn=None, 206 | event='remove subscription (expired)' 207 | ).save() 208 | else: 209 | self.subscribe() 210 | 211 | def extend(self, timedelta=None): 212 | """Extend subscription by `timedelta' or by subscription's 213 | recurrence period.""" 214 | if timedelta is not None: 215 | self.expires += timedelta 216 | else: 217 | if self.subscription.recurrence_unit: 218 | self.expires = utils.extend_date_by( 219 | self.expires, 220 | self.subscription.recurrence_period, 221 | self.subscription.recurrence_unit) 222 | else: 223 | self.expires = None 224 | 225 | def try_change(self, subscription): 226 | """Check whether upgrading/downgrading to `subscription' is possible. 227 | 228 | If subscription change is possible, returns false value; if 229 | change is impossible, returns a list of reasons to display. 230 | 231 | Checks are performed by sending 232 | subscription.signals.change_check with sender being 233 | UserSubscription object, and additional parameter 234 | `subscription' being new Subscription instance. Signal 235 | listeners should return None if change is possible, or a 236 | reason to display. 237 | """ 238 | if self.subscription == subscription: 239 | if self.active and self.cancelled: 240 | return None # allow resubscribing 241 | return [_(u'This is your current subscription.')] 242 | return [ 243 | res[1] 244 | for res in signals.change_check.send( 245 | self, subscription=subscription) 246 | if res[1]] 247 | 248 | @models.permalink 249 | def get_absolute_url(self): 250 | return ('subscription_usersubscription_detail', (), dict(object_id=str(self.id))) 251 | 252 | def __unicode__(self): 253 | rv = u"%s's %s" % (self.user, self.subscription) 254 | if self.expired(): 255 | rv += u' (expired)' 256 | return rv 257 | 258 | 259 | def unsubscribe_expired(): 260 | """Unsubscribes all users whose subscription has expired. 261 | Loops through all UserSubscription objects with `expires' field 262 | earlier than datetime.date.today() and forces correct group 263 | membership.""" 264 | for us in UserSubscription.objects.get(expires__lt=datetime.date.today()): 265 | us.fix() 266 | 267 | 268 | #### Handle PayPal signals 269 | def _ipn_usersubscription(payment): 270 | class PseudoUS(object): 271 | pk = None 272 | 273 | def __nonzero__(self): 274 | return False 275 | 276 | def __init__(self, user, subscription): 277 | self.user = user 278 | self.subscription = subscription 279 | 280 | try: 281 | s = Subscription.objects.get(id=payment.item_number) 282 | except Subscription.DoesNotExist: 283 | s = None 284 | 285 | try: 286 | u = auth.models.User.objects.get(id=payment.custom) 287 | except auth.models.User.DoesNotExist: 288 | u = None 289 | 290 | if u and s: 291 | try: 292 | us = UserSubscription.objects.get(subscription=s, user=u) 293 | except UserSubscription.DoesNotExist: 294 | us = UserSubscription(user=u, subscription=s, active=False) 295 | Transaction(user=u, subscription=s, ipn=payment, 296 | event='new usersubscription', amount=payment.mc_gross 297 | ).save() 298 | else: 299 | us = PseudoUS(user=u, subscription=s) 300 | 301 | return us 302 | 303 | 304 | def handle_payment_was_successful(sender, **kwargs): 305 | us = _ipn_usersubscription(sender) 306 | u, s = us.user, us.subscription 307 | if us: 308 | if not s.recurrence_unit: 309 | if sender.mc_gross == s.price: 310 | us.subscribe() 311 | us.expires = None 312 | us.active = True 313 | us.save() 314 | Transaction(user=u, subscription=s, ipn=sender, 315 | event='one-time payment', amount=sender.mc_gross 316 | ).save() 317 | signals.signed_up.send(s, ipn=sender, subscription=s, user=u, 318 | usersubscription=us) 319 | else: 320 | Transaction(user=u, subscription=s, ipn=sender, 321 | event='incorrect payment', amount=sender.mc_gross 322 | ).save() 323 | signals.event.send(s, ipn=sender, subscription=s, user=u, 324 | usersubscription=us, event='incorrect payment') 325 | else: 326 | if sender.mc_gross == s.price: 327 | us.extend() 328 | us.save() 329 | Transaction(user=u, subscription=s, ipn=sender, 330 | event='subscription payment', amount=sender.mc_gross 331 | ).save() 332 | signals.paid.send(s, ipn=sender, subscription=s, user=u, 333 | usersubscription=us) 334 | else: 335 | Transaction(user=u, subscription=s, ipn=sender, 336 | event='incorrect payment', amount=sender.mc_gross 337 | ).save() 338 | signals.event.send(s, ipn=sender, subscription=s, user=u, 339 | usersubscription=us, event='incorrect payment') 340 | else: 341 | Transaction(user=u, subscription=s, ipn=sender, 342 | event='unexpected payment', amount=sender.mc_gross 343 | ).save() 344 | signals.event.send(s, ipn=sender, subscription=s, user=u, event='unexpected_payment') 345 | ipn.signals.payment_was_successful.connect(handle_payment_was_successful) 346 | 347 | 348 | def handle_payment_was_flagged(sender, **kwargs): 349 | us = _ipn_usersubscription(sender) 350 | u, s = us.user, us.subscription 351 | Transaction(user=u, subscription=s, ipn=sender, 352 | event='payment flagged', amount=sender.mc_gross 353 | ).save() 354 | signals.event.send(s, ipn=sender, subscription=s, user=u, event='flagged') 355 | ipn.signals.payment_was_flagged.connect(handle_payment_was_flagged) 356 | 357 | 358 | def handle_subscription_signup(sender, **kwargs): 359 | us = _ipn_usersubscription(sender) 360 | u, s = us.user, us.subscription 361 | if us: 362 | # deactivate or delete all user's other subscriptions 363 | for old_us in u.usersubscription_set.all(): 364 | if old_us == us: 365 | continue # don't touch current subscription 366 | if old_us.cancelled: 367 | old_us.delete() 368 | Transaction(user=u, subscription=s, ipn=sender, 369 | event='remove subscription (deactivated)', amount=sender.mc_gross 370 | ).save() 371 | else: 372 | old_us.active = False 373 | old_us.unsubscribe() 374 | old_us.save() 375 | Transaction(user=u, subscription=s, ipn=sender, 376 | event='deactivated', amount=sender.mc_gross 377 | ).save() 378 | 379 | # activate new subscription 380 | us.subscribe() 381 | us.active = True 382 | us.cancelled = False 383 | us.save() 384 | Transaction(user=u, subscription=s, ipn=sender, 385 | event='activated', amount=sender.mc_gross 386 | ).save() 387 | 388 | signals.subscribed.send(s, ipn=sender, subscription=s, user=u, 389 | usersubscription=us) 390 | else: 391 | Transaction(user=u, subscription=s, ipn=sender, 392 | event='unexpected subscription', amount=sender.mc_gross 393 | ).save() 394 | signals.event.send(s, ipn=sender, subscription=s, user=u, 395 | event='unexpected_subscription') 396 | ipn.signals.subscription_signup.connect(handle_subscription_signup) 397 | 398 | 399 | def handle_subscription_cancel(sender, **kwargs): 400 | us = _ipn_usersubscription(sender) 401 | u, s = us.user, us.subscription 402 | if us.pk is not None: 403 | if not us.active: 404 | us.unsubscribe() 405 | us.delete() 406 | Transaction(user=u, subscription=s, ipn=sender, 407 | event='remove subscription (cancelled)', amount=sender.mc_gross 408 | ).save() 409 | else: 410 | us.cancelled = True 411 | us.save() 412 | Transaction(user=u, subscription=s, ipn=sender, 413 | event='cancel subscription', amount=sender.mc_gross 414 | ).save() 415 | signals.unsubscribed.send(s, ipn=sender, subscription=s, user=u, 416 | usersubscription=us, 417 | # refund=refund, reason='cancel') 418 | reason='cancel') 419 | else: 420 | Transaction(user=u, subscription=s, ipn=sender, 421 | event='unexpected cancel', amount=sender.mc_gross 422 | ).save() 423 | signals.event.send(s, ipn=sender, subscription=s, user=u, event='unexpected_cancel') 424 | ipn.signals.subscription_cancel.connect(handle_subscription_cancel) 425 | ipn.signals.subscription_eot.connect(handle_subscription_cancel) 426 | 427 | 428 | def handle_subscription_modify(sender, **kwargs): 429 | us = _ipn_usersubscription(sender) 430 | u, s = us.user, us.subscription 431 | print 'modify', u, s 432 | if us: 433 | # delete all user's other subscriptions 434 | for old_us in u.usersubscription_set.all(): 435 | if old_us == us: 436 | continue # don't touch current subscription 437 | old_us.delete() 438 | Transaction(user=u, subscription=s, ipn=sender, 439 | event='remove subscription (deactivated)', amount=sender.mc_gross 440 | ).save() 441 | 442 | # activate new subscription 443 | us.subscribe() 444 | us.active = True 445 | us.cancelled = False 446 | us.save() 447 | Transaction(user=u, subscription=s, ipn=sender, 448 | event='activated', amount=sender.mc_gross 449 | ).save() 450 | 451 | signals.subscribed.send(s, ipn=sender, subscription=s, user=u, 452 | usersubscription=us) 453 | else: 454 | Transaction(user=u, subscription=u, ipn=sender, 455 | event='unexpected subscription modify', amount=sender.mc_gross 456 | ).save() 457 | signals.event.send(s, ipn=sender, subscription=s, user=u, 458 | event='unexpected_subscription_modify') 459 | ipn.signals.subscription_modify.connect(handle_subscription_modify) 460 | -------------------------------------------------------------------------------- /subscription/providers.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from django.conf import settings 3 | 4 | """ 5 | PROPOSALS (only for payment methods which happens behind the scene) 6 | 7 | 1. New payment methods must be implemented as class (BasePaymentMethod) 8 | 2. According to user selected payment method, build object (Factory DP) 9 | 3. Call proceed() function. 10 | 11 | TODO 12 | 1. Create mappings => to make views more clear, i.e.: 13 | 14 | in urls.py 15 | (r'^(?P\d+)/(?P(standard|pro|authorize))$', 'subscription_detail', {}, 'subscription_detail'), 16 | 17 | in settings.py: 18 | PAYMENT_METHODS_MAPPINGS = { 19 | 'pro': 'WebsitePaymentsPro', #subscription.providers.WebsitePaymentsPro 20 | 'authorize': 'Authorize', #subscription.providers.Authorize 21 | etc.. 22 | } 23 | 24 | in.views.py: 25 | 26 | def subscription_details(request, object_id, payment_method="pro"): 27 | from subscription.providers import PaymentMethodFactory, pick_class 28 | payment_object = PaymentMethodFactory(pick_class(payment_method), ...) 29 | payment_object.proceed(...) 30 | 31 | """ 32 | 33 | def pick_class(payment_method, default_method): 34 | """ 35 | return settings.PAYMENT_METHODS_MAPPINGS.get(payment_method, default_method) 36 | """ 37 | pass 38 | 39 | 40 | class BasePaymentMethod(object): 41 | """This class represents the abstract base class for new payment methods""" 42 | def __init__(self): 43 | self.name = None 44 | 45 | def proceed(self): 46 | """Runs payment process""" 47 | pass 48 | 49 | def get_name(self): 50 | """Returns full name of payment method""" 51 | return self.name 52 | 53 | 54 | class PaymentMethodFactory(object): 55 | """Implementation of Factory Design Pattern""" 56 | @staticmethod 57 | def factory(payment_method, **kwargs): 58 | """ Factory method""" 59 | cls = getattr(sys.modules[__name__], payment_method) 60 | return cls(**kwargs) 61 | 62 | 63 | class WebsitePaymentsPro(BasePaymentMethod): 64 | """Wrapper around django-paypal's PayPalPro""" 65 | def __init__(self, **kwargs): 66 | self.name = 'Website Payments Pro' 67 | self.data = kwargs.get('data') 68 | self.request = kwargs.get('request') 69 | 70 | def proceed(self): 71 | from paypal.pro.views import PayPalPro 72 | ppp = PayPalPro(**self.data) 73 | return ppp(self.request) -------------------------------------------------------------------------------- /subscription/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | ## Our signals 4 | 5 | # one time subscriptions 6 | signed_up = Signal() 7 | 8 | # recurring subscriptions 9 | subscribed = Signal() 10 | unsubscribed = Signal() 11 | paid = Signal() 12 | 13 | # misc. subscription-related events 14 | event = Signal() 15 | 16 | # upgrade/downgrade possibility check 17 | change_check = Signal() 18 | -------------------------------------------------------------------------------- /subscription/tests.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | import calendar 3 | 4 | from django.test import TestCase 5 | 6 | import subscription.utils 7 | 8 | A_LEAP_YEAR = 2012 9 | NOT_A_LEAP_YEAR = 2011 10 | 11 | YEARS = (A_LEAP_YEAR, NOT_A_LEAP_YEAR) 12 | MONTHS = xrange(1, 13) 13 | 14 | class SubscriptionUtil(TestCase): 15 | 16 | def test_month(self): 17 | for year in YEARS: 18 | for month in MONTHS: 19 | for day in xrange(1, calendar.monthrange(year, month)[0]+1): 20 | start = date(year, month, day) 21 | try: 22 | added = subscription.utils.extend_date_by(start, 1, 'M') 23 | except ValueError: 24 | raise ValueError("Cannot extend %s by %s months" % (start, 1)) 25 | 26 | if month == 12: 27 | self.assertEqual(added.month, 1) 28 | else: 29 | self.assertEqual(added.month, start.month + 1) 30 | -------------------------------------------------------------------------------- /subscription/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | import django 4 | 5 | if django.VERSION < (1, 5, 0): 6 | import views 7 | urlpatterns = patterns('', 8 | (r'^$', 'subscription.views.subscription_list', {}, 'subscription_list'), 9 | (r'^done/', 'django.views.generic.simple.direct_to_template', 10 | dict(template='subscription/subscription_done.html'), 'subscription_done'), 11 | (r'^change-done/', 'django.views.generic.simple.direct_to_template', 12 | dict(template='subscription/subscription_change_done.html', 13 | extra_context=dict(cancel_url=views.cancel_url)), 'subscription_change_done'), 14 | (r'^cancel/', 'django.views.generic.simple.direct_to_template', 15 | dict(template='subscription/subscription_cancel.html'), 'subscription_cancel'), 16 | ) 17 | else: 18 | from django.views.generic import TemplateView 19 | urlpatterns = patterns('subscription.views', 20 | url(r'^$', TemplateView.as_view(template_name='subscription/subscription_list.html'), name='subscription_list'), 21 | url(r'^done/', TemplateView.as_view(template_name='subscription/subscription_done.html'), name='subscription_done'), 22 | url(r'^change-done/', TemplateView.as_view(template_name='subscription/subscription_change_done.html'), name='subscription_change_done'), 23 | url(r'^cancel/', TemplateView.as_view(template_name='subscription/subscription_cancel.html'), name='subscription_cancel'), 24 | ) 25 | 26 | urlpatterns += patterns('subscription.views', 27 | (r'^(?P\d+)/$', 'subscription_detail', {}, 'subscription_detail'), 28 | (r'^(?P\d+)/(?P(standard|pro))/$', 'subscription_detail', {}, 'subscription_detail'), 29 | ) 30 | 31 | urlpatterns += patterns('', 32 | (r'^paypal/', include('paypal.standard.ipn.urls')), 33 | ) 34 | -------------------------------------------------------------------------------- /subscription/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import calendar 3 | 4 | def extend_date_by(date, amount, unit): 5 | """Extend date `date' by `amount' of time units `unit'. 6 | 7 | `unit' can by 'D' for days, 'W' for weeks, 'M' for months or 'Y' 8 | for years. 9 | 10 | >>> extend_date_by(datetime.date(2007,04,03),5,'Y') 11 | datetime.date(2012, 4, 3) 12 | 13 | >>> extend_date_by(datetime.date(2007,04,03),5,'M') 14 | datetime.date(2007, 9, 3) 15 | >>> extend_date_by(datetime.date(2007,7,3),5,'M') 16 | datetime.date(2007, 12, 3) 17 | >>> extend_date_by(datetime.date(2007,8,3),5,'M') 18 | datetime.date(2008, 1, 3) 19 | >>> subscription.utils.extend_date_by(datetime.date(2007,10,3),5,'M') 20 | datetime.date(2008, 3, 3) 21 | 22 | >>> subscription.utils.extend_date_by(datetime.date(2007,10,3),1,'W') 23 | datetime.date(2007, 10, 10) 24 | >>> subscription.utils.extend_date_by(datetime.date(2007,10,3),2,'W') 25 | datetime.date(2007, 10, 17) 26 | >>> subscription.utils.extend_date_by(datetime.date(2007,10,3),5,'W') 27 | datetime.date(2007, 11, 7) 28 | >>> subscription.utils.extend_date_by(datetime.date(2007,12,3),5,'W') 29 | datetime.date(2008, 1, 7) 30 | 31 | >>> subscription.utils.extend_date_by(datetime.date(2007,10,3),29,'D') 32 | datetime.date(2007, 11, 1) 33 | >>> subscription.utils.extend_date_by(datetime.date(2007,10,7),29,'D') 34 | datetime.date(2007, 11, 5) 35 | >>> subscription.utils.extend_date_by(datetime.date(2007,10,7),99,'D') 36 | datetime.date(2008, 1, 14) 37 | >>> subscription.utils.extend_date_by(datetime.date(2007,12,3),5,'D') 38 | datetime.date(2007, 12, 8) 39 | >>> subscription.utils.extend_date_by(datetime.date(2007,12,30),5,'D') 40 | datetime.date(2008, 1, 4) 41 | 42 | >>> subscription.utils.extend_date_by(datetime.date(2007,10,7),99,'Q') 43 | Traceback (most recent call last): 44 | ... 45 | Unknown unit. 46 | """ 47 | if unit == 'D': 48 | return date + datetime.timedelta(1)*amount 49 | elif unit == 'W': 50 | return date + datetime.timedelta(7)*amount 51 | elif unit == 'M': 52 | y, m, d = date.year, date.month, date.day 53 | m += amount 54 | y += m / 12 55 | m %= 12 56 | if not m: m, y = 12, y-1 57 | r = calendar.monthrange(y, m)[1] 58 | if d > r: 59 | d = r 60 | return datetime.date(y, m, d) 61 | elif unit == 'Y': 62 | y, m, d = date.year, date.month, date.day 63 | return datetime.date(y+amount, m, d) 64 | else: raise "Unknown unit." 65 | -------------------------------------------------------------------------------- /subscription/views.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | from django.conf import settings 4 | from django.contrib.auth.decorators import login_required 5 | from django.contrib.sites.models import Site 6 | from django.core.urlresolvers import reverse 7 | from django.shortcuts import render, get_object_or_404, redirect 8 | from django.http import Http404 9 | from django.dispatch import Signal 10 | 11 | import django 12 | if django.VERSION <= (1, 5, 0): 13 | from django.views.generic.simple import direct_to_template 14 | 15 | _formclass = getattr(settings, 'SUBSCRIPTION_PAYPAL_FORM', 'paypal.standard.forms.PayPalPaymentsForm') 16 | _formclass_dot = _formclass.rindex('.') 17 | _formclass_module = __import__(_formclass[:_formclass_dot], {}, {}, ['']) 18 | PayPalForm = getattr(_formclass_module, _formclass[_formclass_dot + 1:]) 19 | 20 | from models import Subscription, UserSubscription 21 | 22 | get_paypal_extra_args = Signal(providing_args=['user', 'subscription', 'extra_args']) 23 | 24 | # http://paypaldeveloper.com/pdn/board/message?board.id=basicpayments&message.id=621 25 | if settings.PAYPAL_TEST: 26 | cancel_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_subscr-find&alias=%s' % urllib.quote(settings.PAYPAL_RECEIVER_EMAIL) 27 | else: 28 | cancel_url = 'https://www.paypal.com/cgi-bin/webscr?cmd=_subscr-find&alias=%s' % urllib.quote(settings.PAYPAL_RECEIVER_EMAIL) 29 | 30 | # Reference document for paypal html variables 31 | # https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_html_Appx_websitestandard_htmlvariables 32 | 33 | 34 | def _paypal_form_args(upgrade_subscription=False, **kwargs): 35 | "Return PayPal form arguments derived from kwargs." 36 | def _url(rel): 37 | if not rel.startswith('/'): 38 | rel = '/' + rel 39 | return 'http://%s%s' % (Site.objects.get_current().domain, rel) 40 | 41 | if upgrade_subscription: 42 | returl = reverse('subscription_change_done') 43 | else: 44 | returl = reverse('subscription_done') 45 | 46 | rv = settings.SUBSCRIPTION_PAYPAL_SETTINGS.copy() 47 | rv.update(notify_url=_url(reverse('paypal-ipn')), 48 | return_url=_url(returl), 49 | cancel_return=_url(reverse("subscription_cancel")), 50 | **kwargs) 51 | return rv 52 | 53 | 54 | def _paypal_form(subscription, user, upgrade_subscription=False, **extra_args): 55 | if not user.is_authenticated: 56 | return None 57 | 58 | if subscription.price <= 0: 59 | # Handles the scenario when subscription price is set to 0 or negative 60 | # value. This means it is a "free plan" and should be handled 61 | # appropriately by user of this library 62 | return None 63 | elif subscription.recurrence_unit: 64 | if subscription.trial_unit == '0': 65 | trial = {} 66 | else: 67 | trial = { 68 | 'a1': 0, 69 | 'p1': subscription.trial_period, 70 | 't1': subscription.trial_unit, 71 | } 72 | kwargs = {} 73 | kwargs.update(trial) 74 | kwargs.update(extra_args) 75 | return PayPalForm( 76 | initial=_paypal_form_args( 77 | cmd='_xclick-subscriptions', 78 | item_name='%s: %s' % (Site.objects.get_current().name, subscription.name), 79 | item_number=subscription.id, 80 | custom=user.id, 81 | a3=subscription.price, 82 | p3=subscription.recurrence_period, 83 | t3=subscription.recurrence_unit, 84 | src=1, # make payments recur 85 | sra=1, # reattempt payment on payment error 86 | upgrade_subscription=upgrade_subscription, 87 | # subscription modification (upgrade/downgrade) 88 | modify=upgrade_subscription and 2 or 0, **kwargs), 89 | button_type='subscribe' 90 | ) 91 | else: 92 | return PayPalForm( 93 | initial=_paypal_form_args( 94 | item_name='%s: %s' % (Site.objects.get_current().name, subscription.name), 95 | item_number=subscription.id, 96 | custom=user.id, 97 | amount=subscription.price)) 98 | 99 | 100 | def subscription_list(request): 101 | return direct_to_template( 102 | request, template='subscription/subscription_list.html', 103 | extra_context=dict(object_list=Subscription.objects.all())) 104 | 105 | 106 | def subscription_detail(request, object_id, payment_method="standard"): 107 | 108 | FREE_SUBSCRIPTION_URL_NAME = getattr(settings, 'FREE_SUBSCRIPTION_URL_NAME', None) 109 | if FREE_SUBSCRIPTION_URL_NAME: 110 | return redirect(reverse(FREE_SUBSCRIPTION_URL_NAME)) 111 | 112 | s = get_object_or_404(Subscription, id=object_id) 113 | 114 | try: 115 | user = request.user.usersubscription_set.get( 116 | active=True) 117 | except UserSubscription.DoesNotExist: 118 | change_denied_reasons = None 119 | user = None 120 | else: 121 | change_denied_reasons = user.try_change(s) 122 | 123 | if change_denied_reasons: 124 | form = None 125 | else: 126 | get_paypal_extra_args.send(sender=None, user=user, subscription=s, extra_args={}) 127 | form = _paypal_form(s, request.user, upgrade_subscription=(user is not None) and (user.subscription != s)) 128 | 129 | try: 130 | s_us = request.user.usersubscription_set.get(subscription=s) 131 | except UserSubscription.DoesNotExist: 132 | s_us = None 133 | 134 | from subscription.providers import PaymentMethodFactory 135 | # See PROPOSALS section in providers.py 136 | if payment_method == "pro": 137 | domain = Site.objects.get_current().domain 138 | item = {"amt": s.price, 139 | "inv": "inventory", # unique tracking variable paypal 140 | "custom": "tracking", # custom tracking variable for you 141 | "cancelurl": 'http://%s%s' % (domain, reverse('subscription_cancel')), # Express checkout cancel url 142 | "returnurl": 'http://%s%s' % (domain, reverse('subscription_done'))} # Express checkout return url 143 | 144 | data = {"item": item, 145 | "payment_template": "payment.html", # template name for payment 146 | "confirm_template": "confirmation.html", # template name for confirmation 147 | "success_url": reverse('subscription_done')} # redirect location after success 148 | 149 | o = PaymentMethodFactory.factory('WebsitePaymentsPro', data=data, request=request) 150 | # We return o.proceed() just because django-paypal's PayPalPro returns HttpResponse object 151 | return o.proceed() 152 | 153 | elif payment_method == 'standard': 154 | template_vars = {'object': s, 155 | 'usersubscription': s_us, 156 | 'change_denied_reasons': change_denied_reasons, 157 | 'form': form, 158 | 'cancel_url': cancel_url} 159 | template = 'subscription/subscription_detail.html' 160 | return render(request, template, template_vars) 161 | else: 162 | #should never get here 163 | raise Http404 164 | --------------------------------------------------------------------------------