diff --git a/api/__pycache__/serializers.cpython-37.pyc b/api/__pycache__/serializers.cpython-37.pyc index 696fbe1..57847c6 100644 Binary files a/api/__pycache__/serializers.cpython-37.pyc and b/api/__pycache__/serializers.cpython-37.pyc differ 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 476bff1..51f2482 100644 Binary files a/core/__pycache__/settings.cpython-37.pyc and b/core/__pycache__/settings.cpython-37.pyc differ diff --git a/core/__pycache__/urls.cpython-37.pyc b/core/__pycache__/urls.cpython-37.pyc index 9175ae8..100f936 100644 Binary files a/core/__pycache__/urls.cpython-37.pyc and b/core/__pycache__/urls.cpython-37.pyc differ 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 16e4ad0..90a6769 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ 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 2d24f48..2b28a91 100644 Binary files a/user/__pycache__/models.cpython-37.pyc and b/user/__pycache__/models.cpython-37.pyc differ diff --git a/user/__pycache__/views.cpython-37.pyc b/user/__pycache__/views.cpython-37.pyc index 452fec6..411f03e 100644 Binary files a/user/__pycache__/views.cpython-37.pyc and b/user/__pycache__/views.cpython-37.pyc differ diff --git a/user/models.py b/user/models.py index 4851f05..27f4671 100644 --- a/user/models.py +++ b/user/models.py @@ -3,6 +3,10 @@ from django.db import models class User(AbstractUser): name = models.CharField(blank=True, max_length=255) - + + @property + def qid(self): + return f"U{self.pk}" + def __str__(self): return self.email diff --git a/user/views.py b/user/views.py index 5bdb2dc..6832c1a 100644 --- a/user/views.py +++ b/user/views.py @@ -12,7 +12,7 @@ class UserViewSet(viewsets.ModelViewSet): serializer_class = UserSerializer -class GroupViewSet(viewsets.ModelViewSet): +class GroupViewSet(viewsets.ReadOnlyModelViewSet): """ API endpoint that allows groups to be viewed or edited. """