Compare commits

..

No commits in common. "dev-master" and "master" have entirely different histories.

21 changed files with 67 additions and 574 deletions

View File

@ -1,7 +1,7 @@
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, BankAccount, Institution, Transaction, Slice, Rule, SubscriptionPlan
from qrtr_account.models import Account, Bank, Institution, Transaction, Slice, Rule
from user.models import User
from connection.models import Connection, ConnectionType
from connection.serializers import ConnectionTypeSerializer, ConnectionSerializer
@ -42,20 +42,13 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
class GroupSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Group
fields = ['pk', 'url', 'name']
fields = ['url', 'name']
class SubscriptionPlanSerializer(serializers.HyperlinkedModelSerializer):
class BankSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = SubscriptionPlan
fields = ['pk', 'name', 'status']
class BankAccountSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = BankAccount
model = Bank
fields = [
'pk',
'url',
'qrtr_account',
'connection',
@ -64,10 +57,6 @@ class BankAccountSerializer(serializers.HyperlinkedModelSerializer):
'balance',
'ac_type',
'ac_subtype',
'connection_type',
'institution_name',
'plan',
'plan_name'
]
extra_kwargs = {
'balance': {'read_only': True},
@ -78,10 +67,10 @@ class BankAccountSerializer(serializers.HyperlinkedModelSerializer):
}
class BankAccountSerializerPOST(BankAccountSerializer):
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 BankAccount. This field is only allowed on
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.
@ -94,9 +83,8 @@ class BankAccountSerializerPOST(BankAccountSerializer):
# "credentials": {}})
class Meta:
model = BankAccount
model = Bank
fields = [
'pk',
'url',
'qrtr_account',
'connection',
@ -119,13 +107,13 @@ class BankAccountSerializerPOST(BankAccountSerializer):
class InstitutionSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Institution
fields = ['pk', 'url', 'name']
fields = ['url', 'name']
class TransactionSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Transaction
fields = ['pk', 'url', 'authorized_date',
fields = ['url', 'authorized_date',
'bank', 'name','details','slice','trans_id',
'updated_at','created_at']
@ -133,15 +121,9 @@ class TransactionSerializer(serializers.HyperlinkedModelSerializer):
class SliceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Slice
fields = ['pk', 'url', 'name', 'icon', 'description', 'balance', 'slice_of', 'bank_acc']
fields = ['url', 'name', 'icon', 'budget', 'slice_of']
class SliceTransactionSerializer(serializers.ModelSerializer):
transactions = TransactionSerializer(many=True, read_only=True)
class Meta:
model = Slice
fields = ['pk', 'url', 'name', 'icon', 'description', 'balance', 'slice_of', 'transactions', 'bank_acc']
class RuleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Rule

View File

@ -1,150 +0,0 @@
from .abstract import AbstractConnectionClient
from django.conf import settings
import os
import datetime
import plaid
from plaid.api import plaid_api
from plaid.model.link_token_create_request import LinkTokenCreateRequest
from plaid.model.link_token_create_request_user import LinkTokenCreateRequestUser
from plaid.model.item_public_token_exchange_request import ItemPublicTokenExchangeRequest
from plaid.model.transactions_sync_request import TransactionsSyncRequest
from plaid.model.accounts_get_request import AccountsGetRequest
from plaid.model.products import Products
from plaid.model.country_code import CountryCode
def format_error(e):
return {
'error': {
'display_message': e.display_message,
'error_code': e.code,
'error_type': e.type,
'error_message': e.message}}
class Connection(AbstractConnectionClient):
def __init__(self, credentials, account_id=None):
self.credentials = credentials
self.account_id = account_id
# Fill in your Plaid API keys -
# https://dashboard.plaid.com/account/keys
self.PLAID_CLIENT_ID = settings.PLAID_CLIENT_ID
self.PLAID_SECRET = settings.PLAID_SECRET
self.PLAID_PUBLIC_KEY = settings.PLAID_PUBLIC_KEY
# Use 'sandbox' to test with Plaid's Sandbox environment (usplaid-python==9.2.0ername: user_good,
# password: pass_good)
# Use `development` to test with live users and credentials and `production`
# to go live
self.PLAID_ENV = settings.PLAID_ENV
# PLAID_PRODUCTS is a comma-separated list of products to use when initializing
# Link. Note that this list must contain 'assets' in order for the app to be
# able to create and retrieve asset reports.
self.PLAID_PRODUCTS = settings.PLAID_PRODUCTS
# PLAID_COUNTRY_CODES is a comma-separated list of countries for which users
# will be able to select institutions from.
self.PLAID_COUNTRY_CODES = settings.PLAID_COUNTRY_CODES
configuration = plaid.Configuration(
host=self.PLAID_ENV,
api_key={
'clientId': self.PLAID_CLIENT_ID,
'secret': self.PLAID_SECRET,
}
)
self.api_client = plaid.ApiClient(configuration)
self.client = plaid_api.PlaidApi(self.api_client)
# # Create a link_token for the given user
# request = LinkTokenCreateRequest(
# products=[Products("auth")],
# client_name="Qrtr Plaid",
# country_codes=[CountryCode('US')],
# #redirect_uri='https://domainname.com/oauth-page.html',
# language='en',
# webhook='https://webhook.example.com',
# user=LinkTokenCreateRequestUser(
# client_user_id=self.account_id
# )
# )
# response = client.link_token_create(request)
# resp_dict = response.to_dict()
# resp_dict['expiration'] = resp_dict['expiration'].strftime('%s')
# self.credentials.update(resp_dict)
# return self.credentials
def generate_auth_request(self):
# Create a link_token for the given user
request = LinkTokenCreateRequest(
products=[Products("auth")],
client_name="Qrtr Plaid",
country_codes=[CountryCode('US')],
# redirect_uri='https://domainname.com/oauth-page.html',
language='en',
webhook='https://webhook.example.com',
user=LinkTokenCreateRequestUser(
client_user_id=self.account_id
)
)
response = self.client.link_token_create(request)
resp_dict = response.to_dict()
resp_dict['expiration'] = resp_dict['expiration'].strftime('%s')
self.credentials.update(resp_dict)
def get_auth_token(self, public_token):
try:
exchange_request = ItemPublicTokenExchangeRequest(
public_token=public_token
)
exchange_response = self.client.item_public_token_exchange(
exchange_request)
except Exception as e:
print("Error Occurred")
print(e)
return format_error(e)
access_token = exchange_response['access_token']
item_id = exchange_response['item_id']
self.credentials.update({"access_token": access_token, "item_id": item_id})
return {"access_token": access_token, "item_id": item_id}
def get_accounts(self, auth_token=None):
if not auth_token:
auth_token = self.credentials.get('access_token')
if not auth_token:
raise Exception("Missing Auth Token")
try:
acc_request = AccountsGetRequest(access_token=auth_token)
accounts = self.client.accounts_get(acc_request).to_dict()
except Exception as e:
print(e)
accounts = None
return accounts
def get_transactions(
self,
start_date=None,
end_date=None,
auth_token=None):
if not auth_token:
auth_token = self.credentials.get('access_token')
request = TransactionsSyncRequest(
access_token=auth_token,
)
response = self.client.transactions_sync(request)
transactions = response['added']
# the transactions in the response are paginated, so make multiple calls while incrementing the cursor to
# retrieve all transactions
while (response['has_more']):
request = TransactionsSyncRequest(
access_token=auth_token,
cursor=response['next_cursor']
)
response = self.client.transactions_sync(request)
transactions += response['added']
return transactions

View File

@ -1,6 +1,5 @@
from rest_framework import serializers
from .models import ConnectionType, Connection
from qrtr_account.models import BankAccount
class ConnectionTypeSerializer(serializers.HyperlinkedModelSerializer):
@ -12,24 +11,12 @@ class ConnectionTypeSerializer(serializers.HyperlinkedModelSerializer):
'filename': {'read_only': True}
}
class ConnectionAccountSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Connection
fields = ['pk', 'url', 'name', 'type']
class BankAccountReadSerializer(serializers.HyperlinkedModelSerializer):
connection = ConnectionAccountSerializer(read_only=True)
class Meta:
model = BankAccount
fields = ['pk', 'url', 'connection']
class ConnectionSerializer(serializers.HyperlinkedModelSerializer):
bank_accounts = BankAccountReadSerializer(read_only=True, source='*')
class Meta:
model = Connection
fields = ['url', 'name', 'type', 'credentials', 'bank_accounts']
fields = ['url', 'name', 'type', 'credentials']
extra_kwargs = {
'type': {'write_only': True},
'credentials': {'write_only': True}
}

View File

@ -1,4 +1,4 @@
from qrtr_account.models import Transaction, BankAccount, Account
from qrtr_account.models import Transaction, Bank, Account
from connection.models import Connection
@ -15,7 +15,7 @@ def get_and_save_transactions(connection, start_date=None, end_date=None):
defaults={"authorized_date": trns.get("authorized_date"),
"trans_id": trns.get("transaction_id"),
"details": trns,
"bank": BankAccount.objects.get(acc_id=trns.get("account_id")),
"bank": Bank.objects.get(acc_id=trns.get("account_id")),
"name": trns.get("name")})
return True

