From dd3cf97762d9ee8f1be2e3bfb8ef5757021618a0 Mon Sep 17 00:00:00 2001 From: Samuel Therrien Date: Wed, 8 May 2024 18:10:17 -0400 Subject: [PATCH] Iron out type-checking and formatting --- .pre-commit-config.yaml | 4 +- .../commands/initialize_database.py | 102 ++++--- canopeum_backend/canopeum_backend/models.py | 68 +++-- .../canopeum_backend/permissions.py | 36 ++- .../canopeum_backend/serializers.py | 150 ++++----- canopeum_backend/canopeum_backend/settings.py | 2 +- canopeum_backend/canopeum_backend/urls.py | 44 ++- canopeum_backend/canopeum_backend/views.py | 286 +++++++++++------- canopeum_backend/pyproject.toml | 16 +- canopeum_backend/requirements-dev.txt | 10 +- canopeum_backend/requirements.txt | 27 +- canopeum_backend/scripts/checkers.py | 7 +- canopeum_frontend/.eslintrc.cjs | 2 +- dprint.json | 1 + pyrightconfig.json | 9 +- 15 files changed, 465 insertions(+), 299 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 59324c362..7a0326d9c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ # You can run this locally with `pre-commit run [--all]` repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: trailing-whitespace args: [--markdown-linebreak-ext=md] @@ -14,7 +14,7 @@ repos: - id: check-case-conflict # You can run this locally with `ruff format && ruff check` - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 # must match canopeum_backend/requirements-dev.txt + rev: v0.4.3 # must match canopeum_backend/requirements-dev.txt hooks: # Run the linter. - id: ruff diff --git a/canopeum_backend/canopeum_backend/management/commands/initialize_database.py b/canopeum_backend/canopeum_backend/management/commands/initialize_database.py index 9694bc7d4..807cdb8ef 100644 --- a/canopeum_backend/canopeum_backend/management/commands/initialize_database.py +++ b/canopeum_backend/canopeum_backend/management/commands/initialize_database.py @@ -8,7 +8,7 @@ from django.db import connection from django.utils import timezone -from canopeum_backend import settings +import canopeum_backend.settings from canopeum_backend.models import ( Announcement, Asset, @@ -42,8 +42,8 @@ def create_posts_for_site(site): # Create a post for the site post = Post.objects.create( site=site, - body=f"""{site.name} has planted {random.randint(100, 1000)} new trees today. - Let's continue to grow our forest!""", # noqa: S311 + body=f"{site.name} has planted {random.randint(100, 1000)} new trees today." # noqa: S311 + + "Let's continue to grow our forest!", share_count=share_count, ) # Change created_at date since it is auto-generated on create @@ -72,7 +72,12 @@ def handle(self, *args, **kwargs): self.stdout.write("Erasing existing data...") assets_to_delete = Asset.objects.all().exclude(asset="site_img.png") for asset in assets_to_delete: - path = Path(settings.BASE_DIR) / "canopeum_backend" / "media" / asset.asset.name + path = ( + Path(canopeum_backend.settings.BASE_DIR) + / "canopeum_backend" + / "media" + / asset.asset.name + ) path.unlink(missing_ok=True) call_command("flush", "--noinput") cursor.execute("SET FOREIGN_KEY_CHECKS = 0;") @@ -106,7 +111,9 @@ def handle(self, *args, **kwargs): def create_fertilizer_types(self): fertilizer_types = [["Synthetic", "Synthetique"], ["Innoculant", "Innoculant"]] for _ in fertilizer_types: - Fertilizertype.objects.create(name=FertilizertypeInternationalization.objects.create(en=_[0], fr=_[1])) + Fertilizertype.objects.create( + name=FertilizertypeInternationalization.objects.create(en=_[0], fr=_[1]) + ) def create_mulch_layer_types(self): mulch_layer_types = [ @@ -118,7 +125,9 @@ def create_mulch_layer_types(self): ["Corn husk", "Feuille de maïs"], ] for _ in mulch_layer_types: - Mulchlayertype.objects.create(name=MulchlayertypeInternationalization.objects.create(en=_[0], fr=_[1])) + Mulchlayertype.objects.create( + name=MulchlayertypeInternationalization.objects.create(en=_[0], fr=_[1]) + ) def create_tree_types(self): tree_types = [ @@ -222,7 +231,9 @@ def create_tree_types(self): ] for _ in tree_types: - Treetype.objects.create(name=TreespeciestypeInternationalization.objects.create(en=_[0], fr=_[1])) + Treetype.objects.create( + name=TreespeciestypeInternationalization.objects.create(en=_[0], fr=_[1]) + ) def create_site_types(self): site_types = [ @@ -234,10 +245,14 @@ def create_site_types(self): ] for _ in site_types: - Sitetype.objects.create(name=SitetypeInternationalization.objects.create(en=_[0], fr=_[1])) + Sitetype.objects.create( + name=SitetypeInternationalization.objects.create(en=_[0], fr=_[1]) + ) def create_assets(self): - seeding_images_path = Path(settings.BASE_DIR) / "canopeum_backend" / "seeding" / "images" + seeding_images_path = ( + Path(canopeum_backend.settings.BASE_DIR) / "canopeum_backend" / "seeding" / "images" + ) image_file_names = ( "site_img1.png", "site_img2.jpg", @@ -262,7 +277,7 @@ def create_users(self): User.objects.create_user( username="admin", email="admin@beslogic.com", - password="Adminbeslogic!", # noqa: S106 MOCK_PASSWORD + password="Adminbeslogic!", # noqa: S106 # MOCK_PASSWORD is_staff=True, is_superuser=True, role=Role.objects.get(name="MegaAdmin"), @@ -270,31 +285,31 @@ def create_users(self): User.objects.create_user( username="TyrionLannister", email="tyrion@lannister.com", - password="tyrion123", # noqa: S106 MOCK_PASSWORD + password="tyrion123", # noqa: S106 # MOCK_PASSWORD role=Role.objects.get(name="SiteManager"), ) User.objects.create_user( username="DaenerysTargaryen", email="daenerys@targaryen.com", - password="daenerys123", # noqa: S106 MOCK_PASSWORD + password="daenerys123", # noqa: S106 # MOCK_PASSWORD role=Role.objects.get(name="SiteManager"), ) User.objects.create_user( username="JonSnow", email="jon@snow.com", - password="jon123", # noqa: S106 MOCK_PASSWORD + password="jon123", # noqa: S106 # MOCK_PASSWORD role=Role.objects.get(name="SiteManager"), ) User.objects.create_user( username="OberynMartell", email="oberyn@martell.com", - password="oberyn123", # noqa: S106 MOCK_PASSWORD + password="oberyn123", # noqa: S106 # MOCK_PASSWORD role=Role.objects.get(name="SiteManager"), ) User.objects.create_user( username="NormalUser", email="normal@user.com", - password="normal123", # noqa: S106 MOCK_PASSWORD + password="normal123", # noqa: S106 # MOCK_PASSWORD role=Role.objects.get(name="User"), ) @@ -302,7 +317,9 @@ def create_canopeum_site(self): site = Site.objects.create( name="Canopeum", is_public=True, - site_type=Sitetype.objects.get(name=SitetypeInternationalization.objects.get(en="Parks")), + site_type=Sitetype.objects.get( + name=SitetypeInternationalization.objects.get(en="Parks") + ), coordinate=Coordinate.objects.create( dms_latitude="45°30'06.1\"N", dms_longitude="73°34'02.3\"W", @@ -322,8 +339,9 @@ def create_canopeum_site(self): ), image=Asset.objects.first(), announcement=Announcement.objects.create( - body="We currently have 20000 healthy seedlings of different species, ready to be planted at any time!" - + "Please click the link below to book your favorite seedlings on our website", + body="We currently have 20000 healthy seedlings of different species, ready to " + + "be planted at any time! Please click the link below to book your favorite " + + "seedlings on our website", link="https://www.canopeum-pos.com", ), ) @@ -349,7 +367,7 @@ def create_canopeum_site(self): name="First Batch", site=site, created_at=timezone.now(), - size=100, + size="100", sponsor="Beslogic Inc.", soil_condition="Good", total_number_seed=100, @@ -365,7 +383,9 @@ def create_other_sites(self): site_2 = Site.objects.create( name="Maple Grove Retreat", is_public=True, - site_type=Sitetype.objects.get(name=SitetypeInternationalization.objects.get(en="Parks")), + site_type=Sitetype.objects.get( + name=SitetypeInternationalization.objects.get(en="Parks") + ), coordinate=Coordinate.objects.create( dms_latitude="46°48'33.6\"N", dms_longitude="71°18'40.0\"W", @@ -373,8 +393,8 @@ def create_other_sites(self): dd_longitude=-71.3111, address="123 Forest Trail, Quebec City, QC G1P 3X4", ), - description="""Maple Grove Retreat is a serene escape nestled in the outskirts of Quebec City, - offering a lush forested area with scenic maple groves.""", + description="Maple Grove Retreat is a serene escape nestled in the outskirts of " + + "Quebec City, offering a lush forested area with scenic maple groves.", size="1500", research_partnership=True, visible_map=True, @@ -386,11 +406,9 @@ def create_other_sites(self): ), image=Asset.objects.get(asset__contains="site_img2"), announcement=Announcement.objects.create( - body=""" - Maple Grove Retreat is excited to announce our upcoming Maple Syrup Festival! - Join us on March 15th for a day of maple syrup tastings, nature hikes, - and family fun. Learn more on our website. - """, + body="Maple Grove Retreat is excited to announce our upcoming Maple Syrup " + + "Festival! Join us on March 15th for a day of maple syrup tastings, " + + "nature hikes, and family fun. Learn more on our website.", link="https://www.maplegroveretreat.com/events/maple-syrup-festival", ), ) @@ -399,7 +417,9 @@ def create_other_sites(self): site_3 = Site.objects.create( name="Lakeside Oasis", is_public=True, - site_type=Sitetype.objects.get(name=SitetypeInternationalization.objects.get(en="Parks")), + site_type=Sitetype.objects.get( + name=SitetypeInternationalization.objects.get(en="Parks") + ), coordinate=Coordinate.objects.create( dms_latitude="48°36'05.0\"N", dms_longitude="71°18'27.0\"W", @@ -407,8 +427,8 @@ def create_other_sites(self): dd_longitude=-71.3075, address="456 Lakeview Road, Lac-Saint-Jean, QC G8M 1R9", ), - description="""Lakeside Oasis offers a tranquil retreat by the shores of Lac-Saint-Jean, - with pristine waters and breathtaking sunsets.""", + description="Lakeside Oasis offers a tranquil retreat by the shores of " + + "Lac-Saint-Jean, with pristine waters and breathtaking sunsets.", size="800", research_partnership=False, visible_map=True, @@ -420,8 +440,10 @@ def create_other_sites(self): ), image=Asset.objects.get(asset__contains="site_img3"), announcement=Announcement.objects.create( - body="""Escape to Lakeside Oasis! Our cozy cabins are now open for winter bookings. Enjoy ice fishing, - snowshoeing, and warm campfires by the lake. Book your stay today!""", + body="Escape to Lakeside Oasis! " + + "Our cozy cabins are now open for winter bookings. " + + "Enjoy ice fishing, snowshoeing, and warm campfires by the lake. " + + "Book your stay today!", link="https://www.lakesideoasis.com/winter-getaway", ), ) @@ -430,7 +452,9 @@ def create_other_sites(self): site_4 = Site.objects.create( name="Evergreen Trail", is_public=False, - site_type=Sitetype.objects.get(name=SitetypeInternationalization.objects.get(en="Parks")), + site_type=Sitetype.objects.get( + name=SitetypeInternationalization.objects.get(en="Parks") + ), coordinate=Coordinate.objects.create( dms_latitude="46°12'30.0\"N", dms_longitude="74°35'30.0\"W", @@ -438,8 +462,8 @@ def create_other_sites(self): dd_longitude=-74.5917, address="789 Trailhead Way, Mont-Tremblant, QC J8E 1T7", ), - description="""Evergreen Trail invites you to explore the rugged beauty of Mont-Tremblant's wilderness, - with winding trails and majestic evergreen forests.""", + description="Evergreen Trail invites you to explore the rugged beauty of " + + "Mont-Tremblant's wilderness, with winding trails and majestic evergreen forests.", size="1200", research_partnership=True, visible_map=True, @@ -451,10 +475,10 @@ def create_other_sites(self): ), image=Asset.objects.get(asset__contains="site_img4"), announcement=Announcement.objects.create( - body="""Discover the wonders of Evergreen Trail! - Our guided nature walks are now available every weekend. - Immerse yourself in nature and learn about the diverse - flora and fauna of Mont-Tremblant.""", + body="Discover the wonders of Evergreen Trail!" + + "Our guided nature walks are now available every weekend." + + "Immerse yourself in nature and learn about the diverse" + + "flora and fauna of Mont-Tremblant.", link="https://www.evergreentrail.com/guided-walks", ), ) diff --git a/canopeum_backend/canopeum_backend/models.py b/canopeum_backend/canopeum_backend/models.py index dd6b1ab01..703a74ebc 100644 --- a/canopeum_backend/canopeum_backend/models.py +++ b/canopeum_backend/canopeum_backend/models.py @@ -1,9 +1,15 @@ from datetime import datetime, timedelta -from typing import ClassVar +from typing import TYPE_CHECKING, Any, ClassVar import pytz from django.contrib.auth.models import AbstractUser from django.db import models +from rest_framework.request import Request as drf_Request + +# Pyright won't be able to infer all types here, see: +# https://github.com/typeddjango/django-stubs/issues/579 +# https://github.com/typeddjango/django-stubs/issues/1264 +# For now we have to rely on the mypy plugin class RoleName(models.TextChoices): @@ -11,12 +17,12 @@ class RoleName(models.TextChoices): SITEMANAGER = "SiteManager" MEGAADMIN = "MegaAdmin" - def from_string(self, value): - if value == self.MEGAADMIN: - return self.MEGAADMIN - if value == self.SITEMANAGER: - return self.SITEMANAGER - return self.USER + @classmethod + def from_string(cls, value: str): + try: + return cls(value) + except ValueError: + return cls.USER class Role(models.Model): @@ -34,8 +40,14 @@ class User(AbstractUser): unique=True, ) USERNAME_FIELD = "email" - REQUIRED_FIELDS: ClassVar[list[str]] = [] # type: ignore - role = models.ForeignKey(Role, models.RESTRICT, null=False, default=1) # type: ignore + REQUIRED_FIELDS: ClassVar[list[str]] = [] + role = models.ForeignKey[Role, Role](Role, models.RESTRICT, null=False, default=1) + if TYPE_CHECKING: + # Missing "id" in "Model" or some base "User" class? + id: int + # TODO: I don't know what this type is supposed to be, nor if we're using it correctly + # and why it's not part of the default User models + auth_token: Any class Announcement(models.Model): @@ -101,7 +113,9 @@ class Coordinate(models.Model): class Fertilizertype(models.Model): - name = models.ForeignKey("FertilizertypeInternationalization", models.DO_NOTHING, blank=True, null=True) + name = models.ForeignKey( + "FertilizertypeInternationalization", models.DO_NOTHING, blank=True, null=True + ) class FertilizertypeInternationalization(models.Model): @@ -110,7 +124,9 @@ class FertilizertypeInternationalization(models.Model): class Mulchlayertype(models.Model): - name = models.ForeignKey("MulchlayertypeInternationalization", models.DO_NOTHING, blank=True, null=True) + name = models.ForeignKey( + "MulchlayertypeInternationalization", models.DO_NOTHING, blank=True, null=True + ) class MulchlayertypeInternationalization(models.Model): @@ -142,18 +158,21 @@ class Site(models.Model): image = models.ForeignKey(Asset, models.DO_NOTHING, blank=True, null=True) +# Note: PostAsset must be defined before Post because of a limitation with ManyToManyField type +# inference using string annotations: https://github.com/typeddjango/django-stubs/issues/1802 +# Can't manually annotate because of: https://github.com/typeddjango/django-stubs/issues/760 +class PostAsset(models.Model): + post = models.ForeignKey("Post", models.DO_NOTHING, null=False) + asset = models.ForeignKey(Asset, models.DO_NOTHING, null=False) + + class Post(models.Model): site = models.ForeignKey("Site", models.DO_NOTHING, blank=False, null=False) body = models.TextField(blank=False, null=False) share_count = models.IntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True, blank=False, null=False) # created_by = models.ForeignKey(User, models.DO_NOTHING, blank=True, null=True) - media = models.ManyToManyField(Asset, through="PostAsset", blank=True) - - -class PostAsset(models.Model): - post = models.ForeignKey(Post, models.DO_NOTHING, null=False) - asset = models.ForeignKey(Asset, models.DO_NOTHING, null=False) + media = models.ManyToManyField(Asset, through=PostAsset, blank=True) class Comment(models.Model): @@ -195,7 +214,9 @@ class Sitetreespecies(models.Model): class Sitetype(models.Model): - name = models.ForeignKey("SitetypeInternationalization", models.DO_NOTHING, blank=True, null=True) + name = models.ForeignKey( + "SitetypeInternationalization", models.DO_NOTHING, blank=True, null=True + ) class SitetypeInternationalization(models.Model): @@ -209,7 +230,9 @@ class TreespeciestypeInternationalization(models.Model): class Treetype(models.Model): - name = models.ForeignKey(TreespeciestypeInternationalization, models.DO_NOTHING, blank=True, null=True) + name = models.ForeignKey( + TreespeciestypeInternationalization, models.DO_NOTHING, blank=True, null=True + ) class Widget(models.Model): @@ -226,3 +249,10 @@ class Like(models.Model): class Internationalization(models.Model): en = models.TextField(db_column="EN", blank=True, null=True) fr = models.TextField(db_column="FR", blank=True, null=True) + + +class Request(drf_Request): + """A custom Request type to use for parameter annotations.""" + + # Override with our own User model + user: User # pyright: ignore[reportIncompatibleMethodOverride] diff --git a/canopeum_backend/canopeum_backend/permissions.py b/canopeum_backend/canopeum_backend/permissions.py index ada605397..2d10abd5b 100644 --- a/canopeum_backend/canopeum_backend/permissions.py +++ b/canopeum_backend/canopeum_backend/permissions.py @@ -1,47 +1,57 @@ +# More precise request param +# pyright: reportIncompatibleMethodOverride=false +# mypy: disable_error_code=override + from rest_framework import permissions -from .models import Comment, Site, Siteadmin, User +from .models import Comment, Request, Site, Siteadmin, User class DeleteCommentPermission(permissions.BasePermission): """Deleting a comment is only allowed for admins or the comment's author.""" - def has_object_permission(self, request, view, obj: Comment): + def has_object_permission(self, request: Request, view, obj: Comment): current_user_role = request.user.role.name if current_user_role == "MegaAdmin": return True - is_admin_for_this_post = obj.post.site.siteadmin_set.filter(user__id__exact=request.user.id).exists() + is_admin_for_this_post = obj.post.site.siteadmin_set.filter( + user__id__exact=request.user.id + ).exists() return is_admin_for_this_post or obj.user == request.user class PublicSiteReadPermission(permissions.BasePermission): """Site methods only allowed if they are public, or the user is a site admin.""" - # About the type ignore: Base permission return type is Literal True but should be bool - def has_object_permission(self, request, view, obj: Site) -> bool: # type: ignore - if obj.is_public or (isinstance(request.user, User) and request.user.role.name == "MegaAdmin"): + def has_object_permission(self, request: Request, view, obj: Site) -> bool: + if obj.is_public or ( + isinstance(request.user, User) and request.user.role.name == "MegaAdmin" + ): return True if not isinstance(request.user, User) or request.user.role.name != "SiteManager": return False - return Siteadmin.objects.filter(user__id__exact=request.user.pk).filter(site=obj.pk).exists() + return ( + Siteadmin.objects.filter(user__id__exact=request.user.pk).filter(site=obj.pk).exists() + ) class SiteAdminPermission(permissions.BasePermission): """Allows mega admins and a specific site's admin to perform site actions.""" - # About the type ignore: Base permission return type is Literal True but should be bool - def has_object_permission(self, request, view, obj: Site) -> bool: # type: ignore + def has_object_permission(self, request: Request, view, obj: Site) -> bool: current_user_role = request.user.role.name if current_user_role == "MegaAdmin": return True - return Siteadmin.objects.filter(user__id__exact=request.user.id).filter(site=obj.pk).exists() + return ( + Siteadmin.objects.filter(user__id__exact=request.user.id).filter(site=obj.pk).exists() + ) class MegaAdminPermission(permissions.BasePermission): """Global permission for actions only allowed to MegaAdmin users.""" - def has_permission(self, request, view): + def has_permission(self, request: Request, view): current_user_role = request.user.role.name return current_user_role == "MegaAdmin" @@ -55,7 +65,7 @@ class MegaAdminPermissionReadOnly(permissions.BasePermission): This one will allow GET requests for any user, though. """ - def has_permission(self, request, view): + def has_permission(self, request: Request, view): if request.method in READONLY_METHODS: return True current_user_role = request.user.role.name @@ -65,5 +75,5 @@ def has_permission(self, request, view): class CurrentUserPermission(permissions.BasePermission): """Permission specific to a user, only allowed for this authenticated user.""" - def has_object_permission(self, request, view, obj): + def has_object_permission(self, request: Request, view, obj): return obj == request.user diff --git a/canopeum_backend/canopeum_backend/serializers.py b/canopeum_backend/canopeum_backend/serializers.py index 85594d3f5..3a4b902b4 100644 --- a/canopeum_backend/canopeum_backend/serializers.py +++ b/canopeum_backend/canopeum_backend/serializers.py @@ -1,3 +1,8 @@ +# Pyright does not support duck-typed Meta inner-class +# pyright: reportIncompatibleVariableOverride=false + +from typing import Any + from django.contrib.auth.password_validation import validate_password from drf_spectacular.utils import extend_schema_field from rest_framework import serializers @@ -37,22 +42,25 @@ class IntegerListFieldSerializer(serializers.ListField): child = serializers.IntegerField() -class LoginUserSerializer(serializers.ModelSerializer): +class LoginUserSerializer(serializers.ModelSerializer[User]): class Meta: model = User fields = ("email", "password") -class ChangePasswordSerializer(serializers.Serializer): +# Note about Any: Generic is the type of "instance", not set here +class ChangePasswordSerializer(serializers.Serializer[Any]): current_password = serializers.CharField(write_only=True, required=True) - new_password = serializers.CharField(write_only=True, required=True, validators=[validate_password]) + new_password = serializers.CharField( + write_only=True, required=True, validators=[validate_password] + ) new_password_confirmation = serializers.CharField(write_only=True, required=True) class Meta: fields = ("current_password", "new_password", "new_password_confirmation") -class UpdateUserSerializer(serializers.ModelSerializer): +class UpdateUserSerializer(serializers.ModelSerializer[User]): change_password = ChangePasswordSerializer(required=False) class Meta: @@ -60,9 +68,13 @@ class Meta: fields = ("username", "email", "change_password") -class RegisterUserSerializer(serializers.ModelSerializer): - username = serializers.CharField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) - email = serializers.EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) +class RegisterUserSerializer(serializers.ModelSerializer[User]): + username = serializers.CharField( + required=True, validators=[UniqueValidator(queryset=User.objects.all())] + ) + email = serializers.EmailField( + required=True, validators=[UniqueValidator(queryset=User.objects.all())] + ) password = serializers.CharField(write_only=True, required=True, validators=[validate_password]) password_confirmation = serializers.CharField(write_only=True, required=True) @@ -116,7 +128,7 @@ def create_user(self): return user -class UserSerializer(serializers.ModelSerializer): +class UserSerializer(serializers.ModelSerializer[User]): role = serializers.SerializerMethodField() admin_site_ids = serializers.SerializerMethodField() followed_site_ids = serializers.SerializerMethodField() @@ -127,7 +139,7 @@ class Meta: def get_role(self, obj: User) -> RoleName: role_name = obj.role.name - return RoleName.from_string(RoleName.USER, role_name) + return RoleName.from_string(role_name) # type: ignore[no-any-return] # mypy false-positive def get_admin_site_ids(self, obj: User) -> list[int]: return [siteadmin.site.id for siteadmin in Siteadmin.objects.filter(user=obj)] @@ -136,7 +148,8 @@ def get_followed_site_ids(self, obj: User) -> list[int]: return [site_follower.site.id for site_follower in SiteFollower.objects.filter(user=obj)] -class UserTokenSerializer(serializers.Serializer): +# Note about Any: Generic is the type of "instance", not set here +class UserTokenSerializer(serializers.Serializer[Any]): token = TokenRefreshSerializer() user = UserSerializer() @@ -144,25 +157,25 @@ class Meta: fields = ("token", "user") -class CoordinatesSerializer(serializers.ModelSerializer): +class CoordinatesSerializer(serializers.ModelSerializer[Coordinate]): class Meta: model = Coordinate fields = "__all__" -class WidgetSerializer(serializers.ModelSerializer): +class WidgetSerializer(serializers.ModelSerializer[Widget]): class Meta: model = Widget fields = "__all__" -class InternationalizationSerializer(serializers.ModelSerializer): +class InternationalizationSerializer(serializers.ModelSerializer[Internationalization]): class Meta: model = Internationalization fields = ("en", "fr") -class SiteTypeSerializer(serializers.ModelSerializer): +class SiteTypeSerializer(serializers.ModelSerializer[Sitetype]): en = serializers.SerializerMethodField() fr = serializers.SerializerMethodField() @@ -177,7 +190,7 @@ def get_fr(self, obj): return InternationalizationSerializer(obj.name).data.get("fr", None) -class TreeTypeSerializer(serializers.ModelSerializer): +class TreeTypeSerializer(serializers.ModelSerializer[Treetype]): en = serializers.SerializerMethodField() fr = serializers.SerializerMethodField() @@ -192,19 +205,19 @@ def get_fr(self, obj): return InternationalizationSerializer(obj.name).data.get("fr", None) -class AnnouncementSerializer(serializers.ModelSerializer): +class AnnouncementSerializer(serializers.ModelSerializer[Announcement]): class Meta: model = Announcement fields = "__all__" -class ContactSerializer(serializers.ModelSerializer): +class ContactSerializer(serializers.ModelSerializer[Contact]): class Meta: model = Contact fields = "__all__" -class SitetreespeciesSerializer(serializers.ModelSerializer): +class SitetreespeciesSerializer(serializers.ModelSerializer[Sitetreespecies]): en = serializers.SerializerMethodField() fr = serializers.SerializerMethodField() @@ -219,7 +232,7 @@ def get_fr(self, obj): return TreeTypeSerializer(obj.tree_type).data.get("fr", None) -class AssetSerializer(serializers.ModelSerializer): +class AssetSerializer(serializers.ModelSerializer[Asset]): asset = serializers.FileField() class Meta: @@ -234,13 +247,13 @@ def to_internal_value(self, data): return super().to_internal_value(data) -class SitePostSerializer(serializers.ModelSerializer): +class SitePostSerializer(serializers.ModelSerializer[Site]): class Meta: model = Site fields = "__all__" -class SiteSerializer(serializers.ModelSerializer): +class SiteSerializer(serializers.ModelSerializer[Site]): site_type = SiteTypeSerializer() coordinate = CoordinatesSerializer() site_tree_species = serializers.SerializerMethodField() @@ -257,27 +270,21 @@ def get_site_tree_species(self, obj): return SitetreespeciesSerializer(obj.sitetreespecies_set.all(), many=True).data -class SitePatchSerializer(serializers.Serializer): - site_type = serializers.IntegerField() - - class Meta: - fields = ("site_type",) - - -class UpdateSitePublicStatusSerializer(serializers.Serializer): +# Note about Any: Generic is the type of "instance", not set here +class UpdateSitePublicStatusSerializer(serializers.Serializer[Any]): is_public = serializers.BooleanField(required=True) class Meta: fields = ("is_public",) -class SiteNameSerializer(serializers.ModelSerializer): +class SiteNameSerializer(serializers.ModelSerializer[Site]): class Meta: model = Site fields = ("id", "name") -class AdminUserSitesSerializer(serializers.ModelSerializer): +class AdminUserSitesSerializer(serializers.ModelSerializer[User]): sites = serializers.SerializerMethodField() class Meta: @@ -290,7 +297,7 @@ def get_sites(self, obj): return SiteNameSerializer(sites_list, many=True).data -class SiteSocialSerializer(serializers.ModelSerializer): +class SiteSocialSerializer(serializers.ModelSerializer[Site]): site_type = SiteTypeSerializer() contact = ContactSerializer() announcement = AnnouncementSerializer() @@ -313,9 +320,8 @@ class Meta: "widget", ) - # Bug in the extend_schema_field type annotation, they should allow - # base python types supported by open api specs - @extend_schema_field(list[str]) # pyright: ignore[reportArgumentType] + # https://github.com/tfranzel/drf-spectacular/issues/1212 + @extend_schema_field(list[str]) # type: ignore[arg-type] # pyright: ignore[reportArgumentType] def get_sponsors(self, obj): return self.context.get("sponsors") @@ -324,7 +330,7 @@ def get_widget(self, obj): return WidgetSerializer(obj.widget_set.all(), many=True).data -class BatchfertilizerSerializer(serializers.ModelSerializer): +class BatchfertilizerSerializer(serializers.ModelSerializer[Batchfertilizer]): en = serializers.SerializerMethodField() fr = serializers.SerializerMethodField() @@ -339,7 +345,7 @@ def get_fr(self, obj): return InternationalizationSerializer(obj.fertilizer_type).data.get("fr", None) -class BatchMulchLayerSerializer(serializers.ModelSerializer): +class BatchMulchLayerSerializer(serializers.ModelSerializer[Mulchlayertype]): en = serializers.SerializerMethodField() fr = serializers.SerializerMethodField() @@ -354,7 +360,7 @@ def get_fr(self, obj): return InternationalizationSerializer(obj.mulch_layer_type).data.get("fr", None) -class BatchSupportedSpeciesSerializer(serializers.ModelSerializer): +class BatchSupportedSpeciesSerializer(serializers.ModelSerializer[BatchSupportedSpecies]): en = serializers.SerializerMethodField() fr = serializers.SerializerMethodField() @@ -369,7 +375,7 @@ def get_fr(self, obj): return InternationalizationSerializer(obj.tree_type).data.get("fr", None) -class BatchSeedSerializer(serializers.ModelSerializer): +class BatchSeedSerializer(serializers.ModelSerializer[BatchSeed]): en = serializers.SerializerMethodField() fr = serializers.SerializerMethodField() @@ -384,7 +390,7 @@ def get_fr(self, obj): return InternationalizationSerializer(obj.tree_type).data.get("fr", None) -class BatchSpeciesSerializer(serializers.ModelSerializer): +class BatchSpeciesSerializer(serializers.ModelSerializer[BatchSpecies]): en = serializers.SerializerMethodField() fr = serializers.SerializerMethodField() @@ -399,13 +405,13 @@ def get_fr(self, obj): return InternationalizationSerializer(obj.tree_type).data.get("fr", None) -class BatchSerializer(serializers.ModelSerializer): +class BatchSerializer(serializers.ModelSerializer[Batch]): class Meta: model = Batch fields = "__all__" -class BatchAnalyticsSerializer(serializers.ModelSerializer): +class BatchAnalyticsSerializer(serializers.ModelSerializer[Batch]): fertilizers = serializers.SerializerMethodField() mulch_layers = serializers.SerializerMethodField() supported_species = serializers.SerializerMethodField() @@ -437,19 +443,19 @@ class Meta: "updated_at", ) - @extend_schema_field(int) # pyright: ignore[reportArgumentType] + @extend_schema_field(int) def get_plant_count(self, obj): return self.context.get("plant_count") - @extend_schema_field(int) # pyright: ignore[reportArgumentType] + @extend_schema_field(int) def get_survived_count(self, obj): return self.context.get("survived_count") - @extend_schema_field(int) # pyright: ignore[reportArgumentType] + @extend_schema_field(int) def get_replace_count(self, obj): return self.context.get("replace_count") - @extend_schema_field(int) # pyright: ignore[reportArgumentType] + @extend_schema_field(int) def get_seed_collected_count(self, obj): return self.context.get("seed_collected_count") @@ -474,7 +480,7 @@ def get_species(self, obj): return BatchSpeciesSerializer(obj.batchspecies_set.all(), many=True).data -class SiteAdminSerializer(serializers.ModelSerializer): +class SiteAdminSerializer(serializers.ModelSerializer[Siteadmin]): user = UserSerializer() class Meta: @@ -482,7 +488,8 @@ class Meta: fields = ("user",) -class CreateUserInvitationSerializer(serializers.Serializer): +# Note about Any: Generic is the type of "instance", not set here +class CreateUserInvitationSerializer(serializers.Serializer[Any]): site_ids = IntegerListFieldSerializer() email = serializers.EmailField() @@ -490,7 +497,7 @@ class Meta: fields = ("site_ids", "email") -class UserInvitationSerializer(serializers.ModelSerializer): +class UserInvitationSerializer(serializers.ModelSerializer[UserInvitation]): expires_at = serializers.DateTimeField() class Meta: @@ -498,7 +505,7 @@ class Meta: fields = ("id", "code", "email", "expires_at") -class SiteFollowerSerializer(serializers.ModelSerializer): +class SiteFollowerSerializer(serializers.ModelSerializer[SiteFollower]): user = UserSerializer() site = SiteSerializer() @@ -507,14 +514,15 @@ class Meta: fields = ("user", "site") -class SiteAdminUpdateRequestSerializer(serializers.Serializer): +# Note about Any: Generic is the type of "instance", not set here +class SiteAdminUpdateRequestSerializer(serializers.Serializer[Any]): ids = IntegerListFieldSerializer() class Meta: fields = ("ids",) -class SiteSummarySerializer(serializers.ModelSerializer): +class SiteSummarySerializer(serializers.ModelSerializer[Site]): site_type = SiteTypeSerializer() coordinate = CoordinatesSerializer() plant_count = serializers.SerializerMethodField() @@ -542,29 +550,30 @@ class Meta: "batches", ) - @extend_schema_field(int) # pyright: ignore[reportArgumentType] + @extend_schema_field(int) def get_plant_count(self, obj): return self.context.get("plant_count") - @extend_schema_field(int) # pyright: ignore[reportArgumentType] + @extend_schema_field(int) def get_survived_count(self, obj): return self.context.get("survived_count") - @extend_schema_field(int) # pyright: ignore[reportArgumentType] + @extend_schema_field(int) def get_propagation_count(self, obj): return self.context.get("propagation_count") - @extend_schema_field(float) # pyright: ignore[reportArgumentType] + @extend_schema_field(float) def get_progress(self, obj): return self.context.get("progress") - @extend_schema_field(list[str]) # pyright: ignore[reportArgumentType] + # https://github.com/tfranzel/drf-spectacular/issues/1212 + @extend_schema_field(list[str]) # type: ignore[arg-type] # pyright: ignore[reportArgumentType] def get_sponsors(self, obj): batches = Batch.objects.filter(site=obj) return [batch.sponsor for batch in batches] -class CoordinatesMapSerializer(serializers.ModelSerializer): +class CoordinatesMapSerializer(serializers.ModelSerializer[Coordinate]): latitude = serializers.SerializerMethodField() longitude = serializers.SerializerMethodField() @@ -572,16 +581,16 @@ class Meta: model = Coordinate fields = ("latitude", "longitude", "address") - @extend_schema_field(float) # pyright: ignore[reportArgumentType] + @extend_schema_field(float) def get_latitude(self, obj): return obj.dd_latitude - @extend_schema_field(float) # pyright: ignore[reportArgumentType] + @extend_schema_field(float) def get_longitude(self, obj): return obj.dd_longitude -class SiteMapSerializer(serializers.ModelSerializer): +class SiteMapSerializer(serializers.ModelSerializer[Site]): site_type = SiteTypeSerializer() coordinates = serializers.SerializerMethodField() image = AssetSerializer() @@ -595,7 +604,7 @@ def get_coordinates(self, obj): return CoordinatesMapSerializer(obj.coordinate).data -class SiteOverviewSerializer(serializers.ModelSerializer): +class SiteOverviewSerializer(serializers.ModelSerializer[Site]): image = AssetSerializer() class Meta: @@ -603,13 +612,13 @@ class Meta: fields = ("id", "name", "image") -class PostPostSerializer(serializers.ModelSerializer): +class PostPostSerializer(serializers.ModelSerializer[Post]): class Meta: model = Post fields = ("site", "body", "media") -class PostSerializer(serializers.ModelSerializer): +class PostSerializer(serializers.ModelSerializer[Post]): site = SiteOverviewSerializer() comment_count = serializers.SerializerMethodField() like_count = serializers.SerializerMethodField() @@ -645,7 +654,8 @@ def get_has_liked(self, obj: Post) -> bool: return Like.objects.filter(user=user, post=obj).exists() -class PostPaginationSerializer(serializers.Serializer): +# Note about Any: Generic is the type of "instance", not set here +class PostPaginationSerializer(serializers.Serializer[Any]): count = serializers.IntegerField() next = serializers.CharField(required=False) previous = serializers.CharField(required=False) @@ -655,13 +665,13 @@ class Meta: fields = ("count", "next", "previous", "results") -class CreateCommentSerializer(serializers.ModelSerializer): +class CreateCommentSerializer(serializers.ModelSerializer[Comment]): class Meta: model = Comment fields = ("body",) -class CommentSerializer(serializers.ModelSerializer): +class CommentSerializer(serializers.ModelSerializer[Comment]): author_id = serializers.SerializerMethodField() author_username = serializers.SerializerMethodField() # TODO(NicolasDontigny): Add user avatar image here once implemented @@ -670,7 +680,7 @@ class Meta: model = Comment fields = ("id", "body", "author_id", "author_username", "created_at") - @extend_schema_field(int) # pyright: ignore[reportArgumentType] + @extend_schema_field(int) def get_author_id(self, obj): return obj.user.id @@ -678,13 +688,13 @@ def get_author_username(self, obj): return obj.user.username -class LikePostSerializer(serializers.ModelSerializer): +class LikePostSerializer(serializers.ModelSerializer[Like]): class Meta: model = Like fields = ("post",) -class LikeSerializer(serializers.ModelSerializer): +class LikeSerializer(serializers.ModelSerializer[Like]): class Meta: model = Like fields = "__all__" diff --git a/canopeum_backend/canopeum_backend/settings.py b/canopeum_backend/canopeum_backend/settings.py index a7847b36c..62cc35e46 100644 --- a/canopeum_backend/canopeum_backend/settings.py +++ b/canopeum_backend/canopeum_backend/settings.py @@ -27,7 +27,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS: list[str] = [] # Application definition diff --git a/canopeum_backend/canopeum_backend/urls.py b/canopeum_backend/canopeum_backend/urls.py index 64aec8bb2..400ccae0f 100644 --- a/canopeum_backend/canopeum_backend/urls.py +++ b/canopeum_backend/canopeum_backend/urls.py @@ -17,7 +17,11 @@ path("social/posts/", views.PostListAPIView.as_view(), name="post-list"), path("social/posts//", views.PostDetailAPIView.as_view(), name="post-detail"), # Comment - path("social/posts//comments/", views.CommentListAPIView.as_view(), name="comment-list"), + path( + "social/posts//comments/", + views.CommentListAPIView.as_view(), + name="comment-list", + ), path( "social/posts//comments//", views.CommentDetailAPIView.as_view(), @@ -45,9 +49,13 @@ name="contact-detail", ), # Widget - path("social/sites//widgets/", views.WidgetListAPIView.as_view(), name="widget-list"), path( - "social/sites//widgets//", views.WidgetDetailAPIView.as_view(), name="widget-detail" + "social/sites//widgets/", views.WidgetListAPIView.as_view(), name="widget-list" + ), + path( + "social/sites//widgets//", + views.WidgetDetailAPIView.as_view(), + name="widget-detail", ), # Analytics # Tree Species @@ -56,8 +64,14 @@ # Site path("analytics/sites/", views.SiteListAPIView.as_view(), name="site-list"), path("analytics/sites//", views.SiteDetailAPIView.as_view(), name="site-detail"), - path("analytics/sites/summary", views.SiteSummaryListAPIView.as_view(), name="site-summary-list"), - path("analytics/sites//summary", views.SiteSummaryDetailAPIView.as_view(), name="site-summary-detail"), + path( + "analytics/sites/summary", views.SiteSummaryListAPIView.as_view(), name="site-summary-list" + ), + path( + "analytics/sites//summary", + views.SiteSummaryDetailAPIView.as_view(), + name="site-summary-detail", + ), path( "analytics/sites//admins", views.SiteDetailAdminsAPIView.as_view(), @@ -75,7 +89,9 @@ ), # Batches path("analytics/batches/", views.BatchListAPIView.as_view(), name="batch-list"), - path("analytics/batches//", views.BatchDetailAPIView.as_view(), name="batch-detail"), + path( + "analytics/batches//", views.BatchDetailAPIView.as_view(), name="batch-detail" + ), # Map # Coordinate path("map/sites/", views.SiteMapListAPIView.as_view(), name="coordinate-list-sites"), @@ -84,8 +100,14 @@ path("users/site-managers", views.SiteManagersListAPIView.as_view(), name="site-managers-list"), path("users//", views.UserDetailAPIView.as_view(), name="user-detail"), path("users/current_user/", views.UserCurrentUserAPIView.as_view(), name="current-user"), - path("user-invitations/", views.UserInvitationListAPIView.as_view(), name="user-invitation-list"), - path("user-invitations/", views.UserInvitationDetailAPIView.as_view(), name="user-invitation-list"), + path( + "user-invitations/", views.UserInvitationListAPIView.as_view(), name="user-invitation-list" + ), + path( + "user-invitations/", + views.UserInvitationDetailAPIView.as_view(), + name="user-invitation-list", + ), # Site admins path( "admin-user-sites/", @@ -94,7 +116,11 @@ ), # SWAGGER path("api/schema/", SpectacularAPIView.as_view(), name="schema"), - path("api/schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), + path( + "api/schema/swagger-ui/", + SpectacularSwaggerView.as_view(url_name="schema"), + name="swagger-ui", + ), path("api/schema/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), # JWT path("auth/token/", TokenObtainPairView.as_view(), name="authentication_token_obtain_pair"), diff --git a/canopeum_backend/canopeum_backend/views.py b/canopeum_backend/canopeum_backend/views.py index dda01cb7d..1f632c3c0 100644 --- a/canopeum_backend/canopeum_backend/views.py +++ b/canopeum_backend/canopeum_backend/views.py @@ -34,6 +34,7 @@ Coordinate, Like, Post, + Request, RoleName, Site, Siteadmin, @@ -94,8 +95,12 @@ def get_public_sites_unless_admin(user: User | None): class LoginAPIView(APIView): permission_classes = (AllowAny,) - @extend_schema(request=LoginUserSerializer, responses=UserTokenSerializer, operation_id="authentication_login") - def post(self, request): + @extend_schema( + request=LoginUserSerializer, + responses=UserTokenSerializer, + operation_id="authentication_login", + ) + def post(self, request: Request): email = request.data.get("email") password = request.data.get("password") @@ -103,9 +108,14 @@ def post(self, request): if user is not None: refresh = cast(RefreshToken, RefreshToken.for_user(user)) - refresh_serializer = TokenRefreshSerializer({"refresh": str(refresh), "access": str(refresh.access_token)}) + refresh_serializer = TokenRefreshSerializer({ + "refresh": str(refresh), + "access": str(refresh.access_token), + }) user_serializer = UserSerializer(user) - serializer = UserTokenSerializer(data={"token": refresh_serializer.data, "user": user_serializer.data}) + serializer = UserTokenSerializer( + data={"token": refresh_serializer.data, "user": user_serializer.data} + ) serializer.is_valid() return Response(serializer.data, status=status.HTTP_200_OK) @@ -116,39 +126,44 @@ class RegisterAPIView(APIView): permission_classes = (AllowAny,) @extend_schema( - request=RegisterUserSerializer, responses={201: UserTokenSerializer}, operation_id="authentication_register" + request=RegisterUserSerializer, + responses={201: UserTokenSerializer}, + operation_id="authentication_register", ) - def post(self, request): - # TODO(NicolasDontigny): Find out how to convert request body properties from camel case to lower snake case + def post(self, request: Request): + # TODO(NicolasDontigny): Find out how to convert request body + # properties from camel case to lower snake case request.data["password_confirmation"] = request.data.get("passwordConfirmation") - serializer = RegisterUserSerializer(data=request.data) + register_user_serializer = RegisterUserSerializer(data=request.data) - if serializer.is_valid(): - user = serializer.create_user() + if not register_user_serializer.is_valid(): + user = register_user_serializer.create_user() if user is not None: refresh = cast(RefreshToken, RefreshToken.for_user(user)) - refresh_serializer = TokenRefreshSerializer({ + token_refresh_serializer = TokenRefreshSerializer({ "refresh": str(refresh), "access": str(refresh.access_token), }) user_serializer = UserSerializer(user) - serializer = UserTokenSerializer(data={"token": refresh_serializer.data, "user": user_serializer.data}) - serializer.is_valid() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + user_token_serializer = UserTokenSerializer( + data={"token": token_refresh_serializer.data, "user": user_serializer.data} + ) + user_token_serializer.is_valid() + return Response(user_token_serializer.data, status=status.HTTP_201_CREATED) + return Response(register_user_serializer.errors, status=status.HTTP_400_BAD_REQUEST) class LogoutAPIView(APIView): @extend_schema(responses=status.HTTP_200_OK, operation_id="authentication_logout") - def post(self, request): + def post(self, request: Request): request.user.auth_token.delete() return Response(status=status.HTTP_200_OK) class TreeSpeciesAPIView(APIView): @extend_schema(responses=TreeTypeSerializer(many=True), operation_id="tree_species") - def get(self, request): + def get(self, request: Request): tree_species = Treetype.objects.all() serializer = TreeTypeSerializer(tree_species, many=True) return Response(serializer.data) @@ -156,7 +171,7 @@ def get(self, request): class SiteTypesAPIView(APIView): @extend_schema(responses=SiteTypeSerializer(many=True), operation_id="site_types") - def get(self, request): + def get(self, request: Request): tree_species = Sitetype.objects.all() serializer = SiteTypeSerializer(tree_species, many=True) return Response(serializer.data) @@ -164,7 +179,7 @@ def get(self, request): class SiteListAPIView(APIView): @extend_schema(responses=SiteSerializer(many=True), operation_id="site_all") - def get(self, request): + def get(self, request: Request): sites = get_public_sites_unless_admin(request.user) serializer = SiteSerializer(sites, many=True) return Response(serializer.data) @@ -172,7 +187,8 @@ def get(self, request): parser_classes = (MultiPartParser, FormParser) @extend_schema( - # request={"multipart/form-data": SiteSerializer}, TODO: Add serializer for multipart/form-data + # TODO: Add serializer for multipart/form-data + # request={"multipart/form-data": SiteSerializer}, request={ "multipart/form-data": { "type": "object", @@ -188,7 +204,10 @@ def get(self, request): "type": "array", "items": { "type": "object", - "properties": {"id": {"type": "number"}, "quantity": {"type": "number"}}, + "properties": { + "id": {"type": "number"}, + "quantity": {"type": "number"}, + }, }, }, "researchPartnership": {"type": "boolean"}, @@ -199,14 +218,15 @@ def get(self, request): responses={201: SiteSerializer}, operation_id="site_create", ) - def post(self, request): + def post(self, request: Request): asset = AssetSerializer(data=request.data) if not asset.is_valid(): return Response(data=asset.errors, status=status.HTTP_400_BAD_REQUEST) - asset = asset.save() + image = asset.save() site_type = Sitetype.objects.get(pk=request.data["siteType"]) - # (TODO) For the coordinates, we need to calculate the ddLat and ddLong and also use the Google API for the address + # (TODO) For the coordinates, we need to calculate the ddLat and ddLong + # and also use the Google API for the address coordinate = Coordinate.objects.create( dms_latitude=request.data["latitude"], dms_longitude=request.data["longitude"] ) @@ -216,7 +236,7 @@ def post(self, request): serializer = SitePostSerializer(data=request.data) if serializer.is_valid(): site = serializer.save( - image=asset, + image=image, site_type=site_type, coordinate=coordinate, announcement=announcement, @@ -225,11 +245,14 @@ def post(self, request): research_partnership=json.loads(request.data["researchPartnership"]), visible_map=json.loads(request.data["visibleMap"]), ) - - for tree_type_json in request.data.getlist("species"): + # TODO: Are we sure about getlist ? + # If this is correct, consider raising an issue upstream + for tree_type_json in request.data.getlist("species"): # type:ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue] tree_type_obj = json.loads(tree_type_json) tree_type = Treetype.objects.get(pk=tree_type_obj["id"]) - Sitetreespecies.objects.create(site=site, tree_type=tree_type, quantity=tree_type_obj["quantity"]) + Sitetreespecies.objects.create( + site=site, tree_type=tree_type, quantity=tree_type_obj["quantity"] + ) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -241,7 +264,7 @@ class SiteDetailAPIView(APIView): parser_classes = (MultiPartParser, FormParser) @extend_schema(request=SiteSerializer, responses=SiteSerializer, operation_id="site_detail") - def get(self, request, siteId): + def get(self, request: Request, siteId): try: site = Site.objects.prefetch_related("image").get(pk=siteId) except Site.DoesNotExist: @@ -266,7 +289,10 @@ def get(self, request, siteId): "type": "array", "items": { "type": "object", - "properties": {"id": {"type": "number"}, "quantity": {"type": "number"}}, + "properties": { + "id": {"type": "number"}, + "quantity": {"type": "number"}, + }, }, }, "researchPartnership": {"type": "boolean"}, @@ -277,7 +303,7 @@ def get(self, request, siteId): responses=SiteSerializer, operation_id="site_update", ) - def patch(self, request, siteId): + def patch(self, request: Request, siteId): try: site = Site.objects.get(pk=siteId) except Site.DoesNotExist: @@ -286,10 +312,11 @@ def patch(self, request, siteId): asset = AssetSerializer(data=request.data) if not asset.is_valid(): return Response(data=asset.errors, status=status.HTTP_400_BAD_REQUEST) - asset = asset.save() + image = asset.save() site_type = Sitetype.objects.get(pk=request.data["siteType"]) - # (TODO) For the coordinates, we need to calculate the ddLat and ddLong and also use the Google API for the address + # (TODO) For the coordinates, we need to calculate the ddLat and ddLong + # and also use the Google API for the address coordinate = Coordinate.objects.create( dms_latitude=request.data["latitude"], dms_longitude=request.data["longitude"] ) @@ -299,7 +326,7 @@ def patch(self, request, siteId): serializer = SiteSerializer(site, data=request.data, partial=True) if serializer.is_valid(): site = serializer.save( - image=asset, + image=image, site_type=site_type, coordinate=coordinate, announcement=announcement, @@ -309,15 +336,19 @@ def patch(self, request, siteId): visible_map=json.loads(request.data["visibleMap"]), ) - for tree_type_json in request.data.getlist("species"): + # TODO: Are we sure about getlist ? + # If this is correct, consider raising an issue upstream + for tree_type_json in request.data.getlist("species"): # type:ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue] tree_type_obj = json.loads(tree_type_json) tree_type = Treetype.objects.get(pk=tree_type_obj["id"]) - Sitetreespecies.objects.create(site=site, tree_type=tree_type, quantity=tree_type_obj["quantity"]) + Sitetreespecies.objects.create( + site=site, tree_type=tree_type, quantity=tree_type_obj["quantity"] + ) return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @extend_schema(responses={status.HTTP_204_NO_CONTENT: None}, operation_id="site_delete") - def delete(self, request, siteId): + def delete(self, request: Request, siteId): try: site = Site.objects.get(pk=siteId) except Site.DoesNotExist: @@ -335,7 +366,7 @@ class SiteSocialDetailPublicStatusAPIView(APIView): responses=UpdateSitePublicStatusSerializer, operation_id="site_social_updatePublicStatus", ) - def patch(self, request, siteId): + def patch(self, request: Request, siteId): try: site = Site.objects.get(pk=siteId) except Site.DoesNotExist: @@ -358,7 +389,7 @@ def patch(self, request, siteId): class SiteSummaryListAPIView(APIView): @extend_schema(responses=SiteSummarySerializer(many=True), operation_id="site_summary_all") - def get(self, request): + def get(self, request: Request): sites = get_public_sites_unless_admin(request.user) plant_count = 0 survived_count = 0 @@ -381,7 +412,7 @@ class SiteSummaryDetailAPIView(APIView): permission_classes = (PublicSiteReadPermission,) @extend_schema(responses=SiteSummarySerializer, operation_id="site_summary") - def get(self, request, siteId): + def get(self, request: Request, siteId): try: site = Site.objects.get(pk=siteId) except Site.DoesNotExist: @@ -413,7 +444,7 @@ class SiteDetailAdminsAPIView(APIView): responses=SiteAdminSerializer(many=True), operation_id="site_updateAdmins", ) - def patch(self, request, siteId): + def patch(self, request: Request, siteId): try: site = Site.objects.get(pk=siteId) except Site.DoesNotExist: @@ -434,7 +465,7 @@ def patch(self, request, siteId): for existing_user in existing_admin_users: if existing_user not in updated_admin_users_list: - existing_site_admins.filter(user__id__exact=existing_user.pk).delete() # type: ignore + existing_site_admins.filter(user__id__exact=existing_user.pk).delete() serializer = SiteAdminSerializer(Siteadmin.objects.filter(site=site), many=True) return Response(serializer.data) @@ -442,7 +473,7 @@ def patch(self, request, siteId): class SiteFollowersAPIView(APIView): @extend_schema(responses={201: None}, operation_id="site_follow") - def post(self, request, siteId): + def post(self, request: Request, siteId): try: site = Site.objects.get(pk=siteId) except Site.DoesNotExist: @@ -454,10 +485,12 @@ def post(self, request, siteId): return Response(None, status=status.HTTP_201_CREATED) - return Response("Current user is already following this site", status=status.HTTP_400_BAD_REQUEST) + return Response( + "Current user is already following this site", status=status.HTTP_400_BAD_REQUEST + ) @extend_schema(operation_id="site_unfollow") - def delete(self, request, siteId): + def delete(self, request: Request, siteId): try: site_follower = SiteFollower.objects.get(site_id__exact=siteId, user=request.user) except SiteFollower.DoesNotExist: @@ -469,7 +502,7 @@ def delete(self, request, siteId): class SiteFollowersCurrentUserAPIView(APIView): @extend_schema(responses={200: bool}, operation_id="site_isFollowing") - def get(self, request, siteId): + def get(self, request: Request, siteId): try: site = Site.objects.get(pk=siteId) except Site.DoesNotExist: @@ -487,8 +520,10 @@ class AdminUserSitesAPIView(APIView): responses=AdminUserSitesSerializer(many=True), operation_id="admin-user-sites_all", ) - def get(self, request): - site_manager_users = User.objects.filter(role__name__iexact=RoleName.SITEMANAGER).order_by("username") + def get(self, request: Request): + site_manager_users = User.objects.filter(role__name__iexact=RoleName.SITEMANAGER).order_by( + "username" + ) serializer = AdminUserSitesSerializer(site_manager_users, many=True) return Response(serializer.data) @@ -496,8 +531,10 @@ def get(self, request): class SiteSocialDetailAPIView(APIView): permission_classes = (PublicSiteReadPermission,) - @extend_schema(request=SiteSocialSerializer, responses=SiteSocialSerializer, operation_id="site_social") - def get(self, request, siteId): + @extend_schema( + request=SiteSocialSerializer, responses=SiteSocialSerializer, operation_id="site_social" + ) + def get(self, request: Request, siteId): try: site = Site.objects.get(pk=siteId) except Site.DoesNotExist: @@ -516,25 +553,32 @@ class SiteMapListAPIView(APIView): permission_classes = (IsAuthenticatedOrReadOnly,) @extend_schema(responses=SiteMapSerializer(many=True), operation_id="site_map") - def get(self, request): + def get(self, request: Request): sites = get_public_sites_unless_admin(request.user) serializer = SiteMapSerializer(sites, many=True) return Response(serializer.data) -class PostListAPIView(APIView, PageNumberPagination): +# Incompatible "request" in base types +class PostListAPIView(APIView, PageNumberPagination): # type:ignore[misc] # pyright: ignore[reportIncompatibleVariableOverride] permission_classes = (IsAuthenticatedOrReadOnly,) @extend_schema( responses=PostPaginationSerializer, operation_id="post_all", parameters=[ - OpenApiParameter(name="siteId", type=OpenApiTypes.INT, many=True, location=OpenApiParameter.QUERY), - OpenApiParameter(name="page", type=OpenApiTypes.INT, required=True, location=OpenApiParameter.QUERY), - OpenApiParameter(name="size", type=OpenApiTypes.INT, required=True, location=OpenApiParameter.QUERY), + OpenApiParameter( + name="siteId", type=OpenApiTypes.INT, many=True, location=OpenApiParameter.QUERY + ), + OpenApiParameter( + name="page", type=OpenApiTypes.INT, required=True, location=OpenApiParameter.QUERY + ), + OpenApiParameter( + name="size", type=OpenApiTypes.INT, required=True, location=OpenApiParameter.QUERY + ), ], ) - def get(self, request): + def get(self, request: Request): site_ids = request.GET.getlist("siteId") posts = Post.objects.filter(site__in=site_ids) if site_ids else Post.objects.all() sorted_posts = posts.order_by("-created_at") @@ -542,8 +586,15 @@ def get(self, request): page = request.GET.get("page") size = request.GET.get("size") - if not isinstance(page, str) or not page.isnumeric() or not isinstance(size, str) or not size.isnumeric(): - return Response("Page and size are missing or invalid", status=status.HTTP_400_BAD_REQUEST) + if ( + not isinstance(page, str) + or not page.isnumeric() + or not isinstance(size, str) + or not size.isnumeric() + ): + return Response( + "Page and size are missing or invalid", status=status.HTTP_400_BAD_REQUEST + ) posts_paginator = Paginator(object_list=sorted_posts, per_page=int(size)) page_posts = posts_paginator.page(int(page)) @@ -557,7 +608,8 @@ def get(self, request): parser_classes = (MultiPartParser, FormParser) @extend_schema( - # request={"multipart/form-data": PostPostSerializer}, TODO: Add serializer for multipart/form-data + # TODO: Add serializer for multipart/form-data + # request={"multipart/form-data": PostPostSerializer}, request={ "multipart/form-data": { "type": "object", @@ -571,8 +623,10 @@ def get(self, request): responses={201: PostSerializer}, operation_id="post_create", ) - def post(self, request): - assets = request.data.getlist("media") + def post(self, request: Request): + # TODO: Are we sure about getlist ? + # If this is correct, consider raising an issue upstream + assets = request.data.getlist("media") # type:ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue] saved_assets = [] for asset_item in assets: q = QueryDict("", mutable=True) @@ -596,7 +650,7 @@ class PostDetailAPIView(APIView): permission_classes = (IsAuthenticatedOrReadOnly,) @extend_schema(responses=PostSerializer, operation_id="post_detail") - def get(self, request, postId): + def get(self, request: Request, postId): try: post = Post.objects.get(pk=postId) except Post.DoesNotExist: @@ -610,13 +664,17 @@ class CommentListAPIView(APIView): permission_classes = (IsAuthenticatedOrReadOnly,) @extend_schema(responses=CommentSerializer(many=True), operation_id="comment_all") - def get(self, request, postId): + def get(self, request: Request, postId): comments = Comment.objects.filter(post=postId).order_by("-created_at") serializer = CommentSerializer(comments, many=True) return Response(serializer.data) - @extend_schema(request=CreateCommentSerializer, responses={201: CommentSerializer}, operation_id="comment_create") - def post(self, request, postId): + @extend_schema( + request=CreateCommentSerializer, + responses={201: CommentSerializer}, + operation_id="comment_create", + ) + def post(self, request: Request, postId): try: post = Post.objects.get(pk=postId) user = User.objects.get(pk=request.user.id) @@ -635,7 +693,7 @@ class CommentDetailAPIView(APIView): permission_classes = (DeleteCommentPermission,) @extend_schema(operation_id="comment_delete") - def delete(self, request, postId, commentId): + def delete(self, request: Request, postId, commentId): try: comment = Comment.objects.get(pk=commentId) except Comment.DoesNotExist: @@ -647,8 +705,12 @@ def delete(self, request, postId, commentId): class AnnouncementDetailAPIView(APIView): - @extend_schema(request=AnnouncementSerializer, responses=AnnouncementSerializer, operation_id="announcement_update") - def patch(self, request, siteId): + @extend_schema( + request=AnnouncementSerializer, + responses=AnnouncementSerializer, + operation_id="announcement_update", + ) + def patch(self, request: Request, siteId): try: announcement = Announcement.objects.get(site=siteId) except Announcement.DoesNotExist: @@ -662,8 +724,10 @@ def patch(self, request, siteId): class ContactDetailAPIView(APIView): - @extend_schema(request=ContactSerializer, responses=ContactSerializer, operation_id="contact_update") - def patch(self, request, pk): + @extend_schema( + request=ContactSerializer, responses=ContactSerializer, operation_id="contact_update" + ) + def patch(self, request: Request, pk): try: contact = Contact.objects.get(pk=pk) except Contact.DoesNotExist: @@ -677,8 +741,10 @@ def patch(self, request, pk): class WidgetListAPIView(APIView): - @extend_schema(request=WidgetSerializer, responses={201: WidgetSerializer}, operation_id="widget_create") - def post(self, request): + @extend_schema( + request=WidgetSerializer, responses={201: WidgetSerializer}, operation_id="widget_create" + ) + def post(self, request: Request): serializer = WidgetSerializer(data=request.data) if serializer.is_valid(): serializer.save() @@ -687,8 +753,10 @@ def post(self, request): class WidgetDetailAPIView(APIView): - @extend_schema(request=WidgetSerializer, responses=WidgetSerializer, operation_id="widget_update") - def patch(self, request, pk): + @extend_schema( + request=WidgetSerializer, responses=WidgetSerializer, operation_id="widget_update" + ) + def patch(self, request: Request, pk): try: widget = Widget.objects.get(pk=pk) except Widget.DoesNotExist: @@ -701,7 +769,7 @@ def patch(self, request, pk): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @extend_schema(operation_id="widget_delete") - def delete(self, request, pk): + def delete(self, request: Request, pk): try: widget = Widget.objects.get(pk=pk) except Widget.DoesNotExist: @@ -713,7 +781,7 @@ def delete(self, request, pk): class LikeListAPIView(APIView): @extend_schema(request="", responses={201: LikeSerializer}, operation_id="like_likePost") - def post(self, request, postId): + def post(self, request: Request, postId): try: post = Post.objects.get(pk=postId) user = User.objects.get(pk=request.user.id) @@ -726,12 +794,9 @@ def post(self, request, postId): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @extend_schema(responses={201: LikeSerializer}, operation_id="like_delete") - def delete(self, request, postId): + def delete(self, request: Request, postId): try: post = Post.objects.get(pk=postId) - except Post.DoesNotExist: - return Response(status="sef") - try: like = Like.objects.get(post=post, user=request.user) except Like.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) @@ -742,13 +807,15 @@ def delete(self, request, postId): class BatchListAPIView(APIView): @extend_schema(responses=BatchAnalyticsSerializer(many=True), operation_id="batch_all") - def get(self, request): + def get(self, request: Request): batches = Batch.objects.all() serializer = BatchAnalyticsSerializer(batches, many=True) return Response(serializer.data) - @extend_schema(request=BatchSerializer, responses={201: BatchSerializer}, operation_id="batch_create") - def post(self, request): + @extend_schema( + request=BatchSerializer, responses={201: BatchSerializer}, operation_id="batch_create" + ) + def post(self, request: Request): serializer = BatchSerializer(data=request.data) if serializer.is_valid(): serializer.save() @@ -758,7 +825,7 @@ def post(self, request): class BatchDetailAPIView(APIView): @extend_schema(request=BatchSerializer, responses=BatchSerializer, operation_id="batch_update") - def patch(self, request, batchId): + def patch(self, request: Request, batchId): try: batch = Batch.objects.get(pk=batchId) except Batch.DoesNotExist: @@ -771,7 +838,7 @@ def patch(self, request, batchId): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @extend_schema(operation_id="batch_delete") - def delete(self, request, batchId): + def delete(self, request: Request, batchId): try: batch = Batch.objects.get(pk=batchId) except Batch.DoesNotExist: @@ -783,7 +850,7 @@ def delete(self, request, batchId): class UserListAPIView(APIView): @extend_schema(responses=UserSerializer(many=True), operation_id="user_all") - def get(self, request): + def get(self, request: Request): users = User.objects.all().order_by("username") serializer = UserSerializer(users, many=True) return Response(serializer.data) @@ -793,7 +860,7 @@ class SiteManagersListAPIView(APIView): permission_classes = (MegaAdminPermission,) @extend_schema(responses=UserSerializer(many=True), operation_id="user_allSiteManagers") - def get(self, request): + def get(self, request: Request): users = User.objects.filter(role__name__iexact=RoleName.SITEMANAGER).order_by("username") serializer = UserSerializer(users, many=True) return Response(serializer.data) @@ -803,7 +870,7 @@ class UserDetailAPIView(APIView): permission_classes = (CurrentUserPermission,) @extend_schema(request=UserSerializer, responses=UserSerializer, operation_id="user_detail") - def get(self, request, userId): + def get(self, request: Request, userId): try: user = User.objects.get(pk=userId) except User.DoesNotExist: @@ -813,8 +880,10 @@ def get(self, request, userId): serializer = UserSerializer(user) return Response(serializer.data) - @extend_schema(request=UpdateUserSerializer, responses=UserSerializer, operation_id="user_update") - def patch(self, request, userId): + @extend_schema( + request=UpdateUserSerializer, responses=UserSerializer, operation_id="user_update" + ) + def patch(self, request: Request, userId): try: user = User.objects.get(pk=userId) except User.DoesNotExist: @@ -833,9 +902,13 @@ def patch(self, request, userId): if is_valid is not True: return Response("CURRENT_PASSWORD_INVALID", status=status.HTTP_400_BAD_REQUEST) new_password = change_password_request["newPassword"] - new_password_confirmation = current_password = change_password_request["newPasswordConfirmation"] + new_password_confirmation = current_password = change_password_request[ + "newPasswordConfirmation" + ] if new_password != new_password_confirmation: - return Response("NEW_PASSWORDS_DO_NOT_MATCH", status=status.HTTP_400_BAD_REQUEST) + return Response( + "NEW_PASSWORDS_DO_NOT_MATCH", status=status.HTTP_400_BAD_REQUEST + ) user.set_password(new_password) user.save() @@ -848,7 +921,7 @@ def patch(self, request, userId): class UserCurrentUserAPIView(APIView): @extend_schema(responses=UserSerializer, operation_id="user_current") - def get(self, request): + def get(self, request: Request): serializer = UserSerializer(request.user) return Response(serializer.data) @@ -861,12 +934,12 @@ class UserInvitationListAPIView(APIView): responses=UserInvitationSerializer, operation_id="user-invitation_create", ) - def post(self, request): + def post(self, request: Request): site_ids = request.data.get("siteIds") if site_ids is None: return Response("SITE_IDS_INVALID", status=status.HTTP_400_BAD_REQUEST) email = request.data.get("email") - if User.objects.filter(email=email).exists(): + if not email or User.objects.filter(email=email).exists(): return Response("EMAIL_TAKEN", status=status.HTTP_400_BAD_REQUEST) code = secrets.token_urlsafe(32) user_invitation = UserInvitation.objects.create( @@ -888,7 +961,7 @@ class UserInvitationDetailAPIView(APIView): responses=UserInvitationSerializer, operation_id="user-invitation_detail", ) - def get(self, request, code: str): + def get(self, request: Request, code: str): try: user_invitation = UserInvitation.objects.get(code=code) except UserInvitation.DoesNotExist: @@ -900,20 +973,31 @@ def get(self, request, code: str): class TokenRefreshAPIView(APIView): @extend_schema(responses=RefreshToken, operation_id="token_refresh") - def post(self, request): + def post(self, request: Request): refresh = RefreshToken(request.data.get("refresh")) user = User.objects.get(pk=refresh["user_id"]) refresh["role"] = user.role.name - return Response({"refresh": str(refresh), "access": str(refresh.access_token)}, status=status.HTTP_200_OK) + return Response( + {"refresh": str(refresh), "access": str(refresh.access_token)}, + status=status.HTTP_200_OK, + ) class TokenObtainPairAPIView(APIView): @extend_schema(responses=UserSerializer, operation_id="token_obtain_pair") - def post(self, request): - user = cast(User, authenticate(username=request.data.get("username"), password=request.data.get("password"))) + def post(self, request: Request): + user = cast( + User, + authenticate( + username=request.data.get("username"), password=request.data.get("password") + ), + ) if user is not None: refresh = cast(RefreshToken, RefreshToken.for_user(user)) if user.role is not None: refresh["role"] = user.role.name - return Response({"refresh": str(refresh), "access": str(refresh.access_token)}, status=status.HTTP_200_OK) + return Response( + {"refresh": str(refresh), "access": str(refresh.access_token)}, + status=status.HTTP_200_OK, + ) return Response({"error": "Invalid credentials"}, status=status.HTTP_401_UNAUTHORIZED) diff --git a/canopeum_backend/pyproject.toml b/canopeum_backend/pyproject.toml index 368ce7755..e3e76ea78 100644 --- a/canopeum_backend/pyproject.toml +++ b/canopeum_backend/pyproject.toml @@ -1,7 +1,7 @@ # https://docs.astral.sh/ruff/configuration/ [tool.ruff] target-version = "py312" -line-length = 120 +line-length = 100 preview = true # Auto-generated exclude = ["canopeum_backend/migrations/*"] @@ -107,18 +107,17 @@ show_column_numbers = true implicit_reexport = true python_version = "3.12" exclude = [".venv/"] +# https://github.com/typeddjango/django-stubs/issues/579 +# https://github.com/typeddjango/django-stubs/issues/1264 +plugins = ["mypy_django_plugin.main"] -strict = false +strict = true # Implicit return types ! check_untyped_defs = true disallow_untyped_calls = false disallow_untyped_defs = false disallow_incomplete_defs = false -disable_error_code = [ - "return", # Implicit return types - "var-annotated", # Django models ClassVars not seen as annotated -] -# Note: mypy still has issues with some boolean infered returns like `is_valid_hwnd` +# Note: mypy still has issues with some boolean infered returns: # https://github.com/python/mypy/issues/4409 # https://github.com/python/mypy/issues/10149 @@ -126,3 +125,6 @@ disable_error_code = [ # Untyped dependencies module = ["rest_framework.*"] ignore_missing_imports = true + +[tool.django-stubs] +django_settings_module = "canopeum_backend.settings" diff --git a/canopeum_backend/requirements-dev.txt b/canopeum_backend/requirements-dev.txt index b8768c064..5419b2706 100644 --- a/canopeum_backend/requirements-dev.txt +++ b/canopeum_backend/requirements-dev.txt @@ -1,10 +1,10 @@ -r requirements.txt pre-commit==3.6.2 -ruff==0.3.4 # must match .pre-commit-config.yaml -mypy==1.9.0 -pyright==1.1.355 -# Stubs not yet available for 5.0: https://github.com/typeddjango/django-stubs/issues/2020 -django-stubs>=4.2.7 +ruff==0.4.3 # must match .pre-commit-config.yaml +mypy==1.10.0 +pyright==1.1.362 +django-stubs[compatible-mypy]>=5.0.0 +djangorestframework-stubs[compatible-mypy]>=3.14.0 # Not necessarily used directly, just taken from requirements.txt # that are also found in https://github.com/python/typeshed/tree/main/stubs types-colorama>=0.4.6 diff --git a/canopeum_backend/requirements.txt b/canopeum_backend/requirements.txt index 71d9410e5..19439aac8 100644 --- a/canopeum_backend/requirements.txt +++ b/canopeum_backend/requirements.txt @@ -3,29 +3,23 @@ asgiref==3.7.2 attrs==23.2.0 Babel==2.14.0 certifi==2024.2.2 -cfgv==3.4.0 charset-normalizer==3.3.2 click==8.1.7 colorama==0.4.6 cssbeautifier==1.15.1 -distlib==0.3.8 Django==5.0.3 django-cors-headers==4.3.1 -django-stubs==4.2.7 -django-stubs-ext==4.2.7 djangorestframework==3.14.0 djangorestframework-camel-case==1.4.2 djangorestframework-simplejwt==5.3.1 djlint==1.34.1 docutils==0.20.1 drf-orjson-renderer==1.7.2 -drf-spectacular==0.27.1 +drf-spectacular==0.27.2 drf-spectacular-sidecar==2024.3.4 EditorConfig==0.12.4 -filelock==3.13.1 html-tag-names==0.1.2 html-void-elements==0.1.0 -identify==2.5.35 idna==3.6 imagesize==1.4.1 inflection==0.5.1 @@ -35,27 +29,20 @@ json5==0.9.22 jsonschema==4.21.1 jsonschema-specifications==2023.12.1 MarkupSafe==2.0.1 -mypy==1.9.0 -mypy-extensions==1.0.0 mysqlclient==2.2.4 -nodeenv==1.8.0 orjson==3.9.15 packaging==23.2 parsimonious==0.10.0 pathspec==0.12.1 -platformdirs==4.2.0 -pre-commit==3.6.2 Pygments==2.17.2 pyjson5==1.6.6 PyJWT==2.8.0 -pyright==1.1.355 pytz==2024.1 PyYAML==6.0.1 referencing==0.34.0 regex==2023.12.25 requests==2.31.0 rpds-py==0.18.0 -ruff==0.3.4 setuptools==69.2.0 six==1.16.0 snowballstemmer==2.2.0 @@ -70,18 +57,6 @@ sphinxcontrib-qthelp==1.0.7 sphinxcontrib-serializinghtml==1.1.10 sqlparse==0.4.4 tqdm==4.66.2 -types-colorama==0.4.15.20240311 -types-docutils==0.20.0.20240317 -types-jsonschema==4.21.0.20240311 -types-pytz==2024.1.0.20240203 -types-PyYAML==6.0.12.20240311 -types-regex==2023.12.25.20240311 -types-requests==2.31.0.20240311 -types-setuptools==69.2.0.20240317 -types-six==1.16.21.20240311 -types-tqdm==4.66.0.20240106 -typing_extensions==4.10.0 tzdata==2024.1 uritemplate==4.1.1 urllib3==2.2.1 -virtualenv==20.25.1 diff --git a/canopeum_backend/scripts/checkers.py b/canopeum_backend/scripts/checkers.py index b3f6f784e..4000dce87 100644 --- a/canopeum_backend/scripts/checkers.py +++ b/canopeum_backend/scripts/checkers.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 from pathlib import Path -from subprocess import run # noqa: S404 -- Do not pass user input as arguments +from subprocess import run # noqa: S404 # Do not pass user input as arguments -path = (Path(__file__).parent.parent / "canopeum_backend").absolute() +path = (Path(__file__).parent.parent).absolute() +print(path) def main(): @@ -11,7 +12,7 @@ def main(): run(("ruff", "format", path), check=False) run(("ruff", "check", "--fix", path), check=False) print("\nRunning mypy...") - run(("mypy", path), check=False) + run(("mypy", path, "--config-file", path / "pyproject.toml"), check=False) print("\nRunning pyright...") run(("pyright", path), check=False) diff --git a/canopeum_frontend/.eslintrc.cjs b/canopeum_frontend/.eslintrc.cjs index 41a46b115..7d27106d3 100644 --- a/canopeum_frontend/.eslintrc.cjs +++ b/canopeum_frontend/.eslintrc.cjs @@ -1,6 +1,7 @@ /** @type {import("eslint").Linter.Config} */ module.exports = { root: true, + plugins: ['react-refresh'], extends: [ 'beslogic/react', 'beslogic/typescript', @@ -16,7 +17,6 @@ module.exports = { // Auto-generated 'src/services/api.ts', ], - plugins: ['react-refresh'], rules: { 'react-refresh/only-export-components': [ 'warn', diff --git a/dprint.json b/dprint.json index b12f84780..d2556cff7 100644 --- a/dprint.json +++ b/dprint.json @@ -14,6 +14,7 @@ // capacitor folders "**/*/android/app", "**/*/ios/App", + // specific to this project "**/*/services/api.ts" ] } diff --git a/pyrightconfig.json b/pyrightconfig.json index 6e8f96d8b..4018af55b 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -2,8 +2,11 @@ // https://github.com/microsoft/pyright/blob/main/docs/configuration.md#sample-pyprojecttoml-file { "pythonVersion": "3.12", - "include": ["canopeum_backend/"], - "typeCheckingMode" : "standard", + "include": ["canopeum_backend/canopeum_backend/"], + "typeCheckingMode": "standard", + "reportUnnecessaryTypeIgnoreComment": "error", + // Leave type: ignore to mypy + "enableTypeIgnoreComments": false // django-specific mypy plugin does a better job getting serializers data type of dict vs list - "reportAttributeAccessIssue" : "none", + // "reportAttributeAccessIssue": "none" }