From 8317c5b0bf1496bec0f61fd13fe9ae0cda2bf177 Mon Sep 17 00:00:00 2001 From: VishSinh Date: Fri, 28 Jul 2023 01:54:03 +0530 Subject: [PATCH 1/4] Added Resource API --- csoc_backend/settings/base.py | 5 +-- csoc_backend/urls.py | 5 +-- learning_resources/__init__.py | 0 learning_resources/admin.py | 3 ++ learning_resources/apps.py | 6 ++++ learning_resources/migrations/0001_initial.py | 24 +++++++++++++ learning_resources/migrations/__init__.py | 0 learning_resources/models.py | 18 ++++++++++ learning_resources/serializers.py | 34 +++++++++++++++++++ learning_resources/tests.py | 3 ++ learning_resources/urls.py | 8 +++++ learning_resources/views.py | 31 +++++++++++++++++ 12 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 learning_resources/__init__.py create mode 100644 learning_resources/admin.py create mode 100644 learning_resources/apps.py create mode 100644 learning_resources/migrations/0001_initial.py create mode 100644 learning_resources/migrations/__init__.py create mode 100644 learning_resources/models.py create mode 100644 learning_resources/serializers.py create mode 100644 learning_resources/tests.py create mode 100644 learning_resources/urls.py create mode 100644 learning_resources/views.py diff --git a/csoc_backend/settings/base.py b/csoc_backend/settings/base.py index fdb2ab0..8cccc21 100644 --- a/csoc_backend/settings/base.py +++ b/csoc_backend/settings/base.py @@ -22,6 +22,7 @@ 'task', 'rest_framework', 'rest_framework_simplejwt', + 'learning_resources', ] MIDDLEWARE = [ @@ -85,7 +86,8 @@ # token settings SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), # Adjust token lifetime as needed + # Adjust token lifetime as needed + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1), 'SLIDING_TOKEN_REFRESH_LIFETIME_GRACE_PERIOD': timedelta(days=1), 'SLIDING_TOKEN_REFRESH_REVOKE_ON_PASSWORD_CHANGE': True, @@ -99,7 +101,6 @@ } - # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ diff --git a/csoc_backend/urls.py b/csoc_backend/urls.py index 263bdda..d1c99a8 100644 --- a/csoc_backend/urls.py +++ b/csoc_backend/urls.py @@ -20,7 +20,8 @@ # users API path('dsa/', include('dsa.urls')), path('user/', include('user.apis.urls')), - path('task/', include('task.urls')) - ]), + path('task/', include('task.urls')), + path('resources/', include('learning_resources.urls')), + ]), ), ] diff --git a/learning_resources/__init__.py b/learning_resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/learning_resources/admin.py b/learning_resources/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/learning_resources/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/learning_resources/apps.py b/learning_resources/apps.py new file mode 100644 index 0000000..2185a15 --- /dev/null +++ b/learning_resources/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class LearningResourcesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'learning_resources' diff --git a/learning_resources/migrations/0001_initial.py b/learning_resources/migrations/0001_initial.py new file mode 100644 index 0000000..293de8d --- /dev/null +++ b/learning_resources/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.2 on 2023-07-27 19:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Resource', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('domain', models.CharField(choices=[('dsa', 'DSA'), ('web', 'Web'), ('app', 'App'), ('ui', 'UI/UX')], max_length=3)), + ('topic', models.CharField(max_length=100)), + ('name', models.CharField(max_length=100)), + ('link', models.URLField()), + ], + ), + ] diff --git a/learning_resources/migrations/__init__.py b/learning_resources/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/learning_resources/models.py b/learning_resources/models.py new file mode 100644 index 0000000..8985342 --- /dev/null +++ b/learning_resources/models.py @@ -0,0 +1,18 @@ +from django.db import models + + +class Resource(models.Model): + DOMAIN_CHOICES = ( + ('dsa', 'DSA'), + ('web', 'Web'), + ('app', 'App'), + ('ui', 'UI/UX'), + ) + + domain = models.CharField(choices=DOMAIN_CHOICES, max_length=3) + topic = models.CharField(max_length=100) + name = models.CharField(max_length=100) + link = models.URLField() + + def __str__(self): + return f"{self.domain} - {self.topic} - {self.name}" diff --git a/learning_resources/serializers.py b/learning_resources/serializers.py new file mode 100644 index 0000000..aa0f75e --- /dev/null +++ b/learning_resources/serializers.py @@ -0,0 +1,34 @@ +from rest_framework import serializers + +from .models import Resource + + +class ResourceSerializer(serializers.ModelSerializer): + class Meta: + model = Resource + fields = ('domain', 'topic', 'name', 'link') + + def to_representation(self, instance): + grouped_resources = {} + for resource in instance: + domain = resource['domain'] + topic = resource['topic'] + if domain not in grouped_resources: + grouped_resources[domain] = [] + existing_topic = next( + (item for item in grouped_resources[domain] if item['topic'] == topic), None) + if existing_topic: + existing_topic['resources'].append({ + 'name': resource['name'], + 'link': resource['link'] + }) + else: + grouped_resources[domain].append({ + 'topic': topic, + 'resources': [{ + 'name': resource['name'], + 'link': resource['link'] + }] + }) + + return grouped_resources diff --git a/learning_resources/tests.py b/learning_resources/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/learning_resources/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/learning_resources/urls.py b/learning_resources/urls.py new file mode 100644 index 0000000..11b6dcf --- /dev/null +++ b/learning_resources/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from .views import ResourceListCreateAPIView + + +urlpatterns = [ + path('', ResourceListCreateAPIView.as_view(), name='resource_list_create'), +] diff --git a/learning_resources/views.py b/learning_resources/views.py new file mode 100644 index 0000000..ca7da18 --- /dev/null +++ b/learning_resources/views.py @@ -0,0 +1,31 @@ +from rest_framework.generics import ListCreateAPIView +from rest_framework_simplejwt.authentication import JWTAuthentication +from rest_framework.response import Response +from itertools import groupby + +from .models import Resource +from .serializers import ResourceSerializer +from user.apis.permissions import CustomPermissionMixin + + +class ResourceListCreateAPIView(CustomPermissionMixin, ListCreateAPIView): + serializer_class = ResourceSerializer + authentication_classes = [JWTAuthentication] + + def get_queryset(self): + return Resource.objects.all() + + def list(self, request, *args, **kwargs): + resources = self.get_queryset() + grouped_resources = {} + resources = sorted(resources, key=lambda x: (x.domain, x.topic)) + for (domain, topic), group in groupby(resources, key=lambda x: (x.domain, x.topic)): + if domain not in grouped_resources: + grouped_resources[domain] = [] + topic_resources = [ + {"name": resource.name, "link": resource.link} for resource in group + ] + grouped_resources[domain].append( + {"topic": topic, "resources": topic_resources}) + + return Response(grouped_resources) From baef2617af74a24b62d6e432479c5f4aa685f1e5 Mon Sep 17 00:00:00 2001 From: VishSinh Date: Sat, 29 Jul 2023 01:07:54 +0530 Subject: [PATCH 2/4] Optimized the Resource API --- csoc_backend/settings/base.py | 2 +- ...name_name_resource_description_and_more.py | 30 +++++++++++++++ learning_resources/models.py | 14 ++----- learning_resources/serializers.py | 27 +------------- learning_resources/urls.py | 4 +- learning_resources/views.py | 37 ++++++++++--------- user/apis/views.py | 6 --- 7 files changed, 58 insertions(+), 62 deletions(-) create mode 100644 learning_resources/migrations/0002_rename_name_resource_description_and_more.py diff --git a/csoc_backend/settings/base.py b/csoc_backend/settings/base.py index 8cccc21..d0fcd9e 100644 --- a/csoc_backend/settings/base.py +++ b/csoc_backend/settings/base.py @@ -87,7 +87,7 @@ # token settings SIMPLE_JWT = { # Adjust token lifetime as needed - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=1000), 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1), 'SLIDING_TOKEN_REFRESH_LIFETIME_GRACE_PERIOD': timedelta(days=1), 'SLIDING_TOKEN_REFRESH_REVOKE_ON_PASSWORD_CHANGE': True, diff --git a/learning_resources/migrations/0002_rename_name_resource_description_and_more.py b/learning_resources/migrations/0002_rename_name_resource_description_and_more.py new file mode 100644 index 0000000..5c11777 --- /dev/null +++ b/learning_resources/migrations/0002_rename_name_resource_description_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.2 on 2023-07-28 17:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0011_rename_team_invite_team_alter_inquiry_email_and_more'), + ('learning_resources', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='resource', + old_name='name', + new_name='description', + ), + migrations.RemoveField( + model_name='resource', + name='domain', + ), + migrations.AddField( + model_name='resource', + name='program', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='user.program'), + preserve_default=False, + ), + ] diff --git a/learning_resources/models.py b/learning_resources/models.py index 8985342..ea3e9ab 100644 --- a/learning_resources/models.py +++ b/learning_resources/models.py @@ -1,18 +1,12 @@ from django.db import models +from user.models import Program class Resource(models.Model): - DOMAIN_CHOICES = ( - ('dsa', 'DSA'), - ('web', 'Web'), - ('app', 'App'), - ('ui', 'UI/UX'), - ) - - domain = models.CharField(choices=DOMAIN_CHOICES, max_length=3) + program = models.ForeignKey(Program, on_delete=models.CASCADE) topic = models.CharField(max_length=100) - name = models.CharField(max_length=100) + description = models.CharField(max_length=100) link = models.URLField() def __str__(self): - return f"{self.domain} - {self.topic} - {self.name}" + return self.topic diff --git a/learning_resources/serializers.py b/learning_resources/serializers.py index aa0f75e..1b12683 100644 --- a/learning_resources/serializers.py +++ b/learning_resources/serializers.py @@ -6,29 +6,4 @@ class ResourceSerializer(serializers.ModelSerializer): class Meta: model = Resource - fields = ('domain', 'topic', 'name', 'link') - - def to_representation(self, instance): - grouped_resources = {} - for resource in instance: - domain = resource['domain'] - topic = resource['topic'] - if domain not in grouped_resources: - grouped_resources[domain] = [] - existing_topic = next( - (item for item in grouped_resources[domain] if item['topic'] == topic), None) - if existing_topic: - existing_topic['resources'].append({ - 'name': resource['name'], - 'link': resource['link'] - }) - else: - grouped_resources[domain].append({ - 'topic': topic, - 'resources': [{ - 'name': resource['name'], - 'link': resource['link'] - }] - }) - - return grouped_resources + fields = ('program', 'topic', 'description', 'link') diff --git a/learning_resources/urls.py b/learning_resources/urls.py index 11b6dcf..bd7fe84 100644 --- a/learning_resources/urls.py +++ b/learning_resources/urls.py @@ -1,8 +1,8 @@ from django.urls import path -from .views import ResourceListCreateAPIView +from .views import ResourceListAPIView urlpatterns = [ - path('', ResourceListCreateAPIView.as_view(), name='resource_list_create'), + path('', ResourceListAPIView.as_view(), name='resource_list'), ] diff --git a/learning_resources/views.py b/learning_resources/views.py index ca7da18..a1df5cc 100644 --- a/learning_resources/views.py +++ b/learning_resources/views.py @@ -1,31 +1,34 @@ -from rest_framework.generics import ListCreateAPIView +import pandas as pd + +from rest_framework.generics import ListAPIView from rest_framework_simplejwt.authentication import JWTAuthentication from rest_framework.response import Response -from itertools import groupby from .models import Resource from .serializers import ResourceSerializer from user.apis.permissions import CustomPermissionMixin -class ResourceListCreateAPIView(CustomPermissionMixin, ListCreateAPIView): +class ResourceListAPIView(CustomPermissionMixin, ListAPIView): + queryset = Resource.objects.all() serializer_class = ResourceSerializer authentication_classes = [JWTAuthentication] - def get_queryset(self): - return Resource.objects.all() + def generate_grouped_resources(self): + resources = Resource.objects.all().values_list('program__name', 'topic', 'description', 'link') + df = pd.DataFrame.from_records(resources, columns=['program__name', 'topic', 'description', 'link']) + grouped_df = df.groupby(['program__name', 'topic']) + return [ + (program_name, topic, [{"description": row.description, "link": row.link} for row in group.itertuples(index=False)]) + for (program_name, topic), group in grouped_df + ] - def list(self, request, *args, **kwargs): - resources = self.get_queryset() - grouped_resources = {} - resources = sorted(resources, key=lambda x: (x.domain, x.topic)) - for (domain, topic), group in groupby(resources, key=lambda x: (x.domain, x.topic)): - if domain not in grouped_resources: - grouped_resources[domain] = [] - topic_resources = [ - {"name": resource.name, "link": resource.link} for resource in group + def get(self, request, *args, **kwargs): + grouped_resources = { + program_name: [ + {"topic": topic, "resources": resources_list} + for program_name, topic, resources_list in self.generate_grouped_resources() ] - grouped_resources[domain].append( - {"topic": topic, "resources": topic_resources}) - + for program_name, topic, resources_list in self.generate_grouped_resources() + } return Response(grouped_resources) diff --git a/user/apis/views.py b/user/apis/views.py index 19aaa93..6036b55 100644 --- a/user/apis/views.py +++ b/user/apis/views.py @@ -2,9 +2,3 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -def protected_view(request): - user = request.user - # Your logic for the protected view - return Response({'message': f'Hello, {user.username}! This is a protected view.'}) From 859f3ea572952ec9be59c49b14760204c6ef7ee5 Mon Sep 17 00:00:00 2001 From: VishSinh Date: Sat, 29 Jul 2023 01:55:55 +0530 Subject: [PATCH 3/4] Added pandas as requirement --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 41d5ded..81e0be3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,5 @@ phonenumbers==8.13.14 python-dotenv==1.0.0 loguru djangorestframework -djangorestframework-simplejwt \ No newline at end of file +djangorestframework-simplejwt +pandas \ No newline at end of file From b8e997ce6b47bd1a99a5a312ab9af0032a6bd118 Mon Sep 17 00:00:00 2001 From: VishSinh Date: Mon, 31 Jul 2023 23:29:35 +0530 Subject: [PATCH 4/4] Updated the Resources API View to better cater the response --- learning_resources/views.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/learning_resources/views.py b/learning_resources/views.py index a1df5cc..1445e74 100644 --- a/learning_resources/views.py +++ b/learning_resources/views.py @@ -1,8 +1,7 @@ -import pandas as pd - from rest_framework.generics import ListAPIView from rest_framework_simplejwt.authentication import JWTAuthentication from rest_framework.response import Response +from collections import defaultdict from .models import Resource from .serializers import ResourceSerializer @@ -14,21 +13,26 @@ class ResourceListAPIView(CustomPermissionMixin, ListAPIView): serializer_class = ResourceSerializer authentication_classes = [JWTAuthentication] + + def get_resource_data(self, resource_data): + return { + "description": resource_data['description'], + "link": resource_data['link'], + } + def generate_grouped_resources(self): - resources = Resource.objects.all().values_list('program__name', 'topic', 'description', 'link') - df = pd.DataFrame.from_records(resources, columns=['program__name', 'topic', 'description', 'link']) - grouped_df = df.groupby(['program__name', 'topic']) - return [ - (program_name, topic, [{"description": row.description, "link": row.link} for row in group.itertuples(index=False)]) - for (program_name, topic), group in grouped_df + resources = Resource.objects.values('program__name', 'topic', 'description', 'link') + grouped_resources = defaultdict(lambda: defaultdict(list)) + [ + grouped_resources[resource_data['program__name']][resource_data['topic']].append(self.get_resource_data(resource_data)) + for resource_data in resources ] + return grouped_resources def get(self, request, *args, **kwargs): - grouped_resources = { - program_name: [ - {"topic": topic, "resources": resources_list} - for program_name, topic, resources_list in self.generate_grouped_resources() - ] - for program_name, topic, resources_list in self.generate_grouped_resources() + grouped_resources = self.generate_grouped_resources() + response_data = { + program_name: [{"topic": topic, "resources": resources_list} for topic, resources_list in topics.items()] + for program_name, topics in grouped_resources.items() } - return Response(grouped_resources) + return Response(response_data)