View File

@ -11,9 +11,6 @@ import importlib
import json
from .serializers import ConnectionSerializer, ConnectionTypeSerializer
from django.db import transaction
from qrtr_account.mixins import OwnedAccountsMixin
import traceback
# Create your views here.
@ -23,7 +20,7 @@ class ConnectionTypeViewSet(viewsets.ModelViewSet):
serializer_class = ConnectionTypeSerializer
class ConnectionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
class ConnectionViewSet(viewsets.ModelViewSet):
"""API endpoint that allows connections to be seen or created
"""
permission_classes = [IsAuthenticated]
@ -36,67 +33,18 @@ class ConnectionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
'delete',
'options']
def get_queryset(self):
return Connection.objects.filter(
account__in=self.accessible_accounts().values_list('id'))
@action(detail=False, methods=['post'], url_path='plaid/exchange_public_token')
def exchange_public_token(self, request):
print(f"REQUEST: {request.data}")
name = request.data.get("name", "dummyName")
account_id = request.data.get("account")
public_token = request.data.get("public_token")
user = request.user
if request.user.is_anonymous:
accounts = (Account.objects.filter(pk=1))
else:
accounts = (Account.objects.filter(pk=account_id, owner=user) |
Account.objects.filter(pk=account_id,
admin_users__in=[user]))
if not accounts:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data="ERROR: Account ID not found")
else:
print(f"Account Found: {accounts[0]}")
account = accounts[0]
print(request)
plaid_conn = importlib.import_module(f"connection.connections.plaid_client_v2")
conn_type = ConnectionType.objects.get(name="Plaid")
try:
plaid_client = plaid_conn.Connection(request.data.dict(), account_id=account_id)
token = plaid_client.get_auth_token(public_token)
except ValueError:
return Response(status=status.HTTP_503,
data="ERROR: Invalid public_token")
with transaction.atomic():
conn, created = Connection.objects \
.get_or_create(name=name, type=conn_type,
defaults={
"credentials": request.data,
"account": account
})
conn.credentials = plaid_client.credentials
print(f"CREDS: {plaid_client.credentials}")
conn.save()
serializer = self.get_serializer(conn, context={'request':request})
print("DATA:")
print(serializer.data)
return Response(serializer.data)
@action(detail=False, methods=['post'], url_path='plaid')
def authenticate(self, request):
print(request.data)
print(request.data.keys())
# public_token = request.data.get("public_token")
public_token = request.data.get("public_token")
name = request.data.get("name", "dummyName")
account_id = request.data.get("account")
print(f"Account ID Detected: {account_id}")
# if public_token is None:
# return Response(
# status=status.HTTP_400_BAD_REQUEST,
# data="ERROR: missing public_token")
if public_token is None:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data="ERROR: missing public_token")
if account_id is None:
return Response(
status=status.HTTP_400_BAD_REQUEST,
@ -104,12 +52,9 @@ class ConnectionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
user = request.user
# Filter out any accounts with the right id, but the given user
# is not an owner or admin on that account.
if request.user.is_anonymous:
accounts = (Account.objects.filter(pk=1))
else:
accounts = (Account.objects.filter(pk=account_id, owner=user) |
Account.objects.filter(pk=account_id,
admin_users__in=[user]))
accounts = (Account.objects.filter(pk=account_id, owner=user) |
Account.objects.filter(pk=account_id,
admin_users__in=[user]))
if not accounts:
return Response(
status=status.HTTP_400_BAD_REQUEST,
@ -118,17 +63,15 @@ class ConnectionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
print(f"Account Found: {accounts[0]}")
account = accounts[0]
print(request)
plaid_conn = importlib.import_module(f"connection.connections.plaid_client_v2")
plaid_conn = importlib.import_module(f"connection.connections.plaid_client")
conn_type = ConnectionType.objects.get(name="Plaid")
try:
plaid_client = plaid_conn.Connection(request.data.dict(), account_id=account_id)
plaid_client.generate_auth_request()
plaid_client = plaid_conn.Connection(request.data)
except ValueError:
return Response(status=status.HTTP_503,
data="ERROR: Invalid public_token")
except Exception as e:
print(e)
print(traceback.format_exc())
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR,
data="ERROR: Unable to contact Plaid")
with transaction.atomic():
@ -139,9 +82,8 @@ class ConnectionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
"account": account
})
conn.credentials = plaid_client.credentials
print(f"CREDS: {plaid_client.credentials}")
conn.save()
return Response(plaid_client.credentials)
return Response(plaid_client.get_accounts())
@action(detail=False, methods=['post'], url_path='plaid-webhook',
permission_classes=[AllowAny])

