Skip to content
This repository has been archived by the owner on Jan 25, 2018. It is now read-only.

Commit

Permalink
Solitude side of reset pin (bug 802646)
Browse files Browse the repository at this point in the history
  • Loading branch information
wraithan committed Jan 7, 2013
1 parent d4f4de5 commit 498cee5
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 12 deletions.
14 changes: 14 additions & 0 deletions lib/buyers/field.py
@@ -1,6 +1,7 @@
from django.contrib.auth.hashers import (make_password, check_password,
get_hasher)
from django.db.models import CharField, SubfieldBase
from django.forms import CharField as FormCharField


class HashField(CharField):
Expand Down Expand Up @@ -68,3 +69,16 @@ def __repr__(self):

def __len__(self):
return len(self.value)


class FormHashField(FormCharField):
# This was written to pass along the bool that is generated by the
# dehydrate_pin method on BuyerResource. Tastypie does this if PIN
# wasn't passed along in a PATCH. Without this the CharField would
# coerce True to 'True' and we wouldn't be able to tell the
# difference between Tastypie being a pain or a user typing 'True'
# into the PIN entry.
def to_python(self, value):
if value is None or isinstance(value, (str, unicode, bool)):
return value
return unicode(value)
39 changes: 29 additions & 10 deletions lib/buyers/forms.py
Expand Up @@ -3,26 +3,44 @@
from django_paranoia.forms import ParanoidForm, ParanoidModelForm
from tastypie.validation import FormValidation

from .field import FormHashField
from .models import Buyer


class PinMixin(object):
def clean_pin(self):
pin = self.cleaned_data['pin']
def base_clean_pin(form, field_name='pin'):
pin = form.cleaned_data[field_name]

if pin is None or len(pin) == 0:
return pin
# pin will be a boolean if it was filled in by tastypie using the
# dehydrate method. I wrote a custom FormHashField that will pass
# the bool along if tastypie sent it, is so we can tell the
# difference between tastypie doing this or someone typing "True"
# into the PIN entry.
if isinstance(pin, bool):
return form.instance.pin

if not len(pin) == 4:
raise forms.ValidationError('PIN must be exactly 4 numbers long')
if pin is None or len(pin) == 0:
return pin

if not pin.isdigit():
raise forms.ValidationError('PIN may only consists of numbers')
if not len(pin) == 4:
raise forms.ValidationError('PIN must be exactly 4 numbers long')

return pin
if not pin.isdigit():
raise forms.ValidationError('PIN may only consists of numbers')

return pin


class PinMixin(object):
def clean_pin(self):
return base_clean_pin(self)

def clean_new_pin(self):
return base_clean_pin(self, field_name='new_pin')


class BuyerForm(ParanoidModelForm, PinMixin):
pin = FormHashField(required=False)
new_pin = FormHashField(required=False)

class Meta:
model = Buyer
Expand All @@ -43,5 +61,6 @@ def is_valid(self, bundle, request=None):
else:
form = self.form_class(data)
if form.is_valid():
bundle.data.update(form.cleaned_data)
return {}
return form.errors
2 changes: 2 additions & 0 deletions lib/buyers/models.py
Expand Up @@ -11,6 +11,8 @@ class Buyer(Model):
pin = HashField(blank=True, null=True)
pin_confirmed = models.BooleanField(default=False)
active = models.BooleanField(default=True, db_index=True)
new_pin = HashField(blank=True, null=True)
needs_pin_reset = models.BooleanField(default=False)

class Meta(Model.Meta):
db_table = 'buyer'
Expand Down
26 changes: 25 additions & 1 deletion lib/buyers/resources.py
Expand Up @@ -13,7 +13,7 @@ class BuyerResource(ModelResource):

class Meta(ModelResource.Meta):
queryset = Buyer.objects.filter()
fields = ['uuid', 'pin', 'active']
fields = ['uuid', 'pin', 'active', 'new_pin', 'needs_pin_reset']
list_allowed_methods = ['get', 'post', 'put']
allowed_methods = ['get', 'patch', 'put']
resource_name = 'buyer'
Expand All @@ -26,6 +26,9 @@ class Meta(ModelResource.Meta):
def dehydrate_pin(self, bundle):
return bool(bundle.obj.pin)

