diff --git a/src/foobar/admin.py b/src/foobar/admin.py
index cf827fb..7072bde 100644
--- a/src/foobar/admin.py
+++ b/src/foobar/admin.py
@@ -67,7 +67,7 @@ class CardInline(admin.TabularInline):
@admin.register(models.Account)
class AccountAdmin(admin.ModelAdmin):
- list_display = ('id', 'name', 'user', 'balance')
+ list_display = ('id', 'name', 'user', 'balance', 'email')
readonly_fields = ('id', 'wallet_link', 'date_created', 'date_modified')
inlines = (CardInline, PurchaseInline,)
search_fields = ('name',)
@@ -77,6 +77,7 @@ class AccountAdmin(admin.ModelAdmin):
'id',
'user',
'name',
+ 'email'
)
}),
('Additional information', {
diff --git a/src/foobar/api.py b/src/foobar/api.py
index 4aacadf..c072fd6 100644
--- a/src/foobar/api.py
+++ b/src/foobar/api.py
@@ -23,12 +23,23 @@ def get_card(card_id):
return None
-def get_account(card_id):
+def get_account(account_id):
+ try:
+ return Account.objects.get(id=account_id)
+ except Account.DoesNotExist:
+ return None
+
+
+def get_account_by_card(card_id):
card_obj = get_card(card_id)
if card_obj is not None:
return card_obj.account
+def update_account(account_id, **kwargs):
+ Account.objects.filter(id=account_id).update(**kwargs)
+
+
@transaction.atomic
def purchase(account_id, products):
"""
diff --git a/src/foobar/forms.py b/src/foobar/forms.py
index a8804a6..600ad2c 100644
--- a/src/foobar/forms.py
+++ b/src/foobar/forms.py
@@ -5,8 +5,10 @@
class CorrectionForm(forms.Form):
- balance = MoneyField(label='Balance', min_value=0)
- comment = forms.CharField(label='Comment', max_length=128, required=False)
+ balance = MoneyField(label=_('Balance'), min_value=0)
+ comment = forms.CharField(label=_('Comment'),
+ max_length=128,
+ required=False)
class DepositForm(forms.Form):
@@ -23,3 +25,8 @@ def clean_deposit_or_withdrawal(self):
if data.amount < 0 and -data > balance:
raise forms.ValidationError(_('Not enough funds'))
return data
+
+
+class EditProfileForm(forms.Form):
+ name = forms.CharField(label=_("Account Name"), max_length=128)
+ email = forms.EmailField(label=_("E-mail"))
diff --git a/src/foobar/migrations/0020_auto_20170302_1359.py b/src/foobar/migrations/0020_auto_20170302_1359.py
new file mode 100644
index 0000000..0976ed2
--- /dev/null
+++ b/src/foobar/migrations/0020_auto_20170302_1359.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-03-02 13:59
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('foobar', '0019_auto_20170221_1547'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='account',
+ name='email',
+ field=models.EmailField(blank=True, max_length=254, null=True),
+ ),
+ ]
diff --git a/src/foobar/models.py b/src/foobar/models.py
index 8c9a39a..544e8e5 100644
--- a/src/foobar/models.py
+++ b/src/foobar/models.py
@@ -15,7 +15,11 @@
class Account(UUIDModel, TimeStampedModel):
user = models.ForeignKey(User, null=True, blank=True)
name = models.CharField(null=True, blank=True, max_length=128)
- email = models.CharField(null=True, blank=True, max_length=128)
+ email = models.EmailField(null=True, blank=True)
+
+ @property
+ def is_complete(self):
+ return bool(self.email)
def __str__(self):
return str(self.id)
diff --git a/src/foobar/rest/serializers/account.py b/src/foobar/rest/serializers/account.py
index 83b0968..d7c7034 100644
--- a/src/foobar/rest/serializers/account.py
+++ b/src/foobar/rest/serializers/account.py
@@ -1,6 +1,7 @@
from rest_framework import serializers
from foobar.wallet import api as wallet_api
from ..fields import MoneyField
+from django.core import signing
class AccountSerializer(serializers.Serializer):
@@ -8,10 +9,16 @@ def get_balance(self, instance):
_, balance = wallet_api.get_balance(instance.id)
return MoneyField().to_representation(balance)
+ def get_token(self, instance):
+ token = signing.dumps({'id': str(instance.id)})
+ return token
+
id = serializers.UUIDField(read_only=True)
user_id = serializers.UUIDField(read_only=True, source='user.id')
name = serializers.CharField(read_only=True)
balance = serializers.SerializerMethodField()
+ token = serializers.SerializerMethodField()
+ is_complete = serializers.BooleanField(read_only=True)
class AccountQuerySerializer(serializers.Serializer):
diff --git a/src/foobar/rest/views/account.py b/src/foobar/rest/views/account.py
index 058ad84..ef40e00 100644
--- a/src/foobar/rest/views/account.py
+++ b/src/foobar/rest/views/account.py
@@ -2,6 +2,7 @@
from rest_framework.response import Response
from authtoken.permissions import HasTokenScope
+
import foobar.api
from ..serializers.account import AccountQuerySerializer, AccountSerializer
@@ -13,7 +14,7 @@ class AccountAPI(viewsets.ViewSet):
def retrieve(self, request, pk):
serializer = AccountQuerySerializer(data={'card_id': pk})
serializer.is_valid(raise_exception=True)
- account_obj = foobar.api.get_account(card_id=pk)
+ account_obj = foobar.api.get_account_by_card(card_id=pk)
if account_obj is None:
return Response(status=status.HTTP_404_NOT_FOUND)
serializer = AccountSerializer(account_obj)
diff --git a/src/foobar/static/css/profile.css b/src/foobar/static/css/profile.css
new file mode 100644
index 0000000..5bc98e5
--- /dev/null
+++ b/src/foobar/static/css/profile.css
@@ -0,0 +1,44 @@
+body{
+ margin: 0;
+ font-size: 26px;
+}
+.info {
+ background: #dfd;
+ padding: 10px;
+ color: #333;
+ text-align: center;
+}
+#header{
+ background: #a70d0d;
+ font-size: 350%;
+ padding: 2%;
+ color: white;
+ text-align: center;
+ margin: auto;
+ border-bottom: 5px solid #d34a4a;
+}
+.account_form{
+ font-size: 180%;
+ width: 70%;
+ margin: auto;
+ padding-top: 5%
+}
+.account_form >*{
+ width: 100%;
+ height: 40px;
+ margin: 10px;
+ outline-color: #a70d0d;
+}
+input{
+ font-size: 26px;
+}
+#submit_changes{
+ background: #a70d0d;
+ color: white;
+ border: none;
+ padding: 5px;
+ height: 60px;
+ font-size: 100%;
+ -webkit-appearance: none;
+ border-radius: 0;
+}
diff --git a/src/foobar/static/css/scan_card.css b/src/foobar/static/css/scan_card.css
index 18c1b03..0079dc4 100644
--- a/src/foobar/static/css/scan_card.css
+++ b/src/foobar/static/css/scan_card.css
@@ -18,3 +18,9 @@
margin-left: 0 !important;
margin-top: -4px !important;
}
+.info{
+ background: #dfd url(../img/icon-yes.svg) 40px 12px no-repeat;
+ background-size: 16px auto;
+ text-align: center;
+
+}
diff --git a/src/foobar/templates/profile/bad_request.html b/src/foobar/templates/profile/bad_request.html
new file mode 100644
index 0000000..173e6f3
--- /dev/null
+++ b/src/foobar/templates/profile/bad_request.html
@@ -0,0 +1,5 @@
+{% extends 'profile/base_profile.html' %}
+{% load i18n %}
+{% block content %}
+
{% trans "Invalid QRcode" %}
{% trans "Login to get a new" %}
+{% endblock %}
\ No newline at end of file
diff --git a/src/foobar/templates/profile/base_profile.html b/src/foobar/templates/profile/base_profile.html
new file mode 100644
index 0000000..690bec4
--- /dev/null
+++ b/src/foobar/templates/profile/base_profile.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ FooBar
+
+
+
+ {% load i18n %}
+
+ {% block content %}
+ {% endblock %}
+
+
diff --git a/src/foobar/templates/profile/success.html b/src/foobar/templates/profile/success.html
new file mode 100644
index 0000000..013e22c
--- /dev/null
+++ b/src/foobar/templates/profile/success.html
@@ -0,0 +1,17 @@
+{% extends 'profile/base_profile.html' %}
+{% load i18n %}
+{% block content %}
+
+ {% if messages %}
+ {% for message in messages %}
+
{{ message }}
+ {% endfor %}
+ {% endif %}
+
+
+
+{% endblock %}
diff --git a/src/foobar/tests/test_api.py b/src/foobar/tests/test_api.py
index 612baca..f104d1c 100644
--- a/src/foobar/tests/test_api.py
+++ b/src/foobar/tests/test_api.py
@@ -8,6 +8,7 @@
from .factories import AccountFactory, CardFactory
from moneyed import Money
from django.contrib.auth.models import User
+import uuid
class FoobarAPITest(TestCase):
@@ -27,19 +28,41 @@ def test_get_card(self):
self.assertGreater(obj2.date_used, date_used)
def test_get_account(self):
+ # Assure None when missing account
+ id = uuid.uuid4()
+ obj1 = api.get_account(account_id=id)
+ self.assertIsNone(obj1)
+
+ # Create an account
+ account_obj = AccountFactory.create()
+ obj2 = api.get_account(account_id=account_obj.id)
+
+ self.assertIsNotNone(obj2)
+
+ def test_get_account_by_card(self):
# Retrieve an non-existent account
- obj1 = api.get_account(card_id=1337)
+ obj1 = api.get_account_by_card(card_id=1337)
self.assertIsNone(obj1)
# Create an account
CardFactory.create(number=1337)
- obj2 = api.get_account(card_id=1337)
+ obj2 = api.get_account_by_card(card_id=1337)
self.assertIsNotNone(obj2)
account_objs = models.Account.objects.filter(id=obj2.id)
self.assertEqual(account_objs.count(), 1)
+ def test_update_account(self):
+ account_obj = AccountFactory.create()
+ api.update_account(account_id=account_obj.id,
+ name='1337',
+ email='1337@foo.com')
+ account = api.get_account(account_id=account_obj.id)
+ # Test that correct fields are updated
+ self.assertEqual('1337', account.name)
+ self.assertEqual('1337@foo.com', account.email)
+
def test_purchase(self):
account_obj = AccountFactory.create()
wallet_obj = WalletFactory.create(owner_id=account_obj.id)
diff --git a/src/foobar/tests/test_views.py b/src/foobar/tests/test_views.py
index 931d58f..9739b6f 100644
--- a/src/foobar/tests/test_views.py
+++ b/src/foobar/tests/test_views.py
@@ -6,6 +6,7 @@
from wallet.tests.factories import WalletFactory, WalletTrxFactory
from wallet.enums import TrxType
from . import factories
+from django.core import signing
class FoobarViewTest(TestCase):
@@ -23,7 +24,7 @@ def setUp(self):
password=self.TESTUSER_PASS
)
- @mock.patch('foobar.api.get_account')
+ @mock.patch('foobar.api.get_account_by_card')
def test_account_for_card(self, mock_get_account):
url = reverse('account_for_card', kwargs={'card_id': 1337})
mock_get_account.return_value = None
@@ -53,38 +54,65 @@ def test_wallet_management(self, mock_deposit_withdrawal, mock_correction):
)
url = reverse('wallet_management',
kwargs={'obj_id': wallet_obj.owner_id})
- cl = self.client
# Test that deposit or withdrawal
# is not called if balance will get negative
- response = cl.post(url,
- {'deposit_or_withdrawal_1': ['SEK'],
- 'save_deposit': ['Submit'],
- 'comment': ['test'],
- 'deposit_or_withdrawal_0': ['-3000']})
+ response = self.client.post(url,
+ {'deposit_or_withdrawal_1': ['SEK'],
+ 'save_deposit': ['Submit'],
+ 'comment': ['test'],
+ 'deposit_or_withdrawal_0': ['-3000']})
mock_deposit_withdrawal.assert_not_called()
# Test that page can be found
- response = cl.get(url)
+ response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# Test that correction form post is correct and
# calls function with correct params
- response = cl.post(url,
- {'save_correction': ['Submit'],
- 'balance_1': ['SEK'],
- 'comment': ['test'],
- 'balance_0': ['1000']})
+ self.client.post(url,
+ {'save_correction': ['Submit'],
+ 'balance_1': ['SEK'],
+ 'comment': ['test'],
+ 'balance_0': ['1000']})
mock_correction.assert_called_with(Money(1000, 'SEK'),
wallet_obj.owner_id,
self.user,
'test')
# Test that deposit or withdrawal form post is correct and
# calls fucnction with correct params
- response = cl.post(url,
- {'deposit_or_withdrawal_1': ['SEK'],
- 'save_deposit': ['Submit'],
- 'comment': ['test'],
- 'deposit_or_withdrawal_0': ['100']})
+ self.client.post(url,
+ {'deposit_or_withdrawal_1': ['SEK'],
+ 'save_deposit': ['Submit'],
+ 'comment': ['test'],
+ 'deposit_or_withdrawal_0': ['100']})
mock_deposit_withdrawal.assert_called_with(
Money(100, 'SEK'),
wallet_obj.owner_id,
self.user,
'test')
+
+ @mock.patch('foobar.api.update_account')
+ def test_edit_profile(self, mock_update_account):
+ account_obj = factories.AccountFactory.create()
+ token = signing.dumps({'id': str(account_obj.id)})
+ url = reverse('edit_profile', kwargs={'token': token})
+ bad_token = reverse('edit_profile', kwargs={'token': 'bad'})
+ response1 = self.client.get(url)
+ response2 = self.client.get(bad_token)
+
+ # Assert that page can be found
+ self.assertEqual(response1.status_code, 200)
+ self.assertEqual(response2.status_code, 200)
+
+ # Assure update_account not called when url with bad token send POST
+ self.client.post(bad_token, {'name': 'foo',
+ 'email': 'test@test.com',
+ 'save_changes': ['Submit']})
+ mock_update_account.assert_not_called()
+
+ # Assure set_balance is called when token is valid
+ token_data = signing.loads(token, max_age=1800)
+ self.client.post(url, {'name': 'foo',
+ 'email': 'test@test.com',
+ 'save_changes': ['Submit']})
+ mock_update_account.assert_called_with(token_data.get('id'),
+ name='foo',
+ email='test@test.com')
diff --git a/src/foobar/urls.py b/src/foobar/urls.py
index ece19cb..63871b0 100644
--- a/src/foobar/urls.py
+++ b/src/foobar/urls.py
@@ -21,6 +21,9 @@
url(r'^admin/foobar/account/card/(?P\d+)',
foobar.views.account_for_card, name='account_for_card'),
url(r'^admin/', include(admin.site.urls)),
+ url(r'profile/(?P.+)',
+ foobar.views.edit_profile,
+ name="edit_profile")
]
if settings.DEBUG:
diff --git a/src/foobar/views.py b/src/foobar/views.py
index 0063d6d..719888a 100644
--- a/src/foobar/views.py
+++ b/src/foobar/views.py
@@ -4,20 +4,22 @@
from django.utils.translation import ugettext_lazy as _
from . import api
from django.shortcuts import render
-from .forms import CorrectionForm, DepositForm
+from .forms import CorrectionForm, DepositForm, EditProfileForm
from django.contrib import messages
from django.http import HttpResponseRedirect
from foobar.wallet.api import get_wallet
+from django.core import signing
@staff_member_required
@permission_required('foobar.change_account')
def account_for_card(request, card_id):
- account_obj = api.get_account(card_id)
+ account_obj = api.get_account_by_card(card_id)
if account_obj is None:
messages.add_message(request, messages.ERROR,
_('No account has been found for given card.'))
return redirect('admin:foobar_account_changelist')
+
return redirect('admin:foobar_account_change', account_obj.id)
@@ -38,7 +40,7 @@ def wallet_management(request, obj_id):
)
messages.add_message(request,
messages.INFO,
- 'Correction was successfully saved.')
+ _('Correction was successfully saved.'))
return HttpResponseRedirect(request.path)
elif 'save_deposit' in request.POST:
@@ -51,7 +53,7 @@ def wallet_management(request, obj_id):
)
messages.add_message(request,
messages.INFO,
- 'Successfully saved.')
+ _('Successfully saved.'))
return HttpResponseRedirect(request.path)
return render(request,
@@ -59,3 +61,25 @@ def wallet_management(request, obj_id):
{'wallet': wallet,
'form_class': form_class,
'form_class1': form_class1})
+
+
+def edit_profile(request, token):
+ form_class = EditProfileForm(request.POST or None)
+ try:
+ token = signing.loads(token, max_age=1800)
+ except signing.BadSignature:
+ return render(request, "profile/bad_request.html")
+
+ if request.method == 'POST':
+ if form_class.is_valid():
+ api.update_account(token.get('id'),
+ name=form_class.cleaned_data['name'],
+ email=form_class.cleaned_data['email'])
+ messages.add_message(request, messages.INFO,
+ _('Successfully Saved'))
+ return HttpResponseRedirect(request.path)
+
+ account = api.get_account(token.get('id'))
+ form_class = EditProfileForm(initial={'name': account.name,
+ 'email': account.email})
+ return render(request, "profile/success.html", {'form': form_class})