From d37966ba10e3d3268ef46f925b16a4b9645ac5dc Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Mon, 9 Dec 2024 23:55:07 +0300 Subject: [PATCH 1/7] feat: Refactor Portfolio stock relations --- backend/marketfeed/models.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/marketfeed/models.py b/backend/marketfeed/models.py index 2de85dfa..fa6958a3 100644 --- a/backend/marketfeed/models.py +++ b/backend/marketfeed/models.py @@ -66,13 +66,22 @@ def save(self, **kwargs): return super().save(**kwargs) +class PortfolioStock(models.Model): + portfolio = models.ForeignKey('Portfolio', on_delete=models.CASCADE, related_name='portfolio_stocks') + stock = models.ForeignKey('Stock', on_delete=models.CASCADE) + price_bought = models.DecimalField(max_digits=10, decimal_places=2) + added_at = models.DateTimeField(auto_now_add=True) + + class Portfolio(models.Model): name = models.CharField(max_length=50) - description = models.CharField(max_length = 150) + description = models.CharField(max_length=150) user_id = models.ForeignKey(User, on_delete=models.CASCADE) 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") + stocks = models.ManyToManyField( + 'Stock', through='PortfolioStock', verbose_name="list of stocks in the portfolio" + ) class Post(models.Model): From cf882b85c22d3b87b33bb3f858cea53faf527f43 Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Mon, 9 Dec 2024 23:56:18 +0300 Subject: [PATCH 2/7] feat: Add portfolio stock serializers --- backend/marketfeed/serializers.py | 68 +++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/backend/marketfeed/serializers.py b/backend/marketfeed/serializers.py index 7d13c224..0a4ee4db 100644 --- a/backend/marketfeed/serializers.py +++ b/backend/marketfeed/serializers.py @@ -52,30 +52,64 @@ def __init__(self, *args, **kwargs): self.fields['user_id'].required = False -class PortfolioSerializer(serializers.ModelSerializer): - user_id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) - stocks = serializers.PrimaryKeyRelatedField(queryset=Stock.objects.all(), many=True) +class PortfolioStockActionSerializer(serializers.Serializer): + portfolio_id = serializers.PrimaryKeyRelatedField(queryset=Portfolio.objects.all()) + stock = serializers.PrimaryKeyRelatedField(queryset=Stock.objects.all()) + price_bought = serializers.DecimalField(max_digits=10, decimal_places=2, required=False) + + def validate(self, data): + """ + Ensure `price_bought` is provided for adding stocks. + """ + if self.context['request'].method == 'POST' and self.context.get('view').action == 'add_stock': + if 'price_bought' not in data: + raise serializers.ValidationError({'price_bought': 'This field is required for adding a stock.'}) + return data + +class PortfolioStockSerializer(serializers.ModelSerializer): + stock = serializers.PrimaryKeyRelatedField(queryset=Stock.objects.all()) + price_bought = serializers.DecimalField(max_digits=10, decimal_places=2) class Meta: - model = Portfolio - fields = ['id', 'name', 'description', 'user_id', 'created_at', 'updated_at', 'stocks'] - + model = PortfolioStock + fields = ['stock', 'price_bought'] + def __init__(self, *args, **kwargs): - super(PortfolioSerializer, self).__init__(*args, **kwargs) + super(PortfolioStockSerializer, 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 + if request and request.method == 'DELETE': + self.fields['price_bought'].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 PortfolioSerializer(serializers.ModelSerializer): + user_id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) + stocks = PortfolioStockSerializer(source='portfolio_stocks', many=True, required=False) + + class Meta: + model = Portfolio + fields = ['id', 'name', 'description', 'user_id', 'created_at', 'updated_at', 'stocks'] + + def create(self, validated_data): + stocks_data = validated_data.pop('portfolio_stocks', []) + portfolio = Portfolio.objects.create(**validated_data) + for stock_data in stocks_data: + PortfolioStock.objects.create(portfolio=portfolio, **stock_data) + return portfolio + + def update(self, instance, validated_data): + stocks_data = validated_data.pop('portfolio_stocks', []) + instance.name = validated_data.get('name', instance.name) + instance.description = validated_data.get('description', instance.description) + instance.save() + + if stocks_data: + # Update portfolio stocks + instance.portfolio_stocks.all().delete() + for stock_data in stocks_data: + PortfolioStock.objects.create(portfolio=instance, **stock_data) + return instance class CommentSerializer(serializers.ModelSerializer): From 7be3361fc864c63d6e3d42d329e4c75a912484cf Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Mon, 9 Dec 2024 23:56:47 +0300 Subject: [PATCH 3/7] feat: Add portfolio stock views --- backend/marketfeed/views.py | 61 +++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/backend/marketfeed/views.py b/backend/marketfeed/views.py index 69114762..32db71ca 100644 --- a/backend/marketfeed/views.py +++ b/backend/marketfeed/views.py @@ -2,6 +2,10 @@ from rest_framework import viewsets, status, permissions from rest_framework.permissions import IsAuthenticated, AllowAny, IsAuthenticatedOrReadOnly from rest_framework.response import Response +from rest_framework.decorators import action +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi +from rest_framework.viewsets import ViewSet from .serializers import * from .models import * @@ -140,6 +144,63 @@ def destroy(self, request, pk=None): portfolio = self.get_object() portfolio.delete() return Response(status=status.HTTP_204_NO_CONTENT) + + + +class PortfolioStockViewSet(ViewSet): + """ + A viewset for adding and removing stocks from a portfolio. + """ + permission_classes = [permissions.IsAuthenticated] + serializer_class = PortfolioStockActionSerializer + + def get_serializer(self, *args, **kwargs): + context = kwargs.pop('context', {}) + context['request'] = self.request # Add the request object + context['view'] = self # Add the view object + return self.serializer_class(*args, context=context, **kwargs) + + @action(detail=False, methods=['post']) + def add_stock(self, request): + serializer = self.get_serializer(data=request.data, context={'action': 'add_stock'}) + serializer.is_valid(raise_exception=True) + + portfolio = serializer.validated_data['portfolio_id'] + stock = serializer.validated_data['stock'] + price_bought = serializer.validated_data['price_bought'] + + # Check if stock already exists in the portfolio + if PortfolioStock.objects.filter(portfolio=portfolio, stock=stock).exists(): + return Response({'detail': 'This stock is already in the portfolio.'}, status=status.HTTP_400_BAD_REQUEST) + + # Add stock to PortfolioStock model + PortfolioStock.objects.create(portfolio=portfolio, stock=stock, price_bought=price_bought) + + # Add stock to Portfolio's ManyToMany relationship + portfolio.stocks.add(stock) + + return Response({'status': 'Stock added to portfolio'}, status=status.HTTP_201_CREATED) + + @action(detail=False, methods=['post']) + def remove_stock(self, request): + serializer = self.get_serializer(data=request.data, context={'action': 'remove_stock'}) + serializer.is_valid(raise_exception=True) + + portfolio = serializer.validated_data['portfolio_id'] + stock = serializer.validated_data['stock'] + + # Check if stock exists in the PortfolioStock model + portfolio_stock = PortfolioStock.objects.filter(portfolio=portfolio, stock=stock) + if not portfolio_stock.exists(): + return Response({'detail': 'This stock is not in the portfolio.'}, status=status.HTTP_400_BAD_REQUEST) + + # Remove stock from PortfolioStock model + portfolio_stock.delete() + + # Remove stock from Portfolio's ManyToMany relationship + portfolio.stocks.remove(stock) + + return Response({'status': 'Stock removed from portfolio'}, status=status.HTTP_200_OK) class PostViewSet(viewsets.ModelViewSet): From 2ac1a7949bbd528d709ce76f4674fd63a06f3514 Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Mon, 9 Dec 2024 23:57:07 +0300 Subject: [PATCH 4/7] feat: Add portfolio stock urls --- backend/marketfeed/urls.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/marketfeed/urls.py b/backend/marketfeed/urls.py index 2daac1af..b4633559 100644 --- a/backend/marketfeed/urls.py +++ b/backend/marketfeed/urls.py @@ -1,6 +1,6 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .views import CurrencyViewSet, StockViewSet, TagViewSet, PortfolioViewSet, PostViewSet, CommentViewSet +from .views import CurrencyViewSet, StockViewSet, TagViewSet, PortfolioViewSet, PostViewSet, CommentViewSet, PortfolioStockViewSet router = DefaultRouter() router.register(r'currencies', CurrencyViewSet) @@ -9,6 +9,7 @@ router.register(r'portfolios', PortfolioViewSet) router.register(r'posts', PostViewSet) router.register(r'comments', CommentViewSet) +router.register(r'portfolio-stocks', PortfolioStockViewSet, basename='portfolio-stocks') urlpatterns = [ path('', include(router.urls)), From 5f846c69dd41132e795c96f761961ae379a87183 Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Mon, 9 Dec 2024 23:57:49 +0300 Subject: [PATCH 5/7] feat: Add portfolio stock migrations --- ..._remove_portfolio_stocks_portfoliostock.py | 49 +++++++++++++++++++ .../migrations/0005_portfolio_stocks.py | 22 +++++++++ 2 files changed, 71 insertions(+) create mode 100644 backend/marketfeed/migrations/0004_remove_portfolio_stocks_portfoliostock.py create mode 100644 backend/marketfeed/migrations/0005_portfolio_stocks.py diff --git a/backend/marketfeed/migrations/0004_remove_portfolio_stocks_portfoliostock.py b/backend/marketfeed/migrations/0004_remove_portfolio_stocks_portfoliostock.py new file mode 100644 index 00000000..0ea04fc0 --- /dev/null +++ b/backend/marketfeed/migrations/0004_remove_portfolio_stocks_portfoliostock.py @@ -0,0 +1,49 @@ +# Generated by Django 4.2 on 2024-12-09 20:28 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("marketfeed", "0003_stock_last_price_stock_last_updated"), + ] + + operations = [ + migrations.RemoveField( + model_name="portfolio", + name="stocks", + ), + migrations.CreateModel( + name="PortfolioStock", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("price_bought", models.DecimalField(decimal_places=2, max_digits=10)), + ("added_at", models.DateTimeField(auto_now_add=True)), + ( + "portfolio", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="portfolio_stocks", + to="marketfeed.portfolio", + ), + ), + ( + "stock", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="marketfeed.stock", + ), + ), + ], + ), + ] diff --git a/backend/marketfeed/migrations/0005_portfolio_stocks.py b/backend/marketfeed/migrations/0005_portfolio_stocks.py new file mode 100644 index 00000000..9f2751d4 --- /dev/null +++ b/backend/marketfeed/migrations/0005_portfolio_stocks.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2 on 2024-12-09 20:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("marketfeed", "0004_remove_portfolio_stocks_portfoliostock"), + ] + + operations = [ + migrations.AddField( + model_name="portfolio", + name="stocks", + field=models.ManyToManyField( + through="marketfeed.PortfolioStock", + to="marketfeed.stock", + verbose_name="list of stocks in the portfolio", + ), + ), + ] From 8de83ae9fa67c410133b58e0255779719903f46d Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Tue, 10 Dec 2024 21:14:09 +0300 Subject: [PATCH 6/7] feat: Add endpoints to fetch posts and portfolios of a user --- backend/marketfeed/views.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/backend/marketfeed/views.py b/backend/marketfeed/views.py index 12f1186e..f27e22b4 100644 --- a/backend/marketfeed/views.py +++ b/backend/marketfeed/views.py @@ -182,6 +182,12 @@ def destroy(self, request, pk=None): portfolio = self.get_object() portfolio.delete() return Response(status=status.HTTP_204_NO_CONTENT) + + @action(detail=False, methods=['get'], url_path='portfolios-by-user/(?P[^/.]+)') + def user_portfolios(self, request, user_id=None): + portfolios = self.queryset.filter(user_id=user_id) + serializer = self.get_serializer(portfolios, many=True) + return Response(serializer.data) @@ -194,8 +200,8 @@ class PortfolioStockViewSet(ViewSet): def get_serializer(self, *args, **kwargs): context = kwargs.pop('context', {}) - context['request'] = self.request # Add the request object - context['view'] = self # Add the view object + context['request'] = self.request + context['view'] = self return self.serializer_class(*args, context=context, **kwargs) @action(detail=False, methods=['post']) @@ -207,14 +213,11 @@ def add_stock(self, request): stock = serializer.validated_data['stock'] price_bought = serializer.validated_data['price_bought'] - # Check if stock already exists in the portfolio if PortfolioStock.objects.filter(portfolio=portfolio, stock=stock).exists(): return Response({'detail': 'This stock is already in the portfolio.'}, status=status.HTTP_400_BAD_REQUEST) - # Add stock to PortfolioStock model PortfolioStock.objects.create(portfolio=portfolio, stock=stock, price_bought=price_bought) - # Add stock to Portfolio's ManyToMany relationship portfolio.stocks.add(stock) return Response({'status': 'Stock added to portfolio'}, status=status.HTTP_201_CREATED) @@ -227,15 +230,12 @@ def remove_stock(self, request): portfolio = serializer.validated_data['portfolio_id'] stock = serializer.validated_data['stock'] - # Check if stock exists in the PortfolioStock model portfolio_stock = PortfolioStock.objects.filter(portfolio=portfolio, stock=stock) if not portfolio_stock.exists(): return Response({'detail': 'This stock is not in the portfolio.'}, status=status.HTTP_400_BAD_REQUEST) - # Remove stock from PortfolioStock model portfolio_stock.delete() - # Remove stock from Portfolio's ManyToMany relationship portfolio.stocks.remove(stock) return Response({'status': 'Stock removed from portfolio'}, status=status.HTTP_200_OK) @@ -286,6 +286,12 @@ def destroy(self, request, *args, **kwargs): post = self.get_object() post.delete() return Response(status=status.HTTP_204_NO_CONTENT) + + @action(detail=False, methods=['get'], url_path='posts-by-user/(?P[^/.]+)') + def user_posts(self, request, user_id=None): + posts = self.queryset.filter(author=user_id) + serializer = self.get_serializer(posts, many=True) + return Response(serializer.data) class CommentViewSet(viewsets.ModelViewSet): From 25bccfa76fa91f0a3950a6ed76d6a88dec9d0104 Mon Sep 17 00:00:00 2001 From: Rukiye Aslan Date: Tue, 10 Dec 2024 22:46:42 +0300 Subject: [PATCH 7/7] feat: Add quantity to portfolio stock --- .../0007_portfoliostock_quantity.py | 18 +++++++++++++++ backend/marketfeed/models.py | 1 + backend/marketfeed/serializers.py | 23 +++++++++---------- backend/marketfeed/views.py | 18 +++++++++------ 4 files changed, 41 insertions(+), 19 deletions(-) create mode 100644 backend/marketfeed/migrations/0007_portfoliostock_quantity.py diff --git a/backend/marketfeed/migrations/0007_portfoliostock_quantity.py b/backend/marketfeed/migrations/0007_portfoliostock_quantity.py new file mode 100644 index 00000000..7a9b87ef --- /dev/null +++ b/backend/marketfeed/migrations/0007_portfoliostock_quantity.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2 on 2024-12-10 19:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('marketfeed', '0006_merge_20241210_1341'), + ] + + operations = [ + migrations.AddField( + model_name='portfoliostock', + name='quantity', + field=models.PositiveIntegerField(default=1), + ), + ] diff --git a/backend/marketfeed/models.py b/backend/marketfeed/models.py index c308dda9..0367b8ca 100644 --- a/backend/marketfeed/models.py +++ b/backend/marketfeed/models.py @@ -70,6 +70,7 @@ class PortfolioStock(models.Model): portfolio = models.ForeignKey('Portfolio', on_delete=models.CASCADE, related_name='portfolio_stocks') stock = models.ForeignKey('Stock', on_delete=models.CASCADE) price_bought = models.DecimalField(max_digits=10, decimal_places=2) + quantity = models.PositiveIntegerField(default=1) added_at = models.DateTimeField(auto_now_add=True) diff --git a/backend/marketfeed/serializers.py b/backend/marketfeed/serializers.py index 68efd494..1eee5b86 100644 --- a/backend/marketfeed/serializers.py +++ b/backend/marketfeed/serializers.py @@ -94,11 +94,9 @@ class PortfolioStockActionSerializer(serializers.Serializer): portfolio_id = serializers.PrimaryKeyRelatedField(queryset=Portfolio.objects.all()) stock = serializers.PrimaryKeyRelatedField(queryset=Stock.objects.all()) price_bought = serializers.DecimalField(max_digits=10, decimal_places=2, required=False) + quantity = serializers.IntegerField(min_value=1, required=False) def validate(self, data): - """ - Ensure `price_bought` is provided for adding stocks. - """ if self.context['request'].method == 'POST' and self.context.get('view').action == 'add_stock': if 'price_bought' not in data: raise serializers.ValidationError({'price_bought': 'This field is required for adding a stock.'}) @@ -107,10 +105,11 @@ def validate(self, data): class PortfolioStockSerializer(serializers.ModelSerializer): stock = serializers.PrimaryKeyRelatedField(queryset=Stock.objects.all()) price_bought = serializers.DecimalField(max_digits=10, decimal_places=2) + quantity = serializers.IntegerField(min_value=1) class Meta: model = PortfolioStock - fields = ['stock', 'price_bought'] + fields = ['stock', 'price_bought', 'quantity'] def __init__(self, *args, **kwargs): super(PortfolioStockSerializer, self).__init__(*args, **kwargs) @@ -119,10 +118,11 @@ def __init__(self, *args, **kwargs): if request and request.method == 'DELETE': self.fields['price_bought'].required = False + self.fields['quantity'].required = False class PortfolioSerializer(serializers.ModelSerializer): - user_id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) + user_id = serializers.PrimaryKeyRelatedField(read_only=True) stocks = PortfolioStockSerializer(source='portfolio_stocks', many=True, required=False) class Meta: @@ -137,16 +137,15 @@ def create(self, validated_data): return portfolio def update(self, instance, validated_data): - stocks_data = validated_data.pop('portfolio_stocks', []) - instance.name = validated_data.get('name', instance.name) - instance.description = validated_data.get('description', instance.description) - instance.save() - - if stocks_data: - # Update portfolio stocks + if 'portfolio_stocks' in validated_data: + stocks_data = validated_data.pop('portfolio_stocks') instance.portfolio_stocks.all().delete() for stock_data in stocks_data: PortfolioStock.objects.create(portfolio=instance, **stock_data) + + instance.name = validated_data.get('name', instance.name) + instance.description = validated_data.get('description', instance.description) + instance.save() return instance diff --git a/backend/marketfeed/views.py b/backend/marketfeed/views.py index f27e22b4..3e1329d6 100644 --- a/backend/marketfeed/views.py +++ b/backend/marketfeed/views.py @@ -171,11 +171,12 @@ def create(self, request): 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) + def update(self, request, *args, **kwargs): + partial = kwargs.pop('partial', False) + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=partial) serializer.is_valid(raise_exception=True) - serializer.save() + self.perform_update(serializer) return Response(serializer.data) def destroy(self, request, pk=None): @@ -190,7 +191,6 @@ def user_portfolios(self, request, user_id=None): return Response(serializer.data) - class PortfolioStockViewSet(ViewSet): """ A viewset for adding and removing stocks from a portfolio. @@ -212,11 +212,16 @@ def add_stock(self, request): portfolio = serializer.validated_data['portfolio_id'] stock = serializer.validated_data['stock'] price_bought = serializer.validated_data['price_bought'] + quantity = serializer.validated_data.get('quantity', 1) if PortfolioStock.objects.filter(portfolio=portfolio, stock=stock).exists(): return Response({'detail': 'This stock is already in the portfolio.'}, status=status.HTTP_400_BAD_REQUEST) - PortfolioStock.objects.create(portfolio=portfolio, stock=stock, price_bought=price_bought) + PortfolioStock.objects.create( + portfolio=portfolio, + stock=stock, + price_bought=price_bought, + quantity=quantity) portfolio.stocks.add(stock) @@ -337,7 +342,6 @@ def post_comments(self, request, post_id=None): return Response(serializer.data) - class IndexViewSet(viewsets.ModelViewSet): queryset = Index.objects.all() serializer_class = IndexSerializer