def dehydrate_new_pin(self, bundle):
return bool(bundle.obj.new_pin)


class BuyerPaypalResource(ModelResource):
buyer = fields.ToOneField('lib.buyers.resources.BuyerResource',
Expand Down Expand Up @@ -93,3 +96,24 @@ def obj_create(self, bundle, request=None, **kwargs):
else:
bundle.obj.valid = False
return bundle


class BuyerResetPinResource(BuyerEndpointBase):
confirmed = fields.BooleanField(attribute='confirmed')

class Meta(BuyerEndpointBase.Meta):
resource_name = 'reset_confirm_pin'

def obj_create(self, bundle, request=None, **kwargs):
buyer = self.get_data(bundle)
pin = bundle.data.pop('pin')
if buyer.new_pin == pin:
buyer.pin = pin
buyer.new_pin = None
buyer.needs_pin_reset = False
buyer.pin_confirmed = True
buyer.save()
bundle.obj.confirmed = True
else:
bundle.obj.confirmed = False
return bundle
45 changes: 45 additions & 0 deletions lib/buyers/tests/test_api.py
Expand Up @@ -292,3 +292,48 @@ def test_bad_uuid(self):
def test_empty_post(self):
res = self.client.post(self.list_url, data={})
eq_(res.status_code, 400)


class TestBuyerResetPin(APITest):

def setUp(self):
self.api_name = 'generic'
self.uuid = 'sample:uid'
self.pin = '1234'
self.new_pin = '4321'
self.buyer = Buyer.objects.create(uuid=self.uuid, pin=self.pin,
new_pin=self.new_pin,
needs_pin_reset=True)
self.list_url = self.get_list_url('reset_confirm_pin')

def test_good_uuid_and_pin(self):
res = self.client.post(self.list_url, data={'uuid': self.uuid,
'pin': self.new_pin})
eq_(res.status_code, 201)
data = json.loads(res.content)
assert data.get('confirmed', False)
buyer = self.buyer.reget()
assert not buyer.needs_pin_reset
assert buyer.pin_confirmed
eq_(buyer.pin, self.new_pin)
eq_(data['uuid'], self.uuid)

def test_good_uuid_and_bad_pin(self):
res = self.client.post(self.list_url, data={'uuid': self.uuid,
'pin': self.pin})
eq_(res.status_code, 201)
data = json.loads(res.content)
assert not data.get('confirmed', False)
buyer = self.buyer.reget()
assert not buyer.pin == self.new_pin
assert buyer.needs_pin_reset
eq_(data['uuid'], self.uuid)

def test_bad_uuid(self):
res = self.client.post(self.list_url, data={'uuid': 'bad:uuid',
'pin': '4321'})
eq_(res.status_code, 404)

def test_empty_post(self):
res = self.client.post(self.list_url, data={})
eq_(res.status_code, 400)
2 changes: 2 additions & 0 deletions migrations/28-add-reset-to-buyer.sql
@@ -0,0 +1,2 @@
ALTER TABLE `buyer` ADD COLUMN `new_pin` varchar(255);
ALTER TABLE `buyer` ADD COLUMN `needs_pin_reset` boolean NOT NULL DEFAULT 0;
4 changes: 3 additions & 1 deletion solitude/urls.py
Expand Up @@ -5,7 +5,8 @@

from lib.bango.urls import bango
from lib.delayable.resources import DelayableResource, ReplayResource
from lib.buyers.resources import (BuyerConfirmPinResource, BuyerPaypalResource,
from lib.buyers.resources import (BuyerConfirmPinResource,
BuyerResetPinResource, BuyerPaypalResource,
BuyerResource, BuyerVerifyPinResource)
from lib.paypal.urls import paypal
from lib.sellers.resources import (SellerResource, SellerPaypalResource,
Expand All @@ -19,6 +20,7 @@
api.register(BuyerResource())
api.register(BuyerConfirmPinResource())
api.register(BuyerVerifyPinResource())
api.register(BuyerResetPinResource())
api.register(SellerResource())
api.register(SellerProductResource())
api.register(TransactionResource())
Expand Down

0 comments on commit 498cee5

Please sign in to comment.