-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10 from pstrinkle/staging
Staged
- Loading branch information
Showing
35 changed files
with
2,958 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,6 @@ | ||
.idea/ | ||
db.sqlite3 | ||
|
||
# Byte-compiled / optimized / DLL files | ||
__pycache__/ | ||
*.py[cod] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
include LICENSE | ||
include README.rst |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
|
||
|
||
https://docs.djangoproject.com/en/1.10/intro/reusable-apps/ | ||
|
||
http://www.revsys.com/blog/2014/nov/21/recommended-django-project-layout/ | ||
|
||
http://python-packaging.readthedocs.io/en/latest/dependencies.html | ||
|
||
http://www.mkdocs.org/ | ||
|
||
before trying to package for pypi: | ||
|
||
1. `pip install twine` | ||
2. `pip install wheel` | ||
3. `python setup.py sdist` | ||
4. `python setup.py bdist_wheel` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
# drf-coupons | ||
A django-rest-framework application that provides many varieties of coupons | ||
|
||
## Dependencies | ||
|
||
This project depends on: | ||
1. `djangorestframework` | ||
2. `django-filter` | ||
|
||
## Setup instructions | ||
|
||
1. Install `drf-coupons` via pip: | ||
``` | ||
$ pip install drf-coupons | ||
``` | ||
|
||
2. Add `'rest_framework'` to `INSTALLED_APPS` in `settings.py`. | ||
|
||
3. Add `'coupons'` to `INSTALLED_APPS` in `settings.py`. | ||
|
||
4. Migrate database: | ||
|
||
``` | ||
$ python manage.py migrate | ||
``` | ||
|
||
**Note:** this package was not developed to be compatible side-by-side with `django-coupons`, as they serve very similar needs. | ||
|
||
## Usage | ||
|
||
1. Specify permissions for interacting with coupon endpoints. | ||
|
||
You can specify a list of groups that can perform specific actions against the coupons, such as restricting who can | ||
create or list coupons. | ||
|
||
By default all endpoints are open except list. | ||
|
||
`retrieve` does not allow restriction because it doesn't generally need to support such permissions. | ||
|
||
`patch` is not supported as an endpoint and is therefore also not in the `COUPON_PERMISSIONS`. | ||
|
||
``` | ||
COUPON_PERMISSIONS = { | ||
'CREATE': ['groupa', 'groupb'], | ||
'LIST': ['groupa'], | ||
'DELETE': ['groupb'], | ||
'UPDATE': ['groupb'], | ||
'REDEEMED': ['groupc'], | ||
} | ||
``` | ||
|
||
You don't need to specify every endpoint in the list and can provide an empty list for an endpoint. | ||
|
||
The groups specified for `REDEEMED` are used in both `GET /coupon/{pk}/redeemed` and `GET /redeemed`. | ||
|
||
The groups specified for `DELETE` are used in both `DELETE /coupon/{pk}` and `DELETE /redeemed/{pk}`. | ||
|
||
2. Communicate with coupon endpoints. | ||
|
||
You can place the urls into a subpath, however you like: | ||
|
||
``` | ||
urlpatterns = [ | ||
# just adding here, but you can put into a subordinate path. | ||
url(r'^', include('coupons.urls')), | ||
] | ||
``` | ||
|
||
As stated above, by default any user in the system can touch any of the below endpoints, except where specified in bold. | ||
|
||
| Endpoint | Details | | ||
| --------------------------- | --------------------------------------------------------------------------------------- | | ||
| `GET /coupon` | List all coupons in the system, **only superuser or in group can see all**. | | ||
| `GET /coupon/{pk}` | Retrieve details about a coupon by database id | | ||
| `POST /coupon` | Create a new coupon | | ||
| `PUT /coupon/{pk}` | Update a coupon | | ||
| `DELETE /coupon/{pk}` | Delete a coupon | | ||
| `PUT /coupon/{pk}/redeem` | Redeem a coupon by database id | | ||
| `GET /coupon/{pk}/redeemed` | List all times specified coupon was redeemed, **superuser or group member can see all** | | ||
| `PATCH /coupon/{pk}` | **Not supported** | | ||
| `GET /redeemed` | List all redeemed instances, filter-able **only superuser or in group can do see all** | | ||
|
||
## Querying | ||
|
||
`GET /coupon` supports querying by coupon code, and filter by `user`, `bound`, `type` or by ranges of discount via `max_value`, `min_value` | ||
|
||
`GET /redeemed` supports filtering by `user`. | ||
|
||
## Objects | ||
|
||
There are two objects provided: | ||
|
||
1. `Coupon` - allows you to specify the properties of the coupon itself. | ||
|
||
| Field | Type | Meaning | | ||
| --------- | ------------- | ------------------------------------------------------ | | ||
| `code` | `string` | the code for the coupon, case insensitive | | ||
| `code_l` | `string` | automatically set lowercase version of the coupon code | | ||
| `type` | `string` | either `percent` or `value`, how the `value` field should be interpreted | | ||
| `expires` | `datetime` | optional field to set when the coupon expires | | ||
| `value` | `decimal` | the value for the coupon, such as `100` or `0.50` | | ||
| `bound` | `boolean` | if `true` then the coupon can only be used by the specified user in the `user` field | | ||
| `user` | `foreign key` | set when bound to point to the user | | ||
| `repeat` | `integer` | if `0` the coupon can be used infinitely, otherwise it specifies how often any system user can use it | | ||
|
||
2. `ClaimedCoupon` - allows you to track whenever a user redeems a coupon. | ||
|
||
| Field | Type | Meaning | | ||
| ---------- | ------------- | ------------------------------------------------------ | | ||
| `redeemed` | `datetime` | automatically set when a coupon is redeemed | | ||
| `coupon` | `foreign key` | automatically set to point at the coupon when redeemed | | ||
| `user` | `foreign key` | automatically set to point at the coupon when redeemed | | ||
|
||
## Coupon Types | ||
|
||
It supports the following variations of coupons: | ||
|
||
1. Coupons can be a value, or a percentage. | ||
2. They can be bound to a specific user in the system. | ||
3. They can be single-use: | ||
1. per user (a pre-specified user can use it once), `case I` | ||
2. globally (any user in the system can use it, but only once) `case II` | ||
4. They can be infinite: | ||
1. per a specific user (a pre-specified user can use it repeatedly infinitely) `case III` | ||
2. infinite globally (any user can use it repeatedly infinitely) `case IV` | ||
5. They can be used a specific number of times: | ||
1. per user (a pre-specified user can use it a specific number of times) `case V` | ||
2. globally (any user can use it a specific number of times) `case VI` | ||
6. (They can be used by a specific list of users?) ... maybe later. | ||
|
||
You create coupons in the system that are then claimed by users. | ||
|
||
## Developing | ||
|
||
The unit-tests should automatically be run when you run `python manage.py test` and they are isolated. | ||
|
||
If you'd like to contribute, please fork, and develop, branch from the `development` branch to and submit a pull request when ready. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
|
||
DRF Coupons | ||
----------- | ||
|
||
A django-rest-framework application that provides many varieties of coupons | ||
|
||
Detailed documentation is in the README.md file. | ||
|
||
Quick start | ||
----------- | ||
|
||
1. Add "coupons" to your INSTALLED_APPS setting like this:: | ||
|
||
INSTALLED_APPS = [ | ||
... | ||
'coupons', | ||
] | ||
|
||
2. Include the coupons URLconf in your project urls.py like this:: | ||
|
||
url(r'^', include('coupons.urls')), | ||
|
||
3. Run ``python manage.py migrate`` to create the polls models. | ||
|
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
from django_filters import FilterSet, NumberFilter | ||
from django.apps import apps | ||
|
||
|
||
class CouponFilter(FilterSet): | ||
""" | ||
An initial basic filter for Coupons. This could be handled with filter_fields = () until I add in range filtering | ||
on the discount value, then it is more helpful to do this. | ||
""" | ||
|
||
min_value = NumberFilter(name='value', lookup_expr='gte') | ||
max_value = NumberFilter(name='value', lookup_expr='lte') | ||
|
||
class Meta: | ||
model = apps.get_model('coupons.Coupon') | ||
fields = ['user', 'bound', 'type', 'min_value', 'max_value'] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# -*- coding: utf-8 -*- | ||
# Generated by Django 1.10.2 on 2017-01-09 05:05 | ||
from __future__ import unicode_literals | ||
|
||
from django.conf import settings | ||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='ClaimedCoupon', | ||
fields=[ | ||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('redeemed', models.DateTimeField(auto_now_add=True)), | ||
], | ||
), | ||
migrations.CreateModel( | ||
name='Coupon', | ||
fields=[ | ||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('created', models.DateTimeField(auto_now_add=True)), | ||
('updated', models.DateTimeField(auto_now=True)), | ||
('code', models.CharField(max_length=64)), | ||
('code_l', models.CharField(blank=True, max_length=64, unique=True)), | ||
('type', models.CharField(choices=[('percent', 'percent'), ('value', 'value')], max_length=16)), | ||
('expires', models.DateTimeField(blank=True, null=True)), | ||
('value', models.DecimalField(decimal_places=2, default=0.0, max_digits=5)), | ||
('bound', models.BooleanField(default=False)), | ||
('repeat', models.IntegerField(default=0)), | ||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), | ||
], | ||
), | ||
migrations.AddField( | ||
model_name='claimedcoupon', | ||
name='coupon', | ||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='coupons.Coupon'), | ||
), | ||
migrations.AddField( | ||
model_name='claimedcoupon', | ||
name='user', | ||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), | ||
), | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
from __future__ import unicode_literals | ||
|
||
from django.conf import settings | ||
from django.contrib.auth import get_user_model | ||
from django.db import models | ||
|
||
COUPON_TYPES = ( | ||
('percent', 'percent'), | ||
('value', 'value'), | ||
) | ||
|
||
try: | ||
# In case they specified something else in their settings file, which is quite common. | ||
user = settings.AUTH_USER_MODEL | ||
except AttributeError: | ||
# get_user_model isn't working at this point in loading. | ||
from django.contrib.auth.models import User as user | ||
|
||
|
||
class Coupon(models.Model): | ||
""" | ||
These are the coupons that are in the system. | ||
- Coupons can be a value, or a percentage. | ||
- They can be bound to a specific user in the system, or an email address (not yet in the system). | ||
- They can be single-use per user, or single-use globally. | ||
- They can be infinite per a specific user, or infinite globally. | ||
- They can be used a specific number of times per user, or globally. | ||
- (They can be used by a specific list of users?) | ||
""" | ||
|
||
created = models.DateTimeField(auto_now_add=True) | ||
updated = models.DateTimeField(auto_now=True) | ||
|
||
# The coupon code itself (so it can be mixed case in presentation... meh) | ||
code = models.CharField(max_length=64) | ||
# the lowercase version to simplify some code (for now). | ||
# | ||
# usually blank=True goes with null=True, but in this case, we want the admin to know it's optional, but the | ||
# database does require it, and it needs to be unique. | ||
code_l = models.CharField(max_length=64, blank=True, unique=True) | ||
|
||
# Whether it's a percentage off or a value. | ||
type = models.CharField(max_length=16, choices=COUPON_TYPES) | ||
|
||
# When it expires (if it expires) | ||
expires = models.DateTimeField(blank=True, null=True) | ||
|
||
# The values (either percentage based, or value based), if percentage based make sure it's no greater than 1.0 | ||
value = models.DecimalField(default=0.0, max_digits=5, decimal_places=2) | ||
|
||
# Is this coupon bound to a specific user? | ||
bound = models.BooleanField(default=False) | ||
user = models.ForeignKey(user, blank=True, null=True) | ||
|
||
# How many times this coupon can be used, 0 == infinitely, otherwise it's a number, such as 1 or many. | ||
# To determine if you can redeem it, it'll check this value against the number of corresponding ClaimedCoupons. | ||
repeat = models.IntegerField(default=0) | ||
|
||
# single-use per user | ||
# repeat = 1, bound = True, binding = user_id | ||
# single-use globally | ||
# repeat = 1, bound = False | ||
|
||
# infinite-user per user | ||
# repeat = 0, bound = True | ||
# infinite globally | ||
# repeat = 0, bound = False | ||
|
||
# specific number of times per user | ||
# repeat => X, bound = True, binding = user_id | ||
# specific number of times globally | ||
# repeat => X, bound = False | ||
|
||
|
||
class ClaimedCoupon(models.Model): | ||
""" | ||
These are the instances of claimed coupons, each is an individual usage of a coupon by someone in the system. | ||
""" | ||
|
||
redeemed = models.DateTimeField(auto_now_add=True) | ||
|
||
# Every claimed coupon should point back to a Coupon in the system. | ||
coupon = models.ForeignKey('Coupon') | ||
user = models.ForeignKey(user) |
Oops, something went wrong.