diff --git a/README.md b/README.md index 29ee723..49818e9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# numanibnmazid.com +# nim23.com ```diff - [N:B: This site is under construction] diff --git a/TODO.md b/TODO.md index 2196941..1ebfa4d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,14 +1,14 @@ # TODO List -- Refine Project +- (DONE) Refine Project - Remove all templates and static files - Remove django unicorn dependency - Remove all unnecessary dependencies from requirements -- Update Master and Dev Branch with refined project +- (DONE) Update Master and Dev Branch with refined project -- Design Starter Template +- (DONE) Design Starter Template - Design starter template using tailwind CSS official documentation - Minimum Requirements @@ -18,6 +18,30 @@ - A demo body element - Footer +- Utilities + + - (DONE) Configure EmailJS (Contact Form). Better if mail would send via backend api. + - (REMOVED) Fix Utilities Page. + - Fix Every page's meta option. + - (DONE) Fix Snippets, (REMOVED) Newsletter, (REMOVED) RSS, (REMOVED) dev.to. + - (DONE) Fix footer. + - Fix Visitor Count. + - Fix Bar code Scanner. + - Fix Spotify. + - Implement Youtube Stats. + - Add YouTube Videos Page. + - Implement `generate_order` pre_save signal as a decorator and use in all django models where order is used. + - Implement Loader in All pages. + - Create and Update Site Logo. + - Remove all custom model manager as these are not needed in this project scope. + - (DONE) Fix blog slug page. + - (DONE) Generate auto token after expiration or make token to expire never. + - Test Rich Text Editor Image or File Upload and keeo only Tinymce, remove others. + - Implement Tailwind Documentation Page's Background Color. + - Implement Blog Comment, Reply, Like Features. + - Add PDF Reader or Modal on Project Details. + - Implemet Full Text Search. + ## Bug Fix - diff --git a/UTILS.md b/UTILS.md index 82fc78e..911a341 100644 --- a/UTILS.md +++ b/UTILS.md @@ -11,3 +11,18 @@ Generate without hashes $ poetry export -f requirements.txt --output requirements.txt --without-hashes + +## Demo User Authentication Token for Development + +```json +{ + "expiry": "2023-07-05T03:53:01.757821Z", + "token": "c012a83914869d906fc34e514d1c101e9175c652975f48372e731d72091c9bd3", + "user": { + "email": "admin@admin.com" + } +} +``` + +Usage: +Token 9ae5f0396f6b504f493e51f5c9bc77b80812cddad973f0b27e7de50ae70f83fa diff --git a/project/Dockerfile b/backend/Dockerfile similarity index 86% rename from project/Dockerfile rename to backend/Dockerfile index 8559241..6c9f851 100644 --- a/project/Dockerfile +++ b/backend/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.9.12-alpine3.15 ENV PYTHONUNBUFFERED 1 # Install required dependencies -RUN apk update && apk add gcc musl-dev libffi-dev +RUN apk update && apk add gcc musl-dev build-essential libssl-dev libffi-dev RUN mkdir /app WORKDIR /app diff --git a/project/config/__init__.py b/backend/blogs/__init__.py similarity index 100% rename from project/config/__init__.py rename to backend/blogs/__init__.py diff --git a/backend/blogs/admin.py b/backend/blogs/admin.py new file mode 100644 index 0000000..9144e0b --- /dev/null +++ b/backend/blogs/admin.py @@ -0,0 +1,73 @@ +from django.contrib import admin +from django.db import models +from blogs.models import BlogCategory,Blog, BlogViewIP, BlogComment +from utils.mixins import CustomModelAdminMixin +from tinymce.widgets import TinyMCE + + +# ---------------------------------------------------- +# *** BlogCategory *** +# ---------------------------------------------------- + +class BlogCategoryAdmin(CustomModelAdminMixin, admin.ModelAdmin): + class Meta: + model = BlogCategory + +admin.site.register(BlogCategory, BlogCategoryAdmin) + + +# ---------------------------------------------------- +# *** Blog *** +# ---------------------------------------------------- + +class BlogAdmin(admin.ModelAdmin): + formfield_overrides = { + models.TextField: {'widget': TinyMCE()}, + } + + list_display = ( + 'title', 'category', 'image', 'overview', 'author', 'tags', 'get_reading_time', + 'get_total_words', 'status', 'order', 'get_table_of_contents' + ) + + def get_reading_time(self, obj): + return obj.get_reading_time() + + get_reading_time.short_description = 'Reading Time' + + def get_total_words(self, obj): + return obj.get_total_words() + + get_total_words.short_description = 'Total Words' + + def get_table_of_contents(self, obj): + return obj.get_table_of_contents() + + get_table_of_contents.short_description = 'Table of Contents' + + class Meta: + model = Blog + +admin.site.register(Blog, BlogAdmin) + + +# ---------------------------------------------------- +# *** BlogViewIP *** +# ---------------------------------------------------- + +class BlogViewIPAdmin(CustomModelAdminMixin, admin.ModelAdmin): + class Meta: + model = BlogViewIP + +admin.site.register(BlogViewIP, BlogViewIPAdmin) + + +# ---------------------------------------------------- +# *** BlogComment *** +# ---------------------------------------------------- + +class BlogCommentAdmin(CustomModelAdminMixin, admin.ModelAdmin): + class Meta: + model = BlogComment + +admin.site.register(BlogComment, BlogCommentAdmin) diff --git a/backend/blogs/api/routers.py b/backend/blogs/api/routers.py new file mode 100644 index 0000000..d4b48f3 --- /dev/null +++ b/backend/blogs/api/routers.py @@ -0,0 +1,5 @@ +from config.router import router +from blogs.api.views import BlogViewset + + +router.register("blogs", BlogViewset, basename="code_snippets") diff --git a/backend/blogs/api/serializers.py b/backend/blogs/api/serializers.py new file mode 100644 index 0000000..462e874 --- /dev/null +++ b/backend/blogs/api/serializers.py @@ -0,0 +1,33 @@ +from rest_framework import serializers +from blogs.models import Blog, BlogCategory + + +class BlogCategorySerializer(serializers.ModelSerializer): + class Meta: + model = BlogCategory + fields = "__all__" + + +class BlogSerializer(serializers.ModelSerializer): + category = BlogCategorySerializer() + image = serializers.SerializerMethodField() + reading_time = serializers.SerializerMethodField() + total_words = serializers.SerializerMethodField() + table_of_contents = serializers.SerializerMethodField() + + class Meta: + model = Blog + fields = "__all__" + read_only_fields = ("id", "slug", "created_at", "updated_at") + + def get_image(self, obj): + return obj.get_image() + + def get_reading_time(self, obj): + return obj.get_reading_time() + + def get_total_words(self, obj): + return obj.get_total_words() + + def get_table_of_contents(self, obj): + return obj.get_table_of_contents() diff --git a/backend/blogs/api/views.py b/backend/blogs/api/views.py new file mode 100644 index 0000000..497a68e --- /dev/null +++ b/backend/blogs/api/views.py @@ -0,0 +1,27 @@ +from rest_framework import permissions +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin +from utils.helpers import custom_response_wrapper +from blogs.models import Blog +from blogs.api.serializers import BlogSerializer + + +@custom_response_wrapper +class BlogViewset(GenericViewSet, ListModelMixin, RetrieveModelMixin): + permission_classes = (permissions.IsAuthenticated,) + queryset = Blog.objects.filter(status="Published") + serializer_class = BlogSerializer + lookup_field = 'slug' + + def get_queryset(self): + queryset = super().get_queryset() + limit = self.request.query_params.get('_limit') + + if limit: + try: + limit = int(limit) + queryset = queryset[:limit] + except ValueError: + pass + + return queryset diff --git a/backend/blogs/apps.py b/backend/blogs/apps.py new file mode 100644 index 0000000..0e2533b --- /dev/null +++ b/backend/blogs/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BlogsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "blogs" diff --git a/backend/blogs/migrations/0001_initial.py b/backend/blogs/migrations/0001_initial.py new file mode 100644 index 0000000..b9e7fbf --- /dev/null +++ b/backend/blogs/migrations/0001_initial.py @@ -0,0 +1,176 @@ +# Generated by Django 4.2.1 on 2023-07-02 20:32 + +from django.db import migrations, models +import django.db.models.deletion +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Blog", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=255, unique=True)), + ("slug", models.SlugField(blank=True, max_length=255, unique=True)), + ( + "image", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_blog_image_path, + ), + ), + ("content", models.TextField()), + ( + "author", + models.CharField( + blank=True, default="Numan Ibn Mazid", max_length=100 + ), + ), + ("keywords", models.CharField(blank=True, max_length=255)), + ( + "status", + models.CharField( + choices=[ + ("Published", "Published"), + ("Draft", "Draft"), + ("Archived", "Archived"), + ], + default="Published", + max_length=20, + ), + ), + ("reading_time", models.PositiveIntegerField(blank=True, null=True)), + ("order", models.PositiveIntegerField(blank=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Blog", + "verbose_name_plural": "Blogs", + "db_table": "blog", + "ordering": ("order", "-created_at"), + "get_latest_by": "created_at", + }, + ), + migrations.CreateModel( + name="BlogCategory", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255, unique=True)), + ("slug", models.SlugField(blank=True, max_length=255, unique=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Blog Category", + "verbose_name_plural": "Blog Categories", + "db_table": "blog_category", + "ordering": ("-created_at",), + "get_latest_by": "created_at", + }, + ), + migrations.CreateModel( + name="BlogViewIP", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("ip_address", models.CharField(max_length=255, unique=True)), + ("slug", models.SlugField(blank=True, max_length=255, unique=True)), + ("first_visited_at", models.DateTimeField(auto_now_add=True)), + ("last_visited_at", models.DateTimeField(auto_now=True)), + ("liked", models.BooleanField(default=False)), + ( + "blog", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="view_ips", + to="blogs.blog", + ), + ), + ], + options={ + "verbose_name": "Blog View IP", + "verbose_name_plural": "Blog View IPs", + "db_table": "blog_view_ip", + "ordering": ("-last_visited_at",), + "get_latest_by": "created_at", + }, + ), + migrations.CreateModel( + name="BlogComment", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ("email", models.EmailField(max_length=255)), + ("comment", models.TextField()), + ("slug", models.SlugField(blank=True, max_length=255, unique=True)), + ("is_approved", models.BooleanField(default=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "blog", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="comments", + to="blogs.blog", + ), + ), + ], + options={ + "verbose_name": "Blog Comment", + "verbose_name_plural": "Blog Comments", + "db_table": "blog_comment", + "ordering": ("-created_at",), + "get_latest_by": "created_at", + }, + ), + migrations.AddField( + model_name="blog", + name="category", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="blogs", + to="blogs.blogcategory", + ), + ), + ] diff --git a/backend/blogs/migrations/0002_blog_overview.py b/backend/blogs/migrations/0002_blog_overview.py new file mode 100644 index 0000000..798d7aa --- /dev/null +++ b/backend/blogs/migrations/0002_blog_overview.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2023-07-04 16:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("blogs", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="blog", + name="overview", + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/backend/blogs/migrations/0003_rename_keywords_blog_tags.py b/backend/blogs/migrations/0003_rename_keywords_blog_tags.py new file mode 100644 index 0000000..02a078c --- /dev/null +++ b/backend/blogs/migrations/0003_rename_keywords_blog_tags.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2023-07-04 17:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("blogs", "0002_blog_overview"), + ] + + operations = [ + migrations.RenameField( + model_name="blog", + old_name="keywords", + new_name="tags", + ), + ] diff --git a/backend/blogs/migrations/0004_remove_blog_reading_time.py b/backend/blogs/migrations/0004_remove_blog_reading_time.py new file mode 100644 index 0000000..db233f5 --- /dev/null +++ b/backend/blogs/migrations/0004_remove_blog_reading_time.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.1 on 2023-07-08 17:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("blogs", "0003_rename_keywords_blog_tags"), + ] + + operations = [ + migrations.RemoveField( + model_name="blog", + name="reading_time", + ), + ] diff --git a/backend/blogs/migrations/__init__.py b/backend/blogs/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/blogs/models.py b/backend/blogs/models.py new file mode 100644 index 0000000..47f337c --- /dev/null +++ b/backend/blogs/models.py @@ -0,0 +1,223 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.conf import settings +from django.dispatch import receiver +from django.db.models.signals import pre_save +from django.db.models import Max +from utils.helpers import CustomModelManager +from utils.snippets import autoSlugWithFieldAndUUID, autoSlugFromUUID, get_static_file_path, image_as_base64, random_number_generator +from utils.image_upload_helpers import ( + get_blog_image_path, +) +import math +from bs4 import BeautifulSoup +import re + +""" *************** Blog Category *************** """ + + +@autoSlugWithFieldAndUUID(fieldname="name") +class BlogCategory(models.Model): + name = models.CharField(max_length=255, unique=True) + slug = models.SlugField(max_length=255, unique=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'blog_category' + verbose_name = _('Blog Category') + verbose_name_plural = _('Blog Categories') + ordering = ('-created_at',) + get_latest_by = "created_at" + + def __str__(self): + return self.name + + +""" *************** Blog *************** """ + + +@autoSlugWithFieldAndUUID(fieldname="title") +class Blog(models.Model): + class Status(models.TextChoices): + PUBLISHED = 'Published', _('Published') + DRAFT = 'Draft', _('Draft') + ARCHIVED = 'Archived', _('Archived') + + title = models.CharField(max_length=255, unique=True) + slug = models.SlugField(max_length=255, unique=True, blank=True) + category = models.ForeignKey(BlogCategory, on_delete=models.CASCADE, related_name='blogs', blank=True, null=True) + image = models.ImageField(upload_to=get_blog_image_path, blank=True, null=True) + overview = models.CharField(max_length=255, blank=True, null=True) + content = models.TextField() + author = models.CharField(max_length=100, default="Numan Ibn Mazid", blank=True) + tags = models.CharField(max_length=255, blank=True) + status = models.CharField(max_length=20, choices=Status.choices, default=Status.PUBLISHED) + order = models.PositiveIntegerField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'blog' + verbose_name = _('Blog') + verbose_name_plural = _('Blogs') + ordering = ('order', '-created_at') + get_latest_by = "created_at" + + def __str__(self): + return self.title + + def get_image(self): + if self.image: + image_path = settings.MEDIA_ROOT + self.image.url.lstrip("/media/") + else: + image_path = get_static_file_path("icons/blog.png") + return image_as_base64(image_path) + + def get_total_words(self): + content = self.content + # Remove HTML tags from content + soup = BeautifulSoup(content, 'html.parser') + text = soup.get_text(separator=' ') + word_count = len(text.split()) + return word_count + + def get_reading_time(self): + words_per_minute = settings.BLOG_WORDS_PER_MINUTE + total_words = self.get_total_words() + minutes = math.ceil(total_words / words_per_minute) + + if minutes < 60: + reading_time = f"{minutes} minute{'s' if minutes > 1 else ''}" + else: + hours = minutes // 60 + remaining_minutes = minutes % 60 + + if remaining_minutes == 0: + reading_time = f"{hours} hour{'s' if hours > 1 else ''}" + else: + reading_time = f"{hours} hour{'s' if hours > 1 else ''} {remaining_minutes} minute{'s' if remaining_minutes > 1 else ''}" + + return reading_time + + def get_table_of_contents(self): + content = self.content + soup = BeautifulSoup(content, 'html.parser') + + # Find all heading elements (h1, h2, h3, etc.) + headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) + + table_of_contents = [] + + # Extract the text, level and id of each heading + for heading in headings: + text = heading.get_text() + level = int(heading.name[1]) + match = re.search(r'id="([^"]+)"', str(heading)) + if match: + heading_id = match.group(1) + table_of_contents.append({'heading': text, 'level': level, 'id': heading_id}) + + return table_of_contents + +# Signals + +@receiver(pre_save, sender=Blog) +def generate_order(sender, instance, **kwargs): + """ + This method will generate order for new instances only. + Order will be generated automatically like 1, 2, 3, 4 and so on. + If any order is deleted then it will be reused. Like if 3 is deleted then next created order will be 3 instead of 5. + """ + if not instance.pk: # Only generate order for new instances + if instance.order is None: + deleted_orders = Blog.objects.filter(order__isnull=False).values_list('order', flat=True) + max_order = Blog.objects.aggregate(Max('order')).get('order__max') + + if deleted_orders: + deleted_orders = sorted(deleted_orders) + reused_order = None + for i in range(1, max_order + 2): + if i not in deleted_orders: + reused_order = i + break + if reused_order is not None: + instance.order = reused_order + else: + instance.order = max_order + 1 if max_order is not None else 1 + + +@receiver(pre_save, sender=Blog) +def add_unique_ids_to_content_headings(sender, instance, **kwargs): + content = instance.content + soup = BeautifulSoup(content, 'html.parser') + + headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) + + for heading in headings: + heading_id = re.sub(r'\W+', '-', heading.text.lower()) + heading['id'] = heading_id + '-' + random_number_generator(size=3) + heading_string = str(heading) + content = content.replace(heading_string, f'<{heading_string} id="{heading_id}">') + + instance.content = str(soup) + + +""" *************** Blog View IP *************** """ + + +@autoSlugFromUUID() +class BlogViewIP(models.Model): + ip_address = models.CharField(max_length=255, unique=True) + blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name='view_ips') + slug = models.SlugField(max_length=255, unique=True, blank=True) + first_visited_at = models.DateTimeField(auto_now_add=True) + last_visited_at = models.DateTimeField(auto_now=True) + liked = models.BooleanField(default=False) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'blog_view_ip' + verbose_name = _('Blog View IP') + verbose_name_plural = _('Blog View IPs') + ordering = ('-last_visited_at',) + get_latest_by = "created_at" + + def __str__(self): + return self.ip_address + + +""" *************** Blog Comment *************** """ + + +@autoSlugFromUUID() +class BlogComment(models.Model): + name = models.CharField(max_length=255) + email = models.EmailField(max_length=255) + comment = models.TextField() + blog = models.ForeignKey(Blog, on_delete=models.CASCADE, related_name='comments') + slug = models.SlugField(max_length=255, unique=True, blank=True) + is_approved = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'blog_comment' + verbose_name = _('Blog Comment') + verbose_name_plural = _('Blog Comments') + ordering = ('-created_at',) + get_latest_by = "created_at" + + def __str__(self): + return f"{self.name} :- {self.blog.title}" diff --git a/backend/blogs/tests.py b/backend/blogs/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/blogs/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/blogs/views.py b/backend/blogs/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/blogs/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/backend/code_snippets/__init__.py b/backend/code_snippets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code_snippets/admin.py b/backend/code_snippets/admin.py new file mode 100644 index 0000000..6d7d9be --- /dev/null +++ b/backend/code_snippets/admin.py @@ -0,0 +1,20 @@ +from django.contrib import admin +from django.db import models +from code_snippets.models import CodeSnippet +from utils.mixins import CustomModelAdminMixin +from tinymce.widgets import TinyMCE + + +# ---------------------------------------------------- +# *** CodeSnippet *** +# ---------------------------------------------------- + +class CodeSnippetAdmin(CustomModelAdminMixin, admin.ModelAdmin): + formfield_overrides = { + models.TextField: {'widget': TinyMCE()}, + } + class Meta: + model = CodeSnippet + +admin.site.register(CodeSnippet, CodeSnippetAdmin) + diff --git a/backend/code_snippets/api/__init__.py b/backend/code_snippets/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code_snippets/api/routers.py b/backend/code_snippets/api/routers.py new file mode 100644 index 0000000..dc3093d --- /dev/null +++ b/backend/code_snippets/api/routers.py @@ -0,0 +1,5 @@ +from config.router import router +from code_snippets.api.views import CodeSnippetViewset + + +router.register("code-snippets", CodeSnippetViewset, basename="code_snippets") diff --git a/backend/code_snippets/api/serializers.py b/backend/code_snippets/api/serializers.py new file mode 100644 index 0000000..83f6f6b --- /dev/null +++ b/backend/code_snippets/api/serializers.py @@ -0,0 +1,14 @@ +from rest_framework import serializers +from code_snippets.models import CodeSnippet + + +class CodeSnippetSerializer(serializers.ModelSerializer): + image = serializers.SerializerMethodField() + + class Meta: + model = CodeSnippet + fields = "__all__" + read_only_fields = ("id", "slug", "created_at", "updated_at") + + def get_image(self, obj): + return obj.get_image() diff --git a/backend/code_snippets/api/views.py b/backend/code_snippets/api/views.py new file mode 100644 index 0000000..188b2ac --- /dev/null +++ b/backend/code_snippets/api/views.py @@ -0,0 +1,14 @@ +from rest_framework import permissions +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin +from utils.helpers import custom_response_wrapper +from code_snippets.models import CodeSnippet +from code_snippets.api.serializers import CodeSnippetSerializer + + +@custom_response_wrapper +class CodeSnippetViewset(GenericViewSet, ListModelMixin, RetrieveModelMixin): + permission_classes = (permissions.IsAuthenticated,) + queryset = CodeSnippet.objects.all() + serializer_class = CodeSnippetSerializer + lookup_field = 'slug' diff --git a/backend/code_snippets/apps.py b/backend/code_snippets/apps.py new file mode 100644 index 0000000..ca5d482 --- /dev/null +++ b/backend/code_snippets/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CodeSnippetsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "code_snippets" diff --git a/backend/code_snippets/migrations/0001_initial.py b/backend/code_snippets/migrations/0001_initial.py new file mode 100644 index 0000000..fa7d680 --- /dev/null +++ b/backend/code_snippets/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# Generated by Django 4.2.1 on 2023-06-30 19:46 + +from django.db import migrations, models +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="CodeSnippet", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=255, unique=True)), + ("slug", models.SlugField(blank=True, max_length=255, unique=True)), + ("content", models.TextField()), + ( + "image", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_code_snippet_image_path, + ), + ), + ("order", models.PositiveIntegerField(blank=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Code Snippet", + "verbose_name_plural": "Code Snippets", + "db_table": "code_snippet", + "ordering": ("order", "-created_at"), + "get_latest_by": "created_at", + }, + ), + ] diff --git a/backend/code_snippets/migrations/0002_codesnippet_short_description.py b/backend/code_snippets/migrations/0002_codesnippet_short_description.py new file mode 100644 index 0000000..abb3351 --- /dev/null +++ b/backend/code_snippets/migrations/0002_codesnippet_short_description.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2023-06-30 21:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("code_snippets", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="codesnippet", + name="short_description", + field=models.CharField(blank=True, max_length=255), + ), + ] diff --git a/backend/code_snippets/migrations/0003_codesnippet_language.py b/backend/code_snippets/migrations/0003_codesnippet_language.py new file mode 100644 index 0000000..b96d14b --- /dev/null +++ b/backend/code_snippets/migrations/0003_codesnippet_language.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2023-07-01 10:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("code_snippets", "0002_codesnippet_short_description"), + ] + + operations = [ + migrations.AddField( + model_name="codesnippet", + name="language", + field=models.CharField(blank=True, max_length=50), + ), + ] diff --git a/backend/code_snippets/migrations/0004_remove_codesnippet_short_description_and_more.py b/backend/code_snippets/migrations/0004_remove_codesnippet_short_description_and_more.py new file mode 100644 index 0000000..6310577 --- /dev/null +++ b/backend/code_snippets/migrations/0004_remove_codesnippet_short_description_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.1 on 2023-07-04 16:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("code_snippets", "0003_codesnippet_language"), + ] + + operations = [ + migrations.RemoveField( + model_name="codesnippet", + name="short_description", + ), + migrations.AddField( + model_name="codesnippet", + name="overview", + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/backend/code_snippets/migrations/__init__.py b/backend/code_snippets/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/code_snippets/models.py b/backend/code_snippets/models.py new file mode 100644 index 0000000..47e6149 --- /dev/null +++ b/backend/code_snippets/models.py @@ -0,0 +1,74 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.conf import settings +from django.dispatch import receiver +from django.db.models.signals import pre_save +from django.db.models import Max +from utils.helpers import CustomModelManager +from utils.snippets import autoSlugWithFieldAndUUID, get_static_file_path, image_as_base64 +from utils.image_upload_helpers import ( + get_code_snippet_image_path, +) + + +""" *************** Code Snippet *************** """ + + +@autoSlugWithFieldAndUUID(fieldname="title") +class CodeSnippet(models.Model): + title = models.CharField(max_length=255, unique=True) + slug = models.SlugField(max_length=255, unique=True, blank=True) + overview = models.CharField(max_length=255, blank=True, null=True) + image = models.ImageField(upload_to=get_code_snippet_image_path, blank=True, null=True) + language = models.CharField(max_length=50, blank=True) + content = models.TextField() + order = models.PositiveIntegerField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'code_snippet' + verbose_name = _('Code Snippet') + verbose_name_plural = _('Code Snippets') + ordering = ('order', '-created_at') + get_latest_by = "created_at" + + def __str__(self): + return self.title + + def get_image(self): + if self.image: + image_path = settings.MEDIA_ROOT + self.image.url.lstrip("/media/") + else: + image_path = get_static_file_path("icons/code.png") + return image_as_base64(image_path) + + +# Signals + +@receiver(pre_save, sender=CodeSnippet) +def generate_order(sender, instance, **kwargs): + """ + This method will generate order for new instances only. + Order will be generated automatically like 1, 2, 3, 4 and so on. + If any order is deleted then it will be reused. Like if 3 is deleted then next created order will be 3 instead of 5. + """ + if not instance.pk: # Only generate order for new instances + if instance.order is None: + deleted_orders = CodeSnippet.objects.filter(order__isnull=False).values_list('order', flat=True) + max_order = CodeSnippet.objects.aggregate(Max('order')).get('order__max') + + if deleted_orders: + deleted_orders = sorted(deleted_orders) + reused_order = None + for i in range(1, max_order + 2): + if i not in deleted_orders: + reused_order = i + break + if reused_order is not None: + instance.order = reused_order + else: + instance.order = max_order + 1 if max_order is not None else 1 diff --git a/backend/code_snippets/tests.py b/backend/code_snippets/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/code_snippets/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/code_snippets/views.py b/backend/code_snippets/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/code_snippets/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/project/conf.py b/backend/conf.py similarity index 100% rename from project/conf.py rename to backend/conf.py diff --git a/backend/config/__init__.py b/backend/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/config/api_router.py b/backend/config/api_router.py new file mode 100644 index 0000000..afdae75 --- /dev/null +++ b/backend/config/api_router.py @@ -0,0 +1,19 @@ +from .router import router +# Users +from users.api.routers import * +# Portfolios +from portfolios.api.professional_experiences.routers import * +from portfolios.api.skills.routers import * +from portfolios.api.educations.routers import * +from portfolios.api.certifications.routers import * +from portfolios.api.projects.routers import * +from portfolios.api.interests.routers import * +from portfolios.api.movies.routers import * +# Code Snippets +from code_snippets.api.routers import * +# Blogs +from blogs.api.routers import * + + +app_name = "numanibnmazid_portfolio_backend_api" +urlpatterns = router.urls diff --git a/project/config/asgi.py b/backend/config/asgi.py similarity index 100% rename from project/config/asgi.py rename to backend/config/asgi.py diff --git a/backend/config/router.py b/backend/config/router.py new file mode 100644 index 0000000..12b81d7 --- /dev/null +++ b/backend/config/router.py @@ -0,0 +1,9 @@ +from django.conf import settings +from rest_framework.routers import DefaultRouter, SimpleRouter + +router = None + +if settings.DEBUG: + router = DefaultRouter() +else: + router = SimpleRouter() diff --git a/project/config/settings.py b/backend/config/settings.py similarity index 52% rename from project/config/settings.py rename to backend/config/settings.py index 9680244..071cc8e 100644 --- a/project/config/settings.py +++ b/backend/config/settings.py @@ -4,6 +4,8 @@ from pathlib import Path import os from conf import config +from datetime import timedelta +from rest_framework.settings import api_settings # ---------------------------------------------------- # *** Project's BASE DIRECTORY *** @@ -28,21 +30,51 @@ # ---------------------------------------------------- # *** Application Definition *** # ---------------------------------------------------- -THIRD_PARTY_APPS = [] -LOCAL_APPS = [] -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", -] + THIRD_PARTY_APPS + LOCAL_APPS +THIRD_PARTY_APPS = [ + # Django REST Framework + "rest_framework", + # Knox Authentication + "knox", + # Django REST Framework Yet Another Swagger + "drf_yasg", + # Django CORS Headers + "corsheaders", + # Django CKEditor + "ckeditor", + "ckeditor_uploader", + # Django TinyMCE + "tinymce", +] +LOCAL_APPS = [ + "users", + "portfolios", + "code_snippets", + "blogs" +] +INSTALLED_APPS = ( + [ + # Django Filebrowser Needs to be placed before Django Admin + # Start Django Filebrowser + 'grappelli', + 'filebrowser', + # End Django Filebrowser + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + ] + + THIRD_PARTY_APPS + + LOCAL_APPS +) # ---------------------------------------------------- # *** Middleware Definition *** # ---------------------------------------------------- MIDDLEWARE = [ + # Django CORS Headers Middleware + "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", @@ -63,9 +95,7 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [ - os.path.join(BASE_DIR, "config", "templates") - ], + "DIRS": [os.path.join(BASE_DIR, "config", "templates")], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -78,6 +108,13 @@ }, ] +# ---------------------------------------------------- +# *** Authentication Definition *** +# ---------------------------------------------------- + +# https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model +AUTH_USER_MODEL = "users.User" + # ---------------------------------------------------- # *** WSGI Application *** # ---------------------------------------------------- @@ -87,13 +124,13 @@ # *** Database Configuration *** # ---------------------------------------------------- DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': config.DATABASE.NAME, - 'USER': config.DATABASE.USER, - 'PASSWORD': config.DATABASE.PASSWORD, - 'HOST': config.DATABASE.HOST, - 'PORT': config.DATABASE.PORT + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": config.DATABASE.NAME, + "USER": config.DATABASE.USER, + "PASSWORD": config.DATABASE.PASSWORD, + "HOST": config.DATABASE.HOST, + "PORT": config.DATABASE.PORT, } } @@ -135,16 +172,16 @@ # ---------------------------------------------------- # *** Static and Media Files Configuration *** # ---------------------------------------------------- -PUBLIC_ROOT = os.path.join(BASE_DIR, 'public/') +PUBLIC_ROOT = os.path.join(BASE_DIR, "public/") # STATIC & MEDIA URL -STATIC_URL = '/static/' -MEDIA_URL = '/media/' +STATIC_URL = "/static/" +MEDIA_URL = "/media/" # STATIC & MEDIA ROOT -MEDIA_ROOT = os.path.join(PUBLIC_ROOT, 'media/') -STATIC_ROOT = os.path.join(PUBLIC_ROOT, 'static/') +MEDIA_ROOT = os.path.join(PUBLIC_ROOT, "media/") +STATIC_ROOT = os.path.join(PUBLIC_ROOT, "static/") # Static Files Directories STATICFILES_DIRS = [ - os.path.join(PUBLIC_ROOT, 'staticfiles'), + os.path.join(PUBLIC_ROOT, "staticfiles"), ] # https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders @@ -168,7 +205,7 @@ "console": { "level": os.getenv("DJANGO_LOG_LEVEL", "INFO"), "class": "logging.StreamHandler", - "formatter": "verbose" + "formatter": "verbose", } }, "loggers": { @@ -178,10 +215,7 @@ "propagate": True, }, }, - "root": { - "level": os.getenv("DJANGO_LOG_LEVEL", "INFO"), - "handlers": ["console"] - } + "root": {"level": os.getenv("DJANGO_LOG_LEVEL", "INFO"), "handlers": ["console"]}, } # ---------------------------------------------------- @@ -189,3 +223,108 @@ # ---------------------------------------------------- DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" SITE_ID = 1 + +# REST Framework Configuration +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ("knox.auth.TokenAuthentication",), + "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), +} + +# KNOX Configuration +KNOX_TOKEN_MODEL = "knox.AuthToken" + +REST_KNOX = { + # "SECURE_HASH_ALGORITHM": "hashlib.sha512", + "AUTH_TOKEN_CHARACTER_LENGTH": 64, + # "TOKEN_TTL": timedelta(hours=730), + "TOKEN_TTL": None, # Never Expire + "USER_SERIALIZER": "knox.serializers.UserSerializer", + "TOKEN_LIMIT_PER_USER": None, + "AUTO_REFRESH": False, + "MIN_REFRESH_INTERVAL": 60, + "AUTH_HEADER_PREFIX": "Token", + "EXPIRY_DATETIME_FORMAT": api_settings.DATETIME_FORMAT, + "TOKEN_MODEL": "knox.AuthToken", +} + +# Swagger Configuration +SWAGGER_SETTINGS = { + "SECURITY_DEFINITIONS": { + "Bearer": {"type": "apiKey", "name": "Authorization", "in": "header"} + }, + "JSON_EDITOR": True, +} + +# Django CORS Headers Configuration +CORS_ORIGIN_ALLOW_ALL = False +CORS_ORIGIN_WHITELIST = [ + 'http://localhost:3000', # frontend URL here +] + +CORS_ALLOW_METHODS = [ + 'GET', + 'POST', + 'PUT', + 'PATCH', + 'DELETE', + 'OPTIONS', +] + +CORS_ALLOW_HEADERS = [ + 'accept', + 'accept-encoding', + 'authorization', + 'content-type', + 'dnt', + 'origin', + 'user-agent', + 'x-csrftoken', + 'x-requested-with', +] + +# Django CKEditor Configuration +CKEDITOR_UPLOAD_PATH = "ckeditor_uploads/" +CKEDITOR_CONFIGS = { + 'default': { + 'toolbar': 'full', + 'height': 300, + 'width': "100%", + }, +} + +# Django Filebrowser Configuration +FILEBROWSER_DIRECTORY = "" +FILEBROWSER_ADMIN_THUMBNAIL="admin_thumbnail" +FILEBROWSER_ADMIN_VERSIONS=['thumbnail', 'small', 'medium', 'big', 'large'] +FILEBROWSER_VERSION_QUALITY=90 + +# Django Tinymce Configuration +TINYMCE_DEFAULT_CONFIG = { + 'height': 360, + 'width': "100%", + 'cleanup_on_startup': True, + 'custom_undo_redo_levels': 10, + 'selector': 'textarea', + 'plugins': ''' + textcolor save link image media preview codesample contextmenu + table code lists fullscreen insertdatetime nonbreaking + contextmenu directionality searchreplace wordcount visualblocks + visualchars code fullscreen autolink lists charmap print hr + anchor pagebreak + ''', + 'toolbar': ''' + undo redo | formatselect | bold italic backcolor | + alignleft aligncenter alignright alignjustify | + bullist numlist outdent indent | removeformat | table | code | fullscreen + ''', + 'toolbar_sticky': True, + 'skin': 'oxide', + 'menubar': True, + 'statusbar': True, +} + + +# ---------------------------------------------------- +# *** Configurable Values *** +# ---------------------------------------------------- +BLOG_WORDS_PER_MINUTE = 200 diff --git a/project/config/templates/index.html b/backend/config/templates/index.html similarity index 100% rename from project/config/templates/index.html rename to backend/config/templates/index.html diff --git a/backend/config/urls.py b/backend/config/urls.py new file mode 100644 index 0000000..a7dbf6c --- /dev/null +++ b/backend/config/urls.py @@ -0,0 +1,128 @@ +"""Project URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include, re_path +from .views import IndexView +from users.api.views import LoginView +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi +from knox import views as knox_views +from django.conf import settings +from filebrowser.sites import site +from django.views import defaults as default_views + + +# Yet Another Swagger Schema View +schema_view = get_schema_view( + openapi.Info( + title="Numan Ibn Mazid's Portfolio API", + default_version="v1", + description="API Documentation for Numan Ibn Mazid's Portfolio Project's Backend", + terms_of_service="https://www.google.com/policies/terms/", + contact=openapi.Contact(email="numanibnmazid@gmail.com"), + license=openapi.License(name="BSD License"), + ), + public=True, + permission_classes=[permissions.AllowAny], +) + + +THIRD_PARTY_URLS = [ + # ---------------------------------------------------- + # *** REST FRMAEWORK API URLs *** + # ---------------------------------------------------- + path("api/", include("config.api_router")), + # ---------------------------------------------------- + # *** Knox URLs *** + # ---------------------------------------------------- + # path(r'api/auth/', include('knox.urls')), + path(r"api/auth/login/", LoginView.as_view(), name="knox_login"), + path(r"api/auth/logout/", knox_views.LogoutView.as_view(), name="knox_logout"), + path( + r"api/auth/logoutall/", + knox_views.LogoutAllView.as_view(), + name="knox_logoutall", + ), + # ---------------------------------------------------- + # *** Swagger URLs *** + # ---------------------------------------------------- + re_path( + r"^swagger(?P\.json|\.yaml)$", + schema_view.without_ui(cache_timeout=0), + name="schema-json", + ), + re_path( + r"^swagger/$", + schema_view.with_ui("swagger", cache_timeout=0), + name="schema-swagger-ui", + ), + re_path( + r"^redoc/$", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc" + ), + # ---------------------------------------------------- + # *** CKEDITOR URLs *** + # ---------------------------------------------------- + path('ckeditor/', include('ckeditor_uploader.urls')), + # ---------------------------------------------------- + # *** TinyMCE URLs *** + # ---------------------------------------------------- + path('tinymce/', include('tinymce.urls')), +] + +urlpatterns = [ + # ---------------------------------------------------- + # *** Django Filebrowser URLs (Needs to be placed before Django Admin Urls) *** + # ---------------------------------------------------- + path('admin/filebrowser/', site.urls), + path('grappelli/', include('grappelli.urls')), + # ---------------------------------------------------- + # *** Django Admin URLs *** + # ---------------------------------------------------- + path("admin/", admin.site.urls), + # ---------------------------------------------------- + # *** Project URLs *** + # ---------------------------------------------------- + path("", IndexView.as_view(), name="index"), +] + THIRD_PARTY_URLS + + +if settings.DEBUG: + # This allows the error pages to be debugged during development, just visit + # these url in browser to see how these error pages look like. + urlpatterns += [ + path( + "400/", + default_views.bad_request, + kwargs={"exception": Exception("Bad Request!")}, + ), + path( + "403/", + default_views.permission_denied, + kwargs={"exception": Exception("Permission Denied")}, + ), + path( + "404/", + default_views.page_not_found, + kwargs={"exception": Exception("Page not Found")}, + ), + path("500/", default_views.server_error), + ] + # if "debug_toolbar" in settings.INSTALLED_APPS: + # import debug_toolbar + + # urlpatterns = [ + # path("__debug__/", include(debug_toolbar.urls))] + urlpatterns diff --git a/backend/config/views.py b/backend/config/views.py new file mode 100644 index 0000000..1e061af --- /dev/null +++ b/backend/config/views.py @@ -0,0 +1,5 @@ +from django.views.generic import TemplateView + + +class IndexView(TemplateView): + template_name = "index.html" diff --git a/project/config/wsgi.py b/backend/config/wsgi.py similarity index 100% rename from project/config/wsgi.py rename to backend/config/wsgi.py diff --git a/project/entrypoint.py b/backend/entrypoint.py similarity index 100% rename from project/entrypoint.py rename to backend/entrypoint.py diff --git a/project/entrypoint.sh b/backend/entrypoint.sh similarity index 100% rename from project/entrypoint.sh rename to backend/entrypoint.sh diff --git a/project/manage.py b/backend/manage.py similarity index 100% rename from project/manage.py rename to backend/manage.py diff --git a/backend/passenger_wsgi.py b/backend/passenger_wsgi.py new file mode 100644 index 0000000..792a9df --- /dev/null +++ b/backend/passenger_wsgi.py @@ -0,0 +1,4 @@ +import os +import sys +from config.wsgi import application +sys.path.insert(0, os.path.dirname(__file__)) diff --git a/backend/poetry.lock b/backend/poetry.lock new file mode 100644 index 0000000..74f5e59 --- /dev/null +++ b/backend/poetry.lock @@ -0,0 +1,1065 @@ +# This file is automatically @generated by Poetry 1.5.0 and should not be changed by hand. + +[[package]] +name = "argon2-cffi" +version = "21.3.0" +description = "The secure Argon2 password hashing algorithm." +optional = false +python-versions = ">=3.6" +files = [ + {file = "argon2-cffi-21.3.0.tar.gz", hash = "sha256:d384164d944190a7dd7ef22c6aa3ff197da12962bd04b17f64d4e93d934dba5b"}, + {file = "argon2_cffi-21.3.0-py3-none-any.whl", hash = "sha256:8c976986f2c5c0e5000919e6de187906cfd81fb1c72bf9d88c01177e77da7f80"}, +] + +[package.dependencies] +argon2-cffi-bindings = "*" + +[package.extras] +dev = ["cogapp", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "pre-commit", "pytest", "sphinx", "sphinx-notfound-page", "tomli"] +docs = ["furo", "sphinx", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest"] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +description = "Low-level CFFI bindings for Argon2" +optional = false +python-versions = ">=3.6" +files = [ + {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, + {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, +] + +[package.dependencies] +cffi = ">=1.0.1" + +[package.extras] +dev = ["cogapp", "pre-commit", "pytest", "wheel"] +tests = ["pytest"] + +[[package]] +name = "asgiref" +version = "3.6.0" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.7" +files = [ + {file = "asgiref-3.6.0-py3-none-any.whl", hash = "sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac"}, + {file = "asgiref-3.6.0.tar.gz", hash = "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506"}, +] + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "autopep8" +version = "1.7.0" +description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" +optional = false +python-versions = "*" +files = [ + {file = "autopep8-1.7.0-py2.py3-none-any.whl", hash = "sha256:6f09e90a2be784317e84dc1add17ebfc7abe3924239957a37e5040e27d812087"}, + {file = "autopep8-1.7.0.tar.gz", hash = "sha256:ca9b1a83e53a7fad65d731dc7a2a2d50aa48f43850407c59f6a1a306c4201142"}, +] + +[package.dependencies] +pycodestyle = ">=2.9.1" +toml = "*" + +[[package]] +name = "beautifulsoup4" +version = "4.12.2" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, + {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "certifi" +version = "2023.5.7" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, + {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, +] + +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = "*" +files = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "3.1.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, + {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, +] + +[[package]] +name = "coreapi" +version = "2.3.3" +description = "Python client library for Core API." +optional = false +python-versions = "*" +files = [ + {file = "coreapi-2.3.3-py2.py3-none-any.whl", hash = "sha256:bf39d118d6d3e171f10df9ede5666f63ad80bba9a29a8ec17726a66cf52ee6f3"}, + {file = "coreapi-2.3.3.tar.gz", hash = "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb"}, +] + +[package.dependencies] +coreschema = "*" +itypes = "*" +requests = "*" +uritemplate = "*" + +[[package]] +name = "coreschema" +version = "0.0.4" +description = "Core Schema." +optional = false +python-versions = "*" +files = [ + {file = "coreschema-0.0.4-py2-none-any.whl", hash = "sha256:5e6ef7bf38c1525d5e55a895934ab4273548629f16aed5c0a6caa74ebf45551f"}, + {file = "coreschema-0.0.4.tar.gz", hash = "sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607"}, +] + +[package.dependencies] +jinja2 = "*" + +[[package]] +name = "cryptography" +version = "41.0.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-41.0.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699"}, + {file = "cryptography-41.0.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a"}, + {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca"}, + {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43"}, + {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b"}, + {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3"}, + {file = "cryptography-41.0.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db"}, + {file = "cryptography-41.0.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31"}, + {file = "cryptography-41.0.1-cp37-abi3-win32.whl", hash = "sha256:8f4ab7021127a9b4323537300a2acfb450124b2def3756f64dc3a3d2160ee4b5"}, + {file = "cryptography-41.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:1fee5aacc7367487b4e22484d3c7e547992ed726d14864ee33c0176ae43b0d7c"}, + {file = "cryptography-41.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9a6c7a3c87d595608a39980ebaa04d5a37f94024c9f24eb7d10262b92f739ddb"}, + {file = "cryptography-41.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5d092fdfedaec4cbbffbf98cddc915ba145313a6fdaab83c6e67f4e6c218e6f3"}, + {file = "cryptography-41.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a8e6c2de6fbbcc5e14fd27fb24414507cb3333198ea9ab1258d916f00bc3039"}, + {file = "cryptography-41.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb33ccf15e89f7ed89b235cff9d49e2e62c6c981a6061c9c8bb47ed7951190bc"}, + {file = "cryptography-41.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f0ff6e18d13a3de56f609dd1fd11470918f770c6bd5d00d632076c727d35485"}, + {file = "cryptography-41.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7bfc55a5eae8b86a287747053140ba221afc65eb06207bedf6e019b8934b477c"}, + {file = "cryptography-41.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eb8163f5e549a22888c18b0d53d6bb62a20510060a22fd5a995ec8a05268df8a"}, + {file = "cryptography-41.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8dde71c4169ec5ccc1087bb7521d54251c016f126f922ab2dfe6649170a3b8c5"}, + {file = "cryptography-41.0.1.tar.gz", hash = "sha256:d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "django" +version = "4.2.1" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Django-4.2.1-py3-none-any.whl", hash = "sha256:066b6debb5ac335458d2a713ed995570536c8b59a580005acb0732378d5eb1ee"}, + {file = "Django-4.2.1.tar.gz", hash = "sha256:7efa6b1f781a6119a10ac94b4794ded90db8accbe7802281cd26f8664ffed59c"}, +] + +[package.dependencies] +asgiref = ">=3.6.0,<4" +sqlparse = ">=0.3.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-ckeditor" +version = "6.5.1" +description = "Django admin CKEditor integration." +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-ckeditor-6.5.1.tar.gz", hash = "sha256:57cd8fb7cd150adca354cd4e5c35a743fadaab7073f957d2b7167f0d9fe1fcaf"}, + {file = "django_ckeditor-6.5.1-py3-none-any.whl", hash = "sha256:1321f24df392f30698513930ce5c9f6d899f9bd0ef734c3b64fe936d809e11b3"}, +] + +[package.dependencies] +Django = ">=3.2" +django-js-asset = ">=2.0" + +[[package]] +name = "django-cors-headers" +version = "4.1.0" +description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." +optional = false +python-versions = ">=3.7" +files = [ + {file = "django_cors_headers-4.1.0-py3-none-any.whl", hash = "sha256:88a4bfae24b6404dd0e0640203cb27704a2a57fd546a429e5d821dfa53dd1acf"}, + {file = "django_cors_headers-4.1.0.tar.gz", hash = "sha256:36a8d7a6dee6a85f872fe5916cc878a36d0812043866355438dfeda0b20b6b78"}, +] + +[package.dependencies] +Django = ">=3.2" + +[[package]] +name = "django-filebrowser" +version = "4.0.2" +description = "Media-Management with Grappelli" +optional = false +python-versions = "*" +files = [ + {file = "django-filebrowser-4.0.2.tar.gz", hash = "sha256:bca36fe3d91e68ec7dba41edce6122bae77fd79dedaa09c23f2fde7c85485a3c"}, + {file = "django_filebrowser-4.0.2-py3-none-any.whl", hash = "sha256:228ae8dd03978f4ab6d2b5c217870ea153d487580dd658addee5fec3f856e231"}, +] + +[package.dependencies] +django-grappelli = ">=3.0,<3.1" +pillow = "*" +six = "*" + +[[package]] +name = "django-grappelli" +version = "3.0.6" +description = "A jazzy skin for the Django Admin-Interface." +optional = false +python-versions = "*" +files = [ + {file = "django-grappelli-3.0.6.tar.gz", hash = "sha256:7a39f6c5bf5d3333fdbf8345c0427c1129a2fb6a37966d07a1bf74346d708c76"}, + {file = "django_grappelli-3.0.6-py2.py3-none-any.whl", hash = "sha256:6cef98fe717473f1cb2bddfe68218193d5709ffacf184cdb62138093a6bd8fc5"}, +] + +[[package]] +name = "django-js-asset" +version = "2.0.0" +description = "script tag with additional attributes for django.forms.Media" +optional = false +python-versions = ">=3.6" +files = [ + {file = "django_js_asset-2.0.0-py3-none-any.whl", hash = "sha256:86f9f300d682537ddaf0487dc2ab356581b8f50c069bdba91d334a46e449f923"}, + {file = "django_js_asset-2.0.0.tar.gz", hash = "sha256:adc1ee1efa853fad42054b540c02205344bb406c9bddf87c9e5377a41b7db90f"}, +] + +[package.dependencies] +Django = ">=2.2" + +[package.extras] +tests = ["coverage"] + +[[package]] +name = "django-rest-knox" +version = "4.2.0" +description = "Authentication for django rest framework" +optional = false +python-versions = ">=3.6" +files = [ + {file = "django-rest-knox-4.2.0.tar.gz", hash = "sha256:4595f1dc23d6e41af7939e5f2d8fdaf6ade0a74a656218e7b56683db5566fcc9"}, + {file = "django_rest_knox-4.2.0-py3-none-any.whl", hash = "sha256:62b8e374a44cd4e9617eaefe27c915b301bf224fa6550633d3013d3f9f415113"}, +] + +[package.dependencies] +cryptography = "*" +django = ">=3.2" +djangorestframework = "*" + +[[package]] +name = "django-tinymce" +version = "3.6.1" +description = "A Django application that contains a widget to render a form field as a TinyMCE editor." +optional = false +python-versions = "*" +files = [ + {file = "django-tinymce-3.6.1.tar.gz", hash = "sha256:6f4f6227c2c608052081a436a1e3054c441caae24c9e0c8c3010536e24749e29"}, + {file = "django_tinymce-3.6.1-py3-none-any.whl", hash = "sha256:da5732413f51cf854352e3148f06f170b59d95a6c6a43fd9f7ccfdd1849f4bf9"}, +] + +[[package]] +name = "djangorestframework" +version = "3.14.0" +description = "Web APIs for Django, made easy." +optional = false +python-versions = ">=3.6" +files = [ + {file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"}, + {file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"}, +] + +[package.dependencies] +django = ">=3.0" +pytz = "*" + +[[package]] +name = "drf-yasg" +version = "1.21.5" +description = "Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code." +optional = false +python-versions = ">=3.6" +files = [ + {file = "drf-yasg-1.21.5.tar.gz", hash = "sha256:ceef0c3b5dc4389781afd786e6dc3697af2a2fe0d8724ee1f637c23d75bbc5b2"}, + {file = "drf_yasg-1.21.5-py3-none-any.whl", hash = "sha256:ba9cf4bf79f259290daee9b400fa4fcdb0e78d2f043fa5e9f6589c939fd06d05"}, +] + +[package.dependencies] +coreapi = ">=2.3.3" +coreschema = ">=0.0.4" +django = ">=2.2.16" +djangorestframework = ">=3.10.3" +inflection = ">=0.3.1" +packaging = ">=21.0" +pytz = ">=2021.1" +"ruamel.yaml" = ">=0.16.13" +uritemplate = ">=3.0.0" + +[package.extras] +validation = ["swagger-spec-validator (>=2.1.0)"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "inflection" +version = "0.5.1" +description = "A port of Ruby on Rails inflector to Python" +optional = false +python-versions = ">=3.5" +files = [ + {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, + {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, +] + +[[package]] +name = "itypes" +version = "1.2.0" +description = "Simple immutable types for python." +optional = false +python-versions = "*" +files = [ + {file = "itypes-1.2.0-py2.py3-none-any.whl", hash = "sha256:03da6872ca89d29aef62773672b2d408f490f80db48b23079a4b194c86dd04c6"}, + {file = "itypes-1.2.0.tar.gz", hash = "sha256:af886f129dea4a2a1e3d36595a2d139589e4dd287f5cab0b40e799ee81570ff1"}, +] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "pillow" +version = "9.5.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, + {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, + {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, + {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, + {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, + {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, + {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, + {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, + {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, + {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, + {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, + {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, + {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, + {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, + {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, + {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "psycopg2-binary" +version = "2.9.6" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.6" +files = [ + {file = "psycopg2-binary-2.9.6.tar.gz", hash = "sha256:1f64dcfb8f6e0c014c7f55e51c9759f024f70ea572fbdef123f85318c297947c"}, + {file = "psycopg2_binary-2.9.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d26e0342183c762de3276cca7a530d574d4e25121ca7d6e4a98e4f05cb8e4df7"}, + {file = "psycopg2_binary-2.9.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c48d8f2db17f27d41fb0e2ecd703ea41984ee19362cbce52c097963b3a1b4365"}, + {file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe9dc0a884a8848075e576c1de0290d85a533a9f6e9c4e564f19adf8f6e54a7"}, + {file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a76e027f87753f9bd1ab5f7c9cb8c7628d1077ef927f5e2446477153a602f2c"}, + {file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6460c7a99fc939b849431f1e73e013d54aa54293f30f1109019c56a0b2b2ec2f"}, + {file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae102a98c547ee2288637af07393dd33f440c25e5cd79556b04e3fca13325e5f"}, + {file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9972aad21f965599ed0106f65334230ce826e5ae69fda7cbd688d24fa922415e"}, + {file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7a40c00dbe17c0af5bdd55aafd6ff6679f94a9be9513a4c7e071baf3d7d22a70"}, + {file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:cacbdc5839bdff804dfebc058fe25684cae322987f7a38b0168bc1b2df703fb1"}, + {file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7f0438fa20fb6c7e202863e0d5ab02c246d35efb1d164e052f2f3bfe2b152bd0"}, + {file = "psycopg2_binary-2.9.6-cp310-cp310-win32.whl", hash = "sha256:b6c8288bb8a84b47e07013bb4850f50538aa913d487579e1921724631d02ea1b"}, + {file = "psycopg2_binary-2.9.6-cp310-cp310-win_amd64.whl", hash = "sha256:61b047a0537bbc3afae10f134dc6393823882eb263088c271331602b672e52e9"}, + {file = "psycopg2_binary-2.9.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:964b4dfb7c1c1965ac4c1978b0f755cc4bd698e8aa2b7667c575fb5f04ebe06b"}, + {file = "psycopg2_binary-2.9.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afe64e9b8ea66866a771996f6ff14447e8082ea26e675a295ad3bdbffdd72afb"}, + {file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15e2ee79e7cf29582ef770de7dab3d286431b01c3bb598f8e05e09601b890081"}, + {file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfa74c903a3c1f0d9b1c7e7b53ed2d929a4910e272add6700c38f365a6002820"}, + {file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b83456c2d4979e08ff56180a76429263ea254c3f6552cd14ada95cff1dec9bb8"}, + {file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0645376d399bfd64da57148694d78e1f431b1e1ee1054872a5713125681cf1be"}, + {file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e99e34c82309dd78959ba3c1590975b5d3c862d6f279f843d47d26ff89d7d7e1"}, + {file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4ea29fc3ad9d91162c52b578f211ff1c931d8a38e1f58e684c45aa470adf19e2"}, + {file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4ac30da8b4f57187dbf449294d23b808f8f53cad6b1fc3623fa8a6c11d176dd0"}, + {file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e78e6e2a00c223e164c417628572a90093c031ed724492c763721c2e0bc2a8df"}, + {file = "psycopg2_binary-2.9.6-cp311-cp311-win32.whl", hash = "sha256:1876843d8e31c89c399e31b97d4b9725a3575bb9c2af92038464231ec40f9edb"}, + {file = "psycopg2_binary-2.9.6-cp311-cp311-win_amd64.whl", hash = "sha256:b4b24f75d16a89cc6b4cdff0eb6a910a966ecd476d1e73f7ce5985ff1328e9a6"}, + {file = "psycopg2_binary-2.9.6-cp36-cp36m-win32.whl", hash = "sha256:498807b927ca2510baea1b05cc91d7da4718a0f53cb766c154c417a39f1820a0"}, + {file = "psycopg2_binary-2.9.6-cp36-cp36m-win_amd64.whl", hash = "sha256:0d236c2825fa656a2d98bbb0e52370a2e852e5a0ec45fc4f402977313329174d"}, + {file = "psycopg2_binary-2.9.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:34b9ccdf210cbbb1303c7c4db2905fa0319391bd5904d32689e6dd5c963d2ea8"}, + {file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d2222e61f313c4848ff05353653bf5f5cf6ce34df540e4274516880d9c3763"}, + {file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30637a20623e2a2eacc420059be11527f4458ef54352d870b8181a4c3020ae6b"}, + {file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8122cfc7cae0da9a3077216528b8bb3629c43b25053284cc868744bfe71eb141"}, + {file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38601cbbfe600362c43714482f43b7c110b20cb0f8172422c616b09b85a750c5"}, + {file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c7e62ab8b332147a7593a385d4f368874d5fe4ad4e341770d4983442d89603e3"}, + {file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2ab652e729ff4ad76d400df2624d223d6e265ef81bb8aa17fbd63607878ecbee"}, + {file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c83a74b68270028dc8ee74d38ecfaf9c90eed23c8959fca95bd703d25b82c88e"}, + {file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d4e6036decf4b72d6425d5b29bbd3e8f0ff1059cda7ac7b96d6ac5ed34ffbacd"}, + {file = "psycopg2_binary-2.9.6-cp37-cp37m-win32.whl", hash = "sha256:a8c28fd40a4226b4a84bdf2d2b5b37d2c7bd49486b5adcc200e8c7ec991dfa7e"}, + {file = "psycopg2_binary-2.9.6-cp37-cp37m-win_amd64.whl", hash = "sha256:51537e3d299be0db9137b321dfb6a5022caaab275775680e0c3d281feefaca6b"}, + {file = "psycopg2_binary-2.9.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4499e0a83b7b7edcb8dabecbd8501d0d3a5ef66457200f77bde3d210d5debb"}, + {file = "psycopg2_binary-2.9.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7e13a5a2c01151f1208d5207e42f33ba86d561b7a89fca67c700b9486a06d0e2"}, + {file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e0f754d27fddcfd74006455b6e04e6705d6c31a612ec69ddc040a5468e44b4e"}, + {file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d57c3fd55d9058645d26ae37d76e61156a27722097229d32a9e73ed54819982a"}, + {file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71f14375d6f73b62800530b581aed3ada394039877818b2d5f7fc77e3bb6894d"}, + {file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:441cc2f8869a4f0f4bb408475e5ae0ee1f3b55b33f350406150277f7f35384fc"}, + {file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:65bee1e49fa6f9cf327ce0e01c4c10f39165ee76d35c846ade7cb0ec6683e303"}, + {file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:af335bac6b666cc6aea16f11d486c3b794029d9df029967f9938a4bed59b6a19"}, + {file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:cfec476887aa231b8548ece2e06d28edc87c1397ebd83922299af2e051cf2827"}, + {file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:65c07febd1936d63bfde78948b76cd4c2a411572a44ac50719ead41947d0f26b"}, + {file = "psycopg2_binary-2.9.6-cp38-cp38-win32.whl", hash = "sha256:4dfb4be774c4436a4526d0c554af0cc2e02082c38303852a36f6456ece7b3503"}, + {file = "psycopg2_binary-2.9.6-cp38-cp38-win_amd64.whl", hash = "sha256:02c6e3cf3439e213e4ee930308dc122d6fb4d4bea9aef4a12535fbd605d1a2fe"}, + {file = "psycopg2_binary-2.9.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e9182eb20f41417ea1dd8e8f7888c4d7c6e805f8a7c98c1081778a3da2bee3e4"}, + {file = "psycopg2_binary-2.9.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8a6979cf527e2603d349a91060f428bcb135aea2be3201dff794813256c274f1"}, + {file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8338a271cb71d8da40b023a35d9c1e919eba6cbd8fa20a54b748a332c355d896"}, + {file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3ed340d2b858d6e6fb5083f87c09996506af483227735de6964a6100b4e6a54"}, + {file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f81e65376e52f03422e1fb475c9514185669943798ed019ac50410fb4c4df232"}, + {file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfb13af3c5dd3a9588000910178de17010ebcccd37b4f9794b00595e3a8ddad3"}, + {file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4c727b597c6444a16e9119386b59388f8a424223302d0c06c676ec8b4bc1f963"}, + {file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4d67fbdaf177da06374473ef6f7ed8cc0a9dc640b01abfe9e8a2ccb1b1402c1f"}, + {file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0892ef645c2fabb0c75ec32d79f4252542d0caec1d5d949630e7d242ca4681a3"}, + {file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:02c0f3757a4300cf379eb49f543fb7ac527fb00144d39246ee40e1df684ab514"}, + {file = "psycopg2_binary-2.9.6-cp39-cp39-win32.whl", hash = "sha256:c3dba7dab16709a33a847e5cd756767271697041fbe3fe97c215b1fc1f5c9848"}, + {file = "psycopg2_binary-2.9.6-cp39-cp39-win_amd64.whl", hash = "sha256:f6a88f384335bb27812293fdb11ac6aee2ca3f51d3c7820fe03de0a304ab6249"}, +] + +[[package]] +name = "pycodestyle" +version = "2.10.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, + {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, +] + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "pydantic" +version = "1.10.7" +description = "Data validation and settings management using python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d"}, + {file = "pydantic-1.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e"}, + {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a"}, + {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f"}, + {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209"}, + {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"}, + {file = "pydantic-1.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a"}, + {file = "pydantic-1.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1"}, + {file = "pydantic-1.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe"}, + {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd"}, + {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb"}, + {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b"}, + {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca"}, + {file = "pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"}, + {file = "pydantic-1.10.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918"}, + {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe"}, + {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee"}, + {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"}, + {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a"}, + {file = "pydantic-1.10.7-cp37-cp37m-win_amd64.whl", hash = "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914"}, + {file = "pydantic-1.10.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd"}, + {file = "pydantic-1.10.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245"}, + {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d"}, + {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3"}, + {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52"}, + {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209"}, + {file = "pydantic-1.10.7-cp38-cp38-win_amd64.whl", hash = "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e"}, + {file = "pydantic-1.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143"}, + {file = "pydantic-1.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e"}, + {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d"}, + {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f"}, + {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd"}, + {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5"}, + {file = "pydantic-1.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e"}, + {file = "pydantic-1.10.7-py3-none-any.whl", hash = "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6"}, + {file = "pydantic-1.10.7.tar.gz", hash = "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "python-dotenv" +version = "0.21.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.7" +files = [ + {file = "python-dotenv-0.21.1.tar.gz", hash = "sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49"}, + {file = "python_dotenv-0.21.1-py3-none-any.whl", hash = "sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "pytz" +version = "2023.3" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, + {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "ruamel-yaml" +version = "0.17.31" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +optional = false +python-versions = ">=3" +files = [ + {file = "ruamel.yaml-0.17.31-py3-none-any.whl", hash = "sha256:3cf153f0047ced526e723097ac615d3009371779432e304dbd5596b6f3a4c777"}, + {file = "ruamel.yaml-0.17.31.tar.gz", hash = "sha256:098ed1eb6d338a684891a72380277c1e6fc4d4ae0e120de9a447275056dda335"}, +] + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.12\""} + +[package.extras] +docs = ["ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.7" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +optional = false +python-versions = ">=3.5" +files = [ + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:df5828871e6648db72d1c19b4bd24819b80a755c4541d3409f0f7acd0f335c80"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:efa08d63ef03d079dcae1dfe334f6c8847ba8b645d08df286358b1f5293d24ab"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win32.whl", hash = "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:1a6391a7cabb7641c32517539ca42cf84b87b667bad38b78d4d42dd23e957c81"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:9c7617df90c1365638916b98cdd9be833d31d337dbcd722485597b43c4a215bf"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win32.whl", hash = "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_12_0_arm64.whl", hash = "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:370445fd795706fd291ab00c9df38a0caed0f17a6fb46b0f607668ecb16ce763"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win32.whl", hash = "sha256:ecdf1a604009bd35c674b9225a8fa609e0282d9b896c03dd441a91e5f53b534e"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win_amd64.whl", hash = "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2aa261c29a5545adfef9296b7e33941f46aa5bbd21164228e833412af4c9c75f"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f01da5790e95815eb5a8a138508c01c758e5f5bc0ce4286c4f7028b8dd7ac3d0"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:40d030e2329ce5286d6b231b8726959ebbe0404c92f0a578c0e2482182e38282"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c3ca1fbba4ae962521e5eb66d72998b51f0f4d0f608d3c0347a48e1af262efa7"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win32.whl", hash = "sha256:7bdb4c06b063f6fd55e472e201317a3bb6cdeeee5d5a38512ea5c01e1acbdd93"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:be2a7ad8fd8f7442b24323d24ba0b56c51219513cfa45b9ada3b87b76c374d4b"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91a789b4aa0097b78c93e3dc4b40040ba55bef518f84a40d4442f713b4094acb"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:99e77daab5d13a48a4054803d052ff40780278240a902b880dd37a51ba01a307"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3243f48ecd450eddadc2d11b5feb08aca941b5cd98c9b1db14b2fd128be8c697"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8831a2cedcd0f0927f788c5bdf6567d9dc9cc235646a434986a852af1cb54b4b"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win32.whl", hash = "sha256:3110a99e0f94a4a3470ff67fc20d3f96c25b13d24c6980ff841e82bafe827cac"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:92460ce908546ab69770b2e576e4f99fbb4ce6ab4b245345a3869a0a0410488f"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5bc0667c1eb8f83a3752b71b9c4ba55ef7c7058ae57022dd9b29065186a113d9"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:4a4d8d417868d68b979076a9be6a38c676eca060785abaa6709c7b31593c35d1"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf9a6bc4a0221538b1a7de3ed7bca4c93c02346853f44e1cd764be0023cd3640"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7b301ff08055d73223058b5c46c55638917f04d21577c95e00e0c4d79201a6b"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win32.whl", hash = "sha256:d5e51e2901ec2366b79f16c2299a03e74ba4531ddcfacc1416639c557aef0ad8"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:184faeaec61dbaa3cace407cffc5819f7b977e75360e8d5ca19461cd851a5fc5"}, + {file = "ruamel.yaml.clib-0.2.7.tar.gz", hash = "sha256:1f08fd5a2bea9c4180db71678e850b995d2a5f4537be0e94557668cf0f5f9497"}, +] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "soupsieve" +version = "2.4.1" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"}, + {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"}, +] + +[[package]] +name = "sqlparse" +version = "0.4.4" +description = "A non-validating SQL parser." +optional = false +python-versions = ">=3.5" +files = [ + {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, + {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, +] + +[package.extras] +dev = ["build", "flake8"] +doc = ["sphinx"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.5.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, +] + +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[[package]] +name = "uritemplate" +version = "4.1.1" +description = "Implementation of RFC 6570 URI Templates" +optional = false +python-versions = ">=3.6" +files = [ + {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, + {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, +] + +[[package]] +name = "urllib3" +version = "2.0.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.7" +files = [ + {file = "urllib3-2.0.2-py3-none-any.whl", hash = "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e"}, + {file = "urllib3-2.0.2.tar.gz", hash = "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "3.9.13" +content-hash = "ed7b8139eb5f2bd7d613ead7e35650a0df18f748756d17e13d135ecb2b76cfa1" diff --git a/backend/portfolios/__init__.py b/backend/portfolios/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/portfolios/admin.py b/backend/portfolios/admin.py new file mode 100644 index 0000000..52af6f1 --- /dev/null +++ b/backend/portfolios/admin.py @@ -0,0 +1,105 @@ +from django.contrib import admin +from django.db import models +from utils.mixins import CustomModelAdminMixin +from portfolios.models import ( + ProfessionalExperience, Skill, Education, EducationMedia, Certification, CertificationMedia, Project, ProjectMedia, Interest, Movie +) +from ckeditor.widgets import CKEditorWidget + + +# ---------------------------------------------------- +# *** Professional Experience *** +# ---------------------------------------------------- + +class ProfessionalExperienceAdmin(CustomModelAdminMixin, admin.ModelAdmin): + formfield_overrides = { + models.TextField: {'widget': CKEditorWidget}, + } + class Meta: + model = ProfessionalExperience + +admin.site.register(ProfessionalExperience, ProfessionalExperienceAdmin) + + +# ---------------------------------------------------- +# *** Skill *** +# ---------------------------------------------------- + +class SkillAdmin(CustomModelAdminMixin, admin.ModelAdmin): + class Meta: + model = Skill + +admin.site.register(Skill, SkillAdmin) + + +# ---------------------------------------------------- +# *** Education *** +# ---------------------------------------------------- + +class EducationMediaAdmin(admin.StackedInline): + model = EducationMedia + + +class EducationAdmin(CustomModelAdminMixin, admin.ModelAdmin): + inlines = [EducationMediaAdmin] + + class Meta: + model = Education + +admin.site.register(Education, EducationAdmin) + + +# ---------------------------------------------------- +# *** Certification *** +# ---------------------------------------------------- + +class CertificationMediaAdmin(admin.StackedInline): + model = CertificationMedia + + +class CertificationAdmin(CustomModelAdminMixin, admin.ModelAdmin): + inlines = [CertificationMediaAdmin] + + class Meta: + model = Certification + +admin.site.register(Certification, CertificationAdmin) + + +# ---------------------------------------------------- +# *** Project *** +# ---------------------------------------------------- + +class ProjectMediaAdmin(admin.StackedInline): + model = ProjectMedia + + +class ProjectAdmin(CustomModelAdminMixin, admin.ModelAdmin): + inlines = [ProjectMediaAdmin] + + class Meta: + model = Project + +admin.site.register(Project, ProjectAdmin) + + +# ---------------------------------------------------- +# *** Interest *** +# ---------------------------------------------------- + +class InterestAdmin(CustomModelAdminMixin, admin.ModelAdmin): + class Meta: + model = Interest + +admin.site.register(Interest, InterestAdmin) + + +# ---------------------------------------------------- +# *** Movie *** +# ---------------------------------------------------- + +class MovieAdmin(CustomModelAdminMixin, admin.ModelAdmin): + class Meta: + model = Movie + +admin.site.register(Movie, MovieAdmin) diff --git a/backend/portfolios/api/__init__.py b/backend/portfolios/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/portfolios/api/certifications/__init__.py b/backend/portfolios/api/certifications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/portfolios/api/certifications/routers.py b/backend/portfolios/api/certifications/routers.py new file mode 100644 index 0000000..c37dc6e --- /dev/null +++ b/backend/portfolios/api/certifications/routers.py @@ -0,0 +1,5 @@ +from config.router import router +from portfolios.api.certifications.views import CertificationViewset + + +router.register("certifications", CertificationViewset, basename="certifications") diff --git a/backend/portfolios/api/certifications/serializers.py b/backend/portfolios/api/certifications/serializers.py new file mode 100644 index 0000000..4fbd4fc --- /dev/null +++ b/backend/portfolios/api/certifications/serializers.py @@ -0,0 +1,34 @@ +from rest_framework import serializers +from portfolios.models import Certification, CertificationMedia + + +class CertificationMediaSerializer(serializers.ModelSerializer): + file = serializers.SerializerMethodField() + class Meta: + model = CertificationMedia + fields = ("id", "title", "slug", "file", "description") + read_only_fields = ("id", "slug") + + def get_file(self, obj): + return obj.get_file() + + +class CertificationSerializer(serializers.ModelSerializer): + image = serializers.SerializerMethodField() + certification_media = CertificationMediaSerializer(many=True, read_only=True) + issue_date = serializers.SerializerMethodField() + expiration_date = serializers.SerializerMethodField() + + class Meta: + model = Certification + fields = "__all__" + read_only_fields = ("id", "slug", "created_at", "updated_at") + + def get_image(self, obj): + return obj.get_image() + + def get_issue_date(self, obj): + return obj.get_issue_date() + + def get_expiration_date(self, obj): + return obj.get_expiration_date() diff --git a/backend/portfolios/api/certifications/views.py b/backend/portfolios/api/certifications/views.py new file mode 100644 index 0000000..26b7ff1 --- /dev/null +++ b/backend/portfolios/api/certifications/views.py @@ -0,0 +1,13 @@ +from rest_framework import permissions +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin +from utils.helpers import custom_response_wrapper +from portfolios.models import Certification +from portfolios.api.certifications.serializers import CertificationSerializer + + +@custom_response_wrapper +class CertificationViewset(GenericViewSet, ListModelMixin): + permission_classes = (permissions.IsAuthenticated,) + queryset = Certification.objects.all() + serializer_class = CertificationSerializer diff --git a/backend/portfolios/api/educations/__init__.py b/backend/portfolios/api/educations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/portfolios/api/educations/routers.py b/backend/portfolios/api/educations/routers.py new file mode 100644 index 0000000..71a1c21 --- /dev/null +++ b/backend/portfolios/api/educations/routers.py @@ -0,0 +1,5 @@ +from config.router import router +from portfolios.api.educations.views import EducationViewset + + +router.register("educations", EducationViewset, basename="educations") diff --git a/backend/portfolios/api/educations/serializers.py b/backend/portfolios/api/educations/serializers.py new file mode 100644 index 0000000..67a40b8 --- /dev/null +++ b/backend/portfolios/api/educations/serializers.py @@ -0,0 +1,30 @@ +from rest_framework import serializers +from portfolios.models import Education, EducationMedia + + +class EducationMediaSerializer(serializers.ModelSerializer): + file = serializers.SerializerMethodField() + class Meta: + model = EducationMedia + fields = ("id", "title", "slug", "file", "description") + read_only_fields = ("id", "slug") + + def get_file(self, obj): + return obj.get_file() + + +class EducationSerializer(serializers.ModelSerializer): + duration = serializers.SerializerMethodField() + image = serializers.SerializerMethodField() + education_media = EducationMediaSerializer(many=True, read_only=True) + + class Meta: + model = Education + fields = "__all__" + read_only_fields = ("id", "slug", "created_at", "updated_at") + + def get_image(self, obj): + return obj.get_image() + + def get_duration(self, obj): + return obj.get_duration() diff --git a/backend/portfolios/api/educations/views.py b/backend/portfolios/api/educations/views.py new file mode 100644 index 0000000..dea2615 --- /dev/null +++ b/backend/portfolios/api/educations/views.py @@ -0,0 +1,13 @@ +from rest_framework import permissions +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin +from utils.helpers import custom_response_wrapper +from portfolios.models import Education +from portfolios.api.educations.serializers import EducationSerializer + + +@custom_response_wrapper +class EducationViewset(GenericViewSet, ListModelMixin): + permission_classes = (permissions.IsAuthenticated,) + queryset = Education.objects.all() + serializer_class = EducationSerializer diff --git a/backend/portfolios/api/interests/__init__.py b/backend/portfolios/api/interests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/portfolios/api/interests/routers.py b/backend/portfolios/api/interests/routers.py new file mode 100644 index 0000000..c4f1c2b --- /dev/null +++ b/backend/portfolios/api/interests/routers.py @@ -0,0 +1,5 @@ +from config.router import router +from portfolios.api.interests.views import InterestViewset + + +router.register("interests", InterestViewset, basename="interests") diff --git a/backend/portfolios/api/interests/serializers.py b/backend/portfolios/api/interests/serializers.py new file mode 100644 index 0000000..24e4bdc --- /dev/null +++ b/backend/portfolios/api/interests/serializers.py @@ -0,0 +1,14 @@ +from rest_framework import serializers +from portfolios.models import Interest + + +class InterestSerializer(serializers.ModelSerializer): + icon = serializers.SerializerMethodField() + + class Meta: + model = Interest + fields = "__all__" + read_only_fields = ("id", "slug", "created_at", "updated_at") + + def get_icon(self, obj): + return obj.get_icon() diff --git a/backend/portfolios/api/interests/views.py b/backend/portfolios/api/interests/views.py new file mode 100644 index 0000000..eb10c4b --- /dev/null +++ b/backend/portfolios/api/interests/views.py @@ -0,0 +1,13 @@ +from rest_framework import permissions +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin +from utils.helpers import custom_response_wrapper +from portfolios.models import Interest +from portfolios.api.interests.serializers import InterestSerializer + + +@custom_response_wrapper +class InterestViewset(GenericViewSet, ListModelMixin): + permission_classes = (permissions.IsAuthenticated,) + queryset = Interest.objects.all() + serializer_class = InterestSerializer diff --git a/backend/portfolios/api/movies/__init__.py b/backend/portfolios/api/movies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/portfolios/api/movies/routers.py b/backend/portfolios/api/movies/routers.py new file mode 100644 index 0000000..15daedf --- /dev/null +++ b/backend/portfolios/api/movies/routers.py @@ -0,0 +1,5 @@ +from config.router import router +from portfolios.api.movies.views import MovieViewset + + +router.register("movies", MovieViewset, basename="movies") diff --git a/backend/portfolios/api/movies/serializers.py b/backend/portfolios/api/movies/serializers.py new file mode 100644 index 0000000..1dbec6a --- /dev/null +++ b/backend/portfolios/api/movies/serializers.py @@ -0,0 +1,14 @@ +from rest_framework import serializers +from portfolios.models import Movie + + +class MovieSerializer(serializers.ModelSerializer): + image = serializers.SerializerMethodField() + + class Meta: + model = Movie + fields = "__all__" + read_only_fields = ("id", "slug", "created_at", "updated_at") + + def get_image(self, obj): + return obj.get_image() diff --git a/backend/portfolios/api/movies/views.py b/backend/portfolios/api/movies/views.py new file mode 100644 index 0000000..48d1f62 --- /dev/null +++ b/backend/portfolios/api/movies/views.py @@ -0,0 +1,13 @@ +from rest_framework import permissions +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin +from utils.helpers import custom_response_wrapper +from portfolios.models import Movie +from portfolios.api.movies.serializers import MovieSerializer + + +@custom_response_wrapper +class MovieViewset(GenericViewSet, ListModelMixin): + permission_classes = (permissions.IsAuthenticated,) + queryset = Movie.objects.all() + serializer_class = MovieSerializer diff --git a/backend/portfolios/api/professional_experiences/__init__.py b/backend/portfolios/api/professional_experiences/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/portfolios/api/professional_experiences/routers.py b/backend/portfolios/api/professional_experiences/routers.py new file mode 100644 index 0000000..a8e4b8d --- /dev/null +++ b/backend/portfolios/api/professional_experiences/routers.py @@ -0,0 +1,5 @@ +from config.router import router +from portfolios.api.professional_experiences.views import ProfessionalExperienceViewset + + +router.register("professional-experiences", ProfessionalExperienceViewset, basename="professional_experiences") diff --git a/backend/portfolios/api/professional_experiences/serializers.py b/backend/portfolios/api/professional_experiences/serializers.py new file mode 100644 index 0000000..194e3ef --- /dev/null +++ b/backend/portfolios/api/professional_experiences/serializers.py @@ -0,0 +1,22 @@ +from rest_framework import serializers +from portfolios.models import ProfessionalExperience + + +class ProfessionalExperienceSerializer(serializers.ModelSerializer): + company_image = serializers.SerializerMethodField() + duration = serializers.SerializerMethodField() + duration_in_days = serializers.SerializerMethodField() + + class Meta: + model = ProfessionalExperience + fields = "__all__" + read_only_fields = ("id", "slug", "created_at", "updated_at") + + def get_company_image(self, obj): + return obj.get_company_image() + + def get_duration(self, obj): + return obj.get_duration() + + def get_duration_in_days(self, obj): + return obj.get_duration_in_days() diff --git a/backend/portfolios/api/professional_experiences/views.py b/backend/portfolios/api/professional_experiences/views.py new file mode 100644 index 0000000..99af409 --- /dev/null +++ b/backend/portfolios/api/professional_experiences/views.py @@ -0,0 +1,13 @@ +from rest_framework import permissions +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin +from utils.helpers import custom_response_wrapper +from portfolios.models import ProfessionalExperience +from portfolios.api.professional_experiences.serializers import ProfessionalExperienceSerializer + + +@custom_response_wrapper +class ProfessionalExperienceViewset(GenericViewSet, ListModelMixin): + permission_classes = (permissions.IsAuthenticated,) + queryset = ProfessionalExperience.objects.all() + serializer_class = ProfessionalExperienceSerializer diff --git a/backend/portfolios/api/projects/__init__.py b/backend/portfolios/api/projects/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/portfolios/api/projects/routers.py b/backend/portfolios/api/projects/routers.py new file mode 100644 index 0000000..9c952ce --- /dev/null +++ b/backend/portfolios/api/projects/routers.py @@ -0,0 +1,5 @@ +from config.router import router +from portfolios.api.projects.views import ProjectViewset + + +router.register("projects", ProjectViewset, basename="projects") diff --git a/backend/portfolios/api/projects/serializers.py b/backend/portfolios/api/projects/serializers.py new file mode 100644 index 0000000..1af60e8 --- /dev/null +++ b/backend/portfolios/api/projects/serializers.py @@ -0,0 +1,34 @@ +from rest_framework import serializers +from portfolios.models import Project, ProjectMedia + + +class ProjectMediaMediaSerializer(serializers.ModelSerializer): + file = serializers.SerializerMethodField() + class Meta: + model = ProjectMedia + fields = ("id", "title", "slug", "file", "description") + read_only_fields = ("id", "slug") + + def get_file(self, obj): + return obj.get_file() + + +class ProjectSerializer(serializers.ModelSerializer): + image = serializers.SerializerMethodField() + duration = serializers.SerializerMethodField() + duration_in_days = serializers.SerializerMethodField() + project_media = ProjectMediaMediaSerializer(many=True, read_only=True) + + class Meta: + model = Project + fields = "__all__" + read_only_fields = ("id", "slug", "created_at", "updated_at") + + def get_image(self, obj): + return obj.get_image() + + def get_duration(self, obj): + return obj.get_duration() + + def get_duration_in_days(self, obj): + return obj.get_duration_in_days() diff --git a/backend/portfolios/api/projects/views.py b/backend/portfolios/api/projects/views.py new file mode 100644 index 0000000..2b7aa7f --- /dev/null +++ b/backend/portfolios/api/projects/views.py @@ -0,0 +1,14 @@ +from rest_framework import permissions +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin +from utils.helpers import custom_response_wrapper +from portfolios.models import Project +from portfolios.api.projects.serializers import ProjectSerializer + + +@custom_response_wrapper +class ProjectViewset(GenericViewSet, ListModelMixin, RetrieveModelMixin): + permission_classes = (permissions.IsAuthenticated,) + queryset = Project.objects.all() + serializer_class = ProjectSerializer + lookup_field = 'slug' diff --git a/backend/portfolios/api/skills/__init__.py b/backend/portfolios/api/skills/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/portfolios/api/skills/routers.py b/backend/portfolios/api/skills/routers.py new file mode 100644 index 0000000..396ea91 --- /dev/null +++ b/backend/portfolios/api/skills/routers.py @@ -0,0 +1,5 @@ +from config.router import router +from portfolios.api.skills.views import SkillViewset + + +router.register("skills", SkillViewset, basename="skills") diff --git a/backend/portfolios/api/skills/serializers.py b/backend/portfolios/api/skills/serializers.py new file mode 100644 index 0000000..e4fce8b --- /dev/null +++ b/backend/portfolios/api/skills/serializers.py @@ -0,0 +1,14 @@ +from rest_framework import serializers +from portfolios.models import Skill + + +class SkillSerializer(serializers.ModelSerializer): + image = serializers.SerializerMethodField() + + class Meta: + model = Skill + fields = "__all__" + read_only_fields = ("id", "slug", "created_at", "updated_at") + + def get_image(self, obj): + return obj.get_image() diff --git a/backend/portfolios/api/skills/views.py b/backend/portfolios/api/skills/views.py new file mode 100644 index 0000000..83c64dd --- /dev/null +++ b/backend/portfolios/api/skills/views.py @@ -0,0 +1,13 @@ +from rest_framework import permissions +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin +from utils.helpers import custom_response_wrapper +from portfolios.models import Skill +from portfolios.api.skills.serializers import SkillSerializer + + +@custom_response_wrapper +class SkillViewset(GenericViewSet, ListModelMixin): + permission_classes = (permissions.IsAuthenticated,) + queryset = Skill.objects.all() + serializer_class = SkillSerializer diff --git a/backend/portfolios/apps.py b/backend/portfolios/apps.py new file mode 100644 index 0000000..6113f1c --- /dev/null +++ b/backend/portfolios/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PortfoliosConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "portfolios" diff --git a/backend/portfolios/migrations/0001_initial.py b/backend/portfolios/migrations/0001_initial.py new file mode 100644 index 0000000..2f2b00a --- /dev/null +++ b/backend/portfolios/migrations/0001_initial.py @@ -0,0 +1,67 @@ +# Generated by Django 4.2.1 on 2023-06-15 17:25 + +from django.db import migrations, models +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="ProfessionalExperience", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("slug", models.SlugField(max_length=255, unique=True)), + ("company", models.CharField(max_length=150)), + ( + "company_image", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_professional_experience_company_image_path, + ), + ), + ("company_url", models.URLField(blank=True, null=True)), + ("address", models.CharField(blank=True, max_length=254, null=True)), + ("designation", models.CharField(max_length=150)), + ( + "job_type", + models.CharField( + choices=[ + ("Full Time", "Full Time"), + ("Part Time", "Part Time"), + ("Contractual", "Contractual"), + ("Remote", "Remote"), + ], + default="Full Time", + max_length=20, + ), + ), + ("start_date", models.DateField()), + ("end_date", models.DateField(blank=True, null=True)), + ("currently_working", models.BooleanField(default=False)), + ("description", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Professional Experience", + "verbose_name_plural": "Professional Experiences", + "db_table": "professional_experience", + "ordering": ("-currently_working", "-start_date"), + "get_latest_by": "created_at", + }, + ), + ] diff --git a/backend/portfolios/migrations/0002_alter_professionalexperience_description.py b/backend/portfolios/migrations/0002_alter_professionalexperience_description.py new file mode 100644 index 0000000..2416203 --- /dev/null +++ b/backend/portfolios/migrations/0002_alter_professionalexperience_description.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2023-06-15 19:06 + +import ckeditor.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="professionalexperience", + name="description", + field=ckeditor.fields.RichTextField(blank=True, null=True), + ), + ] diff --git a/backend/portfolios/migrations/0003_skill_alter_professionalexperience_company.py b/backend/portfolios/migrations/0003_skill_alter_professionalexperience_company.py new file mode 100644 index 0000000..62ea9ed --- /dev/null +++ b/backend/portfolios/migrations/0003_skill_alter_professionalexperience_company.py @@ -0,0 +1,69 @@ +# Generated by Django 4.2.1 on 2023-06-16 16:53 + +from django.db import migrations, models +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0002_alter_professionalexperience_description"), + ] + + operations = [ + migrations.CreateModel( + name="Skill", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("slug", models.SlugField(max_length=255, unique=True)), + ("title", models.CharField(max_length=150, unique=True)), + ( + "image", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_skill_image_path, + ), + ), + ( + "level", + models.PositiveSmallIntegerField( + blank=True, + choices=[ + ("None", "-----"), + ("1", "1"), + ("2", "2"), + ("3", "3"), + ("4", "4"), + ("5", "5"), + ], + default="None", + null=True, + ), + ), + ("order", models.PositiveIntegerField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Skill", + "verbose_name_plural": "Skills", + "db_table": "skill", + "ordering": ["-created_at"], + "get_latest_by": "created_at", + }, + ), + migrations.AlterField( + model_name="professionalexperience", + name="company", + field=models.CharField(max_length=150, unique=True), + ), + ] diff --git a/backend/portfolios/migrations/0004_alter_skill_level.py b/backend/portfolios/migrations/0004_alter_skill_level.py new file mode 100644 index 0000000..66c12cd --- /dev/null +++ b/backend/portfolios/migrations/0004_alter_skill_level.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.1 on 2023-06-16 16:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0003_skill_alter_professionalexperience_company"), + ] + + operations = [ + migrations.AlterField( + model_name="skill", + name="level", + field=models.CharField( + blank=True, + choices=[ + ("None", "-----"), + ("1", "1"), + ("2", "2"), + ("3", "3"), + ("4", "4"), + ("5", "5"), + ], + default="None", + null=True, + ), + ), + ] diff --git a/backend/portfolios/migrations/0005_alter_skill_options.py b/backend/portfolios/migrations/0005_alter_skill_options.py new file mode 100644 index 0000000..ef6f339 --- /dev/null +++ b/backend/portfolios/migrations/0005_alter_skill_options.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.1 on 2023-06-16 17:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0004_alter_skill_level"), + ] + + operations = [ + migrations.AlterModelOptions( + name="skill", + options={ + "get_latest_by": "created_at", + "ordering": ("order", "-created_at"), + "verbose_name": "Skill", + "verbose_name_plural": "Skills", + }, + ), + ] diff --git a/backend/portfolios/migrations/0006_alter_skill_level.py b/backend/portfolios/migrations/0006_alter_skill_level.py new file mode 100644 index 0000000..7ae1632 --- /dev/null +++ b/backend/portfolios/migrations/0006_alter_skill_level.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.1 on 2023-06-16 18:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0005_alter_skill_options"), + ] + + operations = [ + migrations.AlterField( + model_name="skill", + name="level", + field=models.CharField( + blank=True, + choices=[ + ("0", "-----"), + ("1", "1"), + ("2", "2"), + ("3", "3"), + ("4", "4"), + ("5", "5"), + ], + default="0", + null=True, + ), + ), + ] diff --git a/backend/portfolios/migrations/0007_alter_skill_level.py b/backend/portfolios/migrations/0007_alter_skill_level.py new file mode 100644 index 0000000..4976e18 --- /dev/null +++ b/backend/portfolios/migrations/0007_alter_skill_level.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.1 on 2023-06-16 18:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0006_alter_skill_level"), + ] + + operations = [ + migrations.AlterField( + model_name="skill", + name="level", + field=models.CharField( + blank=True, + choices=[("1", "1"), ("2", "2"), ("3", "3"), ("4", "4"), ("5", "5")], + default=None, + null=True, + ), + ), + ] diff --git a/backend/portfolios/migrations/0008_alter_skill_order.py b/backend/portfolios/migrations/0008_alter_skill_order.py new file mode 100644 index 0000000..7190c74 --- /dev/null +++ b/backend/portfolios/migrations/0008_alter_skill_order.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2023-06-16 18:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0007_alter_skill_level"), + ] + + operations = [ + migrations.AlterField( + model_name="skill", + name="order", + field=models.PositiveIntegerField(blank=True, null=True), + ), + ] diff --git a/backend/portfolios/migrations/0009_alter_skill_slug.py b/backend/portfolios/migrations/0009_alter_skill_slug.py new file mode 100644 index 0000000..bccf12a --- /dev/null +++ b/backend/portfolios/migrations/0009_alter_skill_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2023-06-16 18:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0008_alter_skill_order"), + ] + + operations = [ + migrations.AlterField( + model_name="skill", + name="slug", + field=models.SlugField(blank=True, max_length=255, unique=True), + ), + ] diff --git a/backend/portfolios/migrations/0010_alter_professionalexperience_slug_alter_skill_order.py b/backend/portfolios/migrations/0010_alter_professionalexperience_slug_alter_skill_order.py new file mode 100644 index 0000000..9a47b2d --- /dev/null +++ b/backend/portfolios/migrations/0010_alter_professionalexperience_slug_alter_skill_order.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.1 on 2023-06-16 18:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0009_alter_skill_slug"), + ] + + operations = [ + migrations.AlterField( + model_name="professionalexperience", + name="slug", + field=models.SlugField(blank=True, max_length=255, unique=True), + ), + migrations.AlterField( + model_name="skill", + name="order", + field=models.PositiveIntegerField(blank=True), + ), + ] diff --git a/backend/portfolios/migrations/0011_education_educationmedia.py b/backend/portfolios/migrations/0011_education_educationmedia.py new file mode 100644 index 0000000..e7daf5a --- /dev/null +++ b/backend/portfolios/migrations/0011_education_educationmedia.py @@ -0,0 +1,96 @@ +# Generated by Django 4.2.1 on 2023-06-17 06:16 + +from django.db import migrations, models +import django.db.models.deletion +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0010_alter_professionalexperience_slug_alter_skill_order"), + ] + + operations = [ + migrations.CreateModel( + name="Education", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("school", models.CharField(max_length=150, unique=True)), + ("slug", models.SlugField(max_length=255, unique=True)), + ( + "image", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_education_school_image_path, + ), + ), + ("degree", models.CharField(max_length=150)), + ("address", models.CharField(blank=True, max_length=254, null=True)), + ("field_of_study", models.CharField(max_length=200)), + ("start_date", models.DateField()), + ("end_date", models.DateField(blank=True, null=True)), + ("currently_studying", models.BooleanField(default=False)), + ("grade", models.CharField(blank=True, max_length=254, null=True)), + ("activities", models.CharField(blank=True, max_length=254, null=True)), + ("description", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Education", + "verbose_name_plural": "Educations", + "db_table": "education", + "ordering": ("-end_date", "-created_at"), + "get_latest_by": "created_at", + }, + ), + migrations.CreateModel( + name="EducationMedia", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("slug", models.SlugField(max_length=255, unique=True)), + ( + "file", + models.FileField( + upload_to=utils.image_upload_helpers.get_education_media_path + ), + ), + ("description", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "education", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="education_media", + to="portfolios.education", + ), + ), + ], + options={ + "verbose_name": "Education Media", + "verbose_name_plural": "Education Media", + "db_table": "education_media", + "get_latest_by": "created_at", + "order_with_respect_to": "education", + }, + ), + ] diff --git a/backend/portfolios/migrations/0012_alter_education_slug_alter_educationmedia_slug.py b/backend/portfolios/migrations/0012_alter_education_slug_alter_educationmedia_slug.py new file mode 100644 index 0000000..5ded032 --- /dev/null +++ b/backend/portfolios/migrations/0012_alter_education_slug_alter_educationmedia_slug.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.1 on 2023-06-17 06:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0011_education_educationmedia"), + ] + + operations = [ + migrations.AlterField( + model_name="education", + name="slug", + field=models.SlugField(blank=True, max_length=255, unique=True), + ), + migrations.AlterField( + model_name="educationmedia", + name="slug", + field=models.SlugField(blank=True, max_length=255, unique=True), + ), + ] diff --git a/backend/portfolios/migrations/0013_educationmedia_title.py b/backend/portfolios/migrations/0013_educationmedia_title.py new file mode 100644 index 0000000..b6b1456 --- /dev/null +++ b/backend/portfolios/migrations/0013_educationmedia_title.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2023-06-17 08:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0012_alter_education_slug_alter_educationmedia_slug"), + ] + + operations = [ + migrations.AddField( + model_name="educationmedia", + name="title", + field=models.CharField(default="Media Sample", max_length=150), + preserve_default=False, + ), + ] diff --git a/backend/portfolios/migrations/0014_certification.py b/backend/portfolios/migrations/0014_certification.py new file mode 100644 index 0000000..0146ff4 --- /dev/null +++ b/backend/portfolios/migrations/0014_certification.py @@ -0,0 +1,58 @@ +# Generated by Django 4.2.1 on 2023-06-17 09:34 + +from django.db import migrations, models +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0013_educationmedia_title"), + ] + + operations = [ + migrations.CreateModel( + name="Certification", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=150)), + ("slug", models.SlugField(blank=True, max_length=255, unique=True)), + ("organization", models.CharField(max_length=150)), + ("address", models.CharField(blank=True, max_length=254, null=True)), + ( + "image", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_education_school_image_path, + ), + ), + ("issue_date", models.DateField()), + ("expiration_date", models.DateField(blank=True, null=True)), + ("does_not_expire", models.BooleanField(default=False)), + ( + "credential_id", + models.CharField(blank=True, max_length=254, null=True), + ), + ("credential_url", models.URLField(blank=True, null=True)), + ("description", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Certification", + "verbose_name_plural": "Certifications", + "db_table": "certification", + "ordering": ["-issue_date"], + "get_latest_by": "created_at", + }, + ), + ] diff --git a/backend/portfolios/migrations/0015_certificationmedia.py b/backend/portfolios/migrations/0015_certificationmedia.py new file mode 100644 index 0000000..64f8d64 --- /dev/null +++ b/backend/portfolios/migrations/0015_certificationmedia.py @@ -0,0 +1,55 @@ +# Generated by Django 4.2.1 on 2023-06-17 09:41 + +from django.db import migrations, models +import django.db.models.deletion +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0014_certification"), + ] + + operations = [ + migrations.CreateModel( + name="CertificationMedia", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=150)), + ("slug", models.SlugField(blank=True, max_length=255, unique=True)), + ( + "file", + models.FileField( + upload_to=utils.image_upload_helpers.get_education_media_path + ), + ), + ("description", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "certification", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="certification_media", + to="portfolios.certification", + ), + ), + ], + options={ + "verbose_name": "Certification Media", + "verbose_name_plural": "Certification Media", + "db_table": "certification_media", + "get_latest_by": "created_at", + "order_with_respect_to": "certification", + }, + ), + ] diff --git a/backend/portfolios/migrations/0016_alter_certification_image_and_more.py b/backend/portfolios/migrations/0016_alter_certification_image_and_more.py new file mode 100644 index 0000000..a4a3aa1 --- /dev/null +++ b/backend/portfolios/migrations/0016_alter_certification_image_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.1 on 2023-06-17 09:49 + +from django.db import migrations, models +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0015_certificationmedia"), + ] + + operations = [ + migrations.AlterField( + model_name="certification", + name="image", + field=models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_certification_image_path, + ), + ), + migrations.AlterField( + model_name="certificationmedia", + name="file", + field=models.FileField( + upload_to=utils.image_upload_helpers.get_certification_media_path + ), + ), + ] diff --git a/backend/portfolios/migrations/0017_project_alter_certification_title_projectmedia.py b/backend/portfolios/migrations/0017_project_alter_certification_title_projectmedia.py new file mode 100644 index 0000000..c928e7e --- /dev/null +++ b/backend/portfolios/migrations/0017_project_alter_certification_title_projectmedia.py @@ -0,0 +1,99 @@ +# Generated by Django 4.2.1 on 2023-06-22 17:18 + +from django.db import migrations, models +import django.db.models.deletion +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0016_alter_certification_image_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="Project", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=200, unique=True)), + ("slug", models.SlugField(blank=True, max_length=255, unique=True)), + ( + "image", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_project_image_path, + ), + ), + ("short_description", models.CharField(max_length=254)), + ("technology", models.TextField(blank=True, null=True)), + ("start_date", models.DateField()), + ("end_date", models.DateField(blank=True, null=True)), + ("currently_working", models.BooleanField(default=False)), + ("url", models.URLField(blank=True, null=True)), + ("description", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Project", + "verbose_name_plural": "Projects", + "db_table": "project", + "ordering": ["-created_at"], + "get_latest_by": "created_at", + }, + ), + migrations.AlterField( + model_name="certification", + name="title", + field=models.CharField(max_length=150, unique=True), + ), + migrations.CreateModel( + name="ProjectMedia", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("slug", models.SlugField(max_length=255, unique=True)), + ( + "file", + models.FileField( + upload_to=utils.image_upload_helpers.get_project_media_path + ), + ), + ("description", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_media", + to="portfolios.project", + ), + ), + ], + options={ + "verbose_name": "Project Media", + "verbose_name_plural": "Project Media", + "db_table": "project_media", + "get_latest_by": "created_at", + "order_with_respect_to": "project", + }, + ), + ] diff --git a/backend/portfolios/migrations/0018_remove_certificationmedia_created_at_and_more.py b/backend/portfolios/migrations/0018_remove_certificationmedia_created_at_and_more.py new file mode 100644 index 0000000..5aa8110 --- /dev/null +++ b/backend/portfolios/migrations/0018_remove_certificationmedia_created_at_and_more.py @@ -0,0 +1,69 @@ +# Generated by Django 4.2.1 on 2023-06-22 17:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0017_project_alter_certification_title_projectmedia"), + ] + + operations = [ + migrations.RemoveField( + model_name="certificationmedia", + name="created_at", + ), + migrations.RemoveField( + model_name="certificationmedia", + name="description", + ), + migrations.RemoveField( + model_name="certificationmedia", + name="slug", + ), + migrations.RemoveField( + model_name="certificationmedia", + name="title", + ), + migrations.RemoveField( + model_name="certificationmedia", + name="updated_at", + ), + migrations.RemoveField( + model_name="educationmedia", + name="created_at", + ), + migrations.RemoveField( + model_name="educationmedia", + name="description", + ), + migrations.RemoveField( + model_name="educationmedia", + name="slug", + ), + migrations.RemoveField( + model_name="educationmedia", + name="title", + ), + migrations.RemoveField( + model_name="educationmedia", + name="updated_at", + ), + migrations.RemoveField( + model_name="projectmedia", + name="created_at", + ), + migrations.RemoveField( + model_name="projectmedia", + name="description", + ), + migrations.RemoveField( + model_name="projectmedia", + name="slug", + ), + migrations.RemoveField( + model_name="projectmedia", + name="updated_at", + ), + ] diff --git a/backend/portfolios/migrations/0019_certificationmedia_created_at_and_more.py b/backend/portfolios/migrations/0019_certificationmedia_created_at_and_more.py new file mode 100644 index 0000000..7363e5f --- /dev/null +++ b/backend/portfolios/migrations/0019_certificationmedia_created_at_and_more.py @@ -0,0 +1,101 @@ +# Generated by Django 4.2.1 on 2023-06-22 17:43 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0018_remove_certificationmedia_created_at_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="certificationmedia", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.AddField( + model_name="certificationmedia", + name="description", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="certificationmedia", + name="slug", + field=models.SlugField(blank=True, max_length=255, unique=True), + ), + migrations.AddField( + model_name="certificationmedia", + name="title", + field=models.CharField(default="Example", max_length=150), + preserve_default=False, + ), + migrations.AddField( + model_name="certificationmedia", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="educationmedia", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.AddField( + model_name="educationmedia", + name="description", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="educationmedia", + name="slug", + field=models.SlugField(blank=True, max_length=255, unique=True), + ), + migrations.AddField( + model_name="educationmedia", + name="title", + field=models.CharField(default="Example", max_length=150), + preserve_default=False, + ), + migrations.AddField( + model_name="educationmedia", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="projectmedia", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.AddField( + model_name="projectmedia", + name="description", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="projectmedia", + name="slug", + field=models.SlugField(blank=True, max_length=255, unique=True), + ), + migrations.AddField( + model_name="projectmedia", + name="title", + field=models.CharField(default="Example", max_length=150), + preserve_default=False, + ), + migrations.AddField( + model_name="projectmedia", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/backend/portfolios/migrations/0020_project_order_alter_project_description.py b/backend/portfolios/migrations/0020_project_order_alter_project_description.py new file mode 100644 index 0000000..794fb97 --- /dev/null +++ b/backend/portfolios/migrations/0020_project_order_alter_project_description.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.1 on 2023-06-22 17:55 + +import ckeditor.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0019_certificationmedia_created_at_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="order", + field=models.PositiveIntegerField(blank=True, default=1), + preserve_default=False, + ), + migrations.AlterField( + model_name="project", + name="description", + field=ckeditor.fields.RichTextField(blank=True, null=True), + ), + ] diff --git a/backend/portfolios/migrations/0021_rename_url_project_github_url_project_preview_url.py b/backend/portfolios/migrations/0021_rename_url_project_github_url_project_preview_url.py new file mode 100644 index 0000000..456df1a --- /dev/null +++ b/backend/portfolios/migrations/0021_rename_url_project_github_url_project_preview_url.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.1 on 2023-06-22 18:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0020_project_order_alter_project_description"), + ] + + operations = [ + migrations.RenameField( + model_name="project", + old_name="url", + new_name="github_url", + ), + migrations.AddField( + model_name="project", + name="preview_url", + field=models.URLField(blank=True, null=True), + ), + ] diff --git a/backend/portfolios/migrations/0022_alter_project_options.py b/backend/portfolios/migrations/0022_alter_project_options.py new file mode 100644 index 0000000..fc5785d --- /dev/null +++ b/backend/portfolios/migrations/0022_alter_project_options.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.1 on 2023-06-22 18:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0021_rename_url_project_github_url_project_preview_url"), + ] + + operations = [ + migrations.AlterModelOptions( + name="project", + options={ + "get_latest_by": "created_at", + "ordering": ["order"], + "verbose_name": "Project", + "verbose_name_plural": "Projects", + }, + ), + ] diff --git a/backend/portfolios/migrations/0023_alter_professionalexperience_options_and_more.py b/backend/portfolios/migrations/0023_alter_professionalexperience_options_and_more.py new file mode 100644 index 0000000..f4857ca --- /dev/null +++ b/backend/portfolios/migrations/0023_alter_professionalexperience_options_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.1 on 2023-06-22 19:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0022_alter_project_options"), + ] + + operations = [ + migrations.AlterModelOptions( + name="professionalexperience", + options={ + "get_latest_by": "created_at", + "ordering": ("-present", "-start_date"), + "verbose_name": "Professional Experience", + "verbose_name_plural": "Professional Experiences", + }, + ), + migrations.RenameField( + model_name="education", + old_name="currently_studying", + new_name="present", + ), + migrations.RenameField( + model_name="professionalexperience", + old_name="currently_working", + new_name="present", + ), + migrations.RenameField( + model_name="project", + old_name="currently_working", + new_name="present", + ), + ] diff --git a/backend/portfolios/migrations/0024_interest_alter_project_options.py b/backend/portfolios/migrations/0024_interest_alter_project_options.py new file mode 100644 index 0000000..95d7e41 --- /dev/null +++ b/backend/portfolios/migrations/0024_interest_alter_project_options.py @@ -0,0 +1,57 @@ +# Generated by Django 4.2.1 on 2023-06-26 14:15 + +from django.db import migrations, models +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0023_alter_professionalexperience_options_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="Interest", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=200)), + ("slug", models.SlugField(max_length=255, unique=True)), + ( + "icon", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_interest_image_path, + ), + ), + ("order", models.PositiveIntegerField(blank=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Interest", + "verbose_name_plural": "Interests", + "db_table": "interest", + "ordering": ("order", "-created_at"), + "get_latest_by": "created_at", + }, + ), + migrations.AlterModelOptions( + name="project", + options={ + "get_latest_by": "created_at", + "ordering": ("order", "-created_at"), + "verbose_name": "Project", + "verbose_name_plural": "Projects", + }, + ), + ] diff --git a/backend/portfolios/migrations/0025_alter_interest_slug.py b/backend/portfolios/migrations/0025_alter_interest_slug.py new file mode 100644 index 0000000..a814ceb --- /dev/null +++ b/backend/portfolios/migrations/0025_alter_interest_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2023-06-26 14:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0024_interest_alter_project_options"), + ] + + operations = [ + migrations.AlterField( + model_name="interest", + name="slug", + field=models.SlugField(blank=True, max_length=255, unique=True), + ), + ] diff --git a/backend/portfolios/migrations/0026_movie.py b/backend/portfolios/migrations/0026_movie.py new file mode 100644 index 0000000..2f9cc1c --- /dev/null +++ b/backend/portfolios/migrations/0026_movie.py @@ -0,0 +1,51 @@ +# Generated by Django 4.2.1 on 2023-06-26 15:20 + +from django.db import migrations, models +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + dependencies = [ + ("portfolios", "0025_alter_interest_slug"), + ] + + operations = [ + migrations.CreateModel( + name="Movie", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=200)), + ("slug", models.SlugField(blank=True, max_length=255, unique=True)), + ( + "image", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_movie_image_path, + ), + ), + ("url", models.URLField(blank=True, null=True)), + ("year", models.PositiveIntegerField(blank=True, null=True)), + ("watched", models.BooleanField(default=True)), + ("rating", models.PositiveIntegerField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Movie", + "verbose_name_plural": "Movies", + "db_table": "movie", + "ordering": ("-updated_at",), + "get_latest_by": "created_at", + }, + ), + ] diff --git a/backend/portfolios/migrations/__init__.py b/backend/portfolios/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/portfolios/models.py b/backend/portfolios/models.py new file mode 100644 index 0000000..f33e1cf --- /dev/null +++ b/backend/portfolios/models.py @@ -0,0 +1,440 @@ +from django.db import models +from django.conf import settings +from django.dispatch import receiver +from django.db.models.signals import pre_save +from django.db.models import Max +from django.utils.translation import gettext_lazy as _ +from utils.helpers import CustomModelManager +from utils.mixins import ModelMediaMixin, DurationMixin +from utils.snippets import autoSlugWithFieldAndUUID, autoSlugFromUUID, image_as_base64, get_static_file_path +from utils.image_upload_helpers import ( + get_professional_experience_company_image_path, get_skill_image_path, get_education_school_image_path, get_education_media_path, + get_certification_image_path, get_certification_media_path, get_project_image_path, get_project_media_path, get_interest_image_path, + get_movie_image_path +) +from ckeditor.fields import RichTextField + + +""" *************** Professional Experience *************** """ + + +@autoSlugWithFieldAndUUID(fieldname="company") +class ProfessionalExperience(models.Model, DurationMixin): + """ + Professional Experience model. + Details: Includes Job Experiences and other professional experiences. + """ + class JobType(models.TextChoices): + FULL_TIME = 'Full Time', _('Full Time') + PART_TIME = 'Part Time', _('Part Time') + CONTRACTUAL = 'Contractual', _('Contractual') + REMOTE = 'Remote', _('Remote') + + company = models.CharField(max_length=150, unique=True) + slug = models.SlugField(max_length=255, unique=True, blank=True) + company_image = models.ImageField(upload_to=get_professional_experience_company_image_path, blank=True, null=True) + company_url = models.URLField(blank=True, null=True) + address = models.CharField(max_length=254, blank=True, null=True) + designation = models.CharField(max_length=150) + job_type = models.CharField(max_length=20, choices=JobType.choices, default=JobType.FULL_TIME) + start_date = models.DateField() + end_date = models.DateField(blank=True, null=True) + present = models.BooleanField(default=False) + description = RichTextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'professional_experience' + verbose_name = _('Professional Experience') + verbose_name_plural = _('Professional Experiences') + ordering = ('-present', '-start_date') + get_latest_by = "created_at" + + def __str__(self): + return self.company + + def get_company_image(self): + if self.company_image: + image_path = settings.MEDIA_ROOT + self.company_image.url.lstrip("/media/") + else: + image_path = get_static_file_path("icons/company.png") + return image_as_base64(image_path) + + +""" *************** Skill *************** """ + + +@autoSlugWithFieldAndUUID(fieldname="title") +class Skill(models.Model): + """ + Skill model. + """ + class Level(models.TextChoices): + One = 1, '1' + Two = 2, '2' + Three = 3, '3' + Four = 4, '4' + Five = 5, '5' + + title = models.CharField(max_length=150, unique=True) + slug = models.SlugField(max_length=255, unique=True, blank=True) + image = models.ImageField(upload_to=get_skill_image_path, blank=True, null=True) + level = models.CharField(choices=Level.choices, default=None, blank=True, null=True) + order = models.PositiveIntegerField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'skill' + verbose_name = _('Skill') + verbose_name_plural = _('Skills') + ordering = ('order', '-created_at') + get_latest_by = "created_at" + + def __str__(self): + return self.title + + def get_image(self): + if self.image: + image_path = settings.MEDIA_ROOT + self.image.url.lstrip("/media/") + else: + image_path = get_static_file_path("icons/skill.png") + return image_as_base64(image_path) + + +# Signals + +@receiver(pre_save, sender=Skill) +def generate_order(sender, instance, **kwargs): + """ + This method will generate order for new instances only. + Order will be generated automatically like 1, 2, 3, 4 and so on. + If any order is deleted then it will be reused. Like if 3 is deleted then next created order will be 3 instead of 5. + """ + if not instance.pk: # Only generate order for new instances + if instance.order is None: + deleted_orders = Skill.objects.filter(order__isnull=False).values_list('order', flat=True) + max_order = Skill.objects.aggregate(Max('order')).get('order__max') + + if deleted_orders: + deleted_orders = sorted(deleted_orders) + reused_order = None + for i in range(1, max_order + 2): + if i not in deleted_orders: + reused_order = i + break + if reused_order is not None: + instance.order = reused_order + else: + instance.order = max_order + 1 if max_order is not None else 1 + + +""" *************** Education *************** """ + + +@autoSlugWithFieldAndUUID(fieldname="school") +class Education(models.Model, DurationMixin): + school = models.CharField(max_length=150, unique=True) + slug = models.SlugField(max_length=255, unique=True, blank=True) + image = models.ImageField(upload_to=get_education_school_image_path, blank=True, null=True) + degree = models.CharField(max_length=150) + address = models.CharField(max_length=254, blank=True, null=True) + field_of_study = models.CharField(max_length=200) + start_date = models.DateField() + end_date = models.DateField(blank=True, null=True) + present = models.BooleanField(default=False) + grade = models.CharField(max_length=254, blank=True, null=True) + activities = models.CharField(max_length=254, blank=True, null=True) + description = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'education' + verbose_name = _('Education') + verbose_name_plural = _('Educations') + ordering = ('-end_date', '-created_at') + get_latest_by = "created_at" + + def __str__(self): + return self.school + + def get_image(self): + if self.image: + image_path = settings.MEDIA_ROOT + self.image.url.lstrip("/media/") + else: + image_path = get_static_file_path("icons/school.png") + return image_as_base64(image_path) + + +@autoSlugFromUUID() +class EducationMedia(ModelMediaMixin): + education = models.ForeignKey(Education, on_delete=models.CASCADE, related_name="education_media") + file = models.FileField(upload_to=get_education_media_path) + + class Meta: + db_table = 'education_media' + verbose_name = _('Education Media') + verbose_name_plural = _('Education Media') + get_latest_by = "created_at" + order_with_respect_to = 'education' + + def __str__(self): + return self.education.__str__() + + +""" *************** Certification *************** """ + + +@autoSlugWithFieldAndUUID(fieldname="title") +class Certification(models.Model): + title = models.CharField(max_length=150, unique=True) + slug = models.SlugField(max_length=255, unique=True, blank=True) + organization = models.CharField(max_length=150) + address = models.CharField(max_length=254, blank=True, null=True) + image = models.ImageField(upload_to=get_certification_image_path, blank=True, null=True) + issue_date = models.DateField() + expiration_date = models.DateField(blank=True, null=True) + does_not_expire = models.BooleanField(default=False) + credential_id = models.CharField(max_length=254, blank=True, null=True) + credential_url = models.URLField(blank=True, null=True) + description = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'certification' + verbose_name = _('Certification') + verbose_name_plural = _('Certifications') + ordering = ['-issue_date'] + get_latest_by = "created_at" + + def __str__(self): + return self.title + + def get_image(self): + if self.image: + image_path = settings.MEDIA_ROOT + self.image.url.lstrip("/media/") + else: + image_path = get_static_file_path("icons/certificate.png") + return image_as_base64(image_path) + + def get_issue_date(self): + return self.issue_date.strftime("%-d %B, %Y") + + def get_expiration_date(self): + if self.does_not_expire: + return _('Does not expire') + elif self.expiration_date: + return self.expiration_date.strftime("%-d %B, %Y") + return _('Not Specified') + + +@autoSlugFromUUID() +class CertificationMedia(ModelMediaMixin): + certification = models.ForeignKey(Certification, on_delete=models.CASCADE, related_name="certification_media") + file = models.FileField(upload_to=get_certification_media_path) + + class Meta: + db_table = 'certification_media' + verbose_name = _('Certification Media') + verbose_name_plural = _('Certification Media') + get_latest_by = "created_at" + order_with_respect_to = 'certification' + + def __str__(self): + return self.certification.__str__() + + +""" *************** Project *************** """ + + +@autoSlugWithFieldAndUUID(fieldname="title") +class Project(models.Model, DurationMixin): + title = models.CharField(max_length=200, unique=True) + slug = models.SlugField(max_length=255, unique=True, blank=True) + image = models.ImageField(upload_to=get_project_image_path, blank=True, null=True) + short_description = models.CharField(max_length=254) + technology = models.TextField(blank=True, null=True) + start_date = models.DateField() + end_date = models.DateField(blank=True, null=True) + present = models.BooleanField(default=False) + preview_url = models.URLField(blank=True, null=True) + github_url = models.URLField(blank=True, null=True) + description = RichTextField(blank=True, null=True) + order = models.PositiveIntegerField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'project' + verbose_name = _('Project') + verbose_name_plural = _('Projects') + ordering = ('order', '-created_at') + get_latest_by = "created_at" + + def __str__(self): + return self.title + + def get_image(self): + if self.image: + image_path = settings.MEDIA_ROOT + self.image.url.lstrip("/media/") + else: + image_path = get_static_file_path("icons/project.png") + return image_as_base64(image_path) + + +@autoSlugFromUUID() +class ProjectMedia(ModelMediaMixin): + project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="project_media") + file = models.FileField(upload_to=get_project_media_path) + + class Meta: + db_table = 'project_media' + verbose_name = _('Project Media') + verbose_name_plural = _('Project Media') + get_latest_by = "created_at" + order_with_respect_to = 'project' + + def __str__(self): + return self.project.__str__() + + +# Signals + +@receiver(pre_save, sender=Project) +def generate_order(sender, instance, **kwargs): + """ + This method will generate order for new instances only. + Order will be generated automatically like 1, 2, 3, 4 and so on. + If any order is deleted then it will be reused. Like if 3 is deleted then next created order will be 3 instead of 5. + """ + if not instance.pk: # Only generate order for new instances + if instance.order is None: + deleted_orders = Project.objects.filter(order__isnull=False).values_list('order', flat=True) + max_order = Project.objects.aggregate(Max('order')).get('order__max') + + if deleted_orders: + deleted_orders = sorted(deleted_orders) + reused_order = None + for i in range(1, max_order + 2): + if i not in deleted_orders: + reused_order = i + break + if reused_order is not None: + instance.order = reused_order + else: + instance.order = max_order + 1 if max_order is not None else 1 + + +""" *************** Interest *************** """ + + +@autoSlugWithFieldAndUUID(fieldname="title") +class Interest(models.Model): + title = models.CharField(max_length=200) + slug = models.SlugField(max_length=255, unique=True, blank=True) + icon = models.ImageField(upload_to=get_interest_image_path, blank=True, null=True) + order = models.PositiveIntegerField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'interest' + verbose_name = _('Interest') + verbose_name_plural = _('Interests') + ordering = ('order', '-created_at') + get_latest_by = "created_at" + + def __str__(self): + return self.title + + def get_icon(self): + if self.icon: + icon_path = settings.MEDIA_ROOT + self.icon.url.lstrip("/media/") + else: + icon_path = get_static_file_path("icons/interest.png") + return image_as_base64(icon_path) + + +# Signals + +@receiver(pre_save, sender=Interest) +def generate_order(sender, instance, **kwargs): + """ + This method will generate order for new instances only. + Order will be generated automatically like 1, 2, 3, 4 and so on. + If any order is deleted then it will be reused. Like if 3 is deleted then next created order will be 3 instead of 5. + """ + if not instance.pk: # Only generate order for new instances + if instance.order is None: + deleted_orders = Interest.objects.filter(order__isnull=False).values_list('order', flat=True) + max_order = Interest.objects.aggregate(Max('order')).get('order__max') + + if deleted_orders: + deleted_orders = sorted(deleted_orders) + reused_order = None + for i in range(1, max_order + 2): + if i not in deleted_orders: + reused_order = i + break + if reused_order is not None: + instance.order = reused_order + else: + instance.order = max_order + 1 if max_order is not None else 1 + + + +""" *************** Movie *************** """ + + +@autoSlugWithFieldAndUUID(fieldname="name") +class Movie(models.Model): + name = models.CharField(max_length=200) + slug = models.SlugField(max_length=255, unique=True, blank=True) + image = models.ImageField(upload_to=get_movie_image_path, blank=True, null=True) + url = models.URLField(blank=True, null=True) + year = models.PositiveIntegerField(blank=True, null=True) + watched = models.BooleanField(default=True) + rating = models.PositiveIntegerField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + db_table = 'movie' + verbose_name = _('Movie') + verbose_name_plural = _('Movies') + ordering = ('-updated_at',) + get_latest_by = "created_at" + + def __str__(self): + return self.name + + def get_image(self): + if self.image: + image_path = settings.MEDIA_ROOT + self.image.url.lstrip("/media/") + else: + image_path = get_static_file_path("icons/movie.png") + return image_as_base64(image_path) diff --git a/backend/portfolios/tests.py b/backend/portfolios/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/portfolios/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/portfolios/views.py b/backend/portfolios/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/portfolios/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/backend/public/staticfiles/icons/blog.png b/backend/public/staticfiles/icons/blog.png new file mode 100644 index 0000000..e372e53 Binary files /dev/null and b/backend/public/staticfiles/icons/blog.png differ diff --git a/backend/public/staticfiles/icons/certificate.png b/backend/public/staticfiles/icons/certificate.png new file mode 100644 index 0000000..0bc28fe Binary files /dev/null and b/backend/public/staticfiles/icons/certificate.png differ diff --git a/backend/public/staticfiles/icons/code.png b/backend/public/staticfiles/icons/code.png new file mode 100644 index 0000000..e175806 Binary files /dev/null and b/backend/public/staticfiles/icons/code.png differ diff --git a/backend/public/staticfiles/icons/company.png b/backend/public/staticfiles/icons/company.png new file mode 100644 index 0000000..681483c Binary files /dev/null and b/backend/public/staticfiles/icons/company.png differ diff --git a/backend/public/staticfiles/icons/interest.png b/backend/public/staticfiles/icons/interest.png new file mode 100644 index 0000000..20c02af Binary files /dev/null and b/backend/public/staticfiles/icons/interest.png differ diff --git a/backend/public/staticfiles/icons/movie.png b/backend/public/staticfiles/icons/movie.png new file mode 100644 index 0000000..7e79ed7 Binary files /dev/null and b/backend/public/staticfiles/icons/movie.png differ diff --git a/backend/public/staticfiles/icons/project.png b/backend/public/staticfiles/icons/project.png new file mode 100644 index 0000000..382fbe3 Binary files /dev/null and b/backend/public/staticfiles/icons/project.png differ diff --git a/backend/public/staticfiles/icons/school.png b/backend/public/staticfiles/icons/school.png new file mode 100644 index 0000000..e92ce85 Binary files /dev/null and b/backend/public/staticfiles/icons/school.png differ diff --git a/backend/public/staticfiles/icons/skill.png b/backend/public/staticfiles/icons/skill.png new file mode 100644 index 0000000..1dc2436 Binary files /dev/null and b/backend/public/staticfiles/icons/skill.png differ diff --git a/backend/public/staticfiles/icons/user/avatar-default.png b/backend/public/staticfiles/icons/user/avatar-default.png new file mode 100644 index 0000000..5911d30 Binary files /dev/null and b/backend/public/staticfiles/icons/user/avatar-default.png differ diff --git a/backend/public/staticfiles/icons/user/avatar-female.png b/backend/public/staticfiles/icons/user/avatar-female.png new file mode 100644 index 0000000..d7135f3 Binary files /dev/null and b/backend/public/staticfiles/icons/user/avatar-female.png differ diff --git a/backend/public/staticfiles/icons/user/avatar-male.png b/backend/public/staticfiles/icons/user/avatar-male.png new file mode 100644 index 0000000..f1e2bed Binary files /dev/null and b/backend/public/staticfiles/icons/user/avatar-male.png differ diff --git a/backend/public/staticfiles/icons/user/female-user-circle.png b/backend/public/staticfiles/icons/user/female-user-circle.png new file mode 100644 index 0000000..e090141 Binary files /dev/null and b/backend/public/staticfiles/icons/user/female-user-circle.png differ diff --git a/backend/public/staticfiles/icons/user/female-user.png b/backend/public/staticfiles/icons/user/female-user.png new file mode 100644 index 0000000..caffd7f Binary files /dev/null and b/backend/public/staticfiles/icons/user/female-user.png differ diff --git a/backend/public/staticfiles/icons/user/male-user-circle.png b/backend/public/staticfiles/icons/user/male-user-circle.png new file mode 100644 index 0000000..77d13af Binary files /dev/null and b/backend/public/staticfiles/icons/user/male-user-circle.png differ diff --git a/backend/public/staticfiles/icons/user/male-user.png b/backend/public/staticfiles/icons/user/male-user.png new file mode 100644 index 0000000..9ddc3dd Binary files /dev/null and b/backend/public/staticfiles/icons/user/male-user.png differ diff --git a/backend/public/staticfiles/icons/user/user.png b/backend/public/staticfiles/icons/user/user.png new file mode 100644 index 0000000..2376043 Binary files /dev/null and b/backend/public/staticfiles/icons/user/user.png differ diff --git a/project/pyproject.toml b/backend/pyproject.toml similarity index 64% rename from project/pyproject.toml rename to backend/pyproject.toml index 387b6ad..1984ad9 100644 --- a/project/pyproject.toml +++ b/backend/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "numanibnmazid.com" +name = "nim23.com" version = "0.1.0" description = "Portfolio web application of Numan Ibn Mazid" authors = ["Numan Ibn Mazid "] @@ -13,6 +13,15 @@ pydantic = "^1.10.2" argon2-cffi = "^21.3.0" autopep8 = "^1.7.0" python-dotenv = "^0.21.0" +djangorestframework = "^3.14.0" +drf-yasg = "^1.21.5" +django-rest-knox = "^4.2.0" +pillow = "^9.5.0" +django-cors-headers = "^4.1.0" +django-ckeditor = "^6.5.1" +django-filebrowser = "^4.0.2" +django-tinymce = "^3.6.1" +beautifulsoup4 = "^4.12.2" [tool.poetry.dev-dependencies] diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..8f49a6f --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,45 @@ +argon2-cffi-bindings==21.2.0 ; python_full_version == "3.9.13" +argon2-cffi==21.3.0 ; python_full_version == "3.9.13" +asgiref==3.6.0 ; python_full_version == "3.9.13" +autopep8==1.7.0 ; python_full_version == "3.9.13" +beautifulsoup4==4.12.2 ; python_full_version == "3.9.13" +certifi==2023.5.7 ; python_full_version == "3.9.13" +cffi==1.15.1 ; python_full_version == "3.9.13" +charset-normalizer==3.1.0 ; python_full_version == "3.9.13" +coreapi==2.3.3 ; python_full_version == "3.9.13" +coreschema==0.0.4 ; python_full_version == "3.9.13" +cryptography==41.0.1 ; python_full_version == "3.9.13" +django-ckeditor==6.5.1 ; python_full_version == "3.9.13" +django-cors-headers==4.1.0 ; python_full_version == "3.9.13" +django-filebrowser==4.0.2 ; python_full_version == "3.9.13" +django-grappelli==3.0.6 ; python_full_version == "3.9.13" +django-js-asset==2.0.0 ; python_full_version == "3.9.13" +django-rest-knox==4.2.0 ; python_full_version == "3.9.13" +django-tinymce==3.6.1 ; python_full_version == "3.9.13" +django==4.2.1 ; python_full_version == "3.9.13" +djangorestframework==3.14.0 ; python_full_version == "3.9.13" +drf-yasg==1.21.5 ; python_full_version == "3.9.13" +idna==3.4 ; python_full_version == "3.9.13" +inflection==0.5.1 ; python_full_version == "3.9.13" +itypes==1.2.0 ; python_full_version == "3.9.13" +jinja2==3.1.2 ; python_full_version == "3.9.13" +markupsafe==2.1.3 ; python_full_version == "3.9.13" +packaging==23.1 ; python_full_version == "3.9.13" +pillow==9.5.0 ; python_full_version == "3.9.13" +psycopg2-binary==2.9.6 ; python_full_version == "3.9.13" +pycodestyle==2.10.0 ; python_full_version == "3.9.13" +pycparser==2.21 ; python_full_version == "3.9.13" +pydantic==1.10.7 ; python_full_version == "3.9.13" +python-dotenv==0.21.1 ; python_full_version == "3.9.13" +pytz==2023.3 ; python_full_version == "3.9.13" +requests==2.31.0 ; python_full_version == "3.9.13" +ruamel-yaml-clib==0.2.7 ; platform_python_implementation == "CPython" and python_full_version == "3.9.13" +ruamel-yaml==0.17.31 ; python_full_version == "3.9.13" +six==1.16.0 ; python_full_version == "3.9.13" +soupsieve==2.4.1 ; python_full_version == "3.9.13" +sqlparse==0.4.4 ; python_full_version == "3.9.13" +toml==0.10.2 ; python_full_version == "3.9.13" +typing-extensions==4.5.0 ; python_full_version == "3.9.13" +tzdata==2023.3 ; sys_platform == "win32" and python_full_version == "3.9.13" +uritemplate==4.1.1 ; python_full_version == "3.9.13" +urllib3==2.0.2 ; python_full_version == "3.9.13" diff --git a/backend/users/__init__.py b/backend/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/users/admin.py b/backend/users/admin.py new file mode 100644 index 0000000..556f882 --- /dev/null +++ b/backend/users/admin.py @@ -0,0 +1,78 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth import get_user_model + + +class UserAdmin(BaseUserAdmin): + fieldsets = ( + ( + None, + { + "fields": ( + "email", + "username", + "password", + "slug", + "is_portfolio_user", + "name", + "nickname", + "gender", + "image", + "dob", + "website", + "contact", + "contact_email", + "linkedin", + "github", + "resume_link", + "address", + "about", + "last_login", + "updated_at", + "date_joined", + ) + }, + ), + ( + "Permissions", + { + "fields": ( + "is_active", + "is_staff", + "is_superuser", + "groups", + "user_permissions", + ) + }, + ), + ) + add_fieldsets = ( + (None, {"classes": ("wide",), "fields": ("email", "password1", "password2")}), + ) + + list_display = ( + "id", + "email", + "username", + "is_portfolio_user", + "slug", + "get_dynamic_username", + "name", + "is_staff", + "is_superuser", + "date_joined", + ) + list_filter = ("is_staff", "is_superuser", "is_active", "is_portfolio_user", "groups") + search_fields = ("email", "username") + readonly_fields = ( + "updated_at", + "date_joined", + ) + ordering = ("-date_joined",) + filter_horizontal = ( + "groups", + "user_permissions", + ) + + +admin.site.register(get_user_model(), UserAdmin) diff --git a/backend/users/api/__init__.py b/backend/users/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/users/api/routers.py b/backend/users/api/routers.py new file mode 100644 index 0000000..331ce31 --- /dev/null +++ b/backend/users/api/routers.py @@ -0,0 +1,5 @@ +from .views import UserViewset +from config.router import router + + +router.register("users", UserViewset, basename="users") diff --git a/backend/users/api/serializers.py b/backend/users/api/serializers.py new file mode 100644 index 0000000..d854536 --- /dev/null +++ b/backend/users/api/serializers.py @@ -0,0 +1,24 @@ +from rest_framework import serializers +from django.contrib.auth import get_user_model + + +class UserSerializer(serializers.ModelSerializer): + image = serializers.SerializerMethodField() + + class Meta: + model = get_user_model() + fields = "__all__" + read_only_fields = ( + "id", + "username", + "is_active", + "slug", + "updated_at", + "is_staff", + "is_superuser", + "date_joined", + "last_login", + ) + + def get_image(self, obj): + return obj.get_user_image() diff --git a/backend/users/api/views.py b/backend/users/api/views.py new file mode 100644 index 0000000..8305168 --- /dev/null +++ b/backend/users/api/views.py @@ -0,0 +1,42 @@ +from rest_framework import permissions +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.authtoken.serializers import AuthTokenSerializer +from rest_framework.viewsets import GenericViewSet +from knox.views import LoginView as KnoxLoginView +from django.contrib.auth import login +from drf_yasg.utils import swagger_auto_schema +from django.contrib.auth import get_user_model +from .serializers import UserSerializer +from utils.helpers import custom_response_wrapper + + +class LoginView(KnoxLoginView): + permission_classes = (permissions.AllowAny,) + + @swagger_auto_schema(request_body=AuthTokenSerializer) + def post(self, request, format=None): + serializer = AuthTokenSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.validated_data["user"] + login(request, user) + return super(LoginView, self).post(request, format=None) + + +@custom_response_wrapper +class UserViewset(GenericViewSet): + permission_classes = (permissions.IsAuthenticated,) + queryset = get_user_model().objects.all() + serializer_class = UserSerializer + pagination_class = None + lookup_field = "slug" + + + @action(detail=False, methods=['get']) + def get_portfolio_user(self, request): + user_qs = get_user_model().objects.filter(is_portfolio_user=True) + if user_qs.exists(): + user = user_qs.last() + serializer = self.get_serializer(user) + return Response(serializer.data) + return Response({"message": "No portfolio user found!"}, status=404) diff --git a/backend/users/apps.py b/backend/users/apps.py new file mode 100644 index 0000000..88f7b17 --- /dev/null +++ b/backend/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "users" diff --git a/backend/users/migrations/0001_initial.py b/backend/users/migrations/0001_initial.py new file mode 100644 index 0000000..eabce50 --- /dev/null +++ b/backend/users/migrations/0001_initial.py @@ -0,0 +1,109 @@ +# Generated by Django 4.2.1 on 2023-06-04 16:21 + +from django.db import migrations, models +import users.models +import utils.image_upload_helpers + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ("email", models.EmailField(max_length=254, unique=True)), + ("username", models.CharField(max_length=254, unique=True)), + ("name", models.CharField(blank=True, max_length=100, null=True)), + ("slug", models.SlugField(max_length=254, unique=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("nick_name", models.CharField(blank=True, max_length=100, null=True)), + ( + "gender", + models.CharField( + blank=True, + choices=[ + ("Male", "Male"), + ("Female", "Female"), + ("Other", "Other"), + ("Do not mention", "Do not mention"), + ], + max_length=20, + null=True, + ), + ), + ( + "image", + models.ImageField( + blank=True, + null=True, + upload_to=utils.image_upload_helpers.get_user_image_path, + ), + ), + ( + "dob", + models.DateField( + blank=True, null=True, verbose_name="date of birth" + ), + ), + ("website", models.URLField(blank=True, null=True)), + ("contact", models.CharField(blank=True, max_length=30, null=True)), + ( + "contact_email", + models.EmailField(blank=True, max_length=254, null=True), + ), + ("address", models.CharField(blank=True, max_length=254, null=True)), + ("about", models.TextField(blank=True, null=True)), + ("is_staff", models.BooleanField(default=False)), + ("is_superuser", models.BooleanField(default=False)), + ("is_active", models.BooleanField(default=True)), + ("last_login", models.DateTimeField(blank=True, null=True)), + ("date_joined", models.DateTimeField(auto_now_add=True)), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "User", + "verbose_name_plural": "Users", + "ordering": ["-date_joined"], + }, + managers=[ + ("objects", users.models.UserManager()), + ], + ), + ] diff --git a/backend/users/migrations/0002_user_is_portfolio_user.py b/backend/users/migrations/0002_user_is_portfolio_user.py new file mode 100644 index 0000000..827763b --- /dev/null +++ b/backend/users/migrations/0002_user_is_portfolio_user.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2023-06-04 20:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="is_portfolio_user", + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/users/migrations/0003_rename_nick_name_user_nickname.py b/backend/users/migrations/0003_rename_nick_name_user_nickname.py new file mode 100644 index 0000000..ae5cc51 --- /dev/null +++ b/backend/users/migrations/0003_rename_nick_name_user_nickname.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2023-06-14 19:01 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0002_user_is_portfolio_user"), + ] + + operations = [ + migrations.RenameField( + model_name="user", + old_name="nick_name", + new_name="nickname", + ), + ] diff --git a/backend/users/migrations/0004_user_github_user_linkedin_user_resume_link.py b/backend/users/migrations/0004_user_github_user_linkedin_user_resume_link.py new file mode 100644 index 0000000..87241e0 --- /dev/null +++ b/backend/users/migrations/0004_user_github_user_linkedin_user_resume_link.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.1 on 2023-06-26 17:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0003_rename_nick_name_user_nickname"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="github", + field=models.URLField(blank=True, null=True), + ), + migrations.AddField( + model_name="user", + name="linkedin", + field=models.URLField(blank=True, null=True), + ), + migrations.AddField( + model_name="user", + name="resume_link", + field=models.URLField(blank=True, null=True), + ), + ] diff --git a/backend/users/migrations/__init__.py b/backend/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/users/models.py b/backend/users/models.py new file mode 100644 index 0000000..38b7b20 --- /dev/null +++ b/backend/users/models.py @@ -0,0 +1,155 @@ +from django.db import models +from django.db.models.signals import pre_save +from django.dispatch import receiver +from django.contrib.auth.models import ( + AbstractBaseUser, + BaseUserManager, + PermissionsMixin, +) +from django.utils import timezone +from django.http import Http404 +from utils.snippets import autoSlugFromUUID, generate_unique_username_from_email +from utils.image_upload_helpers import get_user_image_path +from django.utils.translation import gettext_lazy as _ +from django.conf import settings +from utils.snippets import image_as_base64, get_static_file_path + + +class UserManager(BaseUserManager): + use_in_migrations = True + + def _create_user(self, email, password, **extra_fields): + if not email: + raise ValueError(_("Users must have an email address")) + now = timezone.now() + email = self.normalize_email(email) + user = self.model(email=email, last_login=now, **extra_fields) + user.set_password(password) + user.save(using=self._db) + return user + + def create_user(self, email, password, **extra_fields): + extra_fields.setdefault("is_superuser", False) + extra_fields.setdefault("is_staff", False) + return self._create_user(email, password, **extra_fields) + + def create_superuser(self, email, password, **extra_fields): + extra_fields.setdefault("is_superuser", True) + extra_fields.setdefault("is_staff", True) + + if extra_fields.get("is_superuser") is not True: + raise ValueError(_("Superuser must have is_superuser=True.")) + + return self._create_user(email, password, **extra_fields) + + def all(self): + return self.get_queryset() + + def get_by_id(self, id): + try: + instance = self.get_queryset().get(id=id) + except User.DoesNotExist: + raise Http404(_("User Not Found!")) + except User.MultipleObjectsReturned: + qs = self.get_queryset().filter(id=id) + instance = qs.first() + except: + raise Http404(_("Something went wrong!")) + return instance + + def get_by_slug(self, slug): + try: + instance = self.get_queryset().get(slug=slug) + except User.DoesNotExist: + raise Http404(_("User Not Found!")) + except User.MultipleObjectsReturned: + qs = self.get_queryset().filter(slug=slug) + instance = qs.first() + except: + raise Http404(_("Something went wrong!")) + return instance + + +@autoSlugFromUUID() +class User(AbstractBaseUser, PermissionsMixin): + class Gender(models.TextChoices): + MALE = "Male", _("Male") + FEMALE = "Female", _("Female") + OTHER = "Other", _("Other") + UNDEFINED = "Do not mention", _("Do not mention") + + email = models.EmailField(max_length=254, unique=True) + username = models.CharField(max_length=254, unique=True) + """ Additional Fields Starts """ + name = models.CharField(max_length=100, null=True, blank=True) + slug = models.SlugField(unique=True, max_length=254) + updated_at = models.DateTimeField(auto_now=True) + # Fields for Portfolio + nickname = models.CharField(max_length=100, null=True, blank=True) + gender = models.CharField( + max_length=20, choices=Gender.choices, blank=True, null=True + ) + image = models.ImageField(upload_to=get_user_image_path, null=True, blank=True) + dob = models.DateField(null=True, blank=True, verbose_name=_("date of birth")) + website = models.URLField(null=True, blank=True) + contact = models.CharField(max_length=30, null=True, blank=True) + contact_email = models.EmailField(null=True, blank=True) + linkedin = models.URLField(null=True, blank=True) + github = models.URLField(null=True, blank=True) + address = models.CharField(max_length=254, null=True, blank=True) + about = models.TextField(null=True, blank=True) + is_portfolio_user = models.BooleanField(default=False) + resume_link = models.URLField(null=True, blank=True) + """ Additional Fields Ends """ + is_staff = models.BooleanField(default=False) + is_superuser = models.BooleanField(default=False) + is_active = models.BooleanField(default=True) + last_login = models.DateTimeField(null=True, blank=True) + date_joined = models.DateTimeField(auto_now_add=True) + + USERNAME_FIELD = "email" + EMAIL_FIELD = "email" + REQUIRED_FIELDS = [] + + objects = UserManager() + + class Meta: + verbose_name = "User" + verbose_name_plural = "Users" + ordering = ["-date_joined"] + + def __str__(self): + return self.get_dynamic_username() + + def get_dynamic_username(self): + """Get a dynamic username for a specific user instance. if the user has a name then returns the name, + if the user does not have a name but has a username then return username, otherwise returns email as username + """ + if self.name: + return self.name + elif self.username: + return self.username + return self.email + + def get_user_image(self): + if self.image: + image_path = settings.MEDIA_ROOT + self.image.url.lstrip("/media/") + else: + if self.gender and self.gender == "Male": + image_path = get_static_file_path("icons/user/avatar-male.png") + elif self.gender and self.gender == "Female": + image_path = get_static_file_path("icons/user/avatar-female.png") + else: + image_path = get_static_file_path("icons/user/avatar-default.png") + + if image_path: + return image_as_base64(image_path) + + return + + +@receiver(pre_save, sender=User) +def update_username_from_email(sender, instance, **kwargs): + """Generates and updates username from user email on User pre_save hook""" + if not instance.pk: + instance.username = generate_unique_username_from_email(instance=instance) diff --git a/backend/users/tests.py b/backend/users/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/users/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/utils/helpers.py b/backend/utils/helpers.py new file mode 100644 index 0000000..94f8833 --- /dev/null +++ b/backend/utils/helpers.py @@ -0,0 +1,157 @@ +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet +from rest_framework.renderers import JSONRenderer +from rest_framework import permissions +from django.db import models +from django.http import Http404 +from django.utils.translation import gettext_lazy as _ +from functools import wraps + + +class ResponseWrapper(Response, JSONRenderer): + def __init__( + self, + data=None, + error_code=None, + content_type=None, + error_message=None, + message=None, + response_success=True, + status=None, + data_type=None, + ): + """ + Alters the init arguments slightly. + For example, drop 'template_name', and instead use 'data'. + + Setting 'renderer' and 'media_type' will typically be deferred, + For example being set automatically by the `APIView`. + """ + status_by_default_for_gz = 200 + if error_code is None and status is not None: + if status > 299 or status < 200: + error_code = status + response_success = False + else: + status_by_default_for_gz = status + if error_code is not None: + status_by_default_for_gz = error_code + response_success = False + + # manipulate dynamic message + if message is not None and not message == "": + if message.lower() == "list": + message = ( + "List retrieved successfully!" + if response_success + else "Failed to retrieve the list!" + ) + elif message.lower() == "create": + message = ( + "Created successfully!" if response_success else "Failed to create!" + ) + elif message.lower() in ["update", "partial_update"]: + message = ( + "Updated successfully!" if response_success else "Failed to update!" + ) + elif message.lower() == "destroy": + message = ( + "Deleted successfully!" if response_success else "Failed to delete!" + ) + elif message.lower() == "retrieve": + message = ( + "Object retrieved successfully!" + if response_success + else "Failed to retrieve the object!" + ) + else: + message = ( + "SUCCESS!" + if response_success + else "FAILED!" + ) + + output_data = { + "success": response_success, + "status_code": error_code + if not error_code == "" and not error_code == None + else status_by_default_for_gz, + "data": data, + "message": message + if message + else str(error_message) + if error_message + else "Success" + if response_success + else "Failed", + "error": {"code": error_code, "error_details": error_message}, + } + if data_type is not None: + output_data["type"] = data_type + + super().__init__( + data=output_data, status=status_by_default_for_gz, content_type=content_type + ) + + +def custom_response_wrapper(viewset_cls): + """ + Custom decorator to wrap the `finalize_response` method of a ViewSet + with the ResponseWrapper functionality. + """ + original_finalize_response = viewset_cls.finalize_response + + @wraps(original_finalize_response) + def wrapped_finalize_response(self, request, response, *args, **kwargs): + if isinstance(response, ResponseWrapper): + return response + response = ResponseWrapper( + data=response.data, message=self.action, status=response.status_code + ) + return original_finalize_response(self, request, response, *args, **kwargs) + + viewset_cls.finalize_response = wrapped_finalize_response + return viewset_cls + + +class CustomModelManager(models.Manager): + """ + Custom Model Manager + actions: all(), get_by_id(id), get_by_slug(slug) + """ + def all(self): + return self.get_queryset() + + def get_by_id(self, id): + try: + return self.get(id=id) + except self.model.DoesNotExist: + raise Http404(_("Not Found !!!")) + except self.model.MultipleObjectsReturned: + return self.get_queryset().filter(id=id).first() + except Exception: + raise Http404(_("Something went wrong !!!")) + + def get_by_slug(self, slug): + try: + return self.get(slug=slug) + except self.model.DoesNotExist: + raise Http404(_("Not Found !!!")) + except self.model.MultipleObjectsReturned: + return self.get_queryset().filter(id=id).first() + except Exception: + raise Http404(_("Something went wrong !!!")) + + +class ProjectGenericModelViewset(ModelViewSet): + permission_classes = (permissions.IsAuthenticated,) + pagination_class = None + lookup_field = "slug" + + + def get_queryset(self): + queryset = super().get_queryset() + limit = self.request.GET.get('_limit') + if limit: + queryset = queryset[:int(limit)] + return queryset diff --git a/backend/utils/image_upload_helpers.py b/backend/utils/image_upload_helpers.py new file mode 100644 index 0000000..dc056eb --- /dev/null +++ b/backend/utils/image_upload_helpers.py @@ -0,0 +1,110 @@ +import os +import time +from django.utils.text import slugify + + +def get_filename(filepath): + base_name = os.path.basename(filepath) + name, ext = os.path.splitext(base_name) + + new_filename = "{datetime}".format(datetime=time.strftime("%Y%m%d-%H%M%S")) + final_filename = "{new_filename}{ext}".format(new_filename=new_filename, ext=ext) + return final_filename + + +# User Image Path +def get_user_image_path(instance, filename): + new_filename = get_filename(filename) + return "Users/{username}/Images/{final_filename}".format( + username=slugify(instance.username[:50]), final_filename=new_filename + ) + + +# Professional Experience Company Image Path +def get_professional_experience_company_image_path(instance, filename): + new_filename = get_filename(filename) + return "ProfessionalExperiences/{company}/Images/{final_filename}".format( + company=slugify(instance.company[:50]), final_filename=new_filename + ) + + +# Skill Image Path +def get_skill_image_path(instance, filename): + new_filename = get_filename(filename) + return "Skills/{title}/Images/{final_filename}".format( + title=slugify(instance.title[:50]), final_filename=new_filename + ) + + +# Education School Image Path +def get_education_school_image_path(instance, filename): + new_filename = get_filename(filename) + return "Educations/{school}/Images/{final_filename}".format( + school=slugify(instance.school[:50]), final_filename=new_filename + ) + +def get_education_media_path(instance, filename): + new_filename = get_filename(filename) + return "Educations/{school}/Media/{final_filename}".format( + school=slugify(instance.education.school[:50]), final_filename=new_filename + ) + + +# Certification Image Path +def get_certification_image_path(instance, filename): + new_filename = get_filename(filename) + return "Certifications/{organization}/Images/{final_filename}".format( + organization=slugify(instance.organization[:50]), final_filename=new_filename + ) + +def get_certification_media_path(instance, filename): + new_filename = get_filename(filename) + return "Certifications/{organization}/Media/{final_filename}".format( + organization=slugify(instance.certification.organization[:50]), final_filename=new_filename + ) + + +# Project Image Path +def get_project_image_path(instance, filename): + new_filename = get_filename(filename) + return "Projects/{title}/Images/{final_filename}".format( + title=slugify(instance.title[:50]), final_filename=new_filename + ) + +def get_project_media_path(instance, filename): + new_filename = get_filename(filename) + return "Projects/{title}/Media/{final_filename}".format( + title=slugify(instance.project.title[:50]), final_filename=new_filename + ) + + +# Interest Image Path +def get_interest_image_path(instance, filename): + new_filename = get_filename(filename) + return "Interests/{title}/Images/{final_filename}".format( + title=slugify(instance.title[:50]), final_filename=new_filename + ) + + +# Movie Image Path +def get_movie_image_path(instance, filename): + new_filename = get_filename(filename) + return "Movies/{name}/Images/{final_filename}".format( + name=slugify(instance.name[:50]), final_filename=new_filename + ) + + +# Code Snippet Image Path +def get_code_snippet_image_path(instance, filename): + new_filename = get_filename(filename) + return "Code-Snippets/{title}/Images/{final_filename}".format( + title=slugify(instance.title[:50]), final_filename=new_filename + ) + + +# Blog Image Path +def get_blog_image_path(instance, filename): + new_filename = get_filename(filename) + return "Blogs/{title}/Images/{final_filename}".format( + title=slugify(instance.title[:50]), final_filename=new_filename + ) diff --git a/backend/utils/mixins.py b/backend/utils/mixins.py new file mode 100644 index 0000000..2f89fe9 --- /dev/null +++ b/backend/utils/mixins.py @@ -0,0 +1,105 @@ +from django.db import models +from django.conf import settings +from django.utils.timezone import datetime +from django.utils.translation import gettext_lazy as _ +from utils.helpers import CustomModelManager +from utils.snippets import file_as_base64 + + +""" +----------------------- * Custom Model Admin Mixins * ----------------------- +""" + + +class CustomModelAdminMixin(object): + """ + DOCSTRING for CustomModelAdminMixin: + This model mixing automatically displays all fields of a model in admin panel following the criteria. + code: @ Numan Ibn Mazid + """ + + def __init__(self, model, admin_site): + self.list_display = [ + field.name + for field in model._meta.fields + if field.get_internal_type() != "TextField" + ] + super(CustomModelAdminMixin, self).__init__(model, admin_site) + + +""" +----------------------- * Model Media Mixin * ----------------------- +""" + +class ModelMediaMixin(models.Model): + """ + Derived Model Class should have a field named 'file'. + """ + title = models.CharField(max_length=150) + slug = models.SlugField(max_length=255, unique=True, blank=True) + description = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + # custom model manager + objects = CustomModelManager() + + class Meta: + abstract = True + + def get_file(self): + if self.file: + file_path = settings.MEDIA_ROOT + self.file.url.lstrip("/media/") + return file_as_base64(file_path) + return + + +class DurationMixin: + title = None + """ + Derived Model Class must have fields: `start_date`, `end_date` and `present`. + """ + def get_duration(self): + if self.end_date is None and not self.present: + raise ValueError(_("End date is required to calculate duration in days. Please provide end date or mark as present.")) + if self.present and self.end_date is not None: + raise ValueError(_("End date is not required when marked as present. Please remove end date or mark as not present.")) + + end_date = None + if self.end_date is not None: + end_date = self.end_date.strftime("%b %Y") + if self.present: + end_date = _("Present") + start_date = self.start_date.strftime("%b %Y") + return f"{start_date} - {end_date}" + + def get_duration_in_days(self): + if self.end_date is None and not self.present: + raise ValueError(_("End date is required to calculate duration in days. Please provide end date or mark as present.")) + if self.present and self.end_date is not None: + raise ValueError(_("End date is not required when marked as present. Please remove end date or mark as not present.")) + + end_date = None + if self.end_date is not None: + end_date = self.end_date + if self.present: + end_date = datetime.now().date() + + duration = end_date - self.start_date + + years = duration.days // 365 + months = (duration.days % 365) // 30 + days = (duration.days % 365) % 30 + + duration_str = "" + if years > 0: + duration_str += f"{years} Year{'s' if years > 1 else ''}, " + if months > 0: + duration_str += f"{months} Month{'s' if months > 1 else ''}" + # if days > 0: + # duration_str += f"{days} Day{'s' if days > 1 else ''}" + + if years < 1 and months < 1: + duration_str = f"{days} Day{'s' if days > 1 else ''}" + + return duration_str diff --git a/backend/utils/snippets.py b/backend/utils/snippets.py new file mode 100644 index 0000000..4018ef8 --- /dev/null +++ b/backend/utils/snippets.py @@ -0,0 +1,363 @@ +import random +import string +import time +from django.utils.text import slugify +from django.db import models +from django.dispatch import receiver +import uuid +import os +import base64 +from django.contrib.staticfiles import finders + + +def random_string_generator(size=4, chars=string.ascii_lowercase + string.digits): + """[Generates random string] + + Args: + size (int, optional): [size of string to generate]. Defaults to 4. + chars ([str], optional): [characters to use]. Defaults to string.ascii_lowercase+string.digits. + + Returns: + [str]: [Generated random string] + """ + return "".join(random.choice(chars) for _ in range(size)) + + +def random_number_generator(size=4, chars="1234567890"): + """[Generates random number] + + Args: + size (int, optional): [size of number to generate]. Defaults to 4. + chars (str, optional): [numbers to use]. Defaults to '1234567890'. + + Returns: + [str]: [Generated random number] + """ + return "".join(random.choice(chars) for _ in range(size)) + + +def simple_random_string(): + """[Generates simple random string] + + Returns: + [str]: [Generated random string] + """ + timestamp_m = time.strftime("%Y") + timestamp_d = time.strftime("%m") + timestamp_y = time.strftime("%d") + timestamp_now = time.strftime("%H%M%S") + random_str = random_string_generator() + random_num = random_number_generator() + bindings = ( + random_str + + timestamp_d + + random_num + + timestamp_now + + timestamp_y + + random_num + + timestamp_m + ) + return bindings + + +def simple_random_string_with_timestamp(size=None): + """[Generates random string with timestamp] + + Args: + size ([int], optional): [Size of string]. Defaults to None. + + Returns: + [str]: [Generated random string] + """ + timestamp_m = time.strftime("%Y") + timestamp_d = time.strftime("%m") + timestamp_y = time.strftime("%d") + random_str = random_string_generator() + random_num = random_number_generator() + bindings = random_str + timestamp_d + timestamp_m + timestamp_y + random_num + if not size == None: + return bindings[0:size] + return bindings + + +# def unique_slug_generator(instance, field=None, new_slug=None): +# """[Generates unique slug] + +# Args: +# instance ([Model Class instance]): [Django Model class object instance]. +# field ([Django Model Field], optional): [Django Model Class Field]. Defaults to None. +# new_slug ([str], optional): [passed new slug]. Defaults to None. + +# Returns: +# [str]: [Generated unique slug] +# """ +# if field == None: +# field = instance.title +# if new_slug is not None: +# slug = new_slug +# else: +# slug = slugify(field[:50]) + +# Klass = instance.__class__ +# qs_exists = Klass.objects.filter(slug=slug).exists() +# if qs_exists: +# new_slug = "{slug}-{randstr}".format( +# slug=slug, +# randstr=random_string_generator(size=4) +# ) +# return unique_slug_generator(instance, new_slug=new_slug) +# return slug + + +# def is_url(url): +# """[Checks if a provided string is URL or Not] + +# Args: +# url ([str]): [URL String] + +# Returns: +# [bool]: [returns True if provided string is URL, otherwise returns False] +# """ + +# min_attr = ('scheme', 'netloc') + +# try: +# result = urlparse(url) +# if all([result.scheme, result.netloc]): +# return True +# else: +# return False +# except: +# return False + + +# def autoUniqueIdWithField(fieldname): +# """[Generates auto slug integrating model's field value and UUID] + +# Args: +# fieldname ([str]): [Model field name to use to generate slug] +# """ + +# def decorator(model): +# # some sanity checks first +# assert hasattr(model, fieldname), f"Model has no field {fieldname}" +# assert hasattr(model, "slug"), "Model is missing a slug field" + +# @receiver(models.signals.pre_save, sender=model, weak=False) +# def generate_unique_id(sender, instance, *args, raw=False, **kwargs): +# if not raw and not getattr(instance, fieldname): +# source = getattr(instance, fieldname) + +# def generate(): +# uuid = random_number_generator(size=12) +# Klass = instance.__class__ +# qs_exists = Klass.objects.filter(uuid=uuid).exists() +# if qs_exists: +# generate() +# else: +# instance.uuid = uuid +# pass + +# # generate uuid +# generate() + +# return model +# return decorator + + +def autoSlugWithFieldAndUUID(fieldname): + """[Generates auto slug integrating model's field value and UUID] + + Args: + fieldname ([str]): [Model field name to use to generate slug] + """ + + def decorator(model): + # some sanity checks first + assert hasattr(model, fieldname), f"Model has no field {fieldname}" + assert hasattr(model, "slug"), "Model is missing a slug field" + + @receiver(models.signals.pre_save, sender=model, weak=False) + def generate_slug(sender, instance, *args, raw=False, **kwargs): + if not raw and not instance.slug: + source = getattr(instance, fieldname) + try: + slug = slugify(source)[:123] + "-" + str(uuid.uuid4()) + Klass = instance.__class__ + qs_exists = Klass.objects.filter(slug=slug).exists() + if qs_exists: + new_slug = "{slug}-{randstr}".format( + slug=slug, + randstr=random_string_generator(size=4) + ) + instance.slug = new_slug + else: + instance.slug = slug + except Exception as e: + instance.slug = simple_random_string() + return model + return decorator + + +# def autoslugFromField(fieldname): +# """[Generates auto slug from model's field value] + +# Args: +# fieldname ([str]): [Model field name to use to generate slug] +# """ + +# def decorator(model): +# # some sanity checks first +# assert hasattr(model, fieldname), f"Model has no field {fieldname!r}" +# assert hasattr(model, "slug"), "Model is missing a slug field" + +# @receiver(models.signals.pre_save, sender=model, weak=False) +# def generate_slug(sender, instance, *args, raw=False, **kwargs): +# if not raw and not instance.slug: +# source = getattr(instance, fieldname) +# try: +# slug = slugify(source) +# Klass = instance.__class__ +# qs_exists = Klass.objects.filter(slug=slug).exists() +# if qs_exists: +# new_slug = "{slug}-{randstr}".format( +# slug=slug, +# randstr=random_string_generator(size=4) +# ) +# instance.slug = new_slug +# else: +# instance.slug = slug +# except Exception as e: +# instance.slug = simple_random_string() +# return model +# return decorator + + +def autoSlugFromUUID(): + def decorator(model): + assert hasattr(model, "slug"), "Model is missing a slug field" + + @receiver(models.signals.pre_save, sender=model, weak=False) + def generate_slug(sender, instance, *args, raw=False, **kwargs): + if not raw and not instance.slug: + try: + slug = str(uuid.uuid4()) + Klass = instance.__class__ + qs_exists = Klass.objects.filter(slug=slug).exists() + if qs_exists: + new_slug = "{slug}-{randstr}".format( + slug=slug, + randstr=random_string_generator(size=4) + ) + instance.slug = new_slug + else: + instance.slug = slug + except Exception as e: + instance.slug = simple_random_string() + + return model + + return decorator + + +def generate_unique_username_from_email(instance): + """[Generates unique username from email] + + Args: + instance ([model class object instance]): [model class object instance] + + Raises: + ValueError: [If found invalid email] + + Returns: + [str]: [unique username] + """ + + # get email from instance + email = instance.email + + if not email: + raise ValueError("Invalid email!") + + def generate_username(email): + return ( + email.split("@")[0][:15] + + "__" + + simple_random_string_with_timestamp(size=5) + ) + + generated_username = generate_username(email=email) + + Klass = instance.__class__ + qs_exists = Klass.objects.filter(username=generated_username).exists() + + if qs_exists: + # recursive call + generate_unique_username_from_email(instance=instance) + + return generated_username + + +# def image_as_base64(image_file, format='png'): +# """ +# :param `image_file` for the complete path of image. +# :param `format` is format for image, eg: `png` or `jpg`. +# """ +# if not os.path.isfile(image_file): +# return None + +# encoded_string = '' +# with open(image_file, 'rb') as img_f: +# encoded_string = base64.b64encode(img_f.read()) +# return 'data:image/%s;base64,%s' % (format, encoded_string) + + +def get_static_file_path(static_path): + """ + Get the absolute file path for a static file. + :param static_path: The static file path relative to the static root. + :return: The absolute file path or None if the file is not found. + """ + static_file = finders.find(static_path) + if static_file: + return static_file + return + + +def image_as_base64(image_file): + """ + :param `image_file` for the complete path of the image. + """ + if not os.path.isfile(image_file): + print(f"Image file not found: {image_file}") + return + + # Get the file extension dynamically + extension = os.path.splitext(image_file)[1][1:] + encoded_string = "" + + with open(image_file, "rb") as img_f: + encoded_string = base64.b64encode(img_f.read()).decode("utf-8") + + return f"data:image/{extension};base64,{encoded_string}" + + +def file_as_base64(file_path): + """ + Convert a file to base64. + + :param file_path: The complete path of the file. + :return: The base64 representation of the file. + """ + if not os.path.isfile(file_path): + print(f"File not found: {file_path}") + return + + # Get the file extension dynamically + extension = os.path.splitext(file_path)[1][1:] + encoded_string = "" + + with open(file_path, "rb") as file: + encoded_string = base64.b64encode(file.read()).decode("utf-8") + + return f"data:application/{extension};base64,{encoded_string}" diff --git a/docker-compose.yml b/docker-compose.yml index fcee429..3aa79e3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,21 +13,36 @@ services: networks: - net-tier - django_app: + backend: depends_on: - db env_file: .env environment: WAIT_HOSTS: db:5432 DATABASE_HOST: db - build: ./project + build: ./backend volumes: - - ./project:/app + - ./backend:/app ports: - "8000:8000" networks: - net-tier + frontend: + depends_on: + - backend + build: ./frontend + volumes: + - ./frontend/src:/app/src + ports: + - "3000:3000" + stdin_open: true + tty: true + environment: + - CHOKIDAR_USEPOLLING=true + networks: + - net-tier + networks: net-tier: driver: bridge diff --git a/frontend/.env.example b/frontend/.env.example index b064bda..a1608c8 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,8 +1,8 @@ # These properties are need to send an email, you can look the full documentation # visit here- https://www.emailjs.com/docs/sdk/installation/ -NEXT_PUBLIC_YOUR_SERVICE_ID= -NEXT_PUBLIC_YOUR_TEMPLATE_ID= -NEXT_PUBLIC_YOUR_USER_ID= +EMAIL_JS_SERVICE_ID= +EMAIL_JS_TEMPLATE_ID= +EMAIL_JS_PUBLIC_KEY= # I've used this api to get the personal stats of my dev.to blog @@ -40,3 +40,9 @@ REVALIDATE_SECRET= # Backend API URL BACKEND_API_BASE_URL= + +# Backend API TOKEN +BACKEND_API_TOKEN= + +# Github Access TOKEN +GITHUB_ACCESS_TOKEN= diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..94247f1 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,18 @@ +FROM node:20.2.0-alpine + +RUN apk update && apk add --update yarn +RUN yarn global add webpack + +WORKDIR /app + +COPY package*.json /app +COPY yarn.lock /app + +RUN yarn install + +ADD entrypoint.sh /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh + +COPY . /app + +ENTRYPOINT [ "sh", "-c", "/app/entrypoint.sh" ] diff --git a/frontend/components/Blog.tsx b/frontend/components/Blog.tsx index e9039bc..6d415ca 100644 --- a/frontend/components/Blog.tsx +++ b/frontend/components/Blog.tsx @@ -1,80 +1,80 @@ -import Link from "next/link" -import { getFormattedDate } from "@utils/date" -import { FrontMatter } from "@lib/types" -import { useRef } from "react" -import Image from "next/image" -import { homeProfileImage } from "@utils/utils" -import { motion } from "framer-motion" -import { BlogCardAnimation } from "@content/FramerMotionVariants" -import staticData from "@content/StaticData" +import Link from 'next/link' +import { getFormattedDate } from '@utils/date' +import { BlogType } from '@lib/types' +import { useRef } from 'react' +import Image from 'next/image' +import { motion } from 'framer-motion' +import { BlogCardAnimation } from '@content/FramerMotionVariants' - -export default function Blog({ - blog, - animate = false, -}: { - blog: FrontMatter; - animate?: boolean; -}) { - const blogRef = useRef(null); +export default function Blog({ blog, animate = false }: { blog: BlogType; animate?: boolean }) { + const blogRef = useRef(null) return ( -
+
{blog.title}
- - {blog.title} + +