View File

@ -12,7 +12,6 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
import os
import smtplib
from datetime import timedelta
try:
from .local import *
@ -123,20 +122,6 @@ REST_FRAMEWORK = {
REST_USE_JWT = True
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
'ROTATE_REFRESH_TOKENS': True,
}
REST_AUTH = {
"USE_JWT": True,
'JWT_AUTH_RETURN_EXPIRATION': True,
'JWT_AUTH_HTTPONLY': False,
}
AUTH_USER_MODEL = 'user.User'
# Password validation

View File

@ -12,7 +12,6 @@ DATABASES = {"default": DEFAULT_CONNECTION, }
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get("SECRET_KEY")
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

View File

@ -27,12 +27,12 @@ from user.views import (UserViewSet,
)
from qrtr_account.views import (AccountViewSet,
BankAccountViewSet,
BankViewSet,
InstitutionViewSet,
TransactionViewSet,
SliceViewSet, SliceTransactionViewSet,
SliceViewSet,
FacebookLogin,
TwitterLogin, SubscriptionPlanViewSet)
TwitterLogin)
from connection.views import ConnectionViewSet, ConnectionTypeViewSet
@ -59,15 +59,12 @@ router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
router.register(r'groups', GroupViewSet)
router.register(r'accounts', AccountViewSet)
router.register(r'bank-accounts', BankAccountViewSet)
router.register(r'banks', BankViewSet)
router.register(r'institutions', InstitutionViewSet)
router.register(r'transactions', TransactionViewSet)
router.register(r'slices', SliceViewSet)
router.register(r'slices/(?P<slice_pk>\d+)/transactions',
SliceTransactionViewSet, basename='slices')
router.register(r'connections',ConnectionViewSet)
#router.register(r'connections',ConnectionViewSet)
router.register(r'connectiontypes', ConnectionTypeViewSet)
router.register(r'plans', SubscriptionPlanViewSet)
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
@ -86,7 +83,6 @@ apipatterns = [
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include(apipatterns), name='api'),
path('api-auth/', include('rest_framework.urls')),
# path('api/v1/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/v1/docs', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('api/v1/schema/redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),

