diff --git a/core/filters.py b/core/filters.py index 0d85a2e4..c0aff6fd 100644 --- a/core/filters.py +++ b/core/filters.py @@ -10,7 +10,7 @@ class Meta: fields = ['user', 'card_number', 'disabled'] -class NfcCardFilet(django_filters.FilterSet): +class NfcCardFilter(django_filters.FilterSet): user = django_filters.CharFilter(name='user__username') class Meta: diff --git a/core/models.py b/core/models.py index 540d5917..ab076c96 100644 --- a/core/models.py +++ b/core/models.py @@ -111,4 +111,6 @@ class NfcCard(models.Model): comment = models.CharField(max_length=20, blank=True) def __str__(self): - return "NFC Card: [card_uid: %s, user=%s, intern=%s]" % (self.card_uid, self.user, self.intern) + if not self.user: + return self.card_uid + return "%s (%s)" % (self.card_uid, self.user) diff --git a/core/rest.py b/core/rest.py index 4f2f0509..697d342a 100644 --- a/core/rest.py +++ b/core/rest.py @@ -6,7 +6,7 @@ from core.serializers import CardCreateSerializer, CardSerializer, UserExtendedSerializer, NfcCardCreateSerializer, \ NfcCardSerializer from core.models import Card, User, NfcCard -from core.filters import CardFilter, UserFilter, NfcCardFilet +from core.filters import CardFilter, UserFilter, NfcCardFilter class CardViewSet(viewsets.ReadOnlyModelViewSet): @@ -44,7 +44,7 @@ def destroy(self, request): class NfcCardViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = (IsAuthenticated,) - filter_class = NfcCardFilet + filter_class = NfcCardFilter queryset = NfcCard.objects.all() def get_serializer_class(self): diff --git a/core/serializers.py b/core/serializers.py index ac0d92d3..f57ba533 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -31,13 +31,13 @@ class CardSerializer(serializers.ModelSerializer): class Meta: model = Card - fields = ('id', 'user', 'card_number', 'disabled', 'comment') + fields = ('id', 'user', 'card_number', 'card_uid', 'disabled', 'comment') class CardCreateSerializer(serializers.ModelSerializer): class Meta: model = Card - fields = ('user', 'card_number', 'comment') + fields = ('user', 'card_number', 'card_uid', 'comment') extra_kwargs = {'comment': {'default': None}} diff --git a/core/urls.py b/core/urls.py index c19dbdf2..a5916c18 100644 --- a/core/urls.py +++ b/core/urls.py @@ -8,7 +8,7 @@ router = SharedAPIRootRouter() router.register(r'core/users', UserViewSet, base_name='users') router.register(r'core/cards', CardViewSet, base_name='voucher_cards') -router.register(r'core/nfc', NfcCardViewSet, base_name='nfc_cards') +router.register(r'core/nfc', NfcCardViewSet) urlpatterns = patterns('', url(r'^api/me$', me, name='me'), diff --git a/voucher/admin.py b/voucher/admin.py index 3c3edf76..be1892ae 100644 --- a/voucher/admin.py +++ b/voucher/admin.py @@ -1,5 +1,6 @@ from django.contrib import admin -from voucher.models import Wallet, WorkLog, UseLog +from voucher.models import Wallet, WorkLog, UseLog, VoucherWallet, CoffeeWallet, VoucherUseLog, CoffeeUseLog, \ + CoffeeRegisterLog class WalletAdmin(admin.ModelAdmin): @@ -7,6 +8,9 @@ class WalletAdmin(admin.ModelAdmin): readonly_fields = ('cached_balance',) -admin.site.register(Wallet, WalletAdmin) +admin.site.register(VoucherWallet, WalletAdmin) +admin.site.register(CoffeeWallet, WalletAdmin) +admin.site.register(CoffeeRegisterLog) admin.site.register(WorkLog) -admin.site.register(UseLog) +admin.site.register(VoucherUseLog) +admin.site.register(CoffeeUseLog) diff --git a/voucher/filters.py b/voucher/filters.py index 08de3e7d..4b2a6794 100644 --- a/voucher/filters.py +++ b/voucher/filters.py @@ -4,8 +4,9 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.timezone import get_current_timezone -from core.models import Card -from voucher.models import UseLog, Wallet, WorkLog +from core.models import NfcCard +from voucher.models import UseLog, Wallet, WorkLog, VoucherWallet, CoffeeWallet, VoucherUseLog, CoffeeUseLog, \ + CoffeeRegisterLog from voucher.utils import get_valid_semesters @@ -21,15 +22,10 @@ def apply_date_filter(queryset, value, field, lte): class UseLogFilter(django_filters.FilterSet): - user = django_filters.CharFilter(name='wallet__user__username') semester = django_filters.CharFilter(name='wallet__semester') date_from = django_filters.MethodFilter(action='filter_date_from') date_to = django_filters.MethodFilter(action='filter_date_to') - class Meta: - model = UseLog - fields = ['id', 'user', 'semester'] - def filter_date_from(self, queryset, value): return apply_date_filter(queryset, value, 'date_spent', lte=False) @@ -37,29 +33,64 @@ def filter_date_to(self, queryset, value): return apply_date_filter(queryset, value, 'date_spent', lte=True) +class VoucherUseLogFilter(UseLogFilter): + user = django_filters.CharFilter(name='wallet__user__username') + + class Meta: + model = VoucherUseLog + fields = ['id', 'user', 'semester'] + + +class CoffeeUseLogFilter(UseLogFilter): + card = django_filters.CharFilter(name='wallet__card__card_uid') + + class Meta: + model = CoffeeUseLog + fields = ['id', 'card', 'semester'] + + class WalletFilter(django_filters.FilterSet): - user = django_filters.CharFilter(name='user__username') - card_number = django_filters.MethodFilter(action='filter_card_number') valid = django_filters.MethodFilter(action='filter_active') class Meta: model = Wallet - fields = ['user', 'card_number', 'semester'] - - def filter_card_number(self, queryset, value): - cards = Card.objects.filter(card_number=value) - if cards.exists(): - return queryset.filter(user=cards.first().user) - return queryset.none() + fields = ['semester'] def filter_active(self, queryset, value): return queryset.filter(semester__in=get_valid_semesters()) -class WorkLogFilter(django_filters.FilterSet): - user = django_filters.CharFilter(name='wallet__user__username') +class VoucherWalletFilter(WalletFilter): + user = django_filters.CharFilter(name='user__username') + + class Meta: + model = VoucherWallet + fields = ['user', 'semester'] + + +class CoffeeWalletFilter(WalletFilter): + card = django_filters.CharFilter(name='nfccard__card_uid') + + class Meta: + model = CoffeeWallet + fields = ['card', 'semester'] + + +class RegisterLogFilter(django_filters.FilterSet): issuing_user = django_filters.CharFilter(name='issuing_user__username') semester = django_filters.CharFilter(name='wallet__semester') + + +class CoffeeRegisterLogFilter(RegisterLogFilter): + card = django_filters.CharFilter(name='wallet__card__card_uid') + + class Meta: + model = CoffeeRegisterLog + fields = ['id', 'card', 'issuing_user', 'semester'] + + +class WorkLogFilter(RegisterLogFilter): + user = django_filters.CharFilter(name='wallet__user__username') date_from = django_filters.MethodFilter(action='filter_date_from') date_to = django_filters.MethodFilter(action='filter_date_to') diff --git a/voucher/migrations/0008_auto_20160127_0512.py b/voucher/migrations/0008_auto_20160127_0512.py index bf493792..754be8ea 100644 --- a/voucher/migrations/0008_auto_20160127_0512.py +++ b/voucher/migrations/0008_auto_20160127_0512.py @@ -7,10 +7,7 @@ def fix_balance(apps, schema_editor): - print(repr(Wallet)) - for wallet in Wallet.objects.all(): - print(repr(vars(wallet))) - wallet.calculate_balance() + pass def noop(apps, schema_editor): diff --git a/voucher/migrations/0010_rename_voucher.py b/voucher/migrations/0010_rename_voucher.py new file mode 100644 index 00000000..0528a5dd --- /dev/null +++ b/voucher/migrations/0010_rename_voucher.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('voucher', '0009_uselog_issuing_user'), + ] + + operations = [ + migrations.RenameModel('Wallet', 'VoucherWallet'), + migrations.RenameModel('UseLog', 'VoucherUseLog'), + + migrations.AlterField( + model_name='worklog', + name='wallet', + field=models.ForeignKey(to='voucher.VoucherWallet', related_name='worklogs'), + ), + migrations.AlterField( + model_name='voucheruselog', + name='wallet', + field=models.ForeignKey(to='voucher.VoucherWallet', related_name='uselogs'), + ), + migrations.AlterUniqueTogether( + name='voucherwallet', + unique_together=set([('user', 'semester')]), + ), + ] diff --git a/voucher/migrations/0011_coffee_vouchers.py b/voucher/migrations/0011_coffee_vouchers.py new file mode 100644 index 00000000..a0bcc5dc --- /dev/null +++ b/voucher/migrations/0011_coffee_vouchers.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_nfccard'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('voucher', '0010_rename_voucher'), + ] + + operations = [ + migrations.CreateModel( + name='CoffeeUseLog', + fields=[ + ('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)), + ('date_spent', models.DateTimeField(auto_now_add=True)), + ('comment', models.CharField(blank=True, max_length=100, null=True)), + ('vouchers', models.IntegerField()), + ('issuing_user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-date_spent'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='CoffeeWallet', + fields=[ + ('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)), + ('cached_balance', models.DecimalField(max_digits=8, decimal_places=2, editable=False, default=0)), + ('cached_vouchers', models.DecimalField(max_digits=8, decimal_places=2, editable=False, default=0)), + ('cached_vouchers_used', models.IntegerField(editable=False, default=0)), + ('card', models.ForeignKey(to='core.NfcCard')), + ('semester', models.ForeignKey(to='core.Semester')), + ], + options={ + 'ordering': ['card__card_uid'], + }, + ), + migrations.CreateModel( + name='CoffeeRegisterLog', + fields=[ + ('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)), + ('date_issued', models.DateTimeField(auto_now_add=True)), + ('comment', models.CharField(blank=True, max_length=100, null=True)), + ('vouchers', models.DecimalField(decimal_places=2, max_digits=8)), + ('issuing_user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ('wallet', models.ForeignKey(to='voucher.CoffeeWallet', related_name='registerlogs')), + ], + options={ + 'ordering': ['-date_issued'], + 'abstract': False, + }, + ), + migrations.AddField( + model_name='coffeeuselog', + name='wallet', + field=models.ForeignKey(to='voucher.CoffeeWallet', related_name='uselogs'), + ), + migrations.AlterUniqueTogether( + name='coffeewallet', + unique_together=set([('card', 'semester')]), + ), + ] diff --git a/voucher/models.py b/voucher/models.py index fc7eca71..62bddaf3 100644 --- a/voucher/models.py +++ b/voucher/models.py @@ -1,6 +1,6 @@ from django.db import models, transaction from django.db.models import Sum -from core.models import User, Card, Semester +from core.models import User, Card, Semester, NfcCard from decimal import Decimal import calendar @@ -11,23 +11,16 @@ class Wallet(models.Model): - user = models.ForeignKey(User) semester = models.ForeignKey(Semester) cached_balance = models.DecimalField(default=0, max_digits=8, decimal_places=2, editable=False) - cached_hours = models.DecimalField(default=0, max_digits=8, decimal_places=2, editable=False) cached_vouchers = models.DecimalField(default=0, max_digits=8, decimal_places=2, editable=False) cached_vouchers_used = models.IntegerField(default=0, editable=False) class Meta: - unique_together = ("user", "semester") - ordering = ['user__username'] + abstract = True - def calculate_balance(self): - hours = WorkLog.objects.filter(wallet=self).aggregate(sum=Sum('hours'))['sum'] or Decimal(0) - vouchers_earned = WorkLog.objects.filter(wallet=self).aggregate(sum=Sum('vouchers'))['sum'] or Decimal(0) - vouchers_used = UseLog.objects.filter(wallet=self).aggregate(sum=Sum('vouchers'))['sum'] or Decimal(0) + def _calculate_balance(self, vouchers_earned, vouchers_used): self.cached_balance = vouchers_earned - vouchers_used - self.cached_hours = hours self.cached_vouchers = vouchers_earned self.cached_vouchers_used = vouchers_used self.save() @@ -47,59 +40,106 @@ def _is_valid(self): is_valid = property(_is_valid) + +class VoucherWallet(Wallet): + user = models.ForeignKey(User) + cached_hours = models.DecimalField(default=0, max_digits=8, decimal_places=2, editable=False) + + class Meta: + unique_together = ("user", "semester") + ordering = ["user__username"] + + def calculate_balance(self): + vouchers_earned = WorkLog.objects.filter(wallet=self).aggregate(sum=Sum('vouchers'))['sum'] or Decimal(0) + vouchers_used = VoucherUseLog.objects.filter(wallet=self).aggregate(sum=Sum('vouchers'))['sum'] or Decimal(0) + hours = WorkLog.objects.filter(wallet=self).aggregate(sum=Sum('hours'))['sum'] or Decimal(0) + self.cached_hours = hours + return super()._calculate_balance(vouchers_earned, vouchers_used) + def __str__(self): return str(self.user) + " (" + str(self.semester) + ")" -class WorkLog(models.Model): - DEFAULT_VOUCHERS_PER_HOUR = 0.5 +class CoffeeWallet(Wallet): + card = models.ForeignKey(NfcCard) + + class Meta: + unique_together = ("card", "semester") + ordering = ["card__card_uid"] + + def calculate_balance(self): + vouchers_earned = CoffeeRegisterLog.objects.filter(wallet=self).aggregate(sum=Sum('vouchers'))['sum'] or Decimal(0) + vouchers_used = CoffeeUseLog.objects.filter(wallet=self).aggregate(sum=Sum('vouchers'))['sum'] or Decimal(0) + + return super()._calculate_balance(vouchers_earned, vouchers_used) + + def __str__(self): + return str(self.card) + " (" + str(self.semester) + ")" + + +class RegisterLog(models.Model): LOCKED_FOR_EDITING_AFTER_DAYS = 2 - wallet = models.ForeignKey(Wallet, related_name='worklogs') date_issued = models.DateTimeField(auto_now_add=True) - date_worked = models.DateField() - work_group = models.CharField(max_length=20) - hours = models.DecimalField(max_digits=8, decimal_places=2) - vouchers = models.DecimalField(max_digits=8, decimal_places=2, blank=True) issuing_user = models.ForeignKey(User) comment = models.CharField(max_length=100, null=True, blank=True) class Meta: + abstract = True ordering = ['-date_issued'] - def __str__(self): - return '%s %s %s hours' % (self.wallet, self.date_worked, self.hours) - def clean(self): - if self.hours <= 0: - raise ValidationError({'hours': _("Hours must be positive")}) - - if self.vouchers is None: - self.vouchers = self.calculate_vouchers(self.hours) - elif self.vouchers <= 0: + if self.vouchers <= 0: raise ValidationError({'vouchers': _("Vouchers must be positive")}) def save(self, *args, **kwargs): with transaction.atomic(): - super(WorkLog, self).save(*args, **kwargs) + super().save(*args, **kwargs) self.wallet.calculate_balance() - def calculate_vouchers(self, hours): - return round(float(hours) * self.DEFAULT_VOUCHERS_PER_HOUR, 2) - def is_locked(self): now = datetime.datetime.now(datetime.timezone.utc) return (now - self.date_issued).days > self.LOCKED_FOR_EDITING_AFTER_DAYS +class CoffeeRegisterLog(RegisterLog): + wallet = models.ForeignKey(CoffeeWallet, related_name='registerlogs') + vouchers = models.DecimalField(max_digits=8, decimal_places=2) + + def __str__(self): + return '%s %s vouchers' % (self.wallet, self.vouchers) + + +class WorkLog(RegisterLog): + DEFAULT_VOUCHERS_PER_HOUR = 0.5 + + wallet = models.ForeignKey(VoucherWallet, related_name='worklogs') + date_worked = models.DateField() + work_group = models.CharField(max_length=20) + hours = models.DecimalField(max_digits=8, decimal_places=2) + vouchers = models.DecimalField(max_digits=8, decimal_places=2, blank=True) + + def __str__(self): + return '%s %s %s hours' % (self.wallet, self.date_worked, self.hours) + + def clean(self): + if self.vouchers is None: + self.vouchers = self.calculate_vouchers(self.hours) + elif self.hours <= 0: + raise ValidationError({'hours': _("Hours must be positive")}) + + def calculate_vouchers(self, hours): + return round(float(hours) * self.DEFAULT_VOUCHERS_PER_HOUR, 2) + + class UseLog(models.Model): - wallet = models.ForeignKey(Wallet, related_name='uselogs') date_spent = models.DateTimeField(auto_now_add=True) issuing_user = models.ForeignKey(User) comment = models.CharField(max_length=100, null=True, blank=True) vouchers = models.IntegerField() class Meta: + abstract = True ordering = ['-date_spent'] def save(self, *args, **kwargs): @@ -114,3 +154,11 @@ def clean(self): def __str__(self): comment = ' (%s)' % self.comment if self.comment else '' return "%s - %s at %s%s" % (self.wallet, self.vouchers, self.date_spent.strftime('%Y-%m-%d %H:%M'), comment) + + +class VoucherUseLog(UseLog): + wallet = models.ForeignKey(VoucherWallet, related_name='uselogs') + + +class CoffeeUseLog(UseLog): + wallet = models.ForeignKey(CoffeeWallet, related_name='uselogs') diff --git a/voucher/permissions.py b/voucher/permissions.py index a4b4a2de..2665ffed 100644 --- a/voucher/permissions.py +++ b/voucher/permissions.py @@ -4,7 +4,7 @@ from voucher.models import WorkLog -def work_log_has_perm(request, obj, perm_action=None): +def register_log_has_perm(request, obj, perm_action=None): """Check for permission to modify work log. perm_action can be change or delete""" if obj.is_locked(): return False @@ -19,7 +19,7 @@ def work_log_has_perm(request, obj, perm_action=None): return True -class WorkLogPermissions(BasePermission): +class RegisterLogPermissions(BasePermission): def has_object_permission(self, request, view, obj): if view.action not in ['update', 'partial_update', 'destroy']: return True @@ -28,4 +28,4 @@ def has_object_permission(self, request, view, obj): if modelperm.has_permission(request, view): return True - return work_log_has_perm(request, obj) + return register_log_has_perm(request, obj) diff --git a/voucher/rest.py b/voucher/rest.py index 8400348f..c2205c24 100644 --- a/voucher/rest.py +++ b/voucher/rest.py @@ -1,3 +1,4 @@ +from datetime import datetime from collections import OrderedDict from rest_framework import viewsets from rest_framework import mixins @@ -11,22 +12,23 @@ from decimal import Decimal from voucher.serializers import * -from voucher.models import Wallet, WorkLog, UseLog -from voucher.filters import UseLogFilter, WalletFilter, WorkLogFilter -from voucher.permissions import WorkLogPermissions +from voucher.models import VoucherWallet, CoffeeWallet, WorkLog, VoucherUseLog, CoffeeRegisterLog, CoffeeUseLog +from voucher.filters import WorkLogFilter, VoucherUseLogFilter, VoucherWalletFilter, \ + CoffeeWalletFilter, CoffeeRegisterLogFilter, CoffeeUseLogFilter +from voucher.permissions import RegisterLogPermissions from voucher.utils import get_valid_semesters from core.utils import get_semester_of_date -from core.models import Semester +from core.models import Semester, NfcCard -class WalletViewSet(viewsets.ReadOnlyModelViewSet): - serializer_class = WalletSerializer - filter_class = WalletFilter +class VoucherWalletViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = VoucherWalletSerializer + filter_class = VoucherWalletFilter def get_queryset(self): - queryset = Wallet.objects.all() + queryset = VoucherWallet.objects.all() if self.action == 'stats': - return queryset.order_by() + return queryset return queryset.prefetch_related('user', 'semester') @list_route(methods=['get']) @@ -64,24 +66,62 @@ def stats(self, request): row['semester'] = semesters[row['semester']] data[row['semester'].id].update(row) + serializer = VoucherWalletStatsSerializer(data.values(), many=True) + return Response(serializer.data) + + +class CoffeeWalletViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = CoffeeWalletSerializer + filter_class = CoffeeWalletFilter + + def get_queryset(self): + queryset = CoffeeWallet.objects.all() + if self.action == 'stats': + return queryset + return queryset.prefetch_related('card', 'semester') + + @list_route(methods=['get']) + def stats(self, request): + # pull stuff from main table + wallets1 = self.get_queryset() \ + .values('semester') \ + .order_by('-semester__year', '-semester__semester') \ + .annotate(sum_balance=Sum('cached_balance'), + count_users=Count('user', distinct=True)) + + # pull stuff from uselogs + wallets2 = self.get_queryset() \ + .values('semester') \ + .annotate(sum_vouchers_used=Sum('uselogs__vouchers')) + + semesters = {} + for semester in Semester.objects.all(): + semesters[semester.id] = semester + + data = OrderedDict() + for row in wallets1: + row['semester'] = semesters[row['semester']] + data[row['semester'].id] = row + for row in wallets2: + row['semester'] = semesters[row['semester']] + data[row['semester'].id].update(row) + serializer = WalletStatsSerializer(data.values(), many=True) return Response(serializer.data) -class UserViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): +class UserViewSet(viewsets.GenericViewSet): queryset = User.objects.all() lookup_field = 'username' permission_classes = (IsAuthenticatedOrReadOnly,) def get_serializer_class(self): - if self.action in ['create']: - return UserCreateSerializer return UseVouchersSerializer @detail_route(methods=['post']) def use_vouchers(self, request, username=None): user = self.get_object() - wallets = Wallet.objects.filter(user=user, semester__in=get_valid_semesters()).order_by('semester') + wallets = VoucherWallet.objects.filter(user=user, semester__in=get_valid_semesters()).order_by('semester') pending_transactions = [] data = UseVouchersSerializer(data=request.data, context=self) @@ -101,7 +141,7 @@ def use_vouchers(self, request, username=None): continue available_vouchers += wallet.cached_balance - new_log_entry = UseLog(issuing_user=request.user, + new_log_entry = VoucherUseLog(issuing_user=request.user, wallet=wallet, comment=data.data['comment'], vouchers=min(vouchers_to_spend, wallet.cached_balance)) @@ -118,12 +158,99 @@ def use_vouchers(self, request, username=None): for p in pending_transactions: p.save() - return Response([UseLogSerializer(p).data for p in pending_transactions], status=status.HTTP_201_CREATED) + return Response([VoucherUseLogSerializer(p).data for p in pending_transactions], status=status.HTTP_201_CREATED) + + +class CardViewSet(viewsets.GenericViewSet): + queryset = NfcCard.objects.all() + lookup_field = 'card_uid' + permission_classes = (IsAuthenticatedOrReadOnly,) + + def get_serializer_class(self): + return UseVouchersSerializer + + @detail_route(methods=['post']) + def use_vouchers(self, request, card_uid): + card = self.get_object() + wallets = CoffeeWallet.objects.filter(card=card, semester__in=get_valid_semesters()).order_by('semester') + pending_transactions = [] + + data = UseCoffeeVouchersSerializer(data=request.data, context=self) + data.is_valid(raise_exception=True) + + vouchers_to_spend = data.validated_data['vouchers'] + + # we are in a risk of a race condition if multiple requests occur at the same time + # leaving a negative balance - but the risk is low and it is not critical, so we have + # not tried to properly solve it + available_vouchers = 0 + for wallet in wallets: + if vouchers_to_spend == 0: + break + + if wallet.calculate_balance() <= 0: + continue + + available_vouchers += wallet.cached_balance + new_log_entry = CoffeeUseLog(issuing_user=request.user, + wallet=wallet, + comment=data.data['comment'], + vouchers=min(vouchers_to_spend, wallet.cached_balance)) + + vouchers_to_spend -= new_log_entry.vouchers + pending_transactions.append(new_log_entry) + + if vouchers_to_spend != 0: + return Response( + {'error': _('User does not have enough vouchers. Currently having %d available.' % available_vouchers)}, + status=status.HTTP_402_PAYMENT_REQUIRED + ) + + for p in pending_transactions: + p.save() + + return Response([CoffeeUseLogSerializer(p).data for p in pending_transactions], status=status.HTTP_201_CREATED) + + +class CoffeeRegisterLogViewSet(viewsets.ModelViewSet): + queryset = CoffeeRegisterLog.objects.prefetch_related('wallet__card', 'wallet__semester', 'issuing_user').all() + permission_classes = (IsAuthenticatedOrReadOnly, RegisterLogPermissions,) + filter_class = CoffeeRegisterLogFilter + + def get_serializer_class(self): + if self.action in ['create']: + return RegisterLogCreateSerializer + else: + return RegisterLogSerializer + + def create(self, request, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + card_uid = serializer.data['card'].strip().lower() + card = NfcCard.objects.get_or_create(card_uid=card_uid)[0] + if not card: + raise ValidationError(detail=_('Card %(card)s not found') % {'card': serializer.data['card_uid']}) + + wallet = CoffeeWallet.objects.get_or_create( + card=card, semester=get_semester_of_date(datetime.now().date()))[0] + + registerlog = CoffeeRegisterLog( + wallet=wallet, + vouchers=Decimal(serializer.data['vouchers']), + issuing_user=request.user, + comment=serializer.data['comment'] + ) + + registerlog.clean() + registerlog.save() + return Response(RegisterLogSerializer(registerlog, context={'request': self.request}).data, + status=status.HTTP_201_CREATED) class WorkLogViewSet(viewsets.ModelViewSet): queryset = WorkLog.objects.prefetch_related('wallet__user', 'wallet__semester', 'issuing_user').all() - permission_classes = (IsAuthenticatedOrReadOnly, WorkLogPermissions,) + permission_classes = (IsAuthenticatedOrReadOnly, RegisterLogPermissions,) filter_class = WorkLogFilter def get_serializer_class(self): @@ -142,7 +269,7 @@ def create(self, request, **kwargs): raise ValidationError(detail=_('User %(user)s not found') % {'user': serializer.data['user']}) date = serializer.validated_data['date_worked'] - wallet = Wallet.objects.get_or_create(user=user, semester=get_semester_of_date(date))[0] + wallet = VoucherWallet.objects.get_or_create(user=user, semester=get_semester_of_date(date))[0] worklog = WorkLog( wallet=wallet, @@ -159,10 +286,16 @@ def create(self, request, **kwargs): status=status.HTTP_201_CREATED) -class UseLogViewSet(viewsets.ReadOnlyModelViewSet): - serializer_class = UseLogSerializer - queryset = UseLog.objects.prefetch_related('wallet__user', 'wallet__semester').all() - filter_class = UseLogFilter +class VoucherUseLogViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = VoucherUseLogSerializer + queryset = VoucherUseLog.objects.prefetch_related('wallet__user', 'wallet__semester').all() + filter_class = VoucherUseLogFilter + + +class CoffeeUseLogViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = CoffeeUseLogSerializer + queryset = CoffeeUseLog.objects.prefetch_related('wallet__card', 'wallet__semester').all() + filter_class = CoffeeUseLogFilter class WorkGroupsViewSet(viewsets.ReadOnlyModelViewSet): diff --git a/voucher/serializers.py b/voucher/serializers.py index b7f5c3b6..6981676e 100644 --- a/voucher/serializers.py +++ b/voucher/serializers.py @@ -2,33 +2,67 @@ from django.core import validators from django.utils.translation import ugettext_lazy as _ -from voucher.models import UseLog, Wallet, WorkLog +from voucher.models import UseLog, Wallet, WorkLog, VoucherWallet, CoffeeWallet, CoffeeRegisterLog, VoucherUseLog, \ + CoffeeUseLog from voucher.validators import valid_date_worked, ValidVouchers -from voucher.permissions import work_log_has_perm -from core.models import User -from core.serializers import UserSimpleSerializer, SemesterSerializer +from voucher.permissions import register_log_has_perm +from core.models import User, NfcCard +from core.serializers import UserSimpleSerializer, SemesterSerializer, NfcCardSerializer from core.utils import get_semester_of_date class WalletSerializer(serializers.ModelSerializer): - user = UserSimpleSerializer() semester = SemesterSerializer() + +class VoucherWalletSerializer(WalletSerializer): + user = UserSimpleSerializer() + class Meta: - model = Wallet + model = VoucherWallet fields = ('id', 'user', 'semester', 'cached_balance', 'cached_hours', 'cached_vouchers', 'cached_vouchers_used', 'is_valid',) +class CoffeeWalletSerializer(WalletSerializer): + card = NfcCardSerializer() + + class Meta: + model = CoffeeWallet + fields = ('id', 'card', 'semester', 'cached_balance', 'cached_vouchers', + 'cached_vouchers_used', 'is_valid',) + + class UseLogSerializer(serializers.ModelSerializer): - wallet = WalletSerializer() issuing_user = UserSimpleSerializer(read_only=True) + +class VoucherUseLogSerializer(UseLogSerializer): + wallet = VoucherWalletSerializer() + class Meta: - model = UseLog + model = VoucherUseLog fields = ('id', 'wallet', 'date_spent', 'issuing_user', 'comment', 'vouchers',) +class CoffeeUseLogSerializer(UseLogSerializer): + wallet = CoffeeWalletSerializer() + + class Meta: + model = CoffeeUseLog + fields = ('id', 'wallet', 'date_spent', 'issuing_user', 'comment', 'vouchers',) + + +class RegisterLogCreateSerializer(serializers.Serializer): + card = serializers.CharField(max_length=8, + help_text=_('Required. 8 characters. Letters and digits only):'), + validators=[ + validators.RegexValidator(r'^[\w]+$', _('Enter a valid username.'), 'invalid') + ]) + vouchers = serializers.DecimalField(max_digits=8, decimal_places=2, min_value=0.01) + comment = serializers.CharField(max_length=100, allow_blank=True, default=None) + + class WorkLogCreateSerializer(serializers.Serializer): user = serializers.CharField(max_length=30, help_text=_('Required. 30 characters or fewer. Letters, digits and ' @@ -42,18 +76,34 @@ class WorkLogCreateSerializer(serializers.Serializer): comment = serializers.CharField(max_length=100, allow_blank=True, default=None) -class WorkLogSerializer(serializers.ModelSerializer): - wallet = WalletSerializer(read_only=True) +class RegisterLogSerializer(serializers.ModelSerializer): + wallet = CoffeeWalletSerializer(read_only=True) issuing_user = UserSimpleSerializer(read_only=True) - date_worked = serializers.DateField(validators=[valid_date_worked]) can_edit = serializers.SerializerMethodField('_can_edit') can_delete = serializers.SerializerMethodField('_can_delete') def _can_edit(self, instance): - return work_log_has_perm(self.context['request'], instance, 'change') + return register_log_has_perm(self.context['request'], instance, 'change') def _can_delete(self, instance): - return work_log_has_perm(self.context['request'], instance, 'delete') + return register_log_has_perm(self.context['request'], instance, 'delete') + + class Meta: + model = CoffeeRegisterLog + fields = ('id', 'wallet', 'date_issued', 'vouchers', 'issuing_user', 'comment', 'can_edit', 'can_delete',) + read_only_fields = ('id', 'wallet', 'date_issued', 'issuing_user',) + + def update(self, instance, validated_data): + if 'vouchers' in validated_data or validated_data['vouchers'] != instance.vouchers: + instance.vouchers = int(validated_data['vouchers']) + validated_data.pop('vouchers', None) + + return super().update(instance, validated_data) + + +class WorkLogSerializer(RegisterLogSerializer): + wallet = VoucherWalletSerializer(read_only=True) + date_worked = serializers.DateField(validators=[valid_date_worked]) class Meta: model = WorkLog @@ -79,23 +129,29 @@ class UseVouchersSerializer(serializers.ModelSerializer): vouchers = serializers.IntegerField(validators=[ValidVouchers()]) class Meta: - model = UseLog + model = VoucherUseLog fields = ('vouchers', 'comment',) extra_kwargs = {'comment': {'default': None}} -class UserCreateSerializer(serializers.ModelSerializer): +class UseCoffeeVouchersSerializer(serializers.ModelSerializer): + vouchers = serializers.IntegerField(validators=[ValidVouchers()]) + class Meta: - model = User - fields = ('username', 'realname',) + model = CoffeeUseLog + fields = ('vouchers', 'comment',) + extra_kwargs = {'comment': {'default': None}} class WalletStatsSerializer(serializers.Serializer): semester = SemesterSerializer() sum_balance = serializers.DecimalField(max_digits=8, decimal_places=2) - sum_hours = serializers.DecimalField(max_digits=8, decimal_places=2) sum_vouchers = serializers.DecimalField(max_digits=8, decimal_places=2) sum_vouchers_used = serializers.IntegerField() + + +class VoucherWalletStatsSerializer(WalletStatsSerializer): + sum_hours = serializers.DecimalField(max_digits=8, decimal_places=2) count_users = serializers.IntegerField() diff --git a/voucher/urls.py b/voucher/urls.py index 5ff022b9..a439345b 100644 --- a/voucher/urls.py +++ b/voucher/urls.py @@ -3,8 +3,12 @@ # SharedAPIRootRouter is automatically imported in global urls config router = SharedAPIRootRouter() -router.register(r'voucher/wallets', WalletViewSet, base_name='voucher_wallets') -router.register(r'voucher/users', UserViewSet, base_name='voucher_users') +router.register(r'voucher/wallets', VoucherWalletViewSet, base_name='voucher_wallets') +router.register(r'coffee/wallets', CoffeeWalletViewSet, base_name='coffee_wallets') router.register(r'voucher/worklogs', WorkLogViewSet) -router.register(r'voucher/uselogs', UseLogViewSet) +router.register(r'coffee/registerlogs', CoffeeRegisterLogViewSet) +router.register(r'voucher/uselogs', VoucherUseLogViewSet) +router.register(r'coffee/uselogs', CoffeeUseLogViewSet) +router.register(r'voucher/users', UserViewSet, base_name='voucher_users') +router.register(r'coffee/cards', CardViewSet, base_name='coffee_cards') router.register(r'voucher/workgroups', WorkGroupsViewSet, base_name='voucher_workgroups')