From 8119a1da7cd4032279b70f59dd6622ee1c6d7ffb Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Sun, 3 Nov 2024 16:10:33 +0300 Subject: [PATCH 01/11] feat: Add market feed models for stock, tag, portfolio, post, and comment --- backend/backend/settings.py | 9 +++++--- backend/manage.py | 5 +++-- backend/marketfeed/__init__.py | 0 backend/marketfeed/admin.py | 3 +++ backend/marketfeed/apps.py | 6 ++++++ backend/marketfeed/migrations/__init__.py | 0 backend/marketfeed/models.py | 26 +++++++++++++++++++++++ backend/marketfeed/tests.py | 3 +++ backend/marketfeed/views.py | 3 +++ backend/requirements.txt | 1 + 10 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 backend/marketfeed/__init__.py create mode 100644 backend/marketfeed/admin.py create mode 100644 backend/marketfeed/apps.py create mode 100644 backend/marketfeed/migrations/__init__.py create mode 100644 backend/marketfeed/models.py create mode 100644 backend/marketfeed/tests.py create mode 100644 backend/marketfeed/views.py diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 97e6dd9..e1f2f7f 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -33,12 +33,14 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'onboarding', + 'marketfeed', 'drf_spectacular', 'rest_framework', 'rest_framework_simplejwt', 'rest_framework_simplejwt.token_blacklist', ] + MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', @@ -50,9 +52,11 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] + CORS_ALLOW_ALL_ORIGINS = True ROOT_URLCONF = 'backend.urls' + TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -72,14 +76,13 @@ WSGI_APPLICATION = 'backend.wsgi.application' - DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': os.getenv("MYSQL_DATABASE"), - 'USER': os.getenv("MYSQL_USER"), + 'USER': 'root', 'PASSWORD': os.getenv("MYSQL_PASSWORD"), - 'HOST': 'mysql-db', + 'HOST': '127.0.0.1', 'PORT': '3306', } } diff --git a/backend/manage.py b/backend/manage.py index 3b65d54..6bb7a2d 100755 --- a/backend/manage.py +++ b/backend/manage.py @@ -4,7 +4,7 @@ import sys import time -import mysql.connector +# import mysql.connector import os from django.core.management import execute_from_command_line from django.db import connection, OperationalError @@ -20,7 +20,8 @@ def main(): connection.ensure_connection() print("Db is ready") break - except OperationalError: + except OperationalError as e: + print(e) print("Database not ready yet. Waiting...") time.sleep(5) diff --git a/backend/marketfeed/__init__.py b/backend/marketfeed/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/marketfeed/admin.py b/backend/marketfeed/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/marketfeed/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/marketfeed/apps.py b/backend/marketfeed/apps.py new file mode 100644 index 0000000..ff0d2f0 --- /dev/null +++ b/backend/marketfeed/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MarketfeedConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "marketfeed" diff --git a/backend/marketfeed/migrations/__init__.py b/backend/marketfeed/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/marketfeed/models.py b/backend/marketfeed/models.py new file mode 100644 index 0000000..afca27b --- /dev/null +++ b/backend/marketfeed/models.py @@ -0,0 +1,26 @@ +from django.db import models +from onboarding.models import * + +# Models for market feed such as post, portfolio, stock, comment + +class Tag(models.Model): + name = models.CharField(max_length=40) + user_id = models.ForeignKey(User, on_delete=models.CASCADE) + +class Post(models.Model): + title = models.CharField(max_length=50) + content = models.CharField(max_length=250) + created_at = models.DateField() + updated_at = models.DateField(null=True) + liked_by = models.ManyToManyField(User, verbose_name="list of users who liked the post") + tags = models.ManyToManyField(Tag, verbose_name="list of tags added to the post") + +class Comment(models.Model): + post_id=models.ForeignKey(Post, on_delete=models.CASCADE) + user_id=models.ForeignKey(User, on_delete=models.CASCADE) + content = models.CharField(max_length=250) + + + + + diff --git a/backend/marketfeed/tests.py b/backend/marketfeed/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/marketfeed/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/marketfeed/views.py b/backend/marketfeed/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/marketfeed/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/backend/requirements.txt b/backend/requirements.txt index ba6f02f..26d9744 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,3 +11,4 @@ djangorestframework-simplejwt==5.3.1 PyJWT==2.8.0 drf-spectacular==0.27.2 django-cors-headers==4.5.0 +corsheaders From 3a2a1c332ff03e411f803b42532754425d67f55d Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Sun, 3 Nov 2024 16:12:12 +0300 Subject: [PATCH 02/11] feat: Add stock and portfolio details --- backend/marketfeed/models.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/backend/marketfeed/models.py b/backend/marketfeed/models.py index afca27b..b4e6ecd 100644 --- a/backend/marketfeed/models.py +++ b/backend/marketfeed/models.py @@ -3,10 +3,24 @@ # Models for market feed such as post, portfolio, stock, comment +class Stock(models.Model): + name = models.CharField(max_length=250) + symbol = models.CharField(max_length=250) + price = models.IntegerField + + class Tag(models.Model): name = models.CharField(max_length=40) user_id = models.ForeignKey(User, on_delete=models.CASCADE) + +class Portfolio(models.Model): + name = models.CharField(max_length=50) + description = models.CharField(max_length = 150) + user_id = models.ForeignKey(User, on_delete=models.CASCADE) + stocks = models.ManyToManyField(Stock, verbose_name="list of stocks in the portfolio") + + class Post(models.Model): title = models.CharField(max_length=50) content = models.CharField(max_length=250) @@ -14,13 +28,11 @@ class Post(models.Model): updated_at = models.DateField(null=True) liked_by = models.ManyToManyField(User, verbose_name="list of users who liked the post") tags = models.ManyToManyField(Tag, verbose_name="list of tags added to the post") + portfolios = models.ManyToManyField(Portfolio, verbose_name="list of portfolios added to the post") + class Comment(models.Model): post_id=models.ForeignKey(Post, on_delete=models.CASCADE) user_id=models.ForeignKey(User, on_delete=models.CASCADE) content = models.CharField(max_length=250) - - - - From 50882eadf37d333d4b96d07a20c8bb480dfcaf45 Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Sun, 3 Nov 2024 17:26:37 +0300 Subject: [PATCH 03/11] feat: Add currency --- backend/marketfeed/models.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/backend/marketfeed/models.py b/backend/marketfeed/models.py index b4e6ecd..d83e4df 100644 --- a/backend/marketfeed/models.py +++ b/backend/marketfeed/models.py @@ -3,29 +3,38 @@ # Models for market feed such as post, portfolio, stock, comment +class Currency(models.Model): + name = models.CharField(max_length=40) + code = models.CharField(max_length=5) + + class Stock(models.Model): name = models.CharField(max_length=250) symbol = models.CharField(max_length=250) - price = models.IntegerField + price = models.DecimalField(max_digits=19, decimal_places=10) + currency = models.ForeignKey(Currency, on_delete=models.CASCADE) class Tag(models.Model): name = models.CharField(max_length=40) user_id = models.ForeignKey(User, on_delete=models.CASCADE) + created_at = models.DateTimeField() class Portfolio(models.Model): name = models.CharField(max_length=50) description = models.CharField(max_length = 150) user_id = models.ForeignKey(User, on_delete=models.CASCADE) + created_at = models.DateTimeField() + updated_at = models.DateTimeField(null=True, auto_now=True) stocks = models.ManyToManyField(Stock, verbose_name="list of stocks in the portfolio") class Post(models.Model): title = models.CharField(max_length=50) content = models.CharField(max_length=250) - created_at = models.DateField() - updated_at = models.DateField(null=True) + created_at = models.DateTimeField() + updated_at = models.DateTimeField(null=True, auto_now=True) liked_by = models.ManyToManyField(User, verbose_name="list of users who liked the post") tags = models.ManyToManyField(Tag, verbose_name="list of tags added to the post") portfolios = models.ManyToManyField(Portfolio, verbose_name="list of portfolios added to the post") @@ -34,5 +43,5 @@ class Post(models.Model): class Comment(models.Model): post_id=models.ForeignKey(Post, on_delete=models.CASCADE) user_id=models.ForeignKey(User, on_delete=models.CASCADE) + created_at = models.DateTimeField() content = models.CharField(max_length=250) - From 86315dff51b5a03ad464b1da3c2999e09f71023e Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Sun, 3 Nov 2024 17:43:01 +0300 Subject: [PATCH 04/11] feat: Add stock manager --- backend/marketfeed/models.py | 43 ++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/backend/marketfeed/models.py b/backend/marketfeed/models.py index d83e4df..d41591d 100644 --- a/backend/marketfeed/models.py +++ b/backend/marketfeed/models.py @@ -3,22 +3,40 @@ # Models for market feed such as post, portfolio, stock, comment +"""-----------------------------------------------------MANAGERS-----------------------------------------------------""" + +class StockManager(models.Manager): + def get_queryset(self): + queryset = super().get_queryset() + for stock in query_set: + stock.price = stock.fetch_current_stock_price() + return queryset + + +"""-------------------------------------------------------MODELS-------------------------------------------------------""" + class Currency(models.Model): name = models.CharField(max_length=40) - code = models.CharField(max_length=5) + code = models.CharField(max_length=5, unique=True) class Stock(models.Model): name = models.CharField(max_length=250) - symbol = models.CharField(max_length=250) - price = models.DecimalField(max_digits=19, decimal_places=10) + symbol = models.CharField(max_length=250, unique=True) currency = models.ForeignKey(Currency, on_delete=models.CASCADE) + objects = StockManager() + + @property + def fetch_current_stock_price(): + #TODO: Stock fetching mechanism to be implemented + return 10 + class Tag(models.Model): - name = models.CharField(max_length=40) + name = models.CharField(max_length=40, unique=True) user_id = models.ForeignKey(User, on_delete=models.CASCADE) - created_at = models.DateTimeField() + created_at = models.DateTimeField(auto_now_add=True) class Portfolio(models.Model): @@ -32,16 +50,17 @@ class Portfolio(models.Model): class Post(models.Model): title = models.CharField(max_length=50) - content = models.CharField(max_length=250) - created_at = models.DateTimeField() - updated_at = models.DateTimeField(null=True, auto_now=True) - liked_by = models.ManyToManyField(User, verbose_name="list of users who liked the post") - tags = models.ManyToManyField(Tag, verbose_name="list of tags added to the post") - portfolios = models.ManyToManyField(Portfolio, verbose_name="list of portfolios added to the post") + content = models.TextField() + author = models.ForeignKey(User, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + liked_by = models.ManyToManyField(User, related_name="liked_posts") + tags = models.ManyToManyField(Tag, verbose_name="list of tags") + portfolios = models.ManyToManyField(Portfolio, verbose_name="list of portfolios") class Comment(models.Model): post_id=models.ForeignKey(Post, on_delete=models.CASCADE) user_id=models.ForeignKey(User, on_delete=models.CASCADE) - created_at = models.DateTimeField() + created_at = models.DateTimeField(auto_now_add=True) content = models.CharField(max_length=250) From 8b62c7d9eecab0de6275e953d3ce99369f659509 Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Sun, 3 Nov 2024 18:48:27 +0300 Subject: [PATCH 05/11] feat: Add post create and get endpoints --- backend/backend/urls.py | 2 +- backend/marketfeed/models.py | 5 ++++- backend/marketfeed/serializers.py | 33 +++++++++++++++++++++++++++ backend/marketfeed/urls.py | 9 ++++++++ backend/marketfeed/views.py | 37 ++++++++++++++++++++++++++++++- backend/onboarding/serializers.py | 1 + 6 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 backend/marketfeed/serializers.py create mode 100644 backend/marketfeed/urls.py diff --git a/backend/backend/urls.py b/backend/backend/urls.py index 1bbbd5f..e7f1d14 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -36,7 +36,7 @@ name="swagger-ui", ), path("", include("onboarding.urls")), - + path("", include("marketfeed.urls")), ] if settings.DEBUG: diff --git a/backend/marketfeed/models.py b/backend/marketfeed/models.py index d41591d..804b69c 100644 --- a/backend/marketfeed/models.py +++ b/backend/marketfeed/models.py @@ -28,6 +28,9 @@ class Stock(models.Model): objects = StockManager() @property + def price(self): + return self.fetch_current_stock_price() + def fetch_current_stock_price(): #TODO: Stock fetching mechanism to be implemented return 10 @@ -53,7 +56,7 @@ class Post(models.Model): content = models.TextField() author = models.ForeignKey(User, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) + updated_at = models.DateTimeField(auto_now=True, null=True) liked_by = models.ManyToManyField(User, related_name="liked_posts") tags = models.ManyToManyField(Tag, verbose_name="list of tags") portfolios = models.ManyToManyField(Portfolio, verbose_name="list of portfolios") diff --git a/backend/marketfeed/serializers.py b/backend/marketfeed/serializers.py new file mode 100644 index 0000000..c7c2c75 --- /dev/null +++ b/backend/marketfeed/serializers.py @@ -0,0 +1,33 @@ + +from marketfeed.models import * +from rest_framework import serializers + + +class PostSerializer(serializers.ModelSerializer): + # Many-to-Many relationships as custom fields + liked_by = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), many=True, required=False) + tags = serializers.SerializerMethodField() + portfolios = serializers.SerializerMethodField() + + class Meta: + model = Post + fields = ['id', 'title', 'content', 'created_at', 'updated_at', 'liked_by', 'tags', 'portfolios'] + + def get_tags(self, obj): + return [{'id': tag.id, 'name': tag.name} for tag in obj.tags.all()] + + def get_portfolios(self, obj): + return [{'id': portfolio.id, 'name': portfolio.name} for portfolio in obj.portfolios.all()] + + def create(self, validated_data): + liked_by = validated_data.pop('liked_by', []) + tags = self.initial_data.get('tags', []) + portfolios = self.initial_data.get('portfolios', []) + + post = Post.objects.create(**validated_data) + + post.liked_by.set(liked_by) + post.tags.set(tags) + post.portfolios.set(portfolios) + + return post \ No newline at end of file diff --git a/backend/marketfeed/urls.py b/backend/marketfeed/urls.py new file mode 100644 index 0000000..f323870 --- /dev/null +++ b/backend/marketfeed/urls.py @@ -0,0 +1,9 @@ +from django.shortcuts import render +from django.urls import path +from marketfeed.views import * + + +urlpatterns = [ + path('post/', PostViewSet.as_view({'post': 'create', }), name='create_post'), + path('post//', PostViewSet.as_view({'get': 'retrieve'}), name='post-detail'), +] \ No newline at end of file diff --git a/backend/marketfeed/views.py b/backend/marketfeed/views.py index 91ea44a..316bc44 100644 --- a/backend/marketfeed/views.py +++ b/backend/marketfeed/views.py @@ -1,3 +1,38 @@ from django.shortcuts import render - +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.response import Response +from marketfeed.serializers import * # Create your views here. + +class PostViewSet(viewsets.ModelViewSet): + serializer_class = PostSerializer + queryset = Post.objects.all() + + def get_permissions(self): + if self.action == 'create': + self.permission_classes = [IsAuthenticated] + else: + self.permission_classes = [AllowAny] + return super().get_permissions() + + def create(self, request, *args, **kwargs): + user = request.user + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save(author=user) + + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + def retrieve(self, request): + instance = self.get_object() # Retrieves the post instance based on the URL parameter (e.g., post ID) + serializer = self.get_serializer(instance) + + # Customize the response to include detailed information for related fields + data = serializer.data + data['liked_by'] = [user.id for user in instance.liked_by.all()] + data['tags'] = [{'id': tag.id, 'name': tag.name} for tag in instance.tags.all()] + data['portfolios'] = [{'id': portfolio.id, 'name': portfolio.name} for portfolio in instance.portfolios.all()] + + return Response(data, status=status.HTTP_200_OK) diff --git a/backend/onboarding/serializers.py b/backend/onboarding/serializers.py index 0ea5fe8..53cd96f 100644 --- a/backend/onboarding/serializers.py +++ b/backend/onboarding/serializers.py @@ -54,6 +54,7 @@ class Meta: model = User fields = ['url', 'username', 'email'] + class LogoutSerializer(serializers.Serializer): refreshToken = serializers.CharField(required=True, max_length=512) From 1e7b95b8a7fa54b989fdde35cd73933ec9ba765a Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Sun, 3 Nov 2024 21:03:25 +0300 Subject: [PATCH 06/11] feat: Add currency, comment, and portfolio endpoints --- backend/marketfeed/migrations/0001_initial.py | 184 +++++++++++++++ backend/marketfeed/models.py | 4 +- backend/marketfeed/serializers.py | 106 ++++++++- backend/marketfeed/urls.py | 16 +- backend/marketfeed/views.py | 209 +++++++++++++++++- 5 files changed, 488 insertions(+), 31 deletions(-) create mode 100644 backend/marketfeed/migrations/0001_initial.py diff --git a/backend/marketfeed/migrations/0001_initial.py b/backend/marketfeed/migrations/0001_initial.py new file mode 100644 index 0000000..6811d17 --- /dev/null +++ b/backend/marketfeed/migrations/0001_initial.py @@ -0,0 +1,184 @@ +# Generated by Django 4.2 on 2024-11-03 15:49 + +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="Currency", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=40)), + ("code", models.CharField(max_length=5, unique=True)), + ], + ), + migrations.CreateModel( + name="Portfolio", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50)), + ("description", models.CharField(max_length=150)), + ("created_at", models.DateTimeField()), + ("updated_at", models.DateTimeField(auto_now=True, null=True)), + ], + ), + migrations.CreateModel( + name="Tag", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=40, unique=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "user_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="Stock", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=250)), + ("symbol", models.CharField(max_length=250, unique=True)), + ( + "currency", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="marketfeed.currency", + ), + ), + ], + ), + migrations.CreateModel( + name="Post", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=50)), + ("content", models.TextField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True, null=True)), + ( + "author", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "liked_by", + models.ManyToManyField( + related_name="liked_posts", to=settings.AUTH_USER_MODEL + ), + ), + ( + "portfolios", + models.ManyToManyField( + to="marketfeed.portfolio", verbose_name="list of portfolios" + ), + ), + ( + "tags", + models.ManyToManyField( + to="marketfeed.tag", verbose_name="list of tags" + ), + ), + ], + ), + migrations.AddField( + model_name="portfolio", + name="stocks", + field=models.ManyToManyField( + to="marketfeed.stock", verbose_name="list of stocks in the portfolio" + ), + ), + migrations.AddField( + model_name="portfolio", + name="user_id", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + migrations.CreateModel( + name="Comment", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("content", models.CharField(max_length=250)), + ( + "post_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="marketfeed.post", + ), + ), + ( + "user_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/backend/marketfeed/models.py b/backend/marketfeed/models.py index 804b69c..9d53666 100644 --- a/backend/marketfeed/models.py +++ b/backend/marketfeed/models.py @@ -8,8 +8,6 @@ class StockManager(models.Manager): def get_queryset(self): queryset = super().get_queryset() - for stock in query_set: - stock.price = stock.fetch_current_stock_price() return queryset @@ -31,7 +29,7 @@ class Stock(models.Model): def price(self): return self.fetch_current_stock_price() - def fetch_current_stock_price(): + def fetch_current_stock_price(self): #TODO: Stock fetching mechanism to be implemented return 10 diff --git a/backend/marketfeed/serializers.py b/backend/marketfeed/serializers.py index c7c2c75..73e2069 100644 --- a/backend/marketfeed/serializers.py +++ b/backend/marketfeed/serializers.py @@ -1,33 +1,115 @@ -from marketfeed.models import * +from .models import * from rest_framework import serializers +from onboarding.models import User + +class CurrencySerializer(serializers.ModelSerializer): + class Meta: + model = Currency + fields = ['id', 'name', 'code'] + + +class StockSerializer(serializers.ModelSerializer): + currency = serializers.PrimaryKeyRelatedField(queryset=Currency.objects.all()) + price = serializers.ReadOnlyField() + + class Meta: + model = Stock + fields = ['id', 'name', 'symbol', 'currency', 'price'] + + def __init__(self, *args, **kwargs): + super(StockSerializer, self).__init__(*args, **kwargs) + + # Get the request method if available + request = self.context.get('request', None) + + if request and request.method == 'POST': + self.fields['currency'].required = True + self.fields['price'].required = False + elif request and request.method == 'PUT': + self.fields['currency'].required = False + self.fields['price'].required = False + self.fields['name'].required = False + self.fields['symbol'].required = False + +class TagSerializer(serializers.ModelSerializer): + user_id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) + + class Meta: + model = Tag + fields = ['id', 'name', 'user_id'] + + +class PortfolioSerializer(serializers.ModelSerializer): + user_id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) + stocks = serializers.PrimaryKeyRelatedField(queryset=Stock.objects.all(), many=True) + + class Meta: + model = Portfolio + fields = ['id', 'name', 'description', 'user_id', 'stocks'] + + +class CommentSerializer(serializers.ModelSerializer): + user_id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) + post_id = serializers.PrimaryKeyRelatedField(queryset=Post.objects.all()) + + class Meta: + model = Comment + fields = ['id', 'post_id', 'user_id', 'content'] + + def __init__(self, *args, **kwargs): + super(CommentSerializer, self).__init__(*args, **kwargs) + + # Get the request method if available + request = self.context.get('request', None) + + if request and request.method == 'PUT': + # Make `post_id` and `user_id` optional for PUT requests + self.fields['post_id'].required = False + self.fields['user_id'].required = False + elif request and request.method == 'POST': + # Ensure `post_id` and `user_id` are required for POST requests + self.fields['post_id'].required = True + self.fields['user_id'].required = True class PostSerializer(serializers.ModelSerializer): - # Many-to-Many relationships as custom fields + # author = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) liked_by = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), many=True, required=False) - tags = serializers.SerializerMethodField() - portfolios = serializers.SerializerMethodField() + tags = serializers.PrimaryKeyRelatedField(queryset=Tag.objects.all(), many=True, required=False) + portfolios = serializers.PrimaryKeyRelatedField(queryset=Portfolio.objects.all(), many=True, required=False) class Meta: model = Post fields = ['id', 'title', 'content', 'created_at', 'updated_at', 'liked_by', 'tags', 'portfolios'] - def get_tags(self, obj): - return [{'id': tag.id, 'name': tag.name} for tag in obj.tags.all()] + def __init__(self, *args, **kwargs): + super(PostSerializer, self).__init__(*args, **kwargs) + + # Get the request method if available + request = self.context.get('request', None) + + if request: + if request.method == 'PUT' or request.method == 'DELETE': + self.fields['title'].required = False + self.fields['content'].required = False + + elif request.method == 'POST': + self.fields['title'].required = True + self.fields['content'].required = True - def get_portfolios(self, obj): - return [{'id': portfolio.id, 'name': portfolio.name} for portfolio in obj.portfolios.all()] def create(self, validated_data): liked_by = validated_data.pop('liked_by', []) - tags = self.initial_data.get('tags', []) - portfolios = self.initial_data.get('portfolios', []) + tags = validated_data.pop('tags', []) + portfolios = validated_data.pop('portfolios', []) post = Post.objects.create(**validated_data) + #Many-to-Many relationships post.liked_by.set(liked_by) post.tags.set(tags) post.portfolios.set(portfolios) - - return post \ No newline at end of file + + return post + \ No newline at end of file diff --git a/backend/marketfeed/urls.py b/backend/marketfeed/urls.py index f323870..2daac1a 100644 --- a/backend/marketfeed/urls.py +++ b/backend/marketfeed/urls.py @@ -1,9 +1,15 @@ -from django.shortcuts import render -from django.urls import path -from marketfeed.views import * +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import CurrencyViewSet, StockViewSet, TagViewSet, PortfolioViewSet, PostViewSet, CommentViewSet +router = DefaultRouter() +router.register(r'currencies', CurrencyViewSet) +router.register(r'stocks', StockViewSet) +router.register(r'tags', TagViewSet) +router.register(r'portfolios', PortfolioViewSet) +router.register(r'posts', PostViewSet) +router.register(r'comments', CommentViewSet) urlpatterns = [ - path('post/', PostViewSet.as_view({'post': 'create', }), name='create_post'), - path('post//', PostViewSet.as_view({'get': 'retrieve'}), name='post-detail'), + path('', include(router.urls)), ] \ No newline at end of file diff --git a/backend/marketfeed/views.py b/backend/marketfeed/views.py index 316bc44..4148c64 100644 --- a/backend/marketfeed/views.py +++ b/backend/marketfeed/views.py @@ -1,10 +1,148 @@ from django.shortcuts import render -from rest_framework import viewsets -from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework import viewsets, status, permissions +from rest_framework.permissions import IsAuthenticated, AllowAny, IsAuthenticatedOrReadOnly from rest_framework.response import Response -from marketfeed.serializers import * +from .serializers import * +from .models import * + # Create your views here. +class CurrencyViewSet(viewsets.ModelViewSet): + queryset = Currency.objects.all() + serializer_class = CurrencySerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def list(self, request): + currencies = self.get_queryset() + serializer = self.get_serializer(currencies, many=True) + return Response(serializer.data) + + def retrieve(self, request, pk=None): + currency = self.get_object() + serializer = self.get_serializer(currency) + return Response(serializer.data) + + def create(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def update(self, request, pk=None): + currency = self.get_object() + serializer = self.get_serializer(currency, data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) + + def destroy(self, request, pk=None): + currency = self.get_object() + currency.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class StockViewSet(viewsets.ModelViewSet): + queryset = Stock.objects.all() + serializer_class = StockSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def list(self, request): + stocks = self.get_queryset() + serializer = self.get_serializer(stocks, many=True) + return Response(serializer.data) + + def retrieve(self, request, pk=None): + stock = self.get_object() + serializer = self.get_serializer(stock) + return Response(serializer.data) + + def create(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def update(self, request, pk=None): + stock = self.get_object() + serializer = self.get_serializer(stock, data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) + + def destroy(self, request, pk=None): + stock = self.get_object() + stock.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class TagViewSet(viewsets.ModelViewSet): + queryset = Tag.objects.all() + serializer_class = TagSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def list(self, request): + tags = self.get_queryset() + serializer = self.get_serializer(tags, many=True) + return Response(serializer.data) + + def retrieve(self, request, pk=None): + tag = self.get_object() + serializer = self.get_serializer(tag) + return Response(serializer.data) + + def create(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def update(self, request, pk=None): + tag = self.get_object() + serializer = self.get_serializer(tag, data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) + + def destroy(self, request, pk=None): + tag = self.get_object() + tag.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class PortfolioViewSet(viewsets.ModelViewSet): + queryset = Portfolio.objects.all() + serializer_class = PortfolioSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def list(self, request): + portfolios = self.get_queryset() + serializer = self.get_serializer(portfolios, many=True) + return Response(serializer.data) + + def retrieve(self, request, pk=None): + portfolio = self.get_object() + serializer = self.get_serializer(portfolio) + return Response(serializer.data) + + def create(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save(user_id=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def update(self, request, pk=None): + portfolio = self.get_object() + serializer = self.get_serializer(portfolio, data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) + + def destroy(self, request, pk=None): + portfolio = self.get_object() + portfolio.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + class PostViewSet(viewsets.ModelViewSet): serializer_class = PostSerializer queryset = Post.objects.all() @@ -16,23 +154,72 @@ def get_permissions(self): self.permission_classes = [AllowAny] return super().get_permissions() - def create(self, request, *args, **kwargs): - user = request.user - serializer = self.serializer_class(data=request.data) + def list(self, request): + posts = self.get_queryset() + serializer = self.get_serializer(posts, many=True) + return Response(serializer.data) + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - serializer.save(author=user) - - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + serializer.save(author=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) - def retrieve(self, request): + def retrieve(self, request, *args, **kwargs): instance = self.get_object() # Retrieves the post instance based on the URL parameter (e.g., post ID) serializer = self.get_serializer(instance) # Customize the response to include detailed information for related fields data = serializer.data + data['author'] = instance.author.id data['liked_by'] = [user.id for user in instance.liked_by.all()] data['tags'] = [{'id': tag.id, 'name': tag.name} for tag in instance.tags.all()] data['portfolios'] = [{'id': portfolio.id, 'name': portfolio.name} for portfolio in instance.portfolios.all()] return Response(data, status=status.HTTP_200_OK) + + def update(self, request, *args, **kwargs): + post = self.get_object() + serializer = self.get_serializer(post, data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) + + def destroy(self, request, *args, **kwargs): + post = self.get_object() + post.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class CommentViewSet(viewsets.ModelViewSet): + queryset = Comment.objects.all() + serializer_class = CommentSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def list(self, request): + comments = self.get_queryset() + serializer = self.get_serializer(comments, many=True) + return Response(serializer.data) + + def retrieve(self, request, pk=None): + comment = self.get_object() + serializer = self.get_serializer(comment) + return Response(serializer.data) + + def create(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save(user_id=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def update(self, request, pk=None): + comment = self.get_object() + serializer = self.get_serializer(comment, data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) + + def destroy(self, request, pk=None): + comment = self.get_object() + comment.delete() + return Response(status=status.HTTP_204_NO_CONTENT) From a8ac36cc1afcc9bb5e4f0e7830ff7eee113c4e09 Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Sun, 3 Nov 2024 21:42:08 +0300 Subject: [PATCH 07/11] feat: Refactor portfolio request validations --- backend/marketfeed/models.py | 2 +- backend/marketfeed/serializers.py | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/backend/marketfeed/models.py b/backend/marketfeed/models.py index 9d53666..7753b24 100644 --- a/backend/marketfeed/models.py +++ b/backend/marketfeed/models.py @@ -44,7 +44,7 @@ class Portfolio(models.Model): name = models.CharField(max_length=50) description = models.CharField(max_length = 150) user_id = models.ForeignKey(User, on_delete=models.CASCADE) - created_at = models.DateTimeField() + created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(null=True, auto_now=True) stocks = models.ManyToManyField(Stock, verbose_name="list of stocks in the portfolio") diff --git a/backend/marketfeed/serializers.py b/backend/marketfeed/serializers.py index 73e2069..e84f6a4 100644 --- a/backend/marketfeed/serializers.py +++ b/backend/marketfeed/serializers.py @@ -46,8 +46,24 @@ class PortfolioSerializer(serializers.ModelSerializer): class Meta: model = Portfolio - fields = ['id', 'name', 'description', 'user_id', 'stocks'] + fields = ['id', 'name', 'description', 'user_id', 'created_at', 'updated_at', 'stocks'] + + def __init__(self, *args, **kwargs): + super(PortfolioSerializer, self).__init__(*args, **kwargs) + + request = self.context.get('request', None) + + if request and request.method == 'PUT': + self.fields['name'].required = False + self.fields['description'].required = False + self.fields['user_id'].required = False + self.fields['stocks'].required = False + elif request and request.method == 'POST': + self.fields['name'].required = True + self.fields['description'].required = False + self.fields['user_id'].required = True + self.fields['stocks'].required = False class CommentSerializer(serializers.ModelSerializer): user_id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) @@ -60,15 +76,12 @@ class Meta: def __init__(self, *args, **kwargs): super(CommentSerializer, self).__init__(*args, **kwargs) - # Get the request method if available request = self.context.get('request', None) if request and request.method == 'PUT': - # Make `post_id` and `user_id` optional for PUT requests self.fields['post_id'].required = False self.fields['user_id'].required = False elif request and request.method == 'POST': - # Ensure `post_id` and `user_id` are required for POST requests self.fields['post_id'].required = True self.fields['user_id'].required = True @@ -86,7 +99,6 @@ class Meta: def __init__(self, *args, **kwargs): super(PostSerializer, self).__init__(*args, **kwargs) - # Get the request method if available request = self.context.get('request', None) if request: From 989c254f09f6262d36799846449966672401b6ac Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Sun, 3 Nov 2024 21:58:18 +0300 Subject: [PATCH 08/11] feat: Add tag endpoints and refactor post --- backend/marketfeed/models.py | 4 ++++ backend/marketfeed/serializers.py | 26 +++++++++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/backend/marketfeed/models.py b/backend/marketfeed/models.py index 7753b24..2a5781e 100644 --- a/backend/marketfeed/models.py +++ b/backend/marketfeed/models.py @@ -39,6 +39,10 @@ class Tag(models.Model): user_id = models.ForeignKey(User, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) + def save(self, **kwargs): + self.name = self.name.lower() + return super().save(**kwargs) + class Portfolio(models.Model): name = models.CharField(max_length=50) diff --git a/backend/marketfeed/serializers.py b/backend/marketfeed/serializers.py index e84f6a4..2a2441c 100644 --- a/backend/marketfeed/serializers.py +++ b/backend/marketfeed/serializers.py @@ -3,6 +3,7 @@ from rest_framework import serializers from onboarding.models import User + class CurrencySerializer(serializers.ModelSerializer): class Meta: model = Currency @@ -32,6 +33,7 @@ def __init__(self, *args, **kwargs): self.fields['name'].required = False self.fields['symbol'].required = False + class TagSerializer(serializers.ModelSerializer): user_id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) @@ -39,6 +41,16 @@ class Meta: model = Tag fields = ['id', 'name', 'user_id'] + def __init__(self, *args, **kwargs): + super(TagSerializer, self).__init__(*args, **kwargs) + + # Get the request method if available + request = self.context.get('request', None) + + if request and request.method == 'PUT': + self.fields['name'].required = True + self.fields['user_id'].required = False + class PortfolioSerializer(serializers.ModelSerializer): user_id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) @@ -65,6 +77,7 @@ def __init__(self, *args, **kwargs): self.fields['user_id'].required = True self.fields['stocks'].required = False + class CommentSerializer(serializers.ModelSerializer): user_id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) post_id = serializers.PrimaryKeyRelatedField(queryset=Post.objects.all()) @@ -87,14 +100,14 @@ def __init__(self, *args, **kwargs): class PostSerializer(serializers.ModelSerializer): - # author = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) + author = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) liked_by = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), many=True, required=False) tags = serializers.PrimaryKeyRelatedField(queryset=Tag.objects.all(), many=True, required=False) portfolios = serializers.PrimaryKeyRelatedField(queryset=Portfolio.objects.all(), many=True, required=False) class Meta: model = Post - fields = ['id', 'title', 'content', 'created_at', 'updated_at', 'liked_by', 'tags', 'portfolios'] + fields = ['id', 'title', 'content', 'author', 'created_at', 'updated_at', 'liked_by', 'tags', 'portfolios'] def __init__(self, *args, **kwargs): super(PostSerializer, self).__init__(*args, **kwargs) @@ -105,11 +118,10 @@ def __init__(self, *args, **kwargs): if request.method == 'PUT' or request.method == 'DELETE': self.fields['title'].required = False self.fields['content'].required = False - - elif request.method == 'POST': - self.fields['title'].required = True - self.fields['content'].required = True - + self.fields['liked_by'].required = False + self.fields['tags'].required = False + self.fields['portfolios'].required = False + self.fields['author'].required = False def create(self, validated_data): liked_by = validated_data.pop('liked_by', []) From fbd6932dc16975d3e555d5fb84ff2a04c1be3765 Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Sun, 3 Nov 2024 22:55:20 +0300 Subject: [PATCH 09/11] feat: Add profile model --- backend/marketfeed/views.py | 1 - backend/onboarding/__init__.py | 1 + backend/onboarding/apps.py | 7 +++++++ backend/onboarding/models.py | 16 +++++++++++++++ backend/onboarding/serializers.py | 33 ++++++++++++++++++++++++++++-- backend/onboarding/signals.py | 12 +++++++++++ backend/onboarding/urls.py | 9 ++++++-- backend/onboarding/views.py | 34 ++++++++++++++++++++++++++++++- backend/requirements.txt | 2 +- 9 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 backend/onboarding/signals.py diff --git a/backend/marketfeed/views.py b/backend/marketfeed/views.py index 4148c64..80e434c 100644 --- a/backend/marketfeed/views.py +++ b/backend/marketfeed/views.py @@ -5,7 +5,6 @@ from .serializers import * from .models import * -# Create your views here. class CurrencyViewSet(viewsets.ModelViewSet): queryset = Currency.objects.all() diff --git a/backend/onboarding/__init__.py b/backend/onboarding/__init__.py index e69de29..5bfe8c8 100644 --- a/backend/onboarding/__init__.py +++ b/backend/onboarding/__init__.py @@ -0,0 +1 @@ +default_app_config = 'profiles.apps.ProfilesConfig' \ No newline at end of file diff --git a/backend/onboarding/apps.py b/backend/onboarding/apps.py index 78c919c..5a89e52 100644 --- a/backend/onboarding/apps.py +++ b/backend/onboarding/apps.py @@ -3,3 +3,10 @@ class OnboardingConfig(AppConfig): name = 'onboarding' + +class ProfilesConfig(AppConfig): + name = 'profiles' + + def ready(self): + # Import signals to ensure they're registered + import profiles.signals \ No newline at end of file diff --git a/backend/onboarding/models.py b/backend/onboarding/models.py index c7c9544..ddd0d11 100644 --- a/backend/onboarding/models.py +++ b/backend/onboarding/models.py @@ -2,9 +2,25 @@ from django.conf import settings from django.contrib.auth.models import AbstractUser + +class Badge(models.Model): + name = models.CharField(max_length=50) + description = models.CharField(max_length=100) + + class User(AbstractUser): is_verified = models.BooleanField(default=False) + + +class Profile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") + profile_picture = models.ImageField(upload_to="profile_pics/", blank=True, null=True) + followers = models.ManyToManyField('self', symmetrical=False, related_name='following', blank=True) + following = models.ManyToManyField('self', symmetrical=False, related_name='following', blank=True) + bio = models.TextField(max_length=500, blank=True) + location = models.CharField(max_length=100, blank=True) + class BlacklistedToken(models.Model): token = models.CharField(max_length=500) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) diff --git a/backend/onboarding/serializers.py b/backend/onboarding/serializers.py index 53cd96f..3c2dd93 100644 --- a/backend/onboarding/serializers.py +++ b/backend/onboarding/serializers.py @@ -1,4 +1,4 @@ -from onboarding.models import User as User +from onboarding.models import * from rest_framework import serializers from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from rest_framework.validators import UniqueValidator @@ -61,4 +61,33 @@ class LogoutSerializer(serializers.Serializer): def validate_refresh_token(self, value): if not value: raise serializers.ValidationError("Refresh token is required.") - return value \ No newline at end of file + return value + + +class ProfileSerializer(serializers.ModelSerializer): + user = serializers.PrimaryKeyRelatedField(read_only=True) + followers = serializers.PrimaryKeyRelatedField(queryset=Profile.objects.all(), many=True, required=False) + following = serializers.PrimaryKeyRelatedField(queryset=Profile.objects.all(), many=True, required=False) + + class Meta: + model = Profile + fields = ['user', 'profile_picture', 'badge', 'followers', 'following', 'bio', 'location'] + + def create(self, validated_data): + # Override create to ensure that the user is set from the request context + request = self.context.get('request', None) + if request and request.user.is_authenticated: + validated_data['user'] = request.user + return super().create(validated_data) + + def update(self, instance, validated_data): + # Handle followers and following updates + followers = validated_data.pop('followers', None) + following = validated_data.pop('following', None) + + if followers is not None: + instance.followers.set(followers) + if following is not None: + instance.following.set(following) + + return super().update(instance, validated_data) diff --git a/backend/onboarding/signals.py b/backend/onboarding/signals.py new file mode 100644 index 0000000..5c0660a --- /dev/null +++ b/backend/onboarding/signals.py @@ -0,0 +1,12 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from .models import User, Profile + +@receiver(post_save, sender=User) +def create_user_profile(sender, instance, created, **kwargs): + if created: + Profile.objects.create(user=instance) + +@receiver(post_save, sender=User) +def save_user_profile(sender, instance, **kwargs): + instance.profile.save() \ No newline at end of file diff --git a/backend/onboarding/urls.py b/backend/onboarding/urls.py index a732b3b..d7ec605 100644 --- a/backend/onboarding/urls.py +++ b/backend/onboarding/urls.py @@ -1,11 +1,15 @@ from django.shortcuts import render -from django.urls import path +from django.urls import path, include +from rest_framework.routers import DefaultRouter from onboarding.views import * from rest_framework_simplejwt.views import ( TokenObtainPairView, TokenRefreshView, ) +router = DefaultRouter() +router.register(r'profiles', ProfileViewSet) + urlpatterns = [ path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), @@ -14,5 +18,6 @@ path('login/refresh/', TokenRefreshView.as_view(), name='login/refresh'), path('register/', RegisterView.as_view(), name='auth_register'), path('email-verify/', VerifyEmail.as_view(), name='email-verify'), - path('logout/', LogoutView.as_view(), name='logout') + path('logout/', LogoutView.as_view(), name='logout'), + path('', include(router.urls)), ] \ No newline at end of file diff --git a/backend/onboarding/views.py b/backend/onboarding/views.py index 7a3bf0d..129ea52 100644 --- a/backend/onboarding/views.py +++ b/backend/onboarding/views.py @@ -127,4 +127,36 @@ def post(self, request): return Response({"error": "No Authorization Header"}, status=status.HTTP_400_BAD_REQUEST) - + +class ProfileViewSet(viewsets.ModelViewSet): + queryset = Profile.objects.all() + serializer_class = ProfileSerializer + permission_classes = [permissions.IsAuthenticated] + + def list(self, request): + currencies = self.get_queryset() + serializer = self.get_serializer(currencies, many=True) + return Response(serializer.data) + + def retrieve(self, request, pk=None): + currency = self.get_object() + serializer = self.get_serializer(currency) + return Response(serializer.data) + + def create(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def update(self, request, pk=None): + currency = self.get_object() + serializer = self.get_serializer(currency, data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) + + def destroy(self, request, pk=None): + currency = self.get_object() + currency.delete() + return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 26d9744..2f3d1fd 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,4 +11,4 @@ djangorestframework-simplejwt==5.3.1 PyJWT==2.8.0 drf-spectacular==0.27.2 django-cors-headers==4.5.0 -corsheaders +Pillow From 9d142b1fb69fd9324b9cb2fe2dd833d8b1fae348 Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Sun, 3 Nov 2024 23:15:42 +0300 Subject: [PATCH 10/11] feat: Add profile details --- .../0002_alter_portfolio_created_at.py | 18 +++++ backend/onboarding/__init__.py | 2 +- backend/onboarding/apps.py | 8 +-- .../migrations/0002_badge_profile.py | 65 +++++++++++++++++++ .../migrations/0003_delete_badge.py | 16 +++++ backend/onboarding/migrations/0004_badge.py | 29 +++++++++ backend/onboarding/models.py | 3 - backend/onboarding/serializers.py | 24 ++++++- 8 files changed, 152 insertions(+), 13 deletions(-) create mode 100644 backend/marketfeed/migrations/0002_alter_portfolio_created_at.py create mode 100644 backend/onboarding/migrations/0002_badge_profile.py create mode 100644 backend/onboarding/migrations/0003_delete_badge.py create mode 100644 backend/onboarding/migrations/0004_badge.py diff --git a/backend/marketfeed/migrations/0002_alter_portfolio_created_at.py b/backend/marketfeed/migrations/0002_alter_portfolio_created_at.py new file mode 100644 index 0000000..4063217 --- /dev/null +++ b/backend/marketfeed/migrations/0002_alter_portfolio_created_at.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2 on 2024-11-03 19:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("marketfeed", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="portfolio", + name="created_at", + field=models.DateTimeField(auto_now_add=True), + ), + ] diff --git a/backend/onboarding/__init__.py b/backend/onboarding/__init__.py index 5bfe8c8..4b3cfcb 100644 --- a/backend/onboarding/__init__.py +++ b/backend/onboarding/__init__.py @@ -1 +1 @@ -default_app_config = 'profiles.apps.ProfilesConfig' \ No newline at end of file +default_app_config = 'onboarding.apps.OnboardingConfig' \ No newline at end of file diff --git a/backend/onboarding/apps.py b/backend/onboarding/apps.py index 5a89e52..fd15b7b 100644 --- a/backend/onboarding/apps.py +++ b/backend/onboarding/apps.py @@ -3,10 +3,6 @@ class OnboardingConfig(AppConfig): name = 'onboarding' - -class ProfilesConfig(AppConfig): - name = 'profiles' - + def ready(self): - # Import signals to ensure they're registered - import profiles.signals \ No newline at end of file + import onboarding.signals \ No newline at end of file diff --git a/backend/onboarding/migrations/0002_badge_profile.py b/backend/onboarding/migrations/0002_badge_profile.py new file mode 100644 index 0000000..bbedabd --- /dev/null +++ b/backend/onboarding/migrations/0002_badge_profile.py @@ -0,0 +1,65 @@ +# Generated by Django 4.2 on 2024-11-03 19:59 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("onboarding", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="Badge", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50)), + ("description", models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name="Profile", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "profile_picture", + models.ImageField(blank=True, null=True, upload_to="profile_pics/"), + ), + ("bio", models.TextField(blank=True, max_length=500)), + ("location", models.CharField(blank=True, max_length=100)), + ( + "followers", + models.ManyToManyField( + blank=True, related_name="following", to="onboarding.profile" + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="profile", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/backend/onboarding/migrations/0003_delete_badge.py b/backend/onboarding/migrations/0003_delete_badge.py new file mode 100644 index 0000000..7e50c7c --- /dev/null +++ b/backend/onboarding/migrations/0003_delete_badge.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2 on 2024-11-03 20:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("onboarding", "0002_badge_profile"), + ] + + operations = [ + migrations.DeleteModel( + name="Badge", + ), + ] diff --git a/backend/onboarding/migrations/0004_badge.py b/backend/onboarding/migrations/0004_badge.py new file mode 100644 index 0000000..044ba4e --- /dev/null +++ b/backend/onboarding/migrations/0004_badge.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2 on 2024-11-03 20:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("onboarding", "0003_delete_badge"), + ] + + operations = [ + migrations.CreateModel( + name="Badge", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50)), + ("description", models.CharField(max_length=100)), + ], + ), + ] diff --git a/backend/onboarding/models.py b/backend/onboarding/models.py index ddd0d11..bb4cacd 100644 --- a/backend/onboarding/models.py +++ b/backend/onboarding/models.py @@ -2,12 +2,10 @@ from django.conf import settings from django.contrib.auth.models import AbstractUser - class Badge(models.Model): name = models.CharField(max_length=50) description = models.CharField(max_length=100) - class User(AbstractUser): is_verified = models.BooleanField(default=False) @@ -16,7 +14,6 @@ class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") profile_picture = models.ImageField(upload_to="profile_pics/", blank=True, null=True) followers = models.ManyToManyField('self', symmetrical=False, related_name='following', blank=True) - following = models.ManyToManyField('self', symmetrical=False, related_name='following', blank=True) bio = models.TextField(max_length=500, blank=True) location = models.CharField(max_length=100, blank=True) diff --git a/backend/onboarding/serializers.py b/backend/onboarding/serializers.py index 3c2dd93..ad6f7a4 100644 --- a/backend/onboarding/serializers.py +++ b/backend/onboarding/serializers.py @@ -71,17 +71,35 @@ class ProfileSerializer(serializers.ModelSerializer): class Meta: model = Profile - fields = ['user', 'profile_picture', 'badge', 'followers', 'following', 'bio', 'location'] + fields = ['user', 'profile_picture', 'followers', 'following', 'bio', 'location'] + + def __init__(self, *args, **kwargs): + super(ProfileSerializer, self).__init__(*args, **kwargs) + + request = self.context.get('request', None) + + if request and request.method == 'POST': + self.fields['user'].required = True + self.fields['profile_picture'].required = False + self.fields['followers'].required = False + self.fields['following'].required = False + self.fields['bio'].required = False + self.fields['location'].required = False + elif request and request.method == 'PUT': + self.fields['user'].required = False + self.fields['profile_picture'].required = False + self.fields['followers'].required = False + self.fields['following'].required = False + self.fields['bio'].required = False + self.fields['location'].required = False def create(self, validated_data): - # Override create to ensure that the user is set from the request context request = self.context.get('request', None) if request and request.user.is_authenticated: validated_data['user'] = request.user return super().create(validated_data) def update(self, instance, validated_data): - # Handle followers and following updates followers = validated_data.pop('followers', None) following = validated_data.pop('following', None) From addf4f93fabf5ac4904299eecb7932b0f0bab5cb Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Sun, 3 Nov 2024 23:31:55 +0300 Subject: [PATCH 11/11] fix: remove local environment settings --- backend/backend/settings.py | 4 ++-- backend/manage.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index e1f2f7f..98457f1 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -80,9 +80,9 @@ 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': os.getenv("MYSQL_DATABASE"), - 'USER': 'root', + 'USER': os.getenv("MYSQL_USER"), 'PASSWORD': os.getenv("MYSQL_PASSWORD"), - 'HOST': '127.0.0.1', + 'HOST': 'mysql-db', 'PORT': '3306', } } diff --git a/backend/manage.py b/backend/manage.py index 6bb7a2d..8486438 100755 --- a/backend/manage.py +++ b/backend/manage.py @@ -21,7 +21,6 @@ def main(): print("Db is ready") break except OperationalError as e: - print(e) print("Database not ready yet. Waiting...") time.sleep(5)