Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Staged #10

Merged
merged 47 commits into from
Jan 16, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
a73bbdc
starting to get this going.
Jan 7, 2017
66a9324
.gitignore.
Jan 7, 2017
e7b6276
starting on some coupon stuff.
Jan 7, 2017
aa8fd29
Create README.md
pstrinkle Jan 8, 2017
4b75d87
meh.
Jan 8, 2017
dd601bc
starting serializers.
Jan 8, 2017
766f0e1
some initial views to help move coupons on.
Jan 8, 2017
0cb0e91
Starting to write some unit tests for the coupon application.
Jan 9, 2017
e29b659
some tweaks to the models.
Jan 9, 2017
83b038f
Specifying the repeat field is failing with NOT NULL constraint.
Jan 9, 2017
7e4f8e0
Fixed my mistake, lol. :) I love these types of bugs while I'm workin…
Jan 9, 2017
701103d
disable PATCH.
Jan 9, 2017
25fc499
Yay, unit-tests.
Jan 9, 2017
875bf59
Forcing the Coupon view to return 202 on update success, instead of 2…
Jan 9, 2017
8beb5ce
Some tweaks and some new tests.
Jan 9, 2017
1456436
whitspace, :D
Jan 9, 2017
9e75f8d
ditched the email bit because it didn't really make much sense.
Jan 9, 2017
7a78266
Building up the redeem code.
Jan 10, 2017
6cff8df
Enabled group specification for actions and added a test for the crea…
Jan 12, 2017
eea8f2d
Update README.md
pstrinkle Jan 12, 2017
7bc3cec
Update README.md
pstrinkle Jan 12, 2017
90adaf8
Update README.md
pstrinkle Jan 12, 2017
cfc977b
Update README.md
pstrinkle Jan 12, 2017
c46ec79
Merge remote-tracking branch 'origin/master' into development
Jan 13, 2017
502e4f2
Fixed isolation in the coupons tests.
Jan 13, 2017
3f29221
Added filtering, starting on adding some missing unit-tests in.
Jan 15, 2017
585d36b
Added delete.
Jan 15, 2017
e365603
implemented search unit-tests.
Jan 15, 2017
fbbcef7
Implemented unit-tests to somewhat verify and fix #2
Jan 15, 2017
64a8b28
Added some tests, and it's neato.
Jan 16, 2017
e142956
Redeemed list working and testing fine.
Jan 16, 2017
6ecdca3
more details being checked.
Jan 16, 2017
dbc7ac6
Working on redeemed endpoint.
Jan 16, 2017
4e5f888
promoted to a higher folder scope to reduce extra depth.
Jan 16, 2017
8fdbee3
More tests
Jan 16, 2017
cb47b82
Now you can retrieve a coupon by short or database id.
Jan 16, 2017
e0f3a04
You should be able to delete a claimed coupon instance to basically u…
Jan 16, 2017
982c102
added field table.
Jan 16, 2017
90e87d7
documentation updates.
Jan 16, 2017
89e64b6
table.
Jan 16, 2017
f3fbd62
Some unit-tests to verify stuff.
Jan 16, 2017
1512a4b
More tests
Jan 16, 2017
b234d60
Starting to set up the plugin for upload to pypi. I haven't finished…
Jan 16, 2017
1a7c621
implemented verification that the repeat count is now checked and che…
Jan 16, 2017
98ca119
verified with more tests
Jan 16, 2017
5c07951
Note.
Jan 16, 2017
84f9a5e
Staged.
Jan 16, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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