{blog.title}

-

- {blog.excerpt} -

+ + {blog.overview && ( +

+ {blog.overview} +

+ )} + + {blog.category && ( +
+
+ {blog.category.name} +
+
+ )} + + {blog.tags && ( +
+ {blog.tags.split(',').map((tag, index) => { + return ( + + {tag.toLowerCase()} + + ) + })} +
+ )}
-
- {staticData.personal.name} -
- {staticData.personal.name} + {blog.author} - - {getFormattedDate(new Date(blog.date))} - + {getFormattedDate(new Date(blog.created_at))}

- {blog.readingTime.text} + {blog.reading_time}

- ); + ) } diff --git a/frontend/components/Certificates.tsx b/frontend/components/Certificates.tsx index 16e31c3..88122c7 100644 --- a/frontend/components/Certificates.tsx +++ b/frontend/components/Certificates.tsx @@ -1,16 +1,15 @@ -import { FadeContainer } from "../content/FramerMotionVariants" -import { HomeHeading } from "../pages" -import { motion } from "framer-motion" -import React from "react" +import { FadeContainer } from '../content/FramerMotionVariants' +import { HomeHeading } from '../pages' +import { motion } from 'framer-motion' +import React from 'react' import { useEffect, useState } from 'react' -import { getAllCertificates } from "@lib/backendAPI" -import AnimatedDiv from "@components/FramerMotion/AnimatedDiv" -import Image from "next/image" -import { popUpFromBottomForText } from "@content/FramerMotionVariants" -import Link from "next/link" -import { getFormattedDate } from "@utils/date" -import { CertificateType } from "@lib/types" - +import { getAllCertificates } from '@lib/backendAPI' +import AnimatedDiv from '@components/FramerMotion/AnimatedDiv' +import Image from 'next/image' +import { popUpFromBottomForText } from '@content/FramerMotionVariants' +import Link from 'next/link' +import { CertificateType, MediaType } from '@lib/types' +import MediaModal from '@components/Modals/MediaModal' export default function CertificateSection() { const [certificates, setCertificates] = useState([]) @@ -42,43 +41,88 @@ export default function CertificateSection() { className="grid grid-cols-1 mb-10" >
-

Here are some certificates that I have obtained.

- {certificates.map((cer: CertificateType) => { +

+ Here, I will showcase the certifications and professional achievements I have earned throughout my career. + Each certificate I have obtained represents a milestone in my journey and demonstrates my commitment to excellence. +

+ {certificates.map((certificate: CertificateType) => { return ( -
-
- {cer.orgName} +
+ {certificate.organization} +
+
+
+
+
+ {certificate.credential_url ? ( + + {certificate.title} + + ) : ( +

+ {certificate.title} +

+ )} + +

+ • Organization: {certificate.organization} + {certificate.address ? , {certificate.address} : null} +

+
+
-
- - {cer.title} - -

- {cer.orgName} •{" "} - {getFormattedDate(new Date(cer.issuedDate))} -

+ +
+
+ • Issue Date: {certificate.issue_date} + • Expiration Date: {certificate.expiration_date} + {certificate.credential_id ? ( + • Credential ID: {certificate.credential_id} + ): null} + + {/* Certification Media */} + {certificate.certification_media?.length ? ( + // Here there will be a list of media. bullet points. There will be a button. After clicking the button new modal will open with the list of media. +
+
+

Attachments

+ + {certificate.certification_media.map((media: MediaType, mediaIndex) => ( +
+ +
+ ))} +
+
+ ) : null} +
-

) })} diff --git a/frontend/components/Contact/Contact.tsx b/frontend/components/Contact/Contact.tsx index 5fd3655..f9f9dbd 100644 --- a/frontend/components/Contact/Contact.tsx +++ b/frontend/components/Contact/Contact.tsx @@ -21,9 +21,9 @@ export default function Contact() { variants={popUpFromBottomForText} className="px-4 py-2 font-medium dark:text-gray-300" > - Is there something on your mind you'd like to talk about? Whether it's - related to work or just a casual conversation, I am here and ready to - listen. Please don't hesitate to reach out to me at any time. 🙋‍♂️. + Do you have something on your mind that you'd like to discuss? Whether it's work-related or simply a casual conversation, I'm here and eager to lend an ear. + Please don't hesitate to get in touch with me at any time. 🌟 I'm always ready for engaging discussions and open to connecting with you. + Let's start a conversation and explore new ideas together. 🗣️ diff --git a/frontend/components/Contact/ContactForm.tsx b/frontend/components/Contact/ContactForm.tsx index 8b6b377..e4d5c4f 100644 --- a/frontend/components/Contact/ContactForm.tsx +++ b/frontend/components/Contact/ContactForm.tsx @@ -30,7 +30,7 @@ export default function Form() { }; const emailData = { - to_name: "Jatin Sharma", + to_name: "Numan Ibn Mazid", first_name: target.first_name.value.trim(), last_name: target.last_name.value.trim(), email: target.email.value.trim(), @@ -51,10 +51,10 @@ export default function Form() { emailjs .send( - process.env.NEXT_PUBLIC_YOUR_SERVICE_ID!, - process.env.NEXT_PUBLIC_YOUR_TEMPLATE_ID!, + process.env.EMAIL_JS_SERVICE_ID!, + process.env.EMAIL_JS_TEMPLATE_ID!, emailData!, - process.env.NEXT_PUBLIC_YOUR_USER_ID + process.env.EMAIL_JS_PUBLIC_KEY ) .then(() => { formRef.current.reset(); diff --git a/frontend/components/Education.tsx b/frontend/components/Education.tsx new file mode 100644 index 0000000..6816e78 --- /dev/null +++ b/frontend/components/Education.tsx @@ -0,0 +1,132 @@ +import { FadeContainer, popUp } from '../content/FramerMotionVariants' +import { HomeHeading } from '../pages' +import { motion } from 'framer-motion' +import React from 'react' +import Image from 'next/image' +import { TimelineList } from '@components/TimelineList' +import { EducationType, MediaType } from '@lib/types' +import MediaModal from '@components/Modals/MediaModal' + + +export default function EducationSection({ educations }: { educations: EducationType[] }) { + // ******* Loader Starts ******* + if (educations.length === 0) { + return
Loading...
+ } + // ******* Loader Ends ******* + + return ( +
+ + + +
+

+ I believe that education plays a crucial role in personal and professional growth. Throughout my academic journey, + I have pursued knowledge and embraced learning opportunities that have shaped my skills and perspectives. + Here is an overview of my educational background and academic achievements. +

+ {educations ? ( + + {educations.map((education: EducationType, index) => ( + +
+
+
+ + + +

{education.school}

+ + {education.address ? ( +

{education.address}

+ ) : null} + + {education.image ? ( +
+ {education.school} +

{education.degree}

+
+ ) : null} + +

+ {education.duration} +

+ + {education.field_of_study ? ( +

+ [{education.field_of_study}] +

+ ) : null} + + {education.grade ? ( +

{education.grade}

+ ) : null} +
+
+
+ {education.description ? ( +
+ ) : null} + + {education.activities ? ( +

Activities: {education.activities}

+ ) : null} + + {education.education_media?.length ? ( + // Here there will be a list of media. bullet points. There will be a button. After clicking the button new modal will open with the list of media. +
+
+

Attachments

+ {education.education_media.map((media: MediaType, mediaIndex) => ( +
+ +
+ ))} +
+
+ ) : null} +
+
+
+ ))} +
+ ) : null} +
+
+
+ ) +} diff --git a/frontend/components/Footer.tsx b/frontend/components/Footer.tsx index f424c6b..56d3f99 100644 --- a/frontend/components/Footer.tsx +++ b/frontend/components/Footer.tsx @@ -34,6 +34,7 @@ export default function Footer({ viewport={{ once: true }} className="flex flex-col max-w-4xl gap-5 p-5 mx-auto text-sm border-t-2 border-gray-200 2xl:max-w-5xl 3xl:max-w-7xl dark:border-gray-400/10 sm:text-base" > + {/* Spotify currently playing */}
{currentSong?.isPlaying ? ( @@ -43,11 +44,13 @@ export default function Footer({
+ {/* 1st 5 navigation routes */}
{navigationRoutes.slice(0, 5).map((text, index) => { return ; })}
+ {/* Last navigation routes */}
{navigationRoutes .slice(5, navigationRoutes.length) @@ -57,6 +60,7 @@ export default function Footer({ return ; })}
+ {/* Social Media */}
{socialMedia.slice(0, 5).map((platform, index) => { return ( @@ -81,6 +85,7 @@ export default function Footer({ variants={opacityVariant} className="flex items-center justify-between w-full gap-4 mt-5 " > + {/* Visitors Count */}
@@ -90,6 +95,7 @@ export default function Footer({ visitors in last {visitors?.days} days
+ {/* QRCode Scanner */}
setShowQR(!showQR)} className="p-3 text-white transition-all bg-gray-700 rounded-full cursor-pointer active:scale-90 hover:scale-105" @@ -98,30 +104,21 @@ export default function Footer({
+ {/* Developed By */} - Powered by + Developed by - Next.js - - and - - Vercel + Numan Ibn Mazid diff --git a/frontend/components/Home/BlogsSection.tsx b/frontend/components/Home/BlogsSection.tsx index 26cbdb6..a41e915 100644 --- a/frontend/components/Home/BlogsSection.tsx +++ b/frontend/components/Home/BlogsSection.tsx @@ -1,9 +1,9 @@ -import { HomeHeading } from "../../pages"; -import Link from "next/link"; -import Blog from "../Blog"; -import { FrontMatter } from "@lib/types"; +import { HomeHeading } from "../../pages" +import Link from "next/link" +import Blog from "../Blog" +import { BlogType } from "@lib/types" -export default function BlogsSection({ blogs }: { blogs: FrontMatter[] }) { +export default function BlogsSection({ blogs }: { blogs: BlogType[] }) { return (
@@ -36,5 +36,5 @@ export default function BlogsSection({ blogs }: { blogs: FrontMatter[] }) {
- ); + ) } diff --git a/frontend/components/Home/ExperienceSection.tsx b/frontend/components/Home/ExperienceSection.tsx index c7ecbd7..b9b2f71 100644 --- a/frontend/components/Home/ExperienceSection.tsx +++ b/frontend/components/Home/ExperienceSection.tsx @@ -1,25 +1,19 @@ -import { FadeContainer, popUp } from "../../content/FramerMotionVariants" -import { HomeHeading } from "../../pages" -import { motion } from "framer-motion" -import React from "react" -import { useEffect, useState } from 'react' -import { getAllExperiences } from "@lib/backendAPI" -import { TimelineItem } from "@components/TimelineItem" -import { TimelineList } from "@components/TimelineList" -import { ExperienceType } from "@lib/types" +import { FadeContainer, popUp } from '../../content/FramerMotionVariants' +import { motion } from 'framer-motion' +import React from 'react' +import { TimelineItem } from '@components/TimelineItem' +import { TimelineList } from '@components/TimelineList' +import { ExperienceType } from '@lib/types' +import AnimatedHeading from '@components/FramerMotion/AnimatedHeading' +import { headingFromLeft } from '@content/FramerMotionVariants' +import { useRouter } from 'next/router' +import Link from 'next/link' - -export default function SkillSection() { - const [experiences, setExperiences] = useState([]) - - useEffect(() => { - fetchExperiences() - }, []) - - const fetchExperiences = async () => { - const experiencesData = await getAllExperiences() - setExperiences(experiencesData) - } +export default function ExperienceSection({ experiences }: { experiences: ExperienceType[] }) { + const router = useRouter() + const isHomePage = router.pathname === '/' + // limit experiences to 1 if on home page otherwise show all + const experiencesToDisplay = isHomePage ? experiences.slice(0, 1) : experiences // ******* Loader Starts ******* if (experiences.length === 0) { @@ -29,7 +23,15 @@ export default function SkillSection() { return (
- +
+ + Work Experiences + {experiences.length} + +
-
-

Here's a brief rundown of my most recent experiences.

- {experiences ? ( +
+

+ As an individual, I'm driven by a continuous desire for personal and professional growth. I'm always + seeking opportunities to learn and expand my skill set. Here's a brief rundown of my professional + experiences. +

+ {experiencesToDisplay ? ( - {experiences.map((experience: ExperienceType, index) => ( + {experiencesToDisplay.map((experience: ExperienceType, index) => ( ))} ) : null}
+ + {/* View all experiences link */} + {isHomePage && ( + + View all experiences + + + + + )}
) diff --git a/frontend/components/Home/SkillSection.tsx b/frontend/components/Home/SkillSection.tsx index e3107f6..e379bbb 100644 --- a/frontend/components/Home/SkillSection.tsx +++ b/frontend/components/Home/SkillSection.tsx @@ -1,18 +1,14 @@ -import { FadeContainer, popUp } from "../../content/FramerMotionVariants" -import { HomeHeading } from "../../pages" -import { motion } from "framer-motion" -import { useDarkMode } from "@context/darkModeContext" -import * as WindowsAnimation from "@lib/windowsAnimation" -import React from "react" +import { FadeContainer, popUp } from '../../content/FramerMotionVariants' +import { HomeHeading } from '../../pages' +import { motion } from 'framer-motion' +import React from 'react' import { useEffect, useState } from 'react' import Image from 'next/image' -import { getAllSkills } from "@lib/backendAPI" -import { SkillType } from "@lib/types" - +import { getAllSkills } from '@lib/backendAPI' +import { SkillType } from '@lib/types' export default function SkillSection() { - const { isDarkMode } = useDarkMode() - const [skills, setSkills] = useState([]) + const [skills, setSkills] = useState([]) useEffect(() => { fetchSkills() @@ -31,41 +27,52 @@ export default function SkillSection() { return (
- + - {skills.map((skill: SkillType, index) => { +

+ I possess a diverse range of skills that contribute to my effectiveness in tech industry. + Through experience and continuous learning, I have developed proficiency in various areas. Here are some of my key skills. +

+
+ {skills.map((skill: SkillType, index) => { + const level = Number(skill.level) || 0 // Convert level to a number or use 0 if it's null or invalid + const progressPercentage = (level / 5) * 100 // Calculate the progress percentage + const progressBarStyle = { + width: `${progressPercentage}%`, + } + return ( ) => - WindowsAnimation.showHoverAnimation(e, isDarkMode) - } - onMouseLeave={(e: React.MouseEvent) => - WindowsAnimation.removeHoverAnimation(e) - } - className="flex items-center justify-center gap-4 p-4 origin-center transform border border-gray-300 rounded-sm sm:justify-start bg-gray-50 hover:bg-white dark:bg-darkPrimary hover:dark:bg-darkSecondary dark:border-neutral-700 md:origin-top group" + title={skill.title} + className="p-2 origin-center transform border border-gray-300 rounded-sm sm:justify-start bg-gray-50 hover:bg-white dark:bg-darkPrimary hover:dark:bg-darkSecondary dark:border-neutral-700 md:origin-top group" > -
- {/* @ts-ignore */} - {/* */} - Skill Image - {/* {skill.icon} */} +
+
+ {skill.title} +
+ +

+ {skill.title} +

-

- {skill.name} -

+ {skill.level !== null ? ( +
+
+
+ ) : null} ) })} +
) diff --git a/frontend/components/Interest.tsx b/frontend/components/Interest.tsx new file mode 100644 index 0000000..a782691 --- /dev/null +++ b/frontend/components/Interest.tsx @@ -0,0 +1,69 @@ +import { FadeContainer, popUp } from '../content/FramerMotionVariants' +import { HomeHeading } from '../pages' +import { motion } from 'framer-motion' +import React from 'react' +import { useEffect, useState } from 'react' +import Image from 'next/image' +import { getAllInterests } from '@lib/backendAPI' +import { InterestType } from '@lib/types' + + +export default function InterestSection() { + const [interests, setInterests] = useState([]) + + useEffect(() => { + fetchInterests() + }, []) + + const fetchInterests = async () => { + const interestsData = await getAllInterests() + setInterests(interestsData) + } + + // ******* Loader Starts ******* + if (interests.length === 0) { + return
Loading...
+ } + // ******* Loader Ends ******* + + return ( +
+ + + +

+ Beyond my professional pursuits, I have a diverse range of interests that fuel my creativity, enhance my problem-solving abilities, + and bring balance to my life. Here are a few of my passions outside of work. +

+
+ {interests.map((interest: InterestType, index) => { + return ( + +
+
+ {interest.title} +
+ +

+ {interest.title} +

+
+
+ ) + })} +
+
+
+ ) +} diff --git a/frontend/components/Modals/MediaModal.tsx b/frontend/components/Modals/MediaModal.tsx new file mode 100644 index 0000000..38a833c --- /dev/null +++ b/frontend/components/Modals/MediaModal.tsx @@ -0,0 +1,96 @@ +import { useState } from 'react' +import ReactModal from 'react-modal' +import PDFViewer from '@components/PDFViewer' +import Image from 'next/image' + +// Set the app element to avoid accessibility warnings +ReactModal.setAppElement('#__next') + +interface MediaModalProps { + title: string + file: string + description: string +} + +const MediaModal: React.FC = ({ title, file, description }) => { + const [modalIsOpen, setModalIsOpen] = useState(false) + + const openModal = () => { + setModalIsOpen(true) + } + + const closeModal = () => { + setModalIsOpen(false) + } + + function getFileExtensionFromBase64(base64String: string): string { + const mimeType = base64String.match(/data:(.*?);/)?.[1] + const [, fileExtension] = mimeType?.split('/') ?? [] + + return fileExtension || '' + } + + const renderFile = (file: string) => { + const fileExtension = getFileExtensionFromBase64(file) + if (fileExtension === 'pdf') { + return + } + return {title} + } + + return ( + <> + + + + {/* Modal Body */} +
+ {/* Header Section */} +
+ +

{title}

+
+ + {/* Modal Content */} +
+ {/* File media */} + {renderFile(file)} + +

{description}

+
+ + {/* Modal Footer */} + +
+
+ + ) +} + +export default MediaModal diff --git a/frontend/components/MovieCard.tsx b/frontend/components/MovieCard.tsx index 7164dfc..ed542fe 100644 --- a/frontend/components/MovieCard.tsx +++ b/frontend/components/MovieCard.tsx @@ -8,7 +8,7 @@ import { AiFillStar } from "react-icons/ai"; export default function MovieCard({ movie }: { movie: MovieType }) { return ( - +

- Jatin's Newsletter + Numan Ibn Mazid's Newsletter

- I write monthly Tech, Web Development and chrome extension that will - improve your productivity. Trust me, I won't spam you. + Subscribe to my Personal Blog Newsletter for professional insights, industry trends, and valuable tips. + Stay updated and take your personal and professional growth to new heights. Join now (Spam Free)!

diff --git a/frontend/components/OgImage.tsx b/frontend/components/OgImage.tsx index c040fe7..69da7d6 100644 --- a/frontend/components/OgImage.tsx +++ b/frontend/components/OgImage.tsx @@ -1,7 +1,7 @@ import Image from "next/image"; function OgImage({ src, alt }: { src: string; alt: string }) { return ( -
+
{alt} = ({ base64String }) => { + const [numPages, setNumPages] = useState(null) + const [isSmallDevice, setIsSmallDevice] = useState(false) + + useLayoutEffect(() => { + const handleResize = () => { + setIsSmallDevice(window.innerWidth < 1024) + } + + handleResize() // Initial check + + window.addEventListener('resize', handleResize) + + return () => { + window.removeEventListener('resize', handleResize) + } + }, []) + + const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => { + setNumPages(numPages) + } + + return ( +
+ + {Array.from(new Array(numPages || 0), (_, index) => ( +
+ +
+ ))} +
+
+ ) +} + +export default PDFViewer diff --git a/frontend/components/PageTop.tsx b/frontend/components/PageTop.tsx index 3822bf1..9025b03 100644 --- a/frontend/components/PageTop.tsx +++ b/frontend/components/PageTop.tsx @@ -22,7 +22,7 @@ export default function PageTop({ > {pageTitle} diff --git a/frontend/components/ProjectSection.tsx b/frontend/components/ProjectSection.tsx index b2f6163..63e040a 100644 --- a/frontend/components/ProjectSection.tsx +++ b/frontend/components/ProjectSection.tsx @@ -1,69 +1,138 @@ -import { BsGithub } from "react-icons/bs" -import { MdOutlineLink } from "react-icons/md" -import Link from "next/link" -import OgImage from "@components/OgImage" -import { ProjectType } from "@lib/types" -import { motion } from "framer-motion" -import { popUp } from "../content/FramerMotionVariants" +import { BsGithub } from 'react-icons/bs' +import { MdOutlineLink } from 'react-icons/md' +import Link from 'next/link' +import OgImage from '@components/OgImage' +import { ProjectType } from '@lib/types' +import { motion } from 'framer-motion' +import { popUp } from '../content/FramerMotionVariants' +import { FadeContainer } from '../content/FramerMotionVariants' +import React from 'react' +import AnimatedDiv from '@components/FramerMotion/AnimatedDiv' +import AnimatedHeading from "@components/FramerMotion/AnimatedHeading" +import { headingFromLeft } from "@content/FramerMotionVariants" -export default function Project({ project }: { project: ProjectType }) { - return ( - +export default function ProjectSection({ projects }: { projects: ProjectType[] }) { + // ******* Loader Starts ******* + if (projects.length === 0) { + return
Loading...
+ } + // ******* Loader Ends ******* -
- + return ( +
+
+ + Projects + + {projects.length} + + +
-
-

- {project.name} -

-

- {project.description} + +

+

+ I believe that projects are not just tasks to be completed, but opportunities to bring ideas to life, solve problems, and make a meaningful impact. + In each project, I follow a meticulous approach, combining innovative thinking, strategic planning, and attention to detail. + Here I will showcase some of the exciting projects I have worked on.

+ + {projects.map((project: ProjectType, index) => ( + +
+ -
- {project.tools!.map((tool, index) => { - return ( - - {tool} - - ) - })} -
+
+ +

+ {project.title} +

+ -
- - - +

+ {project.short_description} +

- {project.previewURL && ( - - - - )} -
+

+ + {project.duration} + ({project.duration_in_days}) + +

+ + {project.description && ( +
+ )} + + + {project.technology && ( +
+ {project.technology.split(',').map((technology, index) => { + return ( + + {technology} + + ) + })} +
+ )} + +
+ {project.github_url && ( + + + + )} + + {project.preview_url && ( + + + + )} +
+
+
+
+ ))} +
-
- + +
) } diff --git a/frontend/components/Projects.tsx b/frontend/components/Projects.tsx deleted file mode 100644 index 59dfa62..0000000 --- a/frontend/components/Projects.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { FadeContainer } from "../content/FramerMotionVariants" -import { HomeHeading } from "../pages" -import { motion } from "framer-motion" -import React from "react" -import { useEffect, useState } from 'react' -import { getAllProjects } from "@lib/backendAPI" -import AnimatedDiv from "@components/FramerMotion/AnimatedDiv" -import Project from "@components/ProjectSection" -import { ProjectType } from "@lib/types" - - -export default function ProjectSection() { - const [projects, setProjects] = useState([]) - - useEffect(() => { - fetchProjects() - }, []) - - const fetchProjects = async () => { - const projectsData = await getAllProjects() - setProjects(projectsData) - } - - // ******* Loader Starts ******* - if (projects.length === 0) { - return
Loading...
- } - // ******* Loader Ends ******* - - return ( -
- - - -
-

- I've been making various types of projects some of them were basics - and some of them were complicated. So far I've made{" "} - - {projects.length}+ - {" "} - projects. -

- - {projects.map((project: ProjectType) => { - if (project.name === "" && project.githubURL === "") return null; - return ; - })} - -
-
-
- ) -} diff --git a/frontend/components/ShareOnSocialMedia.tsx b/frontend/components/ShareOnSocialMedia.tsx index c2a65e9..c8a32a6 100644 --- a/frontend/components/ShareOnSocialMedia.tsx +++ b/frontend/components/ShareOnSocialMedia.tsx @@ -76,12 +76,12 @@ export default function ShareOnSocialMedia({ <>
-
+
-
+
@@ -91,16 +91,16 @@ export default function ShareOnSocialMedia({ url={url} source={url} > -
+
-
+
-
+
copyTextToClipboard(url)} @@ -112,7 +112,7 @@ export default function ShareOnSocialMedia({ {isShareSupported && (
diff --git a/frontend/components/SnippetCard.tsx b/frontend/components/SnippetCard.tsx index 7d86c83..1318805 100644 --- a/frontend/components/SnippetCard.tsx +++ b/frontend/components/SnippetCard.tsx @@ -1,26 +1,35 @@ -import { Snippet } from "@lib/types"; -import { snippetsImages } from "@utils/utils"; -import Image from "next/image"; -import Link from "next/link"; +import { CodeSnippetType } from '@lib/types' +import Image from 'next/image' +import Link from 'next/link' -export default function SnippetCard({ snippet }: { snippet: Snippet }) { +export default function SnippetCard({ code_snippet }: { code_snippet: CodeSnippetType }) { return (
- {snippet.image} + {code_snippet.title}
-

- {snippet.title} -

-

{snippet.excerpt}

+

{code_snippet.title}

+ {code_snippet.overview && ( +

{code_snippet.overview}

+ )} + {code_snippet.language && ( +
+ {code_snippet.language.split(',').map((code_snippet, index) => { + return ( + + {code_snippet.toLowerCase()} + + ) + })} +
+ )} - ); + ) } diff --git a/frontend/components/Support.tsx b/frontend/components/Support.tsx index 5ca65b2..1c998c9 100644 --- a/frontend/components/Support.tsx +++ b/frontend/components/Support.tsx @@ -85,7 +85,7 @@ function UPIPaymentForm({ close }: { close: () => void }) { const generatePaymentQR = (e: FormEvent) => { e.preventDefault(); setQrValue( - `upi://pay?pa=${process.env.NEXT_PUBLIC_UPI}&pn=Jatin%20Sharma&am=${amount}&purpose=nothing&cu=INR` + `upi://pay?pa=${process.env.NEXT_PUBLIC_UPI}&pn=Numan%20Ibn%20Mazid&am=${amount}&purpose=nothing&cu=INR` ); }; diff --git a/frontend/components/TableOfContents.tsx b/frontend/components/TableOfContents.tsx index 4288e55..d4f7162 100644 --- a/frontend/components/TableOfContents.tsx +++ b/frontend/components/TableOfContents.tsx @@ -1,13 +1,12 @@ -import useScrollPercentage from "@hooks/useScrollPercentage"; -import { lockScroll, removeScrollLock } from "@utils/functions"; -import { useEffect, useState } from "react"; -import AnimatedHeading from "./FramerMotion/AnimatedHeading"; -import { FadeContainer, opacityVariant } from "@content/FramerMotionVariants"; -import Link from "next/link"; -import { stringToSlug } from "@lib/toc"; -import useWindowSize from "@hooks/useWindowSize"; -import AnimatedDiv from "./FramerMotion/AnimatedDiv"; -import { CgSearch } from "react-icons/cg"; +import useScrollPercentage from "@hooks/useScrollPercentage" +import { lockScroll, removeScrollLock } from "@utils/functions" +import { useEffect, useState } from "react" +import AnimatedHeading from "./FramerMotion/AnimatedHeading" +import { FadeContainer, opacityVariant } from "@content/FramerMotionVariants" +import Link from "next/link" +import useWindowSize from "@hooks/useWindowSize" +import AnimatedDiv from "./FramerMotion/AnimatedDiv" +import { CgSearch } from "react-icons/cg" export default function TableOfContents({ tableOfContents, @@ -15,32 +14,32 @@ export default function TableOfContents({ isTOCActive, }: { tableOfContents: { - level: number; - heading: string; - }[]; - setIsTOCActive: (val: boolean) => void; - isTOCActive: boolean; + level: number + heading: string + }[] + setIsTOCActive: (val: boolean) => void + isTOCActive: boolean }) { - const [searchValue, setSearchValue] = useState(""); - const [toc, setToc] = useState(tableOfContents); + const [searchValue, setSearchValue] = useState("") + const [toc, setToc] = useState(tableOfContents) - const scrollPercentage = useScrollPercentage(); - const size = useWindowSize(); + const scrollPercentage = useScrollPercentage() + const size = useWindowSize() useEffect(() => { // In Case user exists from mobile to desktop then remove the scroll lock and TOC active to false if (size.width > 768) { - removeScrollLock(); - setIsTOCActive(false); + removeScrollLock() + setIsTOCActive(false) } - }, [size, setIsTOCActive]); + }, [size, setIsTOCActive]) useEffect(() => { setToc( tableOfContents.filter((table: any) => table.heading.toLowerCase().includes(searchValue.trim().toLowerCase()) ) - ); - }, [searchValue, tableOfContents, ]); + ) + }, [searchValue, tableOfContents, ]) return ( <> {tableOfContents.length > 0 && ( @@ -81,21 +80,21 @@ export default function TableOfContents({ return ( { if (size.width < 768) { lockScroll(); - setIsTOCActive(false); + setIsTOCActive(false) } - setIsTOCActive(false); - removeScrollLock(); + setIsTOCActive(false) + removeScrollLock() }} > {content.heading} - ); + ) })} {/* When you search but found nothing */} @@ -106,7 +105,7 @@ export default function TableOfContents({ +
+ Created at: + {getFormattedDate(new Date(blog.created_at))} +
+ + {getFormattedDate(new Date(blog.created_at)) !== getFormattedDate(new Date(blog.updated_at)) && ( +
+ Last Update: + {getFormattedDate(new Date(blog.updated_at))} +
+ )} + + {blog.category && ( +
+ Category: + {blog.category.name} +
+ )} + + {blog.reading_time && ( +
+ Reading Time: + {blog.reading_time} +
+ )} + + {blog.total_words && ( +
+ Total Words: + {blog.total_words} +
+ )} + + {blog.overview && ( +
+ Overview: + {blog.overview} +
+ )} + + {blog.tags && ( +
+ Tags: + {blog.tags.split(',').map((tag, index) => { + return ( + + {tag.toLowerCase()} + + ) + })} +
+ )}
+ {/* Horizontal Line */} +
+
+ Content +
+
+ + {/* Blog Content */} + - {children} +
+ + {/* NewsLetter */} + + {/* Social Media */}
-

+

Share on Social Media:

-
+
window.print()} />
- ); + ) } diff --git a/frontend/layout/SnippetLayout.tsx b/frontend/layout/SnippetLayout.tsx index 86f9aa7..0b3ef37 100644 --- a/frontend/layout/SnippetLayout.tsx +++ b/frontend/layout/SnippetLayout.tsx @@ -1,44 +1,105 @@ -import { opacityVariant } from "@content/FramerMotionVariants"; -import AnimatedDiv from "@components/FramerMotion/AnimatedDiv"; -import { PostType } from "@lib/types"; -import { snippetsImages } from "@utils/utils"; -import Image from "next/image"; +import { opacityVariant } from "@content/FramerMotionVariants" +import AnimatedDiv from "@components/FramerMotion/AnimatedDiv" +import { CodeSnippetType } from "@lib/types" +import Image from "next/image" +import cn from 'classnames' +import { useEffect } from 'react' +import { getFormattedDate } from "@utils/date" +import Prism from '../prismSetup' + + export default function SnippetLayout({ - snippet, - children, + code_snippet, }: { - snippet: PostType; - children: JSX.Element; + code_snippet: CodeSnippetType, + children: React.ReactNode }) { + + const hasCode = code_snippet && code_snippet.content.includes('') + + const injectStyle = () => { + if (hasCode) { + const style = document.createElement('style'); + style.innerHTML = ` + .text-code code { + color: #5292a1 + } + ` + document.head.appendChild(style) + } + } + + useEffect(() => { + injectStyle() + + // Prism JS + if (typeof window !== 'undefined') { + Prism.highlightAll() + Prism.plugins.lineNumbers = true + } + }, [hasCode]) + return (
-
+
-

- {snippet.meta.title} +

+ {code_snippet.title}

- -
+
{snippet.meta.title}
-

{snippet.meta.excerpt}

+ {code_snippet.overview && ( +

{code_snippet.overview}

+ )} + + {code_snippet.language && ( +
+ {code_snippet.language.split(',').map((code_snippet, index) => { + return ( + + {code_snippet.toLowerCase()} + + ) + })} +
+ )} + +
+ Created at: + {getFormattedDate(new Date(code_snippet.created_at))} +
+ + {getFormattedDate(new Date(code_snippet.created_at)) !== getFormattedDate(new Date(code_snippet.updated_at)) && ( +
+ Last Update: + {getFormattedDate(new Date(code_snippet.updated_at))} +
+ )} + {/* Content */} - {children} +
- ); + ) } diff --git a/frontend/lib/backendAPI.ts b/frontend/lib/backendAPI.ts index 965db17..995266e 100644 --- a/frontend/lib/backendAPI.ts +++ b/frontend/lib/backendAPI.ts @@ -1,209 +1,365 @@ + const BACKEND_API_BASE_URL = process.env.BACKEND_API_BASE_URL +const BACKEND_API_TOKEN = process.env.BACKEND_API_TOKEN -// *** SKILLS *** -// Skills URL -const SKILLS_PATH = "/todos?_limit=10" -const SKILLS_ENDPOINT = BACKEND_API_BASE_URL + SKILLS_PATH +// *** PROFILE *** +// Profile URL +const PROFILE_PATH = "users/get_portfolio_user/" +const PROFILE_ENDPOINT = BACKEND_API_BASE_URL + PROFILE_PATH /** - * Makes a request to the BACKEND API to retrieve all Skills Data. + * Makes a request to the BACKEND API to retrieve Portfolio User Information. */ -export const getAllSkills = async () => { - // Make a request to the DEV API to retrieve a specific page of posts - const allSkills = await fetch( - SKILLS_ENDPOINT - // { - // headers: { - // api_key: DEV_API!, - // }, - // } +export const getProfileInfo = async () => { + const portfolioProfile = await fetch( + PROFILE_ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } ) - .then((response) => response.json()) - .catch((error) => console.log('Error fetching skills:', error)) - - // TODO:Integrate with backend API - // ******* Faking data Starts ******* - const fakeSkillsData = allSkills.map((_skill: { title: string }, index: number) => ({ - name: `Python ${index + 1}`, - icon: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQYY0pvHu6oaaJRADcCoacoP5BKwJN0i1nqFNCnmKvN&s" - })) - // Need to return `fakeSkillsData` - // ******* Faking data Ends ******* - - return fakeSkillsData + + if (portfolioProfile.ok) { + const responseData = await portfolioProfile.json() + return responseData.data + } else { + const errorMessage = `Error fetching portfolio profile: ${portfolioProfile.status} ${portfolioProfile.statusText}` + // Handle the error or display the error message + console.log(errorMessage) + } } +// *** EXPERIENCE *** -// *** BLOGS *** +// Experience URL -// Blogs URL -const BLOGS_PATH = "/posts" -const BLOGS_ENDPOINT = BACKEND_API_BASE_URL + BLOGS_PATH +const EXPERIENCE_PATH = "professional-experiences/" +const EXPERIENCE_ENDPOINT = BACKEND_API_BASE_URL + EXPERIENCE_PATH /** - * Makes a request to the BACKEND API to retrieve all Blogs Data. + * Makes a request to the BACKEND API to retrieve all Experience Data. */ -export const getAllBlogs = async (length?: number | undefined) => { +export const getAllExperiences = async (length?: number | undefined) => { let ENDPOINT = null // Set limit if length is not undefined if (length !== undefined) { - ENDPOINT = BLOGS_ENDPOINT + `?_limit=${length}` + ENDPOINT = EXPERIENCE_ENDPOINT + `?_limit=${length}` } else { - ENDPOINT = BLOGS_ENDPOINT + ENDPOINT = EXPERIENCE_ENDPOINT } - const allBlogs = await fetch( - ENDPOINT + const allExperiences = await fetch( + ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } ) - .then((response) => response.json()) - .catch((error) => console.log('Error fetching blogs:', error)) - - // TODO:Integrate with backend API - // ******* Faking data Starts ******* - const fakeBlogsData = allBlogs.map((blog: { title: any, body: any }, index: number) => ({ - title: blog.title, - slug: `blog-${index + 1}`, - url: "https://github.com/NumanIbnMazid", - image: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQYY0pvHu6oaaJRADcCoacoP5BKwJN0i1nqFNCnmKvN&s", - excerpt: blog.body, - date: new Date(), - readingTime: {text: "3 min read"}, - })) - // Need to return `allBlogs` - // ******* Faking data Ends ******* - - return fakeBlogsData + + if (allExperiences.ok) { + const responseData = await allExperiences.json() + return responseData.data + } else { + const errorMessage = `Error fetching professional experiences: ${allExperiences.status} ${allExperiences.statusText}` + // Handle the error or display the error message + console.log(errorMessage) + } } -// *** EXPERIENCE *** +// *** SKILLS *** -// Experience URL -const EXPERIENCE_PATH = "/posts?_limit=5" -const EXPERIENCE_ENDPOINT = BACKEND_API_BASE_URL + EXPERIENCE_PATH +// Skills URL +const SKILLS_PATH = "skills/" +const SKILLS_ENDPOINT = BACKEND_API_BASE_URL + SKILLS_PATH /** - * Makes a request to the BACKEND API to retrieve all Experience Data. + * Makes a request to the BACKEND API to retrieve all Skills Data. */ -export const getAllExperiences = async () => { +export const getAllSkills = async () => { + const allSkills = await fetch( + SKILLS_ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } + ) - const allExperiences = await fetch( - EXPERIENCE_ENDPOINT + if (allSkills.ok) { + const responseData = await allSkills.json() + return responseData.data + } else { + const errorMessage = `Error fetching Skills: ${allSkills.status} ${allSkills.statusText}` + console.log(errorMessage) + } +} + +// *** EDUCATIONS *** + +// Educations URL +const EDUCATIONS_PATH = "educations/" +const EDUCATIONS_ENDPOINT = BACKEND_API_BASE_URL + EDUCATIONS_PATH + +/** + * Makes a request to the BACKEND API to retrieve all Educations Data. + */ +export const getAllEducations = async () => { + const allEducations = await fetch( + EDUCATIONS_ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } + ) + + if (allEducations.ok) { + const responseData = await allEducations.json() + return responseData.data + } else { + const errorMessage = `Error fetching Educations: ${allEducations.status} ${allEducations.statusText}` + console.log(errorMessage) + } +} + +// *** CERTIFICATES *** + +// Certificates URL +const CERTIFICATES_PATH = "certifications/" +const CERTIFICATES_ENDPOINT = BACKEND_API_BASE_URL + CERTIFICATES_PATH + +/** + * Makes a request to the BACKEND API to retrieve all Certificates Data. + */ +export const getAllCertificates = async () => { + const allCertificates = await fetch( + CERTIFICATES_ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } ) - .then((response) => response.json()) - .catch((error) => console.log('Error fetching experiences:', error)) - - // TODO:Integrate with backend API - // ******* Faking data Starts ******* - const fakeExperiencesData = allExperiences.map((experience: { title: any, body: any }) => ({ - title: "Software Engineer", - company: experience.title.split(' ').slice(0, 3).join(' ').toUpperCase(), - company_url: "https://github.com/NumanIbnMazid", - duration: "2018 - 2019", - description: experience.body - })) - // Need to return `allExperiences` - // ******* Faking data Ends ******* - - return fakeExperiencesData + + if (allCertificates.ok) { + const responseData = await allCertificates.json() + return responseData.data + } else { + const errorMessage = `Error fetching Educations: ${allCertificates.status} ${allCertificates.statusText}` + console.log(errorMessage) + } } // *** PROJECTS *** -// Certificate URL -const PROJECTS_PATH = "/posts?_limit=5" +// Projects URL +const PROJECTS_PATH = "projects/" const PROJECTS_ENDPOINT = BACKEND_API_BASE_URL + PROJECTS_PATH /** - * Makes a request to the BACKEND API to retrieve all Certificate Data. + * Makes a request to the BACKEND API to retrieve all Projects Data. */ export const getAllProjects = async () => { - const allProjects = await fetch( - PROJECTS_ENDPOINT + PROJECTS_ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } + ) + + if (allProjects.ok) { + const responseData = await allProjects.json() + return responseData.data + } else { + const errorMessage = `Error fetching Projects: ${allProjects.status} ${allProjects.statusText}` + console.log(errorMessage) + } +} + +export const getProjectDetails = async (slug: string) => { + const projectDetails = await fetch( + PROJECTS_ENDPOINT + slug, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } ) - .then((response) => response.json()) - .catch((error) => console.log('Error fetching Projects:', error)) - - // TODO:Integrate with backend API - // ******* Faking data Starts ******* - const fakeProjectsData = allProjects.map((project: { title: any, body: any }, index: number) => ({ - id: index, - name: project.title.split(' ').slice(0, 3).join(' ').toUpperCase(), - description: project.body, - coverImage: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQYY0pvHu6oaaJRADcCoacoP5BKwJN0i1nqFNCnmKvN&s", - tools: ["Python", "Django", "JavaScript", "React", "Redux", "Node.js", "Express.js", "MongoDB", "PostgreSQL", "Docker", "AWS"], - githubURL: "https://github.com/NumanIbnMazid", - previewURL: "https://github.com/NumanIbnMazid" - })) - // Need to return `allExperiences` - // ******* Faking data Ends ******* - - return fakeProjectsData + + if (projectDetails.ok) { + const responseData = await projectDetails.json() + return responseData.data + } else { + const errorMessage = `Error fetching Project Details: ${projectDetails.status} ${projectDetails.statusText}` + console.log(errorMessage) + } } -// *** CERTIFICATES *** -// Certificate URL -const CERTIFICATES_PATH = "/posts?_limit=5" -const CERTIFICATES_ENDPOINT = BACKEND_API_BASE_URL + CERTIFICATES_PATH +// *** INTERESTS *** + +// Interests URL +const INTERESTS_PATH = "interests/" +const INTERESTS_ENDPOINT = BACKEND_API_BASE_URL + INTERESTS_PATH /** - * Makes a request to the BACKEND API to retrieve all Certificate Data. + * Makes a request to the BACKEND API to retrieve all Interests Data. */ -export const getAllCertificates = async () => { - - const allCertificates = await fetch( - CERTIFICATES_ENDPOINT +export const getAllInterests = async () => { + const allInterests = await fetch( + INTERESTS_ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } ) - .then((response) => response.json()) - .catch((error) => console.log('Error fetching Certificates:', error)) - - // TODO:Integrate with backend API - // ******* Faking data Starts ******* - const fakeCertificatesData = allCertificates.map((certificate: { title: any, body: any }, index: number) => ({ - id: index, - title: certificate.title.split(' ').slice(0, 3).join(' ').toUpperCase(), - orgName: "Hackerrank", - orgLogo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQYY0pvHu6oaaJRADcCoacoP5BKwJN0i1nqFNCnmKvN&s", - issuedDate: new Date(), - url: "https://github.com/NumanIbnMazid" - })) - // Need to return `allExperiences` - // ******* Faking data Ends ******* - - return fakeCertificatesData + + if (allInterests.ok) { + const responseData = await allInterests.json() + return responseData.data + } else { + const errorMessage = `Error fetching Interests: ${allInterests.status} ${allInterests.statusText}` + console.log(errorMessage) + } } // *** MOVIES *** -// Experience URL -const MOVIE_PATH = "/posts?_limit=5" +// Movies URL +const MOVIE_PATH = "movies/" const MOVIE_ENDPOINT = BACKEND_API_BASE_URL + MOVIE_PATH /** - * Makes a request to the BACKEND API to retrieve all Movie Data. + * Makes a request to the BACKEND API to retrieve all Movies Data. */ export const getAllMovies = async () => { - const allMovies = await fetch( - MOVIE_ENDPOINT + MOVIE_ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } ) - .then((response) => response.json()) - .catch((error) => console.log('Error fetching Movies:', error)) - - // ******* Faking data Starts ******* - const fakeMoviesData = allMovies.map((movie: { title: any, body: any }, index: number) => ({ - id: index, - url: "https://github.com/NumanIbnMazid", - name: movie.title.split(' ').slice(0, 3).join(' ').toUpperCase(), - image: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQYY0pvHu6oaaJRADcCoacoP5BKwJN0i1nqFNCnmKvN&s", - watched: false, - rating: 4 - })) - // Need to return `allMovies` - // ******* Faking data Ends ******* - return fakeMoviesData + + if (allMovies.ok) { + const responseData = await allMovies.json() + return responseData.data + } else { + const errorMessage = `Error fetching Movies: ${allMovies.status} ${allMovies.statusText}` + console.log(errorMessage) + } +} + + +// *** CODE_SNIPPETS *** + +// Code Snippets URL +const CODE_SNIPPETS_PATH = "code-snippets/" +const CODE_SNIPPETS_ENDPOINT = BACKEND_API_BASE_URL + CODE_SNIPPETS_PATH + +/** + * Makes a request to the BACKEND API to retrieve all Code Snippets Data. + */ +export const getAllCodeSnippets = async () => { + const allCodeSnippets = await fetch( + CODE_SNIPPETS_ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } + ) + + if (allCodeSnippets.ok) { + const responseData = await allCodeSnippets.json() + return responseData.data + } else { + const errorMessage = `Error fetching Code Snippets: ${allCodeSnippets.status} ${allCodeSnippets.statusText}` + console.log(errorMessage) + } +} + +export const getCodeSnippetDetails = async (slug: string) => { + const codeSnippetDetails = await fetch( + CODE_SNIPPETS_ENDPOINT + slug, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } + ) + + if (codeSnippetDetails.ok) { + const responseData = await codeSnippetDetails.json() + return responseData.data + } else { + const errorMessage = `Error fetching Code Snippet Details: ${codeSnippetDetails.status} ${codeSnippetDetails.statusText}` + console.log(errorMessage) + } +} + +// *** BLOGS *** + +// Blogs URL +const BLOGS_PATH = "blogs/" +const BLOGS_ENDPOINT = BACKEND_API_BASE_URL + BLOGS_PATH + +/** + * Makes a request to the BACKEND API to retrieve all Blogs Data. + */ +export const getAllBlogs = async (length?: number | undefined) => { + let ENDPOINT = null + // Set limit if length is not undefined + if (length !== undefined) { + ENDPOINT = BLOGS_ENDPOINT + `?_limit=${length}` + } + else { + ENDPOINT = BLOGS_ENDPOINT + } + console.log(ENDPOINT); + + const allBlogs = await fetch( + ENDPOINT, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } + ) + + if (allBlogs.ok) { + const responseData = await allBlogs.json() + return responseData.data + } else { + const errorMessage = `Error fetching Code Blogs: ${allBlogs.status} ${allBlogs.statusText}` + console.log(errorMessage) + } +} + +export const getBlogDetails = async (slug: string) => { + const blogDetails = await fetch( + BLOGS_ENDPOINT + slug, + { + headers: { + Authorization: `Token ${BACKEND_API_TOKEN}` + } + } + ) + + if (blogDetails.ok) { + const responseData = await blogDetails.json() + return responseData.data + } else { + const errorMessage = `Error fetching Blog Details: ${blogDetails.status} ${blogDetails.statusText}` + console.log(errorMessage) + } } diff --git a/frontend/lib/generateRSS.ts b/frontend/lib/generateRSS.ts index 5ac9bcf..c849745 100644 --- a/frontend/lib/generateRSS.ts +++ b/frontend/lib/generateRSS.ts @@ -8,7 +8,7 @@ export default async function getRSS() { // Create a new RSS object const feed = new RSS({ - title: "Jatin Sharma", + title: "Numan Ibn Mazid", description: `I've been writing online since 2021, mostly about web development and tech careers. In total, I've written ${allBlogs.length} articles till now.`, @@ -16,7 +16,7 @@ export default async function getRSS() { feed_url: `${siteURL}/feed.xml`, language: "en", pubDate: new Date(), - copyright: `All rights reserved ${new Date().getFullYear()}, Jatin Sharma`, + copyright: `All rights reserved ${new Date().getFullYear()}, Numan Ibn Mazid`, }); // Add all blog posts to the RSS feed diff --git a/frontend/lib/github.ts b/frontend/lib/github.ts index e3b8348..55fd44e 100644 --- a/frontend/lib/github.ts +++ b/frontend/lib/github.ts @@ -1,59 +1,74 @@ -import { GithubRepo } from "./types"; +import { GithubRepo } from "./types" + +const GitHubAccessToken = process.env.GITHUB_ACCESS_TOKEN const tempData = { - login: "j471n", - id: 55713505, - node_id: "MDQ6VXNlcjU1NzEzNTA1", - avatar_url: "https://avatars.githubusercontent.com/u/55713505?v=4", - gravatar_id: "", - url: "https://api.github.com/users/j471n", - html_url: "https://github.com/j471n", - followers_url: "https://api.github.com/users/j471n/followers", - following_url: "https://api.github.com/users/j471n/following{/other_user}", - gists_url: "https://api.github.com/users/j471n/gists{/gist_id}", - starred_url: "https://api.github.com/users/j471n/starred{/owner}{/repo}", - subscriptions_url: "https://api.github.com/users/j471n/subscriptions", - organizations_url: "https://api.github.com/users/j471n/orgs", - repos_url: "https://api.github.com/users/j471n/repos", - events_url: "https://api.github.com/users/j471n/events{/privacy}", - received_events_url: "https://api.github.com/users/j471n/received_events", - type: "User", - site_admin: false, - name: "Jatin Sharma", - company: null, - blog: "j471n.in", - location: "India", - email: null, - hireable: true, - bio: "React Developer", - twitter_username: "j471n_", - public_repos: 31, - public_gists: 10, - followers: 8, - following: 1, - created_at: "2019-09-23T18:37:14Z", - updated_at: "2022-07-02T03:07:58Z", -}; + "login": "NumanIbnMazid", + "id": 38869177, + "node_id": "MDQ6VXNlcjM4ODY5MTc3", + "avatar_url": "https://avatars.githubusercontent.com/u/38869177?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/NumanIbnMazid", + "html_url": "https://github.com/NumanIbnMazid", + "followers_url": "https://api.github.com/users/NumanIbnMazid/followers", + "following_url": "https://api.github.com/users/NumanIbnMazid/following{/other_user}", + "gists_url": "https://api.github.com/users/NumanIbnMazid/gists{/gist_id}", + "starred_url": "https://api.github.com/users/NumanIbnMazid/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/NumanIbnMazid/subscriptions", + "organizations_url": "https://api.github.com/users/NumanIbnMazid/orgs", + "repos_url": "https://api.github.com/users/NumanIbnMazid/repos", + "events_url": "https://api.github.com/users/NumanIbnMazid/events{/privacy}", + "received_events_url": "https://api.github.com/users/NumanIbnMazid/received_events", + "type": "User", + "site_admin": false, + "name": "Numan Ibn Mazid", + "company": "SELISE DIGITAL PLATFORMS", + "blog": "https://www.linkedin.com/in/numanibnmazid/", + "location": "Dhaka, Bangladesh", + "email": "numanibnmazid@gmail.com", + "hireable": true, + "bio": "Experienced professional Software Engineer who enjoys developing innovative software solutions that are tailored to customer desirability and usability.", + "twitter_username": "NumanIbnMazid", + "public_repos": 84, + "public_gists": 0, + "followers": 13, + "following": 35, + "created_at": "2018-04-30T21:30:32Z", + "updated_at": "2023-06-24T11:38:55Z" +} // its for /api/stats/github export async function fetchGithub() { - const fake = false; - if (fake) return tempData; - return fetch("https://api.github.com/users/j471n").then((res) => res.json()); + const fake = false + if (fake) return tempData + + return fetch( + "https://api.github.com/users/NumanIbnMazid", + { + headers: { + Authorization: `Bearer ${GitHubAccessToken}`, + }, + } + ).then((res) => res.json()) } // its for getting temporary old data export function getOldStats() { - return tempData; + return tempData } /* Retrieves the number of stars and forks for the user's repositories on GitHub. */ export async function getGithubStarsAndForks() { // Fetch user's repositories from the GitHub API const res = await fetch( - "https://api.github.com/users/j471n/repos?per_page=100" - ); - const userRepos = await res.json(); + "https://api.github.com/users/NumanIbnMazid/repos?per_page=100", + { + headers: { + Authorization: `Bearer ${GitHubAccessToken}`, + }, + } + ) + const userRepos = await res.json() /* Default Static Data: If use exceeded the rate limit of api */ if ( @@ -61,30 +76,30 @@ export async function getGithubStarsAndForks() { "https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting") ) { return { - githubStars: 74, - forks: 33, - }; + githubStars: 7, + forks: 4, + } } // filter those repos that are forked const mineRepos: GithubRepo[] = userRepos?.filter( (repo: GithubRepo) => !repo.fork - ); + ) // Calculate the total number of stars for the user's repositories const githubStars = mineRepos.reduce( (accumulator: number, repository: GithubRepo) => { - return accumulator + repository["stargazers_count"]; + return accumulator + repository["stargazers_count"] }, 0 - ); + ) // Calculate the total number of forks for the user's repositories const forks = mineRepos.reduce( (accumulator: number, repository: GithubRepo) => { - return accumulator + repository["forks_count"]; + return accumulator + repository["forks_count"] }, 0 - ); + ) - return { githubStars, forks }; + return { githubStars, forks } } diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index 9e624cb..8aeb3b5 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -9,13 +9,42 @@ import { ReadTimeResults } from "reading-time" export type PersonalStaticData = { name: string, profession: string, - current_position: string + current_position: string, + about: string } export type StaticData = { personal: PersonalStaticData } +/* Profile Types */ +export type ProfileType = { + id: number + username: string + email: string + name: string + slug: string + nickname: string + gender: string + image: string + dob: string + website: string + contact: string + contact_email: string + linkedin: string + github: string + address: string + about: string + is_portfolio_user: string + resume_link: string + is_active: string + is_staff: string + is_superuser: string + date_joined: string + last_login: string + updated_at: string +} + /* Custom Animated Components types */ export type AnimatedTAGProps = { variants: Variants @@ -46,38 +75,105 @@ export type SpotifyArtist = { popularity: number } +export type MediaType = { + id: number + title: string + slug: string + file: string + description: string + created_at: string + updated_at: string +} + export type ProjectType = { id: string - name: string - coverImage: string - description: string - githubURL: string - previewURL?: string - tools?: string[] - pinned?: boolean + title: string + slug: string + image: string + short_description: string + technology?: string + duration: string + duration_in_days?: string + preview_url?: string + github_url?: string + description?: string + project_media?: MediaType[] + created_at: string + updated_at: string } export type ExperienceType = { - title: string + id: number + slug: string company: string + company_image: string company_url: string + address: string + designation: string + job_type: string + start_date: string + end_date: string duration: string + duration_in_days: string + currently_working: string description: string + created_at: string + updated_at: string } export type SkillType = { - name: string - icon: string + id: number + slug: string + title: string + image: string + level: string + order: number + created_at: string + updated_at: string +} + +export type EducationType = { + id: number + slug: string + school: string + image?: string + degree: string + address?: string + field_of_study?: string + duration: string + grade?: string + activities?: string + description?: string + education_media?: MediaType[] + created_at: string + updated_at: string } export type CertificateType = { id: string title: string - issuedDate: string - orgName: string - orgLogo: string - url: string - pinned: boolean + slug: string + organization: string + address?: string + image: string + issue_date: string + expiration_date?: string + credential_id?: string + credential_url?: string + description?: string + certification_media?: MediaType[] + created_at: string + updated_at: string +} + +export type InterestType = { + id: number + slug: string + title: string + icon: string + order: number + created_at: string + updated_at: string } export type SocialPlatform = { @@ -103,6 +199,51 @@ export type Utilities = { data: UtilityType[] } +export type CodeSnippetType = { + slug: string + title: string + overview?: string + image: string + language?: string + content: string + order: number + created_at: string + updated_at: string +} + +export type BlogCategoryType = { + id: number + name: string + slug: string + created_at: string + updated_at: string +} + +export type TableOfContents = { + id: string + level: number + heading: string +} + +export type BlogType = { + id: number + slug: string + title: string + category?: BlogCategoryType + image: string + overview?: string + content: string + author: string + tags?: string + status: string + reading_time?: string + total_words?: number + order: number + table_of_contents: TableOfContents[] + created_at: string + updated_at: string +} + export type FrontMatter = { slug: string readingTime: ReadTimeResults @@ -120,11 +261,6 @@ export type PostType = { tableOfContents: TableOfContents[] } -export type TableOfContents = { - level: number - heading: string -} - export type SupportMe = { name: string url: string @@ -179,20 +315,15 @@ export type PageMeta = { snippets: PageData } -export type Snippet = { - slug: string - title: string - date: string - excerpt: string - image: string -} - export type MovieType = { id: number + slug: string name: string image: string url: string year: number watched: boolean rating: number + created_at: string + updated_at: string } diff --git a/frontend/next.config.js b/frontend/next.config.js index 8c1ca8d..9ae89d4 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -32,6 +32,11 @@ module.exports = withPWA({ ignoreBuildErrors: false, }, env:{ - BACKEND_API_BASE_URL : process.env.BACKEND_API_BASE_URL + BACKEND_API_BASE_URL : process.env.BACKEND_API_BASE_URL, + BACKEND_API_TOKEN : process.env.BACKEND_API_TOKEN, + GITHUB_ACCESS_TOKEN : process.env.GITHUB_ACCESS_TOKEN, + EMAIL_JS_SERVICE_ID : process.env.EMAIL_JS_SERVICE_ID, + EMAIL_JS_TEMPLATE_ID : process.env.EMAIL_JS_TEMPLATE_ID, + EMAIL_JS_PUBLIC_KEY : process.env.EMAIL_JS_PUBLIC_KEY, } }); diff --git a/frontend/package-local.json b/frontend/package-local.json new file mode 100644 index 0000000..f24d8f4 --- /dev/null +++ b/frontend/package-local.json @@ -0,0 +1,68 @@ +{ + "name": "portfolio-next", + "homepage": "http://nim23.com", + "version": "0.1.0", + "private": true, + "license": "MIT", + "scripts": { + "dev": "next dev", + "tsc": "tsc", + "build": "next build", + "start": "next start", + "tsc-watch": "tsc --watch", + "lint": "next lint", + "find:unused": "next-unused" + }, + "dependencies": { + "@emailjs/browser": "^3.6.2", + "@google-analytics/data": "^3.1.2", + "@tailwindcss/line-clamp": "^0.4.0", + "@types/react-modal": "^3.16.0", + "framer-motion": "^6.3.3", + "globby": "^13.1.1", + "gray-matter": "^4.0.3", + "next": "^13.1.6", + "next-mdx-remote": "^4.2.0", + "next-pwa": "^5.6.0", + "nextjs-google-analytics": "^1.2.0", + "nprogress": "^0.2.0", + "prismjs": "^1.29.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-icons": "^4.3.1", + "react-modal": "^3.16.1", + "react-pdf": "^7.1.2", + "react-qr-code": "^2.0.7", + "react-ripples": "^2.2.1", + "react-share": "^4.4.0", + "react-toastify": "^9.0.1", + "react-toggle-dark-mode": "^1.1.1", + "reading-time": "^1.5.0", + "rehype-autolink-headings": "^6.1.1", + "rehype-pretty-code": "^0.3.1", + "rehype-slug": "^5.0.1", + "rss": "^1.2.2", + "sharp": "^0.32.1", + "shiki": "^0.10.1", + "swr": "^1.3.0" + }, + "devDependencies": { + "@supabase/supabase-js": "^2.2.3", + "@tailwindcss/typography": "^0.5.8", + "@types/next-pwa": "^5.6.0", + "@types/nprogress": "^0.2.0", + "@types/rss": "^0.0.29", + "@typescript-eslint/eslint-plugin": "^5.59.7", + "autoprefixer": "^10.4.13", + "eslint": "8.14.0", + "eslint-config-next": "^13.1.6", + "next-unused": "^0.0.6", + "postcss": "^8.4.20", + "tailwindcss": "^3.2.4", + "typescript": "^4.9.4" + }, + "resolutions": { + "@mdx-js/mdx": "2.1.5", + "@mdx-js/react": "2.1.5" + } +} diff --git a/frontend/package.json b/frontend/package.json index 15a6eda..87f8e3d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,6 @@ { "name": "portfolio-next", + "homepage": "http://nim23.com", "version": "0.1.0", "private": true, "license": "MIT", @@ -7,7 +8,7 @@ "dev": "next dev", "tsc": "tsc", "build": "next build", - "start": "next start", + "start": "NODE_ENV=production node server.js", "tsc-watch": "tsc --watch", "lint": "next lint", "find:unused": "next-unused" @@ -16,6 +17,7 @@ "@emailjs/browser": "^3.6.2", "@google-analytics/data": "^3.1.2", "@tailwindcss/line-clamp": "^0.4.0", + "@types/react-modal": "^3.16.0", "framer-motion": "^6.3.3", "globby": "^13.1.1", "gray-matter": "^4.0.3", @@ -24,9 +26,12 @@ "next-pwa": "^5.6.0", "nextjs-google-analytics": "^1.2.0", "nprogress": "^0.2.0", + "prismjs": "^1.29.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.3.1", + "react-modal": "^3.16.1", + "react-pdf": "^7.1.2", "react-qr-code": "^2.0.7", "react-ripples": "^2.2.1", "react-share": "^4.4.0", @@ -37,6 +42,7 @@ "rehype-pretty-code": "^0.3.1", "rehype-slug": "^5.0.1", "rss": "^1.2.2", + "sharp": "^0.32.1", "shiki": "^0.10.1", "swr": "^1.3.0" }, diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index 99dbeef..18a26fa 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -1,38 +1,38 @@ -import "@styles/globals.css"; -import Layout from "@layout/Layout"; -import { useEffect } from "react"; -import { useRouter } from "next/router"; -import NProgress from "nprogress"; -import "nprogress/nprogress.css"; -import { DarkModeProvider } from "@context/darkModeContext"; -import { GoogleAnalytics } from "nextjs-google-analytics"; -import { AppProps } from "next/app"; +import "@styles/globals.css" +import Layout from "@layout/Layout" +import { useEffect } from "react" +import { useRouter } from "next/router" +import NProgress from "nprogress" +import "nprogress/nprogress.css" +import { DarkModeProvider } from "@context/darkModeContext" +import { GoogleAnalytics } from "nextjs-google-analytics" +import { AppProps } from "next/app" NProgress.configure({ easing: "ease", speed: 800, showSpinner: false, -}); +}) function MyApp({ Component, pageProps }: AppProps) { - const router = useRouter(); + const router = useRouter() useEffect(() => { const start = () => { - NProgress.start(); + NProgress.start() }; const end = () => { - NProgress.done(); + NProgress.done() }; - router.events.on("routeChangeStart", start); - router.events.on("routeChangeComplete", end); - router.events.on("routeChangeError", end); + router.events.on("routeChangeStart", start) + router.events.on("routeChangeComplete", end) + router.events.on("routeChangeError", end) return () => { - router.events.off("routeChangeStart", start); - router.events.off("routeChangeComplete", end); - router.events.off("routeChangeError", end); - }; - }, [router.events]); + router.events.off("routeChangeStart", start) + router.events.off("routeChangeComplete", end) + router.events.off("routeChangeError", end) + } + }, [router.events]) return ( @@ -43,7 +43,7 @@ function MyApp({ Component, pageProps }: AppProps) { - ); + ) } -export default MyApp; +export default MyApp diff --git a/frontend/pages/about.tsx b/frontend/pages/about.tsx index b72cdc4..162f9ae 100644 --- a/frontend/pages/about.tsx +++ b/frontend/pages/about.tsx @@ -1,8 +1,8 @@ import MDXContent from "@lib/MDXContent" import pageMeta from "@content/meta" -import { MovieType, PostType } from "@lib/types" +import { MovieType, PostType, ExperienceType, EducationType, ProjectType } from "@lib/types" import StaticPage from "@components/StaticPage" -import { getAllMovies } from "@lib/backendAPI" +import { getAllExperiences, getAllEducations, getAllProjects, getAllMovies } from "@lib/backendAPI" import { useEffect, useState } from 'react' import MovieCard from "@components/MovieCard" import { motion } from "framer-motion" @@ -10,8 +10,10 @@ import { FadeContainer, opacityVariant } from "@content/FramerMotionVariants" import AnimatedDiv from "@components/FramerMotion/AnimatedDiv" import SkillSection from "@components/Home/SkillSection" import ExperienceSection from "@components/Home/ExperienceSection" +import Education from "@components/Education" import Certificates from "@components/Certificates" -import Projects from "@components/Projects" +import ProjectSection from "@components/ProjectSection" +import InterestSection from "@components/Interest" export default function About({ @@ -21,17 +23,38 @@ export default function About({ movies: MovieType[] }) { + const [experiences, setExperiences] = useState([]) + const [educations, setEducations] = useState([]) + const [projects, setProjects] = useState([]) const [movies, setMovies] = useState([]) - useEffect(() => { - fetchMovies() - }, []) + const fetchExperiences = async () => { + const experiencesData: ExperienceType[] = await getAllExperiences() + setExperiences(experiencesData) + } + + const fetchEducations = async () => { + const educationsData: EducationType[] = await getAllEducations() + setEducations(educationsData) + } + + const fetchProjects = async () => { + const projectsData: ProjectType[] = await getAllProjects() + setProjects(projectsData) + } const fetchMovies = async () => { const moviesData = await getAllMovies() setMovies(moviesData) } + useEffect(() => { + fetchExperiences() + fetchEducations() + fetchProjects() + fetchMovies() + }, []) + // ******* Loader Starts ******* if (movies.length === 0) { return
Loading...
@@ -48,13 +71,15 @@ export default function About({ whileInView="visible" variants={FadeContainer} viewport={{ once: true }} - className="grid min-h-screen py-20 place-content-center" + className="grid min-h-screen py-7 place-content-center" >
- + - + + +
diff --git a/frontend/pages/blank.tsx b/frontend/pages/blank.tsx new file mode 100644 index 0000000..aa37942 --- /dev/null +++ b/frontend/pages/blank.tsx @@ -0,0 +1,44 @@ +import { motion } from 'framer-motion' +import { FadeContainer } from '../content/FramerMotionVariants' +import { HomeHeading } from '.' +import React from 'react' +import AnimatedDiv from '@components/FramerMotion/AnimatedDiv' +import { opacityVariant } from '@content/FramerMotionVariants' + +export default function ProjectDetailsSection() { + const item = 123; + + return ( + <> + {item && ( +
+ +
+ + + + + + {/* place content here */} + + + +
+
+
+ )} + + ) +} diff --git a/frontend/pages/blogs/[slug].tsx b/frontend/pages/blogs/[slug].tsx index e0d30dd..077384b 100644 --- a/frontend/pages/blogs/[slug].tsx +++ b/frontend/pages/blogs/[slug].tsx @@ -1,44 +1,66 @@ -import { useEffect } from "react"; -import BlogLayout from "@layout/BlogLayout"; -import Metadata from "@components/MetaData"; -import MDXComponents from "@components/MDXComponents"; -import PageNotFound from "pages/404"; -import MDXContent from "@lib/MDXContent"; -import { MDXRemote } from "next-mdx-remote"; -import { GetStaticPropsContext } from "next"; -import { PostType } from "@lib/types"; +import BlogLayout from "@layout/BlogLayout" +import PageNotFound from "pages/404" +import { ProfileType, BlogType } from "@lib/types" +import { getBlogDetails, getProfileInfo } from "@lib/backendAPI" +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' + export default function Post({ - post, error, }: { - post: PostType; error: boolean; }) { - // Adding Views to the supabase database - useEffect(() => { - const registerView = () => - fetch(`/api/views/${post.meta.slug}`, { - method: "POST", - }); + if (error) return + + const router = useRouter() + const { slug } = router.query // Retrieve the slug parameter from the URL + + const [blog, setBlog] = useState() + + const [profileInfo, setProfileInfo] = useState() - post != null && registerView(); - }, [post]); + const fetchProfileInfo = async () => { + const profileData: ProfileType = await getProfileInfo() + setProfileInfo(profileData) + } - if (error) return ; + const fetchBlogDetail = async (slug: string) => { + try { + const blogData: BlogType = await getBlogDetails(slug) + setBlog(blogData) + } catch (error) { + // Handle error case + console.error(error) + } + } + + // Add this useEffect to trigger the API request when slug is available + useEffect(() => { + if (typeof slug === 'string') { + fetchProfileInfo() + fetchBlogDetail(slug) + } + }, [slug]) return ( <> - + /> */} + + {blog && profileInfo ? ( + + ) : ( +

Loading...

+ )} - - - - - ); -} - -type StaticProps = GetStaticPropsContext & { - params: { - slug: string; - }; -}; - -export async function getStaticProps({ params }: StaticProps) { - const { slug } = params; - const { post } = await new MDXContent("posts").getPostFromSlug(slug); + /> */} - if (post != null) { - return { - props: { - error: false, - post, - }, - }; - } else { - return { - props: { - error: true, - post: null, - }, - }; - } -} - -export async function getStaticPaths() { - const paths = new MDXContent("posts") - .getSlugs() - .map((slug) => ({ params: { slug } })); - - return { - paths, - fallback: false, - }; + + ) } diff --git a/frontend/pages/blogs/bookmark.tsx b/frontend/pages/blogs/bookmark.tsx deleted file mode 100644 index 4a48c9d..0000000 --- a/frontend/pages/blogs/bookmark.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { AnimatePresence } from "framer-motion"; -import { FadeContainer } from "@content/FramerMotionVariants"; -import Blog from "@components/Blog"; -import Metadata from "@components/MetaData"; -import AnimatedDiv from "@components/FramerMotion/AnimatedDiv"; -import PageTop from "@components/PageTop"; -import useBookmarkBlogs from "@hooks/useBookmarkBlogs"; -import pageMeta from "@content/meta"; - -export default function Blogs() { - const { bookmarkedBlogs } = useBookmarkBlogs("blogs", []); - - return ( - <> - - -
- - Here you can find article bookmarked by you for Later use. - - -
- - {bookmarkedBlogs?.length != 0 ? ( - - {bookmarkedBlogs?.map((blog, index) => { - return ; - })} - - ) : ( -
- Nothing to see here. -
- )} -
-
-
- - ); -} diff --git a/frontend/pages/blogs/index.tsx b/frontend/pages/blogs/index.tsx index 18fe8a6..56665e4 100644 --- a/frontend/pages/blogs/index.tsx +++ b/frontend/pages/blogs/index.tsx @@ -2,20 +2,16 @@ import React, { useState, useEffect, useRef } from "react" import { AnimatePresence, motion } from "framer-motion" import { FadeContainer, - popUp, popUpFromBottomForText, searchBarSlideAnimation, } from "@content/FramerMotionVariants" -import Link from "next/link" import Blog from "@components/Blog" import Metadata from "@components/MetaData" -import { BiRss } from "react-icons/bi" import { RiCloseCircleLine } from "react-icons/ri" -import { BsBookmark } from "react-icons/bs" import AnimatedDiv from "@components/FramerMotion/AnimatedDiv" import PageTop from "@components/PageTop" import pageMeta from "@content/meta" -import { FrontMatter } from "@lib/types" +import { BlogType } from "@lib/types" import { CgSearch } from "react-icons/cg" import { getAllBlogs } from "@lib/backendAPI" @@ -36,7 +32,7 @@ export default function Blogs() { useEffect(() => { setFilteredBlogs( - blogs.filter((post: FrontMatter) => + blogs.filter((post: BlogType) => post.title.toLowerCase().includes(searchValue.trim().toLowerCase()) ) ) @@ -65,8 +61,10 @@ export default function Blogs() {
- I've been writing online since 2021, mostly about web development and - tech careers. In total, I've written {blogs.length} articles till now. + Welcome to my blog page! + Here, you will find a collection of insightful and informative articles that I have written on various topics. + As a passionate writer and avid learner, I believe in the power of sharing knowledge and experiences through the written word. + Till now, I've written {blogs.length} articles. All Posts ({filteredBlogs.length}) - -
- - - - - - - - - - - -
+ {/* Blog Section */} {filteredBlogs.map((blog, index) => { return @@ -147,11 +126,3 @@ export default function Blogs() { ) } - -// export async function getStaticProps() { -// // const blogs = new MDXContent("posts").getAllPosts(); -// const blogs = await getAllBlogs() -// return { -// props: { blogs }, -// }; -// } diff --git a/frontend/pages/code-snippets/[slug].tsx b/frontend/pages/code-snippets/[slug].tsx new file mode 100644 index 0000000..0dfbb0b --- /dev/null +++ b/frontend/pages/code-snippets/[slug].tsx @@ -0,0 +1,57 @@ +import PageNotFound from "pages/404" +import { CodeSnippetType } from "@lib/types" +import SnippetLayout from "@layout/SnippetLayout" +// import pageMeta from "@content/meta" +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import { getCodeSnippetDetails } from '@lib/backendAPI' + +export default function SnippetPage({ + error, +}: { + error: boolean; +}) { + if (error) return + + const router = useRouter() + const { slug } = router.query // Retrieve the slug parameter from the URL + + const [code_snippet, setCodeSnippet] = useState() + + const fetchCodeSnippetDetail = async (slug: string) => { + try { + const codeSnippetData: CodeSnippetType = await getCodeSnippetDetails(slug) + setCodeSnippet(codeSnippetData) + } catch (error) { + // Handle error case + console.error(error) + } + } + + // Add this useEffect to trigger the API request when slug is available + useEffect(() => { + if (typeof slug === 'string') { + fetchCodeSnippetDetail(slug) + } + }, [slug]) + + return ( + <> + {/* */} + + {code_snippet ? ( + + + + ) : ( +

Loading...

+ )} + + ) +} diff --git a/frontend/pages/code-snippets/index.tsx b/frontend/pages/code-snippets/index.tsx new file mode 100644 index 0000000..e6f7d22 --- /dev/null +++ b/frontend/pages/code-snippets/index.tsx @@ -0,0 +1,55 @@ +import { AnimatePresence } from "framer-motion" +import { FadeContainer } from "@content/FramerMotionVariants" +import Metadata from "@components/MetaData" +import AnimatedDiv from "@components/FramerMotion/AnimatedDiv" +import PageTop from "@components/PageTop" +import pageMeta from "@content/meta" +import SnippetCard from "@components/SnippetCard" +import { useEffect, useState } from "react" +import { getAllCodeSnippets } from "@lib/backendAPI" +import { CodeSnippetType } from "@lib/types" + +export default function CodeSnippets() { + + const [code_snippets, setCodeSnippets] = useState([]) + + const fetchCodeSnippets = async () => { + const code_snippetsData: CodeSnippetType[] = await getAllCodeSnippets() + setCodeSnippets(code_snippetsData) + } + + useEffect(() => { + fetchCodeSnippets() + }, []) + + return ( + <> + {/* TODO: Fix Meta */} + + +
+ + {pageMeta.snippets.description} + + +
+ + + {code_snippets.map((code_snippet, index) => { + return + })} + + +
+
+ + ) +} diff --git a/frontend/pages/contact/ContactForm.tsx b/frontend/pages/contact/ContactForm.tsx new file mode 100644 index 0000000..e4d5c4f --- /dev/null +++ b/frontend/pages/contact/ContactForm.tsx @@ -0,0 +1,220 @@ +import React from "react"; +import { ToastContainer, toast } from "react-toastify"; +import { useDarkMode } from "@context/darkModeContext"; +import emailjs from "@emailjs/browser"; +import { motion } from "framer-motion"; +import { + FadeContainer, + mobileNavItemSideways, +} from "@content/FramerMotionVariants"; +import Ripples from "react-ripples"; +import { useRef } from "react"; +import { FormInput } from "@lib/types"; + +export default function Form() { + const { isDarkMode } = useDarkMode(); + const sendButtonRef = useRef(null!); + const formRef = useRef(null!); + + const FailToastId = "failed"; + + function sendEmail(e: React.SyntheticEvent) { + e.preventDefault(); + + const target = e.target as typeof e.target & { + first_name: { value: string }; + last_name: { value: string }; + email: { value: string }; + subject: { value: string }; + message: { value: string }; + }; + + const emailData = { + to_name: "Numan Ibn Mazid", + first_name: target.first_name.value.trim(), + last_name: target.last_name.value.trim(), + email: target.email.value.trim(), + subject: target.subject.value.trim(), + message: target.message.value.trim(), + }; + + if (!validateForm(emailData) && !toast.isActive(FailToastId)) + return toast.error("Looks like you have not filled the form", { + toastId: FailToastId, + }); + + // Making submit button disable + sendButtonRef.current.setAttribute("disabled", "true"); + + // Creating a loading toast + const toastId = toast.loading("Processing ⌛"); + + emailjs + .send( + process.env.EMAIL_JS_SERVICE_ID!, + process.env.EMAIL_JS_TEMPLATE_ID!, + emailData!, + process.env.EMAIL_JS_PUBLIC_KEY + ) + .then(() => { + formRef.current.reset(); + toast.update(toastId, { + render: "Message Sent ✌", + type: "success", + isLoading: false, + autoClose: 3000, + }); + sendButtonRef.current.removeAttribute("disabled"); + }) + .catch((err) => { + toast.update(toastId, { + render: "😢 " + err.text, + type: "error", + isLoading: false, + autoClose: 3000, + }); + sendButtonRef.current.removeAttribute("disabled"); + }); + } + + function validateForm(data: FormInput): boolean { + for (const key in data) { + if (data[key as keyof FormInput] === "") return false; + } + return true; + } + + return ( + <> + + {/* First Name And Last Name */} +
+ + + + + + + + +
+ + + + + + + + + +