View File

@ -1,5 +1,5 @@
from django.contrib import admin
from .models import Account, Institution, BankAccount, Transaction, Slice, SubscriptionPlan
from .models import Account, Institution, Bank, Transaction, Slice
@admin.register(Account)
@ -12,8 +12,8 @@ class InstitutionAdmin(admin.ModelAdmin):
pass
@admin.register(BankAccount)
class BankAccountAdmin(admin.ModelAdmin):
@admin.register(Bank)
class BankAdmin(admin.ModelAdmin):
pass
@ -25,7 +25,3 @@ class TransactionAdmin(admin.ModelAdmin):
@admin.register(Slice)
class SliceAdmin(admin.ModelAdmin):
pass
@admin.register(SubscriptionPlan)
class SubscriptionPlanAdmin(admin.ModelAdmin):
pass

View File

@ -1,9 +1,9 @@
from qrtr_account.models import Account, Institution, BankAccount
from qrtr_account.models import Account, Institution, Bank
from connection.models import Connection
dummy_ac = Account.objects.all()[0]
dummy_isnt = Institution.objects.all()[0]
dummy_bank = BankAccount.objects.all()[0]
dummy_bank = Bank.objects.all()[0]
conn_dummy = Connection.objects.all()[0]
@ -142,5 +142,5 @@ for account in accounts:
"balance":account.get("balances",{}).get("current"),
"balance_limit":account.get("balances",{}).get("limit")
}
BankAccount.objects.update_or_create(qrtr_account=dummy_ac, acc_id=account.get("account_id"),
Bank.objects.update_or_create(qrtr_account=dummy_ac, acc_id=account.get("account_id"),
defaults=fields)

View File

@ -669,11 +669,11 @@ transactions = [
print(len(transactions))
for transaction in transactions:
bank = BankAccount.objects.filter(acc_id=transaction.get("account_id",[None]))[0]
bank = Bank.objects.filter(acc_id=transaction.get("account_id",[None]))[0]
print(bank)
if bank:
fields = {
"datetime":datetime.strptime(transaction.get("date"),"%Y-%m-%d"),
"details":transaction
}
Transaction.objects.update_or_create(BankAccount=bank,defaults=fields)
Transaction.objects.update_or_create(Bank=bank,defaults=fields)

View File

@ -1,29 +0,0 @@
# Generated by Django 3.2.3 on 2023-07-20 01:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('connection', '0003_auto_20201125_2242'),
('qrtr_account', '0013_auto_20211229_1935'),
]
operations = [
migrations.RenameModel(
old_name='Bank',
new_name='BankAccount',
),
migrations.RenameField(
model_name='slice',
old_name='budget',
new_name='balance',
),
migrations.AlterField(
model_name='transaction',
name='slice',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transactions', to='qrtr_account.slice'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 3.2.3 on 2023-07-20 01:15
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('qrtr_account', '0014_auto_20230720_0105'),
]
operations = [
migrations.AlterField(
model_name='bankaccount',
name='institution',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bank_accounts', to='qrtr_account.institution'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 3.2.3 on 2023-08-31 02:45
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('qrtr_account', '0015_alter_bankaccount_institution'),
]
operations = [
migrations.AddField(
model_name='slice',
name='bank_acc',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='qrtr_account.bankaccount'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.3 on 2023-08-31 03:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('qrtr_account', '0016_slice_bank_acc'),
]
operations = [
migrations.AlterField(
model_name='slice',
name='balance',
field=models.DecimalField(blank=True, decimal_places=3, max_digits=100, null=True),
),
]

View File

@ -1,27 +0,0 @@
# Generated by Django 3.2.3 on 2024-01-18 03:19
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('qrtr_account', '0017_alter_slice_balance'),
]
operations = [
migrations.CreateModel(
name='SubscriptionPlan',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=250)),
('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive')], max_length=10)),
],
),
migrations.AddField(
model_name='bankaccount',
name='plan',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='qrtr_account.subscriptionplan'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 3.2.3 on 2024-08-01 00:55
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('qrtr_account', '0018_auto_20240118_0319'),
]
operations = [
migrations.AddField(
model_name='rule',
name='bank_acc',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='qrtr_account.bankaccount'),
),
]

