diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ac6621f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..034e848 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/api/views.py b/api/views.py index 282199b..7fb05c9 100644 --- a/api/views.py +++ b/api/views.py @@ -4,21 +4,30 @@ from . import serializers from . import models -from .permissions import IsLinkOwner, IsSubTopicLinkOwner, IsSubTopicOwner, IsTopicOwner, IsCurationOwner +from .permissions import ( + IsLinkOwner, + IsSubTopicLinkOwner, + IsSubTopicOwner, + IsTopicOwner, + IsCurationOwner, +) from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.reverse import reverse -@api_view(['GET']) +@api_view(["GET"]) def api_root(request, format=None): - return Response({ - 'subjects': reverse('subject-list', request=request, format=format), - 'curations': reverse('curation-list', request=request, format=format) - }) -class SubjectList(mixins.ListModelMixin, - generics.GenericAPIView): + return Response( + { + "subjects": reverse("subject-list", request=request, format=format), + "curations": reverse("curation-list", request=request, format=format), + } + ) + + +class SubjectList(mixins.ListModelMixin, generics.GenericAPIView): queryset = models.Subject.objects.all() serializer_class = serializers.SubjectSerializer @@ -26,8 +35,7 @@ def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) -class SubjectDetail(mixins.RetrieveModelMixin, - generics.GenericAPIView): +class SubjectDetail(mixins.RetrieveModelMixin, generics.GenericAPIView): queryset = models.Subject.objects.all() serializer_class = serializers.SubjectSerializer @@ -35,133 +43,142 @@ def retrieve(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) -class CurationList(mixins.ListModelMixin, - mixins.CreateModelMixin, - generics.GenericAPIView): - - queryset = models.Curation.objects.all() - serializer_class = serializers.CurationSerializer - permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsCurationOwner] +class CurationList( + mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView +): + queryset = models.Curation.objects.all() + serializer_class = serializers.CurationSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsCurationOwner] + + def get_queryset(self): + """ + Optionally restricts the returned purchases to a given user, + by filtering against a `username` query parameter in the URL. + """ + queryset = models.Curation.objects.all() + subject = self.request.query_params.get("subject") + if subject is not None: + queryset = queryset.filter(subject=subject) + return queryset + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) - def get_queryset(self): - """ - Optionally restricts the returned purchases to a given user, - by filtering against a `username` query parameter in the URL. - """ - queryset = models.Curation.objects.all() - subject = self.request.query_params.get('subject') - if subject is not None: - queryset = queryset.filter(subject=subject) - return queryset + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) - def post(self, request, *args, **kwargs): - return self.create(request, *args, **kwargs) + def perform_create(self, serializer): + serializer.save(owner=self.request.user) - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def perform_create(self, serializer): - serializer.save(owner=self.request.user) +class CurationDetail( + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + mixins.UpdateModelMixin, + generics.GenericAPIView, +): + queryset = models.Curation.objects.all() + serializer_class = serializers.CurationSerializer -class CurationDetail(mixins.RetrieveModelMixin, - mixins.DestroyModelMixin, - mixins.UpdateModelMixin, - generics.GenericAPIView): + permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsCurationOwner] - queryset = models.Curation.objects.all() - serializer_class = serializers.CurationSerializer + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) - permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsCurationOwner] + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) - def get(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) - - def put(self, request, *args, **kwargs): - return self.update(request, *args, **kwargs) - - def delete(self, request, *args, **kwargs): - return super().destroy(request, *args, **kwargs) + def delete(self, request, *args, **kwargs): + return super().destroy(request, *args, **kwargs) class TopicCreate(mixins.CreateModelMixin, generics.GenericAPIView): + queryset = models.Topic.objects.all() + serializer_class = serializers.TopicSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsTopicOwner] + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) - queryset = models.Topic.objects.all() - serializer_class = serializers.TopicSerializer - permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsTopicOwner] - def post(self, request, *args, **kwargs): - return self.create(request, *args, **kwargs) +class TopicDetail( + mixins.DestroyModelMixin, mixins.UpdateModelMixin, generics.GenericAPIView +): + queryset = models.Topic.objects.all() + serializer_class = serializers.TopicSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsTopicOwner] -class TopicDetail(mixins.DestroyModelMixin, mixins.UpdateModelMixin, generics.GenericAPIView): - queryset = models.Topic.objects.all() - serializer_class = serializers.TopicSerializer - permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsTopicOwner] + def delete(self, request, *args, **kwargs): + return super().destroy(request, *args, **kwargs) - def delete(self, request, *args, **kwargs): - return super().destroy(request, *args, **kwargs) - - def put(self, request, *args, **kwargs): - return self.update(request, *args, **kwargs) + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) class LinkCreate(mixins.CreateModelMixin, generics.GenericAPIView): + queryset = models.Link.objects.all() + serializer_class = serializers.LinkSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsLinkOwner] + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) - queryset = models.Link.objects.all() - serializer_class = serializers.LinkSerializer - permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsLinkOwner] - def post(self, request, *args, **kwargs): - return self.create(request, *args, **kwargs) +class LinkDetail( + mixins.DestroyModelMixin, mixins.UpdateModelMixin, generics.GenericAPIView +): + queryset = models.Link.objects.all() + serializer_class = serializers.LinkSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsLinkOwner] -class LinkDetail(mixins.DestroyModelMixin, mixins.UpdateModelMixin, generics.GenericAPIView): - queryset = models.Link.objects.all() - serializer_class = serializers.LinkSerializer - permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsLinkOwner] + def delete(self, request, *args, **kwargs): + return super().destroy(request, *args, **kwargs) + + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) - def delete(self, request, *args, **kwargs): - return super().destroy(request, *args, **kwargs) - - def put(self, request, *args, **kwargs): - return self.update(request, *args, **kwargs) class SubTopicCreate(mixins.CreateModelMixin, generics.GenericAPIView): + queryset = models.SubTopic.objects.all() + serializer_class = serializers.SubTopicSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsSubTopicOwner] + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) + - queryset = models.SubTopic.objects.all() - serializer_class = serializers.SubTopicSerializer - permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsSubTopicOwner] +class SubTopicDetail( + mixins.DestroyModelMixin, mixins.UpdateModelMixin, generics.GenericAPIView +): + queryset = models.SubTopic.objects.all() + serializer_class = serializers.SubTopicSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsSubTopicOwner] + def delete(self, request, *args, **kwargs): + return super().destroy(request, *args, **kwargs) - def post(self, request, *args, **kwargs): - return self.create(request, *args, **kwargs) + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) -class SubTopicDetail(mixins.DestroyModelMixin, mixins.UpdateModelMixin, generics.GenericAPIView): - queryset = models.SubTopic.objects.all() - serializer_class = serializers.SubTopicSerializer - permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsSubTopicOwner] - def delete(self, request, *args, **kwargs): - return super().destroy(request, *args, **kwargs) - - def put(self, request, *args, **kwargs): - return self.update(request, *args, **kwargs) - class SubTopicLinkCreate(mixins.CreateModelMixin, generics.GenericAPIView): + queryset = models.SubTopicLink.objects.all() + serializer_class = serializers.SubTopicLinkSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsSubTopicLinkOwner] + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) - queryset = models.SubTopicLink.objects.all() - serializer_class = serializers.SubTopicLinkSerializer - permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsSubTopicLinkOwner] - def post(self, request, *args, **kwargs): - return self.create(request, *args, **kwargs) +class SubTopicLinkDetail( + mixins.DestroyModelMixin, mixins.UpdateModelMixin, generics.GenericAPIView +): + queryset = models.SubTopicLink.objects.all() + serializer_class = serializers.SubTopicLinkSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsSubTopicLinkOwner] -class SubTopicLinkDetail(mixins.DestroyModelMixin, mixins.UpdateModelMixin, generics.GenericAPIView): - queryset = models.SubTopicLink.objects.all() - serializer_class = serializers.SubTopicLinkSerializer - permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsSubTopicLinkOwner] + def delete(self, request, *args, **kwargs): + return super().destroy(request, *args, **kwargs) - def delete(self, request, *args, **kwargs): - return super().destroy(request, *args, **kwargs) - - def put(self, request, *args, **kwargs): - return self.update(request, *args, **kwargs) \ No newline at end of file + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) diff --git a/curations/migrations/0001_initial.py b/curations/migrations/0001_initial.py index 5a91cff..744e97b 100644 --- a/curations/migrations/0001_initial.py +++ b/curations/migrations/0001_initial.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ @@ -15,64 +14,143 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Curation', + name="Curation", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=64)), - ('description', models.TextField()), - ('upvotes', models.IntegerField(default=0)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='curations', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=64)), + ("description", models.TextField()), + ("upvotes", models.IntegerField(default=0)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="curations", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.CreateModel( - name='Subject', + name="Subject", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=64)), - ('description', models.TextField()), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=64)), + ("description", models.TextField()), ], ), migrations.CreateModel( - name='SubTopic', + name="SubTopic", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=64)), - ('description', models.TextField()), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=64)), + ("description", models.TextField()), ], ), migrations.CreateModel( - name='Topic', + name="Topic", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=64)), - ('curation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='curations.curation')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=64)), + ( + "curation", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="curations.curation", + ), + ), ], ), migrations.CreateModel( - name='SubTopicLink', + name="SubTopicLink", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('url', models.URLField()), - ('subtopic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='curations.subtopic')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("url", models.URLField()), + ( + "subtopic", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="curations.subtopic", + ), + ), ], ), migrations.AddField( - model_name='subtopic', - name='topic', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='curations.topic'), + model_name="subtopic", + name="topic", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="curations.topic" + ), ), migrations.CreateModel( - name='Link', + name="Link", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('url', models.URLField()), - ('topic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='curations.topic')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("url", models.URLField()), + ( + "topic", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="curations.topic", + ), + ), ], ), migrations.AddField( - model_name='curation', - name='subject', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='curations', to='curations.subject'), + model_name="curation", + name="subject", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="curations", + to="curations.subject", + ), ), ] diff --git a/curations/models.py b/curations/models.py index 295ffc4..8945ef1 100644 --- a/curations/models.py +++ b/curations/models.py @@ -4,6 +4,7 @@ from django.db import models from django.contrib.auth import get_user, get_user_model + # Create your models here. class Curation(models.Model): title = models.CharField(max_length=64) diff --git a/curatorbackendapi/forms.py b/curatorbackendapi/forms.py index c8acf55..5888d63 100644 --- a/curatorbackendapi/forms.py +++ b/curatorbackendapi/forms.py @@ -1,21 +1,23 @@ from django import forms + # from django.contrib.auth.models import User from django.contrib.auth import get_user_model -from django.contrib.auth.forms import UserCreationForm -from django.core.exceptions import ValidationError +from django.contrib.auth.forms import UserCreationForm +from django.core.exceptions import ValidationError + class CustomUserCreationForm(UserCreationForm): email = forms.EmailField() class Meta: - model=get_user_model() + model = get_user_model() fields = ("email", "first_name", "last_name", "password1", "password2") def clean_email(self): - User=get_user_model() - email = self.cleaned_data['email'].lower() - new = User.objects.filter(email=email) - if new.count(): + User = get_user_model() + email = self.cleaned_data["email"].lower() + new = User.objects.filter(email=email) + if new.count(): raise ValidationError("Email Already Exist") - return email \ No newline at end of file + return email diff --git a/curatorbackendapi/views.py b/curatorbackendapi/views.py index f66ab78..0b51f17 100644 --- a/curatorbackendapi/views.py +++ b/curatorbackendapi/views.py @@ -12,7 +12,6 @@ class HomePageView(View): - template_name = "pages/home.html" def get(self, request): @@ -38,11 +37,9 @@ class SubjectListView(ListView): class SubjectPageView(View): - template_name = "pages/subjects.html" def get(self, request, *args, **kwargs): - # defining vars title = kwargs["sub"] subject = Subject.objects.filter(title=title)[0] @@ -78,7 +75,6 @@ def get(self, request, *args, **kwargs): def signUp(request): - if request.method == "POST": form = CustomUserCreationForm(request.POST) if form.is_valid(): diff --git a/userProfiles/admin.py b/userProfiles/admin.py index 001e316..9f65a32 100644 --- a/userProfiles/admin.py +++ b/userProfiles/admin.py @@ -9,20 +9,31 @@ class CustomUserAdmin(UserAdmin): add_form = CustomUserCreationForm form = CustomUserChangeForm model = CustomUser - list_display = ('email', 'is_staff', 'is_active',) - list_filter = ('email', 'is_staff', 'is_active',) + list_display = ( + "email", + "is_staff", + "is_active", + ) + list_filter = ( + "email", + "is_staff", + "is_active", + ) fieldsets = ( - (None, {'fields': ('email', 'password', 'bio', 'profile_pic')}), - ('Permissions', {'fields': ('is_staff', 'is_active')}), + (None, {"fields": ("email", "password", "bio", "profile_pic")}), + ("Permissions", {"fields": ("is_staff", "is_active")}), ) add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ('email', 'password1', 'password2', 'is_staff', 'is_active')} + ( + None, + { + "classes": ("wide",), + "fields": ("email", "password1", "password2", "is_staff", "is_active"), + }, ), ) - search_fields = ('email',) - ordering = ('email',) + search_fields = ("email",) + ordering = ("email",) -admin.site.register(CustomUser, CustomUserAdmin) \ No newline at end of file +admin.site.register(CustomUser, CustomUserAdmin) diff --git a/userProfiles/apps.py b/userProfiles/apps.py index 3e91856..ad72e40 100644 --- a/userProfiles/apps.py +++ b/userProfiles/apps.py @@ -2,5 +2,5 @@ class UserprofilesConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'userProfiles' + default_auto_field = "django.db.models.BigAutoField" + name = "userProfiles" diff --git a/userProfiles/forms.py b/userProfiles/forms.py index 7ea83c3..1b4e275 100644 --- a/userProfiles/forms.py +++ b/userProfiles/forms.py @@ -4,19 +4,20 @@ class CustomUserCreationForm(UserCreationForm): - class Meta: model = CustomUser - fields = ('email','bio', 'first_name', 'last_name') + fields = ("email", "bio", "first_name", "last_name") class CustomUserChangeForm(UserChangeForm): class Meta: model = CustomUser - fields = ('email', 'bio', 'first_name', 'last_name', 'profile_pic') + fields = ("email", "bio", "first_name", "last_name", "profile_pic") + class CustomUserUpdateForm(UserChangeForm): password = None + class Meta: model = CustomUser - fields = ('first_name', 'last_name', 'bio', 'profile_pic') + fields = ("first_name", "last_name", "bio", "profile_pic") diff --git a/userProfiles/models.py b/userProfiles/models.py index 8e71b3a..26d1d6d 100644 --- a/userProfiles/models.py +++ b/userProfiles/models.py @@ -42,9 +42,13 @@ class CustomUser(AbstractUser): username = None email = models.EmailField(_("email address"), unique=True) bio = models.TextField(blank=True, null=True) - profile_pic = models.ImageField(upload_to="profile_pic/",default="/static/images/default_profile.jpg", null=True, blank=True) + profile_pic = models.ImageField( + upload_to="profile_pic/", + default="/static/images/default_profile.jpg", + null=True, + blank=True, + ) - USERNAME_FIELD = "email" REQUIRED_FIELDS = [] @@ -52,9 +56,13 @@ class CustomUser(AbstractUser): def __str__(self): return self.email - + def get_profile_pic(self): # variable PATH_TO_DEFAULT_STATIC_IMAGE depends on the enviroment # on development, it would be something like "localhost:8000/static/default_avatar.png" # on production, it would be something like "https://BUCKET_NAME.s3.amazonaws.com/static/default_avatar.png" - return self.profile_pic if self.profile_pic else "/static/images/default_profile.jpg" + return ( + self.profile_pic + if self.profile_pic + else "/static/images/default_profile.jpg" + ) diff --git a/userProfiles/views.py b/userProfiles/views.py index 59c03cd..7367f23 100644 --- a/userProfiles/views.py +++ b/userProfiles/views.py @@ -4,29 +4,33 @@ from django.views.generic.base import View from .forms import CustomUserUpdateForm + + class UserProfile(LoginRequiredMixin, View): - def get(self, request, *args, **kwargs): user = request.user user_form = CustomUserUpdateForm(instance=user) - curations_count=user.curations.all().count() + curations_count = user.curations.all().count() print(user_form) context = { "user": user, "user_form": user_form, - "curations_count":curations_count + "curations_count": curations_count, } return render(request, "userProfiles/profile.html", context) def post(self, request, *args, **kwargs): - user_form = CustomUserUpdateForm(request.POST, instance=request.user) - if user_form.is_valid(): + if user_form.is_valid(): user_form.save() # messages.add_message(request, messages.SUCCESS, "Your profile has been updated") return redirect(reverse("profile")) - return render(request, "customer/profile.html", - { "user_form": user_form, - }) + return render( + request, + "customer/profile.html", + { + "user_form": user_form, + }, + )