Skip to content

Commit

Permalink
Merge pull request #10 from pstrinkle/staging
Browse files Browse the repository at this point in the history
Staged
  • Loading branch information
pstrinkle committed Jan 16, 2017
2 parents ee23ca5 + 84f9a5e commit 977a06c
Show file tree
Hide file tree
Showing 35 changed files with 2,958 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
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]
Expand Down
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include LICENSE
include README.rst
16 changes: 16 additions & 0 deletions NOTES.md
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`
137 changes: 137 additions & 0 deletions README.md
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.
24 changes: 24 additions & 0 deletions README.rst
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 added coupons/__init__.py
Empty file.
17 changes: 17 additions & 0 deletions coupons/filters.py
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']

52 changes: 52 additions & 0 deletions coupons/migrations/0001_initial.py
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 added coupons/migrations/__init__.py
Empty file.
85 changes: 85 additions & 0 deletions coupons/models.py
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)
Loading

0 comments on commit 977a06c

Please sign in to comment.