diff --git a/backend/Pipfile b/backend/Pipfile index f732aeb..e2e96ad 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -11,6 +11,11 @@ djangorestframework = "*" python-dotenv = "*" psycopg2 = "*" robin-stocks = "*" +django-cors-headers = "*" +pillow = "*" +djangorestframework-jwt = "*" +gunicorn = "*" +whitenoise = "*" [requires] python_version = "3.8" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index b2a439b..9af40f7 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "512cec129202578daefb819a6bf109831ef31164fad71ade999b9699e8c59402" + "sha256": "cd366320b75b1430efc97f93ca86418b4431990547ddea8eeb3a7f7f35343bc6" }, "pipfile-spec": 6, "requires": { @@ -47,6 +47,14 @@ "index": "pypi", "version": "==3.1.5" }, + "django-cors-headers": { + "hashes": [ + "sha256:1ac2b1213de75a251e2ba04448da15f99bcfcbe164288ae6b5ff929dc49b372f", + "sha256:96069c4aaacace786a34ee7894ff680780ec2644e4268b31181044410fecd12e" + ], + "index": "pypi", + "version": "==3.7.0" + }, "djangorestframework": { "hashes": [ "sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7", @@ -55,6 +63,22 @@ "index": "pypi", "version": "==3.12.2" }, + "djangorestframework-jwt": { + "hashes": [ + "sha256:5efe33032f3a4518a300dc51a51c92145ad95fb6f4b272e5aa24701db67936a7", + "sha256:ab15dfbbe535eede8e2e53adaf52ef0cf018ee27dbfad10cbc4cbec2ab63d38c" + ], + "index": "pypi", + "version": "==1.11.0" + }, + "gunicorn": { + "hashes": [ + "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", + "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" + ], + "index": "pypi", + "version": "==20.0.4" + }, "idna": { "hashes": [ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", @@ -63,6 +87,44 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, + "pillow": { + "hashes": [ + "sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6", + "sha256:1d208e670abfeb41b6143537a681299ef86e92d2a3dac299d3cd6830d5c7bded", + "sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865", + "sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174", + "sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032", + "sha256:3de6b2ee4f78c6b3d89d184ade5d8fa68af0848f9b6b6da2b9ab7943ec46971a", + "sha256:47c0d93ee9c8b181f353dbead6530b26980fe4f5485aa18be8f1fd3c3cbc685e", + "sha256:5e2fe3bb2363b862671eba632537cd3a823847db4d98be95690b7e382f3d6378", + "sha256:604815c55fd92e735f9738f65dabf4edc3e79f88541c221d292faec1904a4b17", + "sha256:6c5275bd82711cd3dcd0af8ce0bb99113ae8911fc2952805f1d012de7d600a4c", + "sha256:731ca5aabe9085160cf68b2dbef95fc1991015bc0a3a6ea46a371ab88f3d0913", + "sha256:7612520e5e1a371d77e1d1ca3a3ee6227eef00d0a9cddb4ef7ecb0b7396eddf7", + "sha256:7916cbc94f1c6b1301ac04510d0881b9e9feb20ae34094d3615a8a7c3db0dcc0", + "sha256:81c3fa9a75d9f1afafdb916d5995633f319db09bd773cb56b8e39f1e98d90820", + "sha256:887668e792b7edbfb1d3c9d8b5d8c859269a0f0eba4dda562adb95500f60dbba", + "sha256:93a473b53cc6e0b3ce6bf51b1b95b7b1e7e6084be3a07e40f79b42e83503fbf2", + "sha256:96d4dc103d1a0fa6d47c6c55a47de5f5dafd5ef0114fa10c85a1fd8e0216284b", + "sha256:a3d3e086474ef12ef13d42e5f9b7bbf09d39cf6bd4940f982263d6954b13f6a9", + "sha256:b02a0b9f332086657852b1f7cb380f6a42403a6d9c42a4c34a561aa4530d5234", + "sha256:b09e10ec453de97f9a23a5aa5e30b334195e8d2ddd1ce76cc32e52ba63c8b31d", + "sha256:b6f00ad5ebe846cc91763b1d0c6d30a8042e02b2316e27b05de04fa6ec831ec5", + "sha256:bba80df38cfc17f490ec651c73bb37cd896bc2400cfba27d078c2135223c1206", + "sha256:c3d911614b008e8a576b8e5303e3db29224b455d3d66d1b2848ba6ca83f9ece9", + "sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8", + "sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59", + "sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d", + "sha256:cf6e33d92b1526190a1de904df21663c46a456758c0424e4f947ae9aa6088bf7", + "sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a", + "sha256:d673c4990acd016229a5c1c4ee8a9e6d8f481b27ade5fc3d95938697fa443ce0", + "sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b", + "sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d", + "sha256:f50e7a98b0453f39000619d845be8b06e611e56ee6e8186f7f60c3b1e2f0feae" + ], + "index": "pypi", + "version": "==8.1.0" + }, "psycopg2": { "hashes": [ "sha256:00195b5f6832dbf2876b8bf77f12bdce648224c89c880719c745b90515233301", @@ -84,6 +146,13 @@ "index": "pypi", "version": "==2.8.6" }, + "pyjwt": { + "hashes": [ + "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", + "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" + ], + "version": "==1.7.1" + }, "pyotp": { "hashes": [ "sha256:2a54d393aff3a244b566d78d597c9cb42e91b3b12f3169cec89d9dfff1c9c5bc", @@ -137,6 +206,14 @@ ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.26.3" + }, + "whitenoise": { + "hashes": [ + "sha256:05ce0be39ad85740a78750c86a93485c40f08ad8c62a6006de0233765996e5c7", + "sha256:05d00198c777028d72d8b0bbd234db605ef6d60e9410125124002518a48e515d" + ], + "index": "pypi", + "version": "==5.2.0" } }, "develop": {} diff --git a/backend/api/migrations/0001_initial.py b/backend/api/migrations/0001_initial.py new file mode 100644 index 0000000..634aca6 --- /dev/null +++ b/backend/api/migrations/0001_initial.py @@ -0,0 +1,59 @@ +# Generated by Django 3.1.5 on 2021-01-30 20:48 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Charity', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ein', models.CharField(blank=True, max_length=50, null=True)), + ('name', models.CharField(blank=True, max_length=50, null=True)), + ('sub_name', models.CharField(blank=True, max_length=50, null=True)), + ('city', models.CharField(blank=True, max_length=20, null=True)), + ('state', models.CharField(blank=True, max_length=2, null=True)), + ], + options={ + 'verbose_name_plural': 'Charities', + }, + ), + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nickname', models.CharField(blank=True, max_length=20, null=True)), + ('profile_pic', models.ImageField(default='default.jpg', upload_to='profile_pics')), + ('percentage', models.DecimalField(decimal_places=2, max_digits=3)), + ('charity', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='api.charity')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name_plural': 'Profiles', + }, + ), + migrations.CreateModel( + name='Stock', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ticker', models.CharField(blank=True, max_length=5, null=True)), + ('buy_price', models.DecimalField(decimal_places=2, max_digits=9)), + ('quantity', models.DecimalField(decimal_places=2, max_digits=9)), + ('uuid', models.UUIDField(editable=False, unique=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stock', to='api.profile')), + ], + options={ + 'verbose_name_plural': 'Stocks', + }, + ), + ] diff --git a/backend/api/migrations/0002_auto_20210130_2301.py b/backend/api/migrations/0002_auto_20210130_2301.py new file mode 100644 index 0000000..a96f7a4 --- /dev/null +++ b/backend/api/migrations/0002_auto_20210130_2301.py @@ -0,0 +1,43 @@ +# Generated by Django 3.1.5 on 2021-01-30 23:01 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='charity', + name='id', + ), + migrations.RemoveField( + model_name='stock', + name='id', + ), + migrations.AlterField( + model_name='charity', + name='ein', + field=models.CharField(max_length=50, primary_key=True, serialize=False), + preserve_default=False, + ), + migrations.AlterField( + model_name='profile', + name='percentage', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True), + ), + migrations.AlterField( + model_name='stock', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stocks', to='api.profile'), + ), + migrations.AlterField( + model_name='stock', + name='uuid', + field=models.UUIDField(primary_key=True, serialize=False, unique=True), + ), + ] diff --git a/backend/api/models.py b/backend/api/models.py index 49b02d5..d4c706f 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -1,13 +1,29 @@ +from django.contrib.auth.models import User from django.db import models +from PIL import Image + # Create your models here. +class Charity (models.Model): + ein = models.CharField(primary_key=True, max_length=50) + name = models.CharField(max_length=50, blank=True, null=True) + sub_name = models.CharField(max_length=50, blank=True, null=True) + city = models.CharField(max_length=20, blank=True, null=True) + state = models.CharField(max_length=2, blank=True, null=True) + + class Meta: + verbose_name_plural = "Charities" + + def __str__(self): + return self.name + class Profile (models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) + charity = models.OneToOneField(Charity, blank=True, null=True, on_delete=models.DO_NOTHING) nickname = models.CharField(max_length=20, blank=True, null=True) profile_pic = models.ImageField(default='default.jpg', upload_to='profile_pics') - charity = models.OneToOneField(Charity, blank=True, null=True) - percentage = models.DecimalField(max_digits=3, decimal_places=2) + percentage = models.DecimalField(max_digits=3, decimal_places=2, blank=True, null=True) def __str__(self): return f'{self.user.username}\'s profile' @@ -22,22 +38,18 @@ class Profile (models.Model): img.thumbnail(size) img.save(self.profile_pic.path) -class Charity (models.Model): - ein = models.CharField(max_length=50, blank=True, null=True) - name = models.CharField(max_length=50, blank=True, null=True) - sub_name = models.CharField(max_length=50, blank=True, null=True) - city = models.CharField(max_length=20, blank=True, null=True) - state = models.CharField(max_length=2, blank=True, null=True) - - def __str__(self): - return self.name + class Meta: + verbose_name_plural = "Profiles" class Stock (models.Model): - user = models.ForeignKey(Profile, related_name='stock', on_delete=models.CASCADE) + user = models.ForeignKey(Profile, related_name='stocks', on_delete=models.CASCADE) ticker = models.CharField(max_length=5, blank=True, null=True) buy_price = models.DecimalField(max_digits=9, decimal_places=2) quantity = models.DecimalField(max_digits=9, decimal_places=2) - uuid = models.UUIDField(editable=False, unique=True) + uuid = models.UUIDField(primary_key=True, unique=True) + + class Meta: + verbose_name_plural = "Stocks" def __str__(self): return f'{self.user.user.username}\'s Stock: {self.ticker} @ {self.buy_price}' diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 82197b8..a59e9e4 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -1,7 +1,56 @@ from rest_framework import serializers +from django.contrib.auth.models import User from . import models class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ('username', 'email', 'first_name', 'last_name') \ No newline at end of file + fields = ('username', 'email', 'first_name', 'last_name') + +class UserCreateSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True) + + def create(self, validated_data): + print("I WAS CALLED!") + user = User.objects.create( + username = validated_data['username'], + email = validated_data['email'], + first_name = validated_data['first_name'], + last_name = validated_data['last_name'] + ) + + user.set_password(validated_data['password']) + user.save() + + profile = models.Profile.objects.create(user=user) + profile.save() + + return user + + class Meta: + model = User + fields = ('username', 'password', 'email', 'first_name', 'last_name') + +class StockSerializer(serializers.ModelSerializer): + class Meta: + model = models.Stock + fields = ('ticker', 'buy_price', 'quantity', 'uuid') + +class StockCreateSerializer(serializers.ModelSerializer): + class Meta: + model = models.Stock + fields = ('user', 'ticker', 'buy_price', 'quantity', 'uuid') + +class CharitySerializer(serializers.ModelSerializer): + class Meta: + model = models.Charity + fields = ('ein', 'name', 'sub_name', 'city', 'state') + +class ProfileSerializer(serializers.ModelSerializer): + user = UserSerializer() + charity = CharitySerializer() + stocks = StockSerializer(many=True) + + class Meta: + model = models.Profile + fields = ('user', 'charity', 'nickname', 'profile_pic', 'percentage', 'stocks') diff --git a/backend/api/urls.py b/backend/api/urls.py index d9d8136..77177e7 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -1,8 +1,16 @@ from rest_framework_jwt.views import obtain_jwt_token -from django.urls import path from . import views +from django.urls import path, include +from rest_framework.routers import DefaultRouter + urlpatterns = [ - path('token/', obtain_jwt_token) + path('charity/', views.CharityViewSet.as_view({'get': 'retrieve'})), + path('charity', views.CharityViewSet.as_view({'get': 'list', 'post': 'create'})), + path('stock/', views.StockViewSet.as_view({'get': 'retrieve'})), + path('stock', views.StockViewSet.as_view({'get': 'list', 'post': 'create'})), + path('profile/create', views.UserProfileCreate.as_view()), + path('profile', views.UserProfileDetail.as_view()), + path('token', obtain_jwt_token) ] diff --git a/backend/api/views.py b/backend/api/views.py index 91ea44a..35896bc 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -1,3 +1,51 @@ -from django.shortcuts import render +from django.shortcuts import render, get_object_or_404 +from django.http import QueryDict -# Create your views here. +from rest_framework import status, permissions +from rest_framework.views import APIView +from rest_framework.viewsets import ModelViewSet +from rest_framework.generics import CreateAPIView +from rest_framework.response import Response + +from .models import * +from .serializers import * + +class CharityViewSet(ModelViewSet): + queryset = models.Charity.objects.all() + serializer_class = CharitySerializer + +class StockViewSet(ModelViewSet): + queryset = '' + serializer_class = StockSerializer + + def list(self, request, *args, **kwargs): + queryset = request.user.profile.stocks.all() + serializer = StockSerializer(queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def retrieve(self, request, pk=None, *args, **kwargs): + queryset = request.user.profile.stocks.filter(uuid=pk) + if queryset.count() != 1: + return Response({"message": "Stock not found."}, status=status.HTTP_404_NOT_FOUND) + serializer = StockSerializer(queryset.first()) + return Response(serializer.data, status=status.HTTP_200_OK) + + + def create(self, request, *args, **kwargs): + data = QueryDict.copy(request.data) + data.update({'user': request.user}) + serializer = StockCreateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + +class UserProfileDetail(APIView): + def get(self, request, format=None): + profile = request.user.profile + serializer = ProfileSerializer(profile) + return Response(serializer.data, status=status.HTTP_200_OK) + +class UserProfileCreate(CreateAPIView): + model = User + permission_classes = [permissions.AllowAny] + serializer_class = UserCreateSerializer \ No newline at end of file diff --git a/backend/config/settings.py b/backend/config/settings.py index 1bd9a1b..1549f0e 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -27,12 +27,12 @@ BASE_DIR = Path(__file__).resolve().parent.parent SECRET_KEY = os.getenv("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.getenv("DEBUG") +DEBUG = os.getenv("DEBUG") == 'True' if DEBUG: ALLOWED_HOSTS = ["*"] else: - ALLOWED_HOSTS = ["reinvest.online"] + ALLOWED_HOSTS = ["api.reinvest.space"] # Application definition @@ -54,6 +54,7 @@ MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -62,6 +63,8 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' + REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', @@ -152,4 +155,7 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.1/howto/static-files/ -STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') + +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_URL = '/media/' diff --git a/backend/config/urls.py b/backend/config/urls.py index e36fd86..672ec29 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -13,10 +13,15 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +from django.conf import settings +from django.conf.urls.static import static from django.contrib import admin -from django.urls import path +from django.urls import path, include urlpatterns = [ - path('', include('api.urls')) + path('api/', include('api.urls')), path('admin/', admin.site.urls), ] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file