View File

@ -1,15 +0,0 @@
from django.db.models import Q
from qrtr_account.models import Account
class OwnedAccountsMixin():
"""Mixin to help getting a list of accounts
the given user is authorized to see
"""
def accessible_accounts(self):
usr = self.request.user
accs = Account.objects.filter(Q(owner=usr) |
Q(id__in=usr.admin_accounts.all().values_list('id')) |
Q(id__in=usr.view_accounts.all().values_list('id')))
return accs

View File

@ -19,13 +19,7 @@ class Account(models.Model):
def __str__(self):
return f"{self.name}"
class SubscriptionPlan(models.Model):
name = models.CharField(max_length=250)
status = models.CharField(choices=[('active','Active'), ('inactive', 'Inactive')], max_length=10)
def __str__(self):
return f"{self.name}"
class Institution(models.Model):
@ -39,13 +33,12 @@ class Institution(models.Model):
return f"{self.name}"
class BankAccount(models.Model):
class Bank(models.Model):
qrtr_account = models.ForeignKey(Account, on_delete=models.CASCADE)
connection = models.ForeignKey('connection.Connection',
on_delete=models.CASCADE)
institution = models.ForeignKey(Institution, on_delete=models.CASCADE,
related_name="bank_accounts")
plan = models.ForeignKey(SubscriptionPlan, on_delete=models.SET_NULL, null=True, blank=True)
related_name="banks")
acc_id = models.CharField(max_length=250, primary_key=True)
nickname = models.CharField(max_length=250)
official_name = models.CharField(max_length=250,blank=True, null=True)
@ -55,18 +48,6 @@ class BankAccount(models.Model):
ac_subtype = models.CharField(max_length=250, blank=True)
mask = models.CharField(max_length=4,blank=True)
@property
def connection_type(self):
return self.connection.type.name
@property
def plan_name(self):
return self.plan.name
@property
def institution_name(self):
return self.institution.name
@property
def qid(self):
return f"B{self.pk}"
@ -78,7 +59,7 @@ class BankAccount(models.Model):
class Slice(models.Model):
name = models.CharField(max_length=250)
icon = models.CharField(max_length=250)
balance = models.DecimalField(decimal_places=3, max_digits=100, null=True, blank=True)
budget = models.DecimalField(decimal_places=3, max_digits=100)
description = models.TextField(max_length=255, null=True, blank=True)
avail_parents = models.Q(
app_label='qrtr_account',
@ -90,7 +71,6 @@ class Slice(models.Model):
limit_choices_to=avail_parents,
on_delete=models.CASCADE, null=True, blank=True)
parent_id = models.PositiveIntegerField(null=True)
bank_acc = models.ForeignKey(BankAccount, on_delete=models.CASCADE, null=True, blank=True)
is_unsliced = models.BooleanField(default=False)
slice_of = GenericForeignKey('parent_type', 'parent_id')
@ -108,7 +88,6 @@ class Schedule(models.Model):
class Rule(models.Model):
bank_acc = models.ForeignKey(BankAccount, on_delete=models.CASCADE, null=True, blank=True)
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)
@ -135,11 +114,11 @@ class Rule(models.Model):
class Transaction(models.Model):
authorized_date = models.DateField(null=True)
bank = models.ForeignKey(BankAccount, on_delete=models.CASCADE,
bank = models.ForeignKey(Bank, on_delete=models.CASCADE,
related_name='transactions')
name = models.CharField(max_length=255)
details = models.JSONField()
slice = models.ForeignKey(Slice, on_delete=models.SET_NULL, null=True, related_name='transactions')
slice = models.ForeignKey(Slice, on_delete=models.SET_NULL, null=True)
trans_id = models.CharField(max_length=255)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)

