Compare commits
42 Commits
master
...
dev-master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd3eee2a76 | ||
|
|
88f70323b0 | ||
|
|
4e188cd57e | ||
|
|
20aa85bf08 | ||
|
|
1411fbdf53 | ||
|
|
0d150971f7 | ||
|
|
8d15f325bb | ||
|
|
b98ad76080 | ||
|
|
05bdc13fc4 | ||
|
|
9c9a397d83 | ||
|
|
5d35596c12 | ||
|
|
8eb3190972 | ||
|
|
eafa2bbe75 | ||
|
|
93aa10d98d | ||
|
|
0c98606234 | ||
|
|
6ec9065f8e | ||
|
|
8bdbc05c59 | ||
|
|
cca13d4a4f | ||
|
|
64e8ae4bab | ||
|
|
8dc43c0132 | ||
|
|
dc1a60910f | ||
|
|
0640571fba | ||
|
|
b711675a0c | ||
|
|
23764c1886 | ||
|
|
42704350a4 | ||
|
|
e942f1a76e | ||
|
|
cd9a8c4d29 | ||
|
|
5b6c7bde1d | ||
|
|
6b377067e1 | ||
|
|
eee001e32b | ||
|
|
70453bcdcf | ||
|
|
0b32c1d20d | ||
|
|
e3b1060624 | ||
|
|
5e33e6a308 | ||
|
|
88b0f6242b | ||
|
|
25bc7dc347 | ||
|
|
899ea476d6 | ||
|
|
6d13ec9655 | ||
|
|
c6b8f16a99 | ||
|
|
8ff8b000e3 | ||
|
|
64a51b362a | ||
|
|
b6e8c0f108 |
@ -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, Bank, Institution, Transaction, Slice, Rule
|
||||
from qrtr_account.models import Account, BankAccount, Institution, Transaction, Slice, Rule, SubscriptionPlan
|
||||
from user.models import User
|
||||
from connection.models import Connection, ConnectionType
|
||||
from connection.serializers import ConnectionTypeSerializer, ConnectionSerializer
|
||||
@ -42,13 +42,20 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class GroupSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Group
|
||||
fields = ['url', 'name']
|
||||
fields = ['pk', 'url', 'name']
|
||||
|
||||
|
||||
class BankSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class SubscriptionPlanSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Bank
|
||||
model = SubscriptionPlan
|
||||
fields = ['pk', 'name', 'status']
|
||||
|
||||
|
||||
class BankAccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = BankAccount
|
||||
fields = [
|
||||
'pk',
|
||||
'url',
|
||||
'qrtr_account',
|
||||
'connection',
|
||||
@ -57,6 +64,10 @@ class BankSerializer(serializers.HyperlinkedModelSerializer):
|
||||
'balance',
|
||||
'ac_type',
|
||||
'ac_subtype',
|
||||
'connection_type',
|
||||
'institution_name',
|
||||
'plan',
|
||||
'plan_name'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'balance': {'read_only': True},
|
||||
@ -67,10 +78,10 @@ class BankSerializer(serializers.HyperlinkedModelSerializer):
|
||||
}
|
||||
|
||||
|
||||
class BankSerializerPOST(BankSerializer):
|
||||
class BankAccountSerializerPOST(BankAccountSerializer):
|
||||
"""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
|
||||
connection record to go with the new BankAccount. 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.
|
||||
@ -83,8 +94,9 @@ class BankSerializerPOST(BankSerializer):
|
||||
# "credentials": {}})
|
||||
|
||||
class Meta:
|
||||
model = Bank
|
||||
model = BankAccount
|
||||
fields = [
|
||||
'pk',
|
||||
'url',
|
||||
'qrtr_account',
|
||||
'connection',
|
||||
@ -107,13 +119,13 @@ class BankSerializerPOST(BankSerializer):
|
||||
class InstitutionSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Institution
|
||||
fields = ['url', 'name']
|
||||
fields = ['pk', 'url', 'name']
|
||||
|
||||
|
||||
class TransactionSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Transaction
|
||||
fields = ['url', 'authorized_date',
|
||||
fields = ['pk', 'url', 'authorized_date',
|
||||
'bank', 'name','details','slice','trans_id',
|
||||
'updated_at','created_at']
|
||||
|
||||
@ -121,9 +133,15 @@ class TransactionSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class SliceSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Slice
|
||||
fields = ['url', 'name', 'icon', 'budget', 'slice_of']
|
||||
fields = ['pk', 'url', 'name', 'icon', 'description', 'balance', 'slice_of', 'bank_acc']
|
||||
|
||||
|
||||
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
|
||||
|
||||
150
connection/connections/plaid_client_v2.py
Executable file
150
connection/connections/plaid_client_v2.py
Executable file
@ -0,0 +1,150 @@
|
||||
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
|
||||
@ -1,5 +1,6 @@
|
||||
from rest_framework import serializers
|
||||
from .models import ConnectionType, Connection
|
||||
from qrtr_account.models import BankAccount
|
||||
|
||||
|
||||
class ConnectionTypeSerializer(serializers.HyperlinkedModelSerializer):
|
||||
@ -11,12 +12,24 @@ class ConnectionTypeSerializer(serializers.HyperlinkedModelSerializer):
|
||||
'filename': {'read_only': True}
|
||||
}
|
||||
|
||||
|
||||
class ConnectionSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class ConnectionAccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Connection
|
||||
fields = ['url', 'name', 'type', 'credentials']
|
||||
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']
|
||||
extra_kwargs = {
|
||||
'type': {'write_only': True},
|
||||
'credentials': {'write_only': True}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from qrtr_account.models import Transaction, Bank, Account
|
||||
from qrtr_account.models import Transaction, BankAccount, 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": Bank.objects.get(acc_id=trns.get("account_id")),
|
||||
"bank": BankAccount.objects.get(acc_id=trns.get("account_id")),
|
||||
"name": trns.get("name")})
|
||||
return True
|
||||
|
||||
|
||||
@ -11,6 +11,9 @@ 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.
|
||||
|
||||
@ -20,7 +23,7 @@ class ConnectionTypeViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ConnectionTypeSerializer
|
||||
|
||||
|
||||
class ConnectionViewSet(viewsets.ModelViewSet):
|
||||
class ConnectionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
|
||||
"""API endpoint that allows connections to be seen or created
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
@ -33,28 +36,24 @@ class ConnectionViewSet(viewsets.ModelViewSet):
|
||||
'delete',
|
||||
'options']
|
||||
|
||||
@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")
|
||||
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")
|
||||
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 account_id is None:
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
data="ERROR: missing account_id")
|
||||
public_token = request.data.get("public_token")
|
||||
user = request.user
|
||||
# Filter out any accounts with the right id, but the given user
|
||||
# is not an owner or admin on that account.
|
||||
accounts = (Account.objects.filter(pk=account_id, owner=user) |
|
||||
Account.objects.filter(pk=account_id,
|
||||
admin_users__in=[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,
|
||||
@ -63,15 +62,73 @@ class ConnectionViewSet(viewsets.ModelViewSet):
|
||||
print(f"Account Found: {accounts[0]}")
|
||||
account = accounts[0]
|
||||
print(request)
|
||||
plaid_conn = importlib.import_module(f"connection.connections.plaid_client")
|
||||
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)
|
||||
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")
|
||||
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 account_id is None:
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
data="ERROR: missing account_id")
|
||||
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]))
|
||||
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)
|
||||
plaid_client.generate_auth_request()
|
||||
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():
|
||||
@ -82,8 +139,9 @@ class ConnectionViewSet(viewsets.ModelViewSet):
|
||||
"account": account
|
||||
})
|
||||
conn.credentials = plaid_client.credentials
|
||||
print(f"CREDS: {plaid_client.credentials}")
|
||||
conn.save()
|
||||
return Response(plaid_client.get_accounts())
|
||||
return Response(plaid_client.credentials)
|
||||
|
||||
@action(detail=False, methods=['post'], url_path='plaid-webhook',
|
||||
permission_classes=[AllowAny])
|
||||
|
||||
@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
|
||||
|
||||
import os
|
||||
import smtplib
|
||||
from datetime import timedelta
|
||||
|
||||
try:
|
||||
from .local import *
|
||||
@ -122,6 +123,20 @@ 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
|
||||
|
||||
@ -12,6 +12,7 @@ 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
|
||||
|
||||
14
core/urls.py
14
core/urls.py
@ -27,12 +27,12 @@ from user.views import (UserViewSet,
|
||||
)
|
||||
|
||||
from qrtr_account.views import (AccountViewSet,
|
||||
BankViewSet,
|
||||
BankAccountViewSet,
|
||||
InstitutionViewSet,
|
||||
TransactionViewSet,
|
||||
SliceViewSet,
|
||||
SliceViewSet, SliceTransactionViewSet,
|
||||
FacebookLogin,
|
||||
TwitterLogin)
|
||||
TwitterLogin, SubscriptionPlanViewSet)
|
||||
|
||||
from connection.views import ConnectionViewSet, ConnectionTypeViewSet
|
||||
|
||||
@ -59,12 +59,15 @@ router = routers.DefaultRouter()
|
||||
router.register(r'users', UserViewSet)
|
||||
router.register(r'groups', GroupViewSet)
|
||||
router.register(r'accounts', AccountViewSet)
|
||||
router.register(r'banks', BankViewSet)
|
||||
router.register(r'bank-accounts', BankAccountViewSet)
|
||||
router.register(r'institutions', InstitutionViewSet)
|
||||
router.register(r'transactions', TransactionViewSet)
|
||||
router.register(r'slices', SliceViewSet)
|
||||
#router.register(r'connections',ConnectionViewSet)
|
||||
router.register(r'slices/(?P<slice_pk>\d+)/transactions',
|
||||
SliceTransactionViewSet, basename='slices')
|
||||
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.
|
||||
@ -83,6 +86,7 @@ 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'),
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from django.contrib import admin
|
||||
from .models import Account, Institution, Bank, Transaction, Slice
|
||||
from .models import Account, Institution, BankAccount, Transaction, Slice, SubscriptionPlan
|
||||
|
||||
|
||||
@admin.register(Account)
|
||||
@ -12,8 +12,8 @@ class InstitutionAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@admin.register(Bank)
|
||||
class BankAdmin(admin.ModelAdmin):
|
||||
@admin.register(BankAccount)
|
||||
class BankAccountAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@ -25,3 +25,7 @@ class TransactionAdmin(admin.ModelAdmin):
|
||||
@admin.register(Slice)
|
||||
class SliceAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
@admin.register(SubscriptionPlan)
|
||||
class SubscriptionPlanAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
@ -1,9 +1,9 @@
|
||||
from qrtr_account.models import Account, Institution, Bank
|
||||
from qrtr_account.models import Account, Institution, BankAccount
|
||||
from connection.models import Connection
|
||||
|
||||
dummy_ac = Account.objects.all()[0]
|
||||
dummy_isnt = Institution.objects.all()[0]
|
||||
dummy_bank = Bank.objects.all()[0]
|
||||
dummy_bank = BankAccount.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")
|
||||
}
|
||||
Bank.objects.update_or_create(qrtr_account=dummy_ac, acc_id=account.get("account_id"),
|
||||
BankAccount.objects.update_or_create(qrtr_account=dummy_ac, acc_id=account.get("account_id"),
|
||||
defaults=fields)
|
||||
|
||||
@ -669,11 +669,11 @@ transactions = [
|
||||
print(len(transactions))
|
||||
|
||||
for transaction in transactions:
|
||||
bank = Bank.objects.filter(acc_id=transaction.get("account_id",[None]))[0]
|
||||
bank = BankAccount.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(Bank=bank,defaults=fields)
|
||||
Transaction.objects.update_or_create(BankAccount=bank,defaults=fields)
|
||||
|
||||
29
qrtr_account/migrations/0014_auto_20230720_0105.py
Normal file
29
qrtr_account/migrations/0014_auto_20230720_0105.py
Normal file
@ -0,0 +1,29 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,19 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
19
qrtr_account/migrations/0016_slice_bank_acc.py
Normal file
19
qrtr_account/migrations/0016_slice_bank_acc.py
Normal file
@ -0,0 +1,19 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
18
qrtr_account/migrations/0017_alter_slice_balance.py
Normal file
18
qrtr_account/migrations/0017_alter_slice_balance.py
Normal file
@ -0,0 +1,18 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
27
qrtr_account/migrations/0018_auto_20240118_0319.py
Normal file
27
qrtr_account/migrations/0018_auto_20240118_0319.py
Normal file
@ -0,0 +1,27 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
19
qrtr_account/migrations/0019_rule_bank_acc.py
Normal file
19
qrtr_account/migrations/0019_rule_bank_acc.py
Normal file
@ -0,0 +1,19 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
15
qrtr_account/mixins.py
Normal file
15
qrtr_account/mixins.py
Normal file
@ -0,0 +1,15 @@
|
||||
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
|
||||
@ -19,7 +19,13 @@ 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):
|
||||
@ -33,12 +39,13 @@ class Institution(models.Model):
|
||||
return f"{self.name}"
|
||||
|
||||
|
||||
class Bank(models.Model):
|
||||
class BankAccount(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="banks")
|
||||
related_name="bank_accounts")
|
||||
plan = models.ForeignKey(SubscriptionPlan, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
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)
|
||||
@ -48,6 +55,18 @@ class Bank(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}"
|
||||
@ -59,7 +78,7 @@ class Bank(models.Model):
|
||||
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)
|
||||
balance = models.DecimalField(decimal_places=3, max_digits=100, null=True, blank=True)
|
||||
description = models.TextField(max_length=255, null=True, blank=True)
|
||||
avail_parents = models.Q(
|
||||
app_label='qrtr_account',
|
||||
@ -71,6 +90,7 @@ 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')
|
||||
|
||||
@ -88,6 +108,7 @@ 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)
|
||||
@ -114,11 +135,11 @@ class Rule(models.Model):
|
||||
|
||||
class Transaction(models.Model):
|
||||
authorized_date = models.DateField(null=True)
|
||||
bank = models.ForeignKey(Bank, on_delete=models.CASCADE,
|
||||
bank = models.ForeignKey(BankAccount, 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)
|
||||
slice = models.ForeignKey(Slice, on_delete=models.SET_NULL, null=True, related_name='transactions')
|
||||
trans_id = models.CharField(max_length=255)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@ -1,22 +1,23 @@
|
||||
from django.shortcuts import render
|
||||
from rest_framework import viewsets, mixins
|
||||
from .models import Account, Bank, Institution, Transaction, Slice, Rule
|
||||
from .models import Account, BankAccount, Institution, Transaction, Slice, Rule, SubscriptionPlan
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.decorators import action
|
||||
from connection.models import Connection, ConnectionType
|
||||
from api.serializers import (AccountReadSerializer, AccountWriteSerializer,
|
||||
BankSerializer, BankSerializerPOST,
|
||||
BankAccountSerializer, BankAccountSerializerPOST,
|
||||
InstitutionSerializer,
|
||||
TransactionSerializer,
|
||||
ConnectionSerializer,
|
||||
ConnectionTypeSerializer,
|
||||
SliceSerializer,
|
||||
RuleSerializer)
|
||||
SliceSerializer, SliceTransactionSerializer,
|
||||
RuleSerializer, SubscriptionPlanSerializer)
|
||||
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):
|
||||
@ -28,51 +29,93 @@ class FacebookLogin(SocialLoginView):
|
||||
adapter_class = FacebookOAuth2Adapter
|
||||
|
||||
|
||||
class AccountViewSet(ReadWriteSerializerMixin, viewsets.ModelViewSet):
|
||||
class AccountViewSet(ReadWriteSerializerMixin, viewsets.ModelViewSet, OwnedAccountsMixin):
|
||||
"""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 BankViewSet(viewsets.ModelViewSet):
|
||||
"""API endpoint that allows Banks to be viewed or edited
|
||||
|
||||
class BankAccountViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
|
||||
"""API endpoint that allows BankAccounts to be viewed or edited
|
||||
"""
|
||||
queryset = Bank.objects.all()
|
||||
# serializer_class = BankSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
queryset = BankAccount.objects.all()
|
||||
# serializer_class = BankAccountSerializer
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'create':
|
||||
return BankSerializerPOST
|
||||
return BankSerializer
|
||||
return BankAccountSerializerPOST
|
||||
return BankAccountSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return BankAccount.objects.filter(
|
||||
qrtr_account__in=self.accessible_accounts().values_list('id'))
|
||||
|
||||
|
||||
class SliceViewSet(viewsets.ModelViewSet):
|
||||
"""API endpoint that allows Banks to be viewed.
|
||||
class SliceViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
|
||||
"""API endpoint that allows BankAccounts 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 Banks to be viewed.
|
||||
"""API endpoint that allows BankAccounts to be viewed.
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
queryset = Institution.objects.all()
|
||||
serializer_class = InstitutionSerializer
|
||||
|
||||
|
||||
class TransactionViewSet(viewsets.ModelViewSet):
|
||||
"""API endpoint that allows Banks to be viewed.
|
||||
class TransactionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
|
||||
"""API endpoint that allows BankAccounts 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', 'isnull'],
|
||||
'trans_id': ['exact', 'lte', 'gte'],
|
||||
'id': ['exact', 'lte', 'gte'],
|
||||
'bank': ['exact']
|
||||
}
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='split')
|
||||
@ -91,9 +134,43 @@ class TransactionViewSet(viewsets.ModelViewSet):
|
||||
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 RuleViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""API endpoint that allows Banks to be viewed.
|
||||
|
||||
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.
|
||||
"""
|
||||
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')
|
||||
)
|
||||
@ -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==2.1.5
|
||||
dj-rest-auth==3.0.0
|
||||
django-rest-swagger==2.2.0
|
||||
Django==3.2.3
|
||||
django-allauth==0.44.0
|
||||
django-allauth==0.50.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==9.2.0
|
||||
psycopg2==2.8.6
|
||||
plaid_python==14.0.0
|
||||
psycopg2-binary==2.8.6
|
||||
pycparser==2.20
|
||||
PyJWT==2.1.0
|
||||
python3-openid==3.2.0
|
||||
|
||||
Loading…
Reference in New Issue
Block a user