From 9ce52a7cc185fffd102b071cd678f3408cdbf3d4 Mon Sep 17 00:00:00 2001 From: DJ Gillespie Date: Mon, 24 Feb 2020 19:41:19 -0700 Subject: [PATCH] refactor for slice and rule functionality. general cleanup --- api/__pycache__/serializers.cpython-37.pyc | Bin 3030 -> 5284 bytes api/serializers.py | 103 +++++++++++++++++++-- connection/admin.py | 6 +- connection/models.py | 19 +++- connection/views.py | 28 ++++++ core/__pycache__/settings.cpython-37.pyc | Bin 2722 -> 2727 bytes core/__pycache__/urls.cpython-37.pyc | Bin 1656 -> 1806 bytes core/settings.py | 2 +- core/urls.py | 9 +- db.sqlite3 | Bin 262144 -> 274432 bytes qrtr_account/admin.py | 26 +++++- qrtr_account/apps.py | 2 +- qrtr_account/models.py | 80 +++++++++++++++- qrtr_account/views.py | 60 +++++++++--- requirements.txt | 1 + user/__pycache__/models.cpython-37.pyc | Bin 557 -> 698 bytes user/__pycache__/views.cpython-37.pyc | Bin 981 -> 1017 bytes user/models.py | 6 +- user/views.py | 2 +- 19 files changed, 308 insertions(+), 36 deletions(-) diff --git a/api/__pycache__/serializers.cpython-37.pyc b/api/__pycache__/serializers.cpython-37.pyc index 696fbe152abede1eda83c8f46dece799bd7d9faa..57847c6bbda81804378c7e86d18da1bf2fc96209 100644 GIT binary patch literal 5284 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 X8=c`sfqv!N&`l|L{z%O0$ diff --git a/api/serializers.py b/api/serializers.py index 59858e5..38c2861 100755 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,8 +1,8 @@ from django.contrib.auth.models import Group from django.contrib.auth import get_user_model from rest_framework import serializers -from qrtr_account.models import Account, Bank, Institution, Transaction -from connection.models import Connection +from qrtr_account.models import Account, Bank, Institution, Transaction, Slice, Rule +from connection.models import Connection, ConnectionType class UserSerializer(serializers.HyperlinkedModelSerializer): @@ -21,20 +21,92 @@ class GroupSerializer(serializers.HyperlinkedModelSerializer): class AccountSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Account - fields = ['url','owner', 'name', 'admin_users', 'view_users'] + fields = ['url', 'owner', 'name', 'admin_users', 'view_users'] + + +class ConnectionTypeSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = ConnectionType + fields = ['url', 'name', 'filename'] + extra_kwargs = { + 'name': {'read_only': True}, + 'filename': {'read_only': True} + } + + +class ConnectionSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Connection + fields = ['url', 'name', 'type', 'credentials'] + extra_kwargs = { + 'type': {'write_only': True}, + 'credentials': {'write_only': True} + } class BankSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Bank - fields = ['url','qrtr_account', 'connection', 'institution', 'nickname', - 'balance', 'ac_type', 'ac_subtype'] + fields = [ + 'url', + 'qrtr_account', + 'connection', + 'institution', + 'nickname', + 'balance', + 'ac_type', + 'ac_subtype', + ] + extra_kwargs = { + 'balance': {'read_only': True}, + 'connection': {'read_only': True}, + 'institution': {'read_only': True}, + 'ac_type': {'read_only': True}, + 'ac_subtype': {'read_only': True} + } + + +class BankSerializerPOST(BankSerializer): + """Separate Serializer for POST requests to create a new bank. This adds + a new field called connection_details that is used to create a new + connection record to go with the new Bank. This field is only allowed on + POST because we don't want to expose this information to the user, or allow + them to change it b/c that could lead to an integrity problem, breaking + their bank functionality. + """ + # connection_details = serializers.JSONField( + # write_only=True, + # required=True, + # initial={ + # "type": f"{' OR '.join(ConnectionType.objects.all().values_list('name', flat=True))}", + # "credentials": {}}) + + class Meta: + model = Bank + fields = [ + 'url', + 'qrtr_account', + 'connection', + 'institution', + 'nickname', + 'balance', + 'ac_type', + 'ac_subtype', + # 'connection_details' + ] + extra_kwargs = { + 'balance': {'read_only': True}, + # 'connection': {'read_only': True}, + 'institution': {'read_only': True}, + 'ac_type': {'read_only': True}, + 'ac_subtype': {'read_only': True} + } class InstitutionSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Institution - fields = ['url','name'] + fields = ['url', 'name'] class TransactionSerializer(serializers.HyperlinkedModelSerializer): @@ -43,7 +115,20 @@ class TransactionSerializer(serializers.HyperlinkedModelSerializer): fields = ['url', 'datetime', 'Bank', 'details'] -class ConnectionSerializer(serializers.HyperlinkedModelSerializer): +class SliceSerializer(serializers.HyperlinkedModelSerializer): class Meta: - model = Connection - fields = ['url', 'name'] \ No newline at end of file + model = Slice + fields = ['url', 'name', 'icon', 'budget', 'slice_of'] + + +class RuleSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Rule + fields = [ + 'url', + 'name', + 'kind', + 'when_to_run', + 'amount', + 'source', + 'destination'] diff --git a/connection/admin.py b/connection/admin.py index e03e319..e8ded83 100644 --- a/connection/admin.py +++ b/connection/admin.py @@ -1,6 +1,10 @@ from django.contrib import admin -from .models import Connection +from .models import Connection, ConnectionType @admin.register(Connection) class ConnectionAdmin(admin.ModelAdmin): pass + +@admin.register(ConnectionType) +class ConnectionTypeAdmin(admin.ModelAdmin): + pass diff --git a/connection/models.py b/connection/models.py index 744b447..32aca17 100644 --- a/connection/models.py +++ b/connection/models.py @@ -2,10 +2,21 @@ from django.db import models import jsonfield -class Connection(models.Model): +class ConnectionType(models.Model): name = models.CharField(max_length=255) - connection_path = models.CharField(max_length=255) - credentials = jsonfield.JSONField() - + filename = models.CharField(max_length=255, unique=True) + + def __str__(self): + return f"{self.name}" + + +class Connection(models.Model): + name = models.CharField(max_length=255) + type = models.ForeignKey( + ConnectionType, + on_delete=models.CASCADE, + null=True) + credentials = jsonfield.JSONField() + def __str__(self): return f"{self.name}" diff --git a/connection/views.py b/connection/views.py index 91ea44a..ac9d43f 100644 --- a/connection/views.py +++ b/connection/views.py @@ -1,3 +1,31 @@ from django.shortcuts import render +from rest_framework import status, viewsets +from rest_framework.response import Response +from .models import Connection +from .serializers import ConnectionSerializer +from rest_framework.decorators import action +import plaid # Create your views here. + + +class ConnectionViewSet(viewsets.ModelViewSet): + """API endpoint that allows connections to be seen or created + """ + queryset = Connection.objects.all() + serializer_class = ConnectionSerializer + # Make connections somewhat immutable from the users perspective + http_method_names = [ + 'get', + 'post', + 'delete', + 'options'] + + @action(detail=False, methods=['post'], url_path='oauth/plaid') + def oauth(self, request, public_token=None): + if public_token is None: + return Response( + status=status.HTTP_400_BAD_REQUEST, + data="ERROR: missing public_token") + print(request) + return Response(200) diff --git a/core/__pycache__/settings.cpython-37.pyc b/core/__pycache__/settings.cpython-37.pyc index 476bff1a2ed400db56cf9a45e6fb09d5c64bb92c..51f248209e1c555bb82cc7a8d95a0a76c9e18796 100644 GIT binary patch delta 251 zcmW-cJ5Iwu7=-;V%Yq%2K)}RKc!Yps5?;aKT_I`;q#z`IBt$XM%9JT-at6x{MGBzd z0KNftpnC6$W%sb`@gdu$lxKkl7SD!=f8{Mwt&Z@sHOw|YJn delta 245 zcmW-cyG{Z@7)JM>3}be5yr3&A;0+hKgMimkOB)L-|Ad6%Ba^kHq_MT#Y>SE7_yGF` z#>cSmIjo%UCFfM9`96b>z}y<6J-6?dpK&)aA^j!wk+L59$R(5Nv(2+cfmc!FHH0Xk zjEeIf_Hlqi>$AZTs#fqij(G!-^&nBh3F>H|X=O}kEu=Xg@8XgRkX&?xjuNi84DsMKud@ctD0>xOl5{p% anF|%UOS35b^&f>)>5Z4JJb8&PoI?j0Qq7axM4YgOSdg0*f;)ql zz36Xf*|HxIwCzu{iq4T5{SJroyzl$*ZqB#-tQOA{CBl%YO&hoKFvEON@jpZ}r{sD^ zFU6auUfhoNbX!LZh>OIMu?;X5CKf?jvc#hthDg8?$N@({2{;OBz%pn7D-a7<1%1Ul z)_?<}lboQ*9Sb~;0f*yQ$B7w>4Q$Rr#7hz{H9N&Hlo>_n^vQ1&l+zm9NZ7P-Z-&t%hzEva@@8z7Mi%{xYk%~4jk*@(E1D!sFkvQ zg+E57@9|Y}sE}_Ms{R|FLmB^rFAD`~6nsgzD(@5Ikc?vVhW4Z|c{dI#3o0^5YH%|8 E1+emXj{pDw delta 367 zcmeC<`@y5`#LLUY00a{z-id8xXJB{?;=llq&)@*W#e5UhBkLI%QrQ;>E@X%jN)=il zoWh>UxsVAc24Zuha>3c0sXTBtS1K=@&7H~zXY-`;!`Zy40$?_43STNqs!*0lGh;JT zDr*XVDoct$ieQRRFH?$eib$_DGf+?zC@9v23#G-Wo&F&<`OWSy+U;x@UAMR@W878`Evs+B^Yp9O6ALo+%MA5zF#{QXni7+bu$nOnPyWs7Jvo3)f>C^OCYuhMJTO#>S%#ehNFzZuK1LBn5f&as0AANmhX4Qo diff --git a/core/settings.py b/core/settings.py index fcecb1d..d2999a0 100644 --- a/core/settings.py +++ b/core/settings.py @@ -25,7 +25,7 @@ SECRET_KEY = 'jc@r$_x4$mp-b84&+m3s@hm7kpl$br-wa&50*&xjx^^fddg6q$' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['*'] # Application definition diff --git a/core/urls.py b/core/urls.py index b81412d..a9ed67f 100644 --- a/core/urls.py +++ b/core/urls.py @@ -23,7 +23,9 @@ from qrtr_account.views import (AccountViewSet, BankViewSet, InstitutionViewSet, TransactionViewSet, - ConnectionViewSet) + SliceViewSet, + ConnectionViewSet, + ConnectionTypeViewSet) router = routers.DefaultRouter() @@ -33,7 +35,9 @@ router.register(r'accounts',AccountViewSet) router.register(r'banks',BankViewSet) router.register(r'institutions',InstitutionViewSet) router.register(r'transactions',TransactionViewSet) -router.register(r'connections',ConnectionViewSet) +router.register(r'slices',SliceViewSet) +#router.register(r'connections',ConnectionViewSet) +router.register(r'connectiontypes',ConnectionTypeViewSet) # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. @@ -42,6 +46,7 @@ apipatterns = [ path('', include(router.urls)), path('auth/', include('rest_framework.urls', namespace='rest_framework'), name='auth'), path('auth/registration/', include('rest_auth.registration.urls'), name='register'), + path('connection/', include('connection.urls'), name='Connection Settings'), ] urlpatterns = [ diff --git a/db.sqlite3 b/db.sqlite3 index 16e4ad09d616d224e3ff18872dbcd59e2907203e..90a67696fee7ee08dc29982805ddf9def382df70 100644 GIT binary patch delta 5582 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 Jf0_Ne@ISmJP1yhd diff --git a/qrtr_account/admin.py b/qrtr_account/admin.py index 8c38f3f..5b6d197 100644 --- a/qrtr_account/admin.py +++ b/qrtr_account/admin.py @@ -1,3 +1,27 @@ from django.contrib import admin +from .models import Account, Institution, Bank, Transaction, Slice -# Register your models here. + +@admin.register(Account) +class AccountAdmin(admin.ModelAdmin): + pass + + +@admin.register(Institution) +class InstitutionAdmin(admin.ModelAdmin): + pass + + +@admin.register(Bank) +class BankAdmin(admin.ModelAdmin): + pass + + +@admin.register(Transaction) +class TransactionAdmin(admin.ModelAdmin): + pass + + +@admin.register(Slice) +class SliceAdmin(admin.ModelAdmin): + pass diff --git a/qrtr_account/apps.py b/qrtr_account/apps.py index 8888794..527dfe7 100644 --- a/qrtr_account/apps.py +++ b/qrtr_account/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class QrtrAccountConfig(AppConfig): - name = 'qrtr_account' + name = 'QRTR Account' diff --git a/qrtr_account/models.py b/qrtr_account/models.py index 6f77a9d..ae21ff2 100644 --- a/qrtr_account/models.py +++ b/qrtr_account/models.py @@ -1,24 +1,34 @@ from django.db import models from user.models import User +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType import jsonfield class Account(models.Model): - owner = models.ForeignKey(User, on_delete=models.CASCADE, + owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="owned_accounts") admin_users = models.ManyToManyField(User, related_name="admin_accounts", blank=True) - view_users = models.ManyToManyField(User, related_name="view_accounts", + view_users = models.ManyToManyField(User, related_name="view_accounts", blank=True) name = models.CharField(max_length=250) + @property + def qid(self): + return f"A{self.pk}" + def __str__(self): - return f"{self.owner}" + return f"{self.name}" class Institution(models.Model): name = models.CharField(max_length=255) - + + @property + def qid(self): + return f"I{self.pk}" + def __str__(self): return f"{self.name}" @@ -34,15 +44,77 @@ class Bank(models.Model): ac_type = models.CharField(max_length=250, blank=True) ac_subtype = models.CharField(max_length=250, blank=True) + @property + def qid(self): + return f"B{self.pk}" + def __str__(self): return f"{self.nickname}" +class Slice(models.Model): + name = models.CharField(max_length=250) + icon = models.CharField(max_length=250) + budget = models.DecimalField(decimal_places=3, max_digits=100) + avail_parents = models.Q( + app_label='qrtr_account', + model='bank') | models.Q( + app_label='qrtr_account', + model='slice') + parent_type = models.ForeignKey( + ContentType, + limit_choices_to=avail_parents, + on_delete=models.CASCADE) + parent_id = models.PositiveIntegerField() + slice_of = GenericForeignKey('parent_type', 'parent_id') + + @property + def qid(self): + return f"S{self.pk}" + + def __str__(self): + return f"{self.name}" + + +class Schedule(models.Model): + name = models.CharField(max_length=255) + # TODO: Hook this up to an events system for Payday scheduling + + +class Rule(models.Model): + kinds = [("refill", "Refill"), ("increase", "Increase"), ("goal", "Goal")] + kind = models.CharField(choices=kinds, max_length=255) + when_to_run = models.ForeignKey(Schedule, on_delete=models.CASCADE) + amount_type = models.CharField( + choices=[ + ("quantity", + "Quantity"), + ("round", + "Round"), + ("percent", + "Percent")], + default="quantity", + max_length=20) + amount = models.DecimalField(decimal_places=3, max_digits=100) + source = models.ForeignKey( + Slice, + on_delete=models.CASCADE, + related_name="rule_source_set") + destination = models.ForeignKey( + Slice, + on_delete=models.CASCADE, + related_name="rule_destination_set") + + class Transaction(models.Model): datetime = models.DateTimeField() Bank = models.ForeignKey(Bank, on_delete=models.CASCADE, related_name='transactions') details = jsonfield.JSONField() + @property + def qid(self): + return f"T{self.pk}" + def __str__(self): return f"{self.Bank} - {self.datetime}" diff --git a/qrtr_account/views.py b/qrtr_account/views.py index 378c726..9459bd2 100644 --- a/qrtr_account/views.py +++ b/qrtr_account/views.py @@ -1,12 +1,15 @@ from django.shortcuts import render -from rest_framework import viewsets -from .models import Account, Bank, Institution, Transaction -from connection.models import Connection +from rest_framework import viewsets, mixins +from .models import Account, Bank, Institution, Transaction, Slice, Rule +from connection.models import Connection, ConnectionType from api.serializers import (AccountSerializer, - BankSerializer, + BankSerializer, BankSerializerPOST, InstitutionSerializer, TransactionSerializer, - ConnectionSerializer) + ConnectionSerializer, + ConnectionTypeSerializer, + SliceSerializer, + RuleSerializer) class AccountViewSet(viewsets.ModelViewSet): @@ -20,20 +23,55 @@ class BankViewSet(viewsets.ModelViewSet): """API endpoint that allows Banks to be viewed or edited """ queryset = Bank.objects.all() - serializer_class = BankSerializer + # serializer_class = BankSerializer -class InstitutionViewSet(viewsets.ModelViewSet): - """API endpoint that allows Banks to be viewed or edited + def get_serializer_class(self): + if self.action == 'create': + return BankSerializerPOST + return BankSerializer + + +class InstitutionViewSet(viewsets.ReadOnlyModelViewSet): + """API endpoint that allows Banks to be viewed. """ queryset = Institution.objects.all() serializer_class = InstitutionSerializer -class TransactionViewSet(viewsets.ModelViewSet): - """API endpoint that allows Banks to be viewed or edited + +class TransactionViewSet(viewsets.ReadOnlyModelViewSet): + """API endpoint that allows Banks to be viewed. """ queryset = Transaction.objects.all() serializer_class = TransactionSerializer + +class ConnectionTypeViewSet(viewsets.ModelViewSet): + queryset = ConnectionType.objects.all() + serializer_class = ConnectionTypeSerializer + + class ConnectionViewSet(viewsets.ModelViewSet): + """API endpoint that allows connections to be seen or created + """ queryset = Connection.objects.all() - serializer_class = ConnectionSerializer \ No newline at end of file + serializer_class = ConnectionSerializer + # Make connections somewhat immutable from the users perspective + http_method_names = [ + 'get', + 'post', + 'delete', + 'options'] + + +class SliceViewSet(viewsets.ReadOnlyModelViewSet): + """API endpoint that allows Banks to be viewed. + """ + queryset = Slice.objects.all() + serializer_class = SliceSerializer + + +class RuleViewSet(viewsets.ReadOnlyModelViewSet): + """API endpoint that allows Banks to be viewed. + """ + queryset = Rule.objects.all() + serializer_class = RuleSerializer diff --git a/requirements.txt b/requirements.txt index 148e247..1c355a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ django-rest-framework==0.1.0 djangorestframework==3.10.3 pytz==2019.3 sqlparse==0.3.0 +plaid-python>=3.0.0 diff --git a/user/__pycache__/models.cpython-37.pyc b/user/__pycache__/models.cpython-37.pyc index 2d24f48857ccac86fd0663be0bfe0d03e1d6323e..2b28a91a9959968843a5c967d02211dfe3266f2d 100644 GIT binary patch delta 365 zcmZ3>vWu0^iI5FiKLVvmn6E-8wS2U!cz##qG@ z3N%&^D&eQeIXRxu%H|eFK~a7|YEemL5iiIbepJIi`inp&7O?;c5H4l|5*&;^3DPeuPA$^U%}+_qDb_2f zyu}_LUtCfYAJ0Dd0HYKm`{dhJ!o?thgOQJEav+nv0LVsU Km6I1RNdW-5Z7tgX diff --git a/user/__pycache__/views.cpython-37.pyc b/user/__pycache__/views.cpython-37.pyc index 452fec668e8fe2d6dd8080a6ad7ada2d69849f6c..411f03eb750b35bd205c17c5776e4b05f7afbd34 100644 GIT binary patch delta 109 zcmcc0{*#^8iI=oY6Im1QQU7H{Ta5@TfKovg@QCl)0Vl$x00pO;hVo1c=J6PB4;9-LY- Ic|Nll0Qr0(EC2ui delta 72 zcmey#ewCfqiI