View File

@ -1,23 +1,22 @@
from django.shortcuts import render
from rest_framework import viewsets, mixins
from .models import Account, BankAccount, Institution, Transaction, Slice, Rule, SubscriptionPlan
from .models import Account, Bank, Institution, Transaction, Slice, Rule
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import action
from connection.models import Connection, ConnectionType
from api.serializers import (AccountReadSerializer, AccountWriteSerializer,
BankAccountSerializer, BankAccountSerializerPOST,
BankSerializer, BankSerializerPOST,
InstitutionSerializer,
TransactionSerializer,
ConnectionSerializer,
ConnectionTypeSerializer,
SliceSerializer, SliceTransactionSerializer,
RuleSerializer, SubscriptionPlanSerializer)
SliceSerializer,
RuleSerializer)
from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter
from dj_rest_auth.registration.views import SocialLoginView
from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter
from dj_rest_auth.social_serializers import TwitterLoginSerializer
from api.mixins import ReadWriteSerializerMixin
from qrtr_account.mixins import OwnedAccountsMixin
class TwitterLogin(SocialLoginView):
@ -29,93 +28,51 @@ class FacebookLogin(SocialLoginView):
adapter_class = FacebookOAuth2Adapter
class AccountViewSet(ReadWriteSerializerMixin, viewsets.ModelViewSet, OwnedAccountsMixin):
class AccountViewSet(ReadWriteSerializerMixin, viewsets.ModelViewSet):
"""API endpoint that allows accounts to be viewed or edited
"""
permission_classes = [IsAuthenticated]
queryset = Account.objects.all()
read_serializer_class = AccountReadSerializer
write_serializer_class = AccountWriteSerializer
def get_queryset(self):
return self.accessible_accounts()
class BankAccountViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
"""API endpoint that allows BankAccounts to be viewed or edited
class BankViewSet(viewsets.ModelViewSet):
"""API endpoint that allows Banks to be viewed or edited
"""
permission_classes = [IsAuthenticated]
queryset = BankAccount.objects.all()
# serializer_class = BankAccountSerializer
queryset = Bank.objects.all()
# serializer_class = BankSerializer
def get_serializer_class(self):
if self.action == 'create':
return BankAccountSerializerPOST
return BankAccountSerializer
def get_queryset(self):
return BankAccount.objects.filter(
qrtr_account__in=self.accessible_accounts().values_list('id'))
return BankSerializerPOST
return BankSerializer
class SliceViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
"""API endpoint that allows BankAccounts to be viewed.
class SliceViewSet(viewsets.ModelViewSet):
"""API endpoint that allows Banks to be viewed.
"""
permission_classes = [IsAuthenticated]
queryset = Slice.objects.all()
serializer_class = SliceSerializer
filterset_fields = {
'id': ['exact', 'lte', 'gte'],
'name': ['exact',],
'balance': ['exact', 'lte', 'gte'],
'bank_acc': ['exact'],
# 'slice_of': ['exact']
}
def get_queryset(self):
return Slice.objects.select_related('bank_acc').filter(
bank_acc__qrtr_account__in=self.accessible_accounts().values_list('id')
)
class SubscriptionPlanViewSet(viewsets.ModelViewSet):
queryset = SubscriptionPlan.objects.all()
serializer_class = SubscriptionPlanSerializer
class InstitutionViewSet(viewsets.ReadOnlyModelViewSet):
"""API endpoint that allows BankAccounts to be viewed.
"""API endpoint that allows Banks to be viewed.
"""
permission_classes = [IsAuthenticated]
queryset = Institution.objects.all()
serializer_class = InstitutionSerializer
class TransactionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
"""API endpoint that allows BankAccounts to be viewed.
class TransactionViewSet(viewsets.ModelViewSet):
"""API endpoint that allows Banks to be viewed.
"""
permission_classes = [IsAuthenticated]
queryset = Transaction.objects.filter(is_split=False)
serializer_class = TransactionSerializer
search_fields = ['name', 'slice__name', 'bank__nickname',
'bank__official_name']
filterset_fields = {
'slice__id': ['exact',],
'slice__name': ['exact', ],
'authorized_date': ['exact', 'lte', 'gte', 'isnull'],
'updated_at': ['exact', 'lte', 'gte', 'isnull'],
'created_at': ['exact', 'lte', 'gte', 'isnull'],
'trans_id': ['exact', 'lte', 'gte'],
'id': ['exact', 'lte', 'gte'],
'bank': ['exact']
'trans_id': ['exact', 'lte', 'gte', 'isnull'],
}
@action(detail=True, methods=['post'], url_path='split')
@ -134,43 +91,9 @@ class TransactionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
child2 = Transaction.objects.create(**base_information)
child2.name = f"{child1.name}.split2"
def get_queryset(self):
return Transaction.objects.select_related('bank').filter(
bank__qrtr_account__in=self.accessible_accounts().values_list('id')
).filter(is_split=False)
class SliceTransactionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
permission_classes = [IsAuthenticated]
serializer_class = SliceTransactionSerializer
queryset = Slice.objects.all()
filterset_fields = {
'id': ['exact', 'lte', 'gte'],
'name': ['exact',],
'balance': ['exact', 'lte', 'gte'],
'bank_acc': ['exact'],
# 'slice_of': ['exact']
}
def get_queryset(self):
return Slice.objects.select_related('bank_acc').filter(
bank_acc__qrtr_account__in=self.accessible_accounts().values_list('id')
)
# def get_queryset(self):
# return Transaction.objects.filter(slice__pk=self.kwargs.get('slice_pk'))
class RuleViewSet(viewsets.ReadOnlyModelViewSet, OwnedAccountsMixin):
"""API endpoint that allows BankAccounts to be viewed.
class RuleViewSet(viewsets.ReadOnlyModelViewSet):
"""API endpoint that allows Banks to be viewed.
"""
permission_classes = [IsAuthenticated]
queryset = Rule.objects.all()
serializer_class = RuleSerializer
def get_queryset(self):
return Rule.objects.select_related('bank_acc').filter(
bank_acc__qrtr_account__in=self.accessible_accounts().values_list('id')
)

View File

@ -5,10 +5,10 @@ chardet==4.0.0
cryptography==3.4.7
defusedxml==0.7.1
dj-database-url==0.5.0
dj-rest-auth==3.0.0
dj-rest-auth==2.1.5
django-rest-swagger==2.2.0
Django==3.2.3
django-allauth==0.50.0
django-allauth==0.44.0
django-cors-headers==3.7.0
django-filter==2.4.0
djangorestframework==3.12.4
@ -16,8 +16,8 @@ djangorestframework-simplejwt==4.6.0
drf-yasg==1.20.0
idna==2.10
oauthlib==3.1.0
plaid_python==14.0.0
psycopg2-binary==2.8.6
plaid-python==9.2.0
psycopg2==2.8.6
pycparser==2.20
PyJWT==2.1.0
python3-openid==3.2.0