From f49ebdf9470fef7479ae0c624bc956812ae515ef Mon Sep 17 00:00:00 2001 From: Samuel Therrien Date: Wed, 8 May 2024 18:10:17 -0400 Subject: [PATCH 01/10] Iron out type-checking and formatting --- .pre-commit-config.yaml | 4 +- README.md | 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 +++++++++++------- .../docker-compose.yml | 0 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 +- 17 files changed, 467 insertions(+), 301 deletions(-) rename docker-compose.yml => canopeum_backend/docker-compose.yml (100%) 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/README.md b/README.md index 867e2f1ac..554c33e19 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ For backend py -3.12 -m venv .venv ``` - Then activate the environemnt (you need to do this everytime if your editor isn't configured to do so): + Then activate the environment (you need to do this everytime if your editor isn't configured to do so): ```shell source .venv/scripts/activate @@ -83,8 +83,8 @@ For backend 5. Set up Django backend and Database: (Skip this section for Frontend only) ```shell - docker compose up cd canopeum_backend + docker compose up python -m pip install -r requirements-dev.txt python manage.py initialize_database python manage.py runserver 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..089bbac57 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 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/docker-compose.yml b/canopeum_backend/docker-compose.yml similarity index 100% rename from docker-compose.yml rename to canopeum_backend/docker-compose.yml 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" } From 0ce64fa70b6c273bf29745e3f84552ba1e48e6ea Mon Sep 17 00:00:00 2001 From: Samuel Therrien Date: Wed, 8 May 2024 20:51:06 -0400 Subject: [PATCH 02/10] Lint fix --- canopeum_backend/canopeum_backend/views.py | 4 ++-- canopeum_frontend/src/components/context/SnackbarContext.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/canopeum_backend/canopeum_backend/views.py b/canopeum_backend/canopeum_backend/views.py index 089bbac57..2d59948b8 100644 --- a/canopeum_backend/canopeum_backend/views.py +++ b/canopeum_backend/canopeum_backend/views.py @@ -225,7 +225,7 @@ def post(self, request: Request): image = asset.save() site_type = Sitetype.objects.get(pk=request.data["siteType"]) - # (TODO) For the coordinates, we need to calculate the ddLat and ddLong + # 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"] @@ -315,7 +315,7 @@ def patch(self, request: Request, siteId): image = asset.save() site_type = Sitetype.objects.get(pk=request.data["siteType"]) - # (TODO) For the coordinates, we need to calculate the ddLat and ddLong + # 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"] diff --git a/canopeum_frontend/src/components/context/SnackbarContext.tsx b/canopeum_frontend/src/components/context/SnackbarContext.tsx index 20f1bc55f..e145dbad0 100644 --- a/canopeum_frontend/src/components/context/SnackbarContext.tsx +++ b/canopeum_frontend/src/components/context/SnackbarContext.tsx @@ -91,9 +91,9 @@ const SnackbarContextProvider: FunctionComponent<{ readonly children?: ReactNode {messageInfo?.message} From b471407d96feef34b6bffa87f20bf443c623a88e Mon Sep 17 00:00:00 2001 From: Samuel T Date: Fri, 10 May 2024 15:23:17 -0400 Subject: [PATCH 03/10] Update canopeum_backend/scripts/checkers.py --- canopeum_backend/scripts/checkers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/canopeum_backend/scripts/checkers.py b/canopeum_backend/scripts/checkers.py index 4000dce87..a01dff1eb 100644 --- a/canopeum_backend/scripts/checkers.py +++ b/canopeum_backend/scripts/checkers.py @@ -4,7 +4,6 @@ from subprocess import run # noqa: S404 # Do not pass user input as arguments path = (Path(__file__).parent.parent).absolute() -print(path) def main(): From 7bb8e0b609ceeaa21ff176b869b8cc429dcab79a Mon Sep 17 00:00:00 2001 From: Samuel Therrien Date: Fri, 10 May 2024 15:39:14 -0400 Subject: [PATCH 04/10] Apply 100 chars with ruff --- .pre-commit-config.yaml | 4 +- .../commands/initialize_database.py | 61 +++++--- canopeum_backend/canopeum_backend/models.py | 16 +- .../canopeum_backend/permissions.py | 16 +- .../canopeum_backend/serializers.py | 12 +- canopeum_backend/canopeum_backend/urls.py | 44 ++++-- canopeum_backend/canopeum_backend/views.py | 142 ++++++++++++++---- canopeum_backend/pyproject.toml | 2 +- canopeum_backend/requirements-dev.txt | 2 +- canopeum_backend/requirements.txt | 1 - canopeum_backend/scripts/checkers.py | 25 +-- 11 files changed, 240 insertions(+), 85 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 59324c362..5ad9335af 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,9 +12,9 @@ repos: - id: check-toml - id: check-merge-conflict - id: check-case-conflict - # You can run this locally with `ruff format && ruff check` + # You can run this locally with `ruff check --fix || ruff format` - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 # must match canopeum_backend/requirements-dev.txt + rev: v0.4.4 # 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 ed784ba3a..36b889408 100644 --- a/canopeum_backend/canopeum_backend/management/commands/initialize_database.py +++ b/canopeum_backend/canopeum_backend/management/commands/initialize_database.py @@ -265,7 +265,12 @@ def handle(self, *args, **kwargs): assets_to_delete = Asset.objects.all().exclude(asset="site_img.png") try: for asset in assets_to_delete: - path = Path(settings.BASE_DIR) / "canopeum_backend" / "media" / asset.asset.name + path = ( + Path(settings.BASE_DIR) + / "canopeum_backend" + / "media" + / asset.asset.name + ) path.unlink(missing_ok=True) except ProgrammingError: # Catch old leftover tables that can't be deleted because they don't exist @@ -305,7 +310,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 = [ @@ -317,11 +324,15 @@ 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): 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 = [ @@ -333,7 +344,9 @@ 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" @@ -401,7 +414,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", @@ -421,7 +436,8 @@ 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!" + 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", ), @@ -429,9 +445,8 @@ def create_canopeum_site(self): create_batches_for_site(site) post = Post.objects.create( site=site, - body=""" - The season is officially started; new plants are starting to grow and our volunteers are very dedicated! - """, + body="The season is officially started; " + + "new plants are starting to grow and our volunteers are very dedicated!", share_count=5, created_at=timezone.now(), ) @@ -452,7 +467,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", @@ -460,8 +477,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, @@ -487,7 +504,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", @@ -508,8 +527,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", ), ) @@ -519,7 +540,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", @@ -527,8 +550,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, diff --git a/canopeum_backend/canopeum_backend/models.py b/canopeum_backend/canopeum_backend/models.py index 3dfeffb4e..f745c69ca 100644 --- a/canopeum_backend/canopeum_backend/models.py +++ b/canopeum_backend/canopeum_backend/models.py @@ -61,7 +61,9 @@ class FertilizertypeInternationalization(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 Batchfertilizer(models.Model): @@ -80,7 +82,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 BatchSpecies(models.Model): @@ -119,7 +123,9 @@ class Coordinate(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): @@ -204,7 +210,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): diff --git a/canopeum_backend/canopeum_backend/permissions.py b/canopeum_backend/canopeum_backend/permissions.py index fab0a7cc2..14cd603dc 100644 --- a/canopeum_backend/canopeum_backend/permissions.py +++ b/canopeum_backend/canopeum_backend/permissions.py @@ -10,7 +10,9 @@ def has_object_permission(self, 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 @@ -19,12 +21,16 @@ class PublicSiteReadPermission(permissions.BasePermission): # 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"): + 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): @@ -35,7 +41,9 @@ def has_object_permission(self, request, view, obj: Site) -> bool: # type: igno 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 MegaAdminOrSiteManagerPermission(permissions.BasePermission): diff --git a/canopeum_backend/canopeum_backend/serializers.py b/canopeum_backend/canopeum_backend/serializers.py index 467ddfdcd..2bbada47c 100644 --- a/canopeum_backend/canopeum_backend/serializers.py +++ b/canopeum_backend/canopeum_backend/serializers.py @@ -47,7 +47,9 @@ class Meta: class ChangePasswordSerializer(serializers.Serializer): 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: @@ -63,8 +65,12 @@ class Meta: 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())]) + 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) 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 80435e5c3..c70234967 100644 --- a/canopeum_backend/canopeum_backend/views.py +++ b/canopeum_backend/canopeum_backend/views.py @@ -105,7 +105,11 @@ def get_admin_sites(user: User): class LoginAPIView(APIView): permission_classes = (AllowAny,) - @extend_schema(request=LoginUserSerializer, responses=UserTokenSerializer, operation_id="authentication_login") + @extend_schema( + request=LoginUserSerializer, + responses=UserTokenSerializer, + operation_id="authentication_login", + ) def post(self, request): email = request.data.get("email") password = request.data.get("password") @@ -114,9 +118,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) @@ -127,10 +136,13 @@ 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 + # 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) @@ -144,7 +156,9 @@ def post(self, request): "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_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -185,7 +199,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", @@ -201,7 +216,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"}, @@ -243,7 +261,9 @@ def post(self, request): for tree_type_json in request.data.getlist("species"): 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) @@ -280,7 +300,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"}, @@ -327,7 +350,9 @@ def patch(self, request, siteId): for tree_type_json in request.data.getlist("species"): 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) @@ -464,7 +489,9 @@ 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): @@ -498,7 +525,9 @@ class AdminUserSitesAPIView(APIView): operation_id="admin-user-sites_all", ) def get(self, request): - site_manager_users = User.objects.filter(role__name__iexact=RoleName.SITEMANAGER).order_by("username") + 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) @@ -506,7 +535,9 @@ def get(self, request): class SiteSocialDetailAPIView(APIView): permission_classes = (PublicSiteReadPermission,) - @extend_schema(request=SiteSocialSerializer, responses=SiteSocialSerializer, operation_id="site_social") + @extend_schema( + request=SiteSocialSerializer, responses=SiteSocialSerializer, operation_id="site_social" + ) def get(self, request, siteId): try: site = Site.objects.get(pk=siteId) @@ -539,9 +570,15 @@ class PostListAPIView(APIView, PageNumberPagination): 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): @@ -552,8 +589,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)) @@ -567,7 +611,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", @@ -625,7 +670,11 @@ def get(self, request, postId): serializer = CommentSerializer(comments, many=True) return Response(serializer.data) - @extend_schema(request=CreateCommentSerializer, responses={201: CommentSerializer}, operation_id="comment_create") + @extend_schema( + request=CreateCommentSerializer, + responses={201: CommentSerializer}, + operation_id="comment_create", + ) def post(self, request, postId): try: post = Post.objects.get(pk=postId) @@ -657,7 +706,11 @@ def delete(self, request, postId, commentId): class AnnouncementDetailAPIView(APIView): - @extend_schema(request=AnnouncementSerializer, responses=AnnouncementSerializer, operation_id="announcement_update") + @extend_schema( + request=AnnouncementSerializer, + responses=AnnouncementSerializer, + operation_id="announcement_update", + ) def patch(self, request, siteId): try: announcement = Announcement.objects.get(site=siteId) @@ -672,7 +725,9 @@ def patch(self, request, siteId): class ContactDetailAPIView(APIView): - @extend_schema(request=ContactSerializer, responses=ContactSerializer, operation_id="contact_update") + @extend_schema( + request=ContactSerializer, responses=ContactSerializer, operation_id="contact_update" + ) def patch(self, request, pk): try: contact = Contact.objects.get(pk=pk) @@ -687,7 +742,9 @@ def patch(self, request, pk): class WidgetListAPIView(APIView): - @extend_schema(request=WidgetSerializer, responses={201: WidgetSerializer}, operation_id="widget_create") + @extend_schema( + request=WidgetSerializer, responses={201: WidgetSerializer}, operation_id="widget_create" + ) def post(self, request): serializer = WidgetSerializer(data=request.data) if serializer.is_valid(): @@ -697,7 +754,9 @@ def post(self, request): class WidgetDetailAPIView(APIView): - @extend_schema(request=WidgetSerializer, responses=WidgetSerializer, operation_id="widget_update") + @extend_schema( + request=WidgetSerializer, responses=WidgetSerializer, operation_id="widget_update" + ) def patch(self, request, pk): try: widget = Widget.objects.get(pk=pk) @@ -757,7 +816,9 @@ def get(self, request): serializer = BatchAnalyticsSerializer(batches, many=True) return Response(serializer.data) - @extend_schema(request=BatchSerializer, responses={201: BatchSerializer}, operation_id="batch_create") + @extend_schema( + request=BatchSerializer, responses={201: BatchSerializer}, operation_id="batch_create" + ) def post(self, request): serializer = BatchSerializer(data=request.data) if serializer.is_valid(): @@ -823,7 +884,9 @@ def get(self, request, userId): serializer = UserSerializer(user) return Response(serializer.data) - @extend_schema(request=UpdateUserSerializer, responses=UserSerializer, operation_id="user_update") + @extend_schema( + request=UpdateUserSerializer, responses=UserSerializer, operation_id="user_update" + ) def patch(self, request, userId): try: user = User.objects.get(pk=userId) @@ -843,9 +906,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() @@ -914,16 +981,27 @@ def post(self, 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"))) + 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..133e4609b 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/*"] diff --git a/canopeum_backend/requirements-dev.txt b/canopeum_backend/requirements-dev.txt index b8768c064..350dd7143 100644 --- a/canopeum_backend/requirements-dev.txt +++ b/canopeum_backend/requirements-dev.txt @@ -1,6 +1,6 @@ -r requirements.txt pre-commit==3.6.2 -ruff==0.3.4 # must match .pre-commit-config.yaml +ruff==0.4.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 diff --git a/canopeum_backend/requirements.txt b/canopeum_backend/requirements.txt index 71d9410e5..25ff89a62 100644 --- a/canopeum_backend/requirements.txt +++ b/canopeum_backend/requirements.txt @@ -55,7 +55,6 @@ 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 diff --git a/canopeum_backend/scripts/checkers.py b/canopeum_backend/scripts/checkers.py index b3f6f784e..cf6a31a08 100644 --- a/canopeum_backend/scripts/checkers.py +++ b/canopeum_backend/scripts/checkers.py @@ -1,19 +1,26 @@ #!/usr/bin/env python3 +from collections.abc import Sequence 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 +from typing import TYPE_CHECKING -path = (Path(__file__).parent.parent / "canopeum_backend").absolute() +if TYPE_CHECKING: + from _typeshed import StrOrBytesPath + +path = (Path(__file__).parent.parent).absolute() + + +def run_command(command: Sequence["StrOrBytesPath"]): + print(f"\nRunning: {" ".join(str(arg) for arg in command)}") + run(command, check=False) def main(): - print("\nRunning Ruff...") - run(("ruff", "format", path), check=False) - run(("ruff", "check", "--fix", path), check=False) - print("\nRunning mypy...") - run(("mypy", path), check=False) - print("\nRunning pyright...") - run(("pyright", path), check=False) + run_command(("ruff", "check", path, "--fix")) + run_command(("ruff", "format", path)) + run_command(("mypy", path, "--config-file", path / "pyproject.toml")) + run_command(("pyright", path)) if __name__ == "__main__": From 5dbb3b1692094a02804ccf333092fad44de97661 Mon Sep 17 00:00:00 2001 From: Samuel Therrien Date: Fri, 10 May 2024 15:55:43 -0400 Subject: [PATCH 05/10] settings usage --- .../canopeum_backend/management/commands/initialize_database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canopeum_backend/canopeum_backend/management/commands/initialize_database.py b/canopeum_backend/canopeum_backend/management/commands/initialize_database.py index 9a77f9e4a..dadbdecbb 100644 --- a/canopeum_backend/canopeum_backend/management/commands/initialize_database.py +++ b/canopeum_backend/canopeum_backend/management/commands/initialize_database.py @@ -266,7 +266,7 @@ def handle(self, *args, **kwargs): try: for asset in assets_to_delete: path = ( - Path(settings.BASE_DIR) + Path(canopeum_backend.settings.BASE_DIR) / "canopeum_backend" / "media" / asset.asset.name From d034823099426f54e43e85527a9b927410604f8f Mon Sep 17 00:00:00 2001 From: Samuel Therrien Date: Wed, 15 May 2024 14:53:13 -0400 Subject: [PATCH 06/10] Fixes post merges --- .../migrations/0002_alter_batch_size.py | 18 ++++++++ canopeum_backend/canopeum_backend/models.py | 2 +- .../canopeum_backend/serializers.py | 3 +- canopeum_backend/canopeum_backend/urls.py | 1 - canopeum_backend/canopeum_backend/views.py | 7 --- canopeum_backend/requirements-dev.txt | 2 +- canopeum_backend/scripts/checkers.py | 12 +++--- canopeum_frontend/canopeum-mockoon.json | 34 --------------- canopeum_frontend/src/services/api.ts | 43 ------------------- 9 files changed, 29 insertions(+), 93 deletions(-) create mode 100644 canopeum_backend/canopeum_backend/migrations/0002_alter_batch_size.py diff --git a/canopeum_backend/canopeum_backend/migrations/0002_alter_batch_size.py b/canopeum_backend/canopeum_backend/migrations/0002_alter_batch_size.py new file mode 100644 index 000000000..a84041379 --- /dev/null +++ b/canopeum_backend/canopeum_backend/migrations/0002_alter_batch_size.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.3 on 2024-05-15 18:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("canopeum_backend", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="batch", + name="size", + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/canopeum_backend/canopeum_backend/models.py b/canopeum_backend/canopeum_backend/models.py index ee8a54e6a..de021b90d 100644 --- a/canopeum_backend/canopeum_backend/models.py +++ b/canopeum_backend/canopeum_backend/models.py @@ -61,7 +61,7 @@ class Batch(models.Model): updated_at = models.DateTimeField(blank=True, null=True) name = models.TextField(blank=True, null=True) sponsor = models.TextField(blank=True, null=True) - size = models.TextField(blank=True, null=True) + size = models.IntegerField(blank=True, null=True) soil_condition = models.TextField(blank=True, null=True) total_number_seed = models.IntegerField(blank=True, null=True) total_propagation = models.IntegerField(blank=True, null=True) diff --git a/canopeum_backend/canopeum_backend/serializers.py b/canopeum_backend/canopeum_backend/serializers.py index b4244ab99..13221fc4b 100644 --- a/canopeum_backend/canopeum_backend/serializers.py +++ b/canopeum_backend/canopeum_backend/serializers.py @@ -170,7 +170,8 @@ class Meta: fields = "__all__" -class InternationalizationSerializer(serializers.ModelSerializer[Internationalization]): +# Any: Accepts any model with "en" and "fr" fields. Unfortunately can't use protocols here +class InternationalizationSerializer(serializers.ModelSerializer[Any]): class Meta: model = Internationalization fields = ("en", "fr") diff --git a/canopeum_backend/canopeum_backend/urls.py b/canopeum_backend/canopeum_backend/urls.py index 400ccae0f..10e40369b 100644 --- a/canopeum_backend/canopeum_backend/urls.py +++ b/canopeum_backend/canopeum_backend/urls.py @@ -11,7 +11,6 @@ path("admin/", admin.site.urls), # Auth path("auth/login/", views.LoginAPIView.as_view(), name="login"), - path("auth/logout/", views.LogoutAPIView.as_view(), name="logout"), path("auth/register/", views.RegisterAPIView.as_view(), name="register"), # Post path("social/posts/", views.PostListAPIView.as_view(), name="post-list"), diff --git a/canopeum_backend/canopeum_backend/views.py b/canopeum_backend/canopeum_backend/views.py index cce1d507f..ee7cd8f9b 100644 --- a/canopeum_backend/canopeum_backend/views.py +++ b/canopeum_backend/canopeum_backend/views.py @@ -165,13 +165,6 @@ def post(self, request: Request): 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: 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: Request): diff --git a/canopeum_backend/requirements-dev.txt b/canopeum_backend/requirements-dev.txt index 244756610..2d828e242 100644 --- a/canopeum_backend/requirements-dev.txt +++ b/canopeum_backend/requirements-dev.txt @@ -2,7 +2,7 @@ pre-commit==3.6.2 ruff==0.4.4 # must match .pre-commit-config.yaml mypy==1.10.0 -pyright==1.1.362 +pyright==1.1.363 django-stubs[compatible-mypy]>=5.0.0 djangorestframework-stubs[compatible-mypy]>=3.14.0 # Not necessarily used directly, just taken from requirements.txt diff --git a/canopeum_backend/scripts/checkers.py b/canopeum_backend/scripts/checkers.py index cf6a31a08..d9af6814e 100644 --- a/canopeum_backend/scripts/checkers.py +++ b/canopeum_backend/scripts/checkers.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import os from collections.abc import Sequence from pathlib import Path from subprocess import run # noqa: S404 # Do not pass user input as arguments @@ -8,7 +9,8 @@ if TYPE_CHECKING: from _typeshed import StrOrBytesPath -path = (Path(__file__).parent.parent).absolute() +backend_root = (Path(__file__).parent.parent).absolute() +os.chdir(backend_root) def run_command(command: Sequence["StrOrBytesPath"]): @@ -17,10 +19,10 @@ def run_command(command: Sequence["StrOrBytesPath"]): def main(): - run_command(("ruff", "check", path, "--fix")) - run_command(("ruff", "format", path)) - run_command(("mypy", path, "--config-file", path / "pyproject.toml")) - run_command(("pyright", path)) + run_command(("ruff", "check", "--fix")) + run_command(("ruff", "format")) + run_command(("mypy", backend_root)) + run_command(("pyright",)) if __name__ == "__main__": diff --git a/canopeum_frontend/canopeum-mockoon.json b/canopeum_frontend/canopeum-mockoon.json index f9daa6dd2..e8d8c161d 100644 --- a/canopeum_frontend/canopeum-mockoon.json +++ b/canopeum_frontend/canopeum-mockoon.json @@ -269,40 +269,6 @@ ], "responseMode": null }, - { - "uuid": "6ad66afe-e470-4c14-b22b-c782220e851d", - "type": "http", - "documentation": "", - "method": "post", - "endpoint": "auth/logout", - "responses": [ - { - "uuid": "30951aaa-f289-4555-abcf-728fac4895e1", - "body": "{}", - "latency": 0, - "statusCode": 200, - "label": "", - "headers": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "bodyType": "INLINE", - "filePath": "", - "databucketID": "", - "sendFileAsBody": false, - "rules": [], - "rulesOperator": "OR", - "disableTemplating": false, - "fallbackTo404": false, - "default": true, - "crudKey": "id", - "callbacks": [] - } - ], - "responseMode": null - }, { "uuid": "c2e02ae6-7616-4de1-9ea2-358651b10223", "type": "http", diff --git a/canopeum_frontend/src/services/api.ts b/canopeum_frontend/src/services/api.ts index ebd24bc37..5723abce7 100644 --- a/canopeum_frontend/src/services/api.ts +++ b/canopeum_frontend/src/services/api.ts @@ -1037,49 +1037,6 @@ export class AuthenticationClient { return Promise.resolve(null as any); } - logout(): Promise<{ [key: string]: any; }> { - let url_ = this.baseUrl + "/auth/logout/"; - url_ = url_.replace(/[?&]$/, ""); - - let options_: RequestInit = { - method: "POST", - headers: { - "Accept": "application/json" - } - }; - - return this.http.fetch(url_, options_).then((_response: Response) => { - return this.processLogout(_response); - }); - } - - protected processLogout(response: Response): Promise<{ [key: string]: any; }> { - const status = response.status; - let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; - if (status === 200) { - return response.text().then((_responseText) => { - let result200: any = null; - let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); - if (resultData200) { - result200 = {} as any; - for (let key in resultData200) { - if (resultData200.hasOwnProperty(key)) - (result200)![key] = resultData200[key] !== undefined ? resultData200[key] : null; - } - } - else { - result200 = null; - } - return result200; - }); - } else if (status !== 200 && status !== 204) { - return response.text().then((_responseText) => { - return throwException("An unexpected server error occurred.", status, _responseText, _headers); - }); - } - return Promise.resolve<{ [key: string]: any; }>(null as any); - } - register(body: RegisterUser): Promise { let url_ = this.baseUrl + "/auth/register/"; url_ = url_.replace(/[?&]$/, ""); From e9d251d0add1aa566638b54b7d8e2d955ceaca40 Mon Sep 17 00:00:00 2001 From: Samuel Therrien Date: Wed, 15 May 2024 14:54:56 -0400 Subject: [PATCH 07/10] Remove unused auth_token --- canopeum_backend/canopeum_backend/models.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/canopeum_backend/canopeum_backend/models.py b/canopeum_backend/canopeum_backend/models.py index de021b90d..ed4fcc3a7 100644 --- a/canopeum_backend/canopeum_backend/models.py +++ b/canopeum_backend/canopeum_backend/models.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Any, ClassVar +from typing import TYPE_CHECKING, ClassVar import pytz from django.contrib.auth.models import AbstractUser @@ -45,9 +45,6 @@ class User(AbstractUser): 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): From f3832fae9a5d01977425b84719ec49f3d901259d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 18:59:49 +0000 Subject: [PATCH 08/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../migrations/0002_alter_batch_size.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/canopeum_backend/canopeum_backend/migrations/0002_alter_batch_size.py b/canopeum_backend/canopeum_backend/migrations/0002_alter_batch_size.py index a84041379..5cb0e3a56 100644 --- a/canopeum_backend/canopeum_backend/migrations/0002_alter_batch_size.py +++ b/canopeum_backend/canopeum_backend/migrations/0002_alter_batch_size.py @@ -1,18 +1,18 @@ -# Generated by Django 5.0.3 on 2024-05-15 18:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("canopeum_backend", "0001_initial"), - ] - - operations = [ - migrations.AlterField( - model_name="batch", - name="size", - field=models.IntegerField(blank=True, null=True), - ), - ] +# Generated by Django 5.0.3 on 2024-05-15 18:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("canopeum_backend", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="batch", + name="size", + field=models.IntegerField(blank=True, null=True), + ), + ] From 0025f0e90c0722cd1021cfbff67f59e675724010 Mon Sep 17 00:00:00 2001 From: Samuel Therrien Date: Wed, 15 May 2024 15:03:03 -0400 Subject: [PATCH 09/10] Use mypy_drf_plugin.main --- canopeum_backend/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canopeum_backend/pyproject.toml b/canopeum_backend/pyproject.toml index e3e76ea78..fc49da1d4 100644 --- a/canopeum_backend/pyproject.toml +++ b/canopeum_backend/pyproject.toml @@ -109,7 +109,7 @@ 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"] +plugins = ["mypy_django_plugin.main", "mypy_drf_plugin.main"] strict = true # Implicit return types ! From ebb2707a3ac8665753e83ef128348a1d7adbe1da Mon Sep 17 00:00:00 2001 From: Samuel Therrien Date: Wed, 15 May 2024 15:05:26 -0400 Subject: [PATCH 10/10] Missing spaces --- .../management/commands/initialize_database.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/canopeum_backend/canopeum_backend/management/commands/initialize_database.py b/canopeum_backend/canopeum_backend/management/commands/initialize_database.py index dadbdecbb..6051fd05a 100644 --- a/canopeum_backend/canopeum_backend/management/commands/initialize_database.py +++ b/canopeum_backend/canopeum_backend/management/commands/initialize_database.py @@ -145,7 +145,7 @@ 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." + body=f"{site.name} has planted {random.randint(100, 1000)} new trees today. " + "Let's continue to grow our forest!", share_count=share_count, ) @@ -439,7 +439,7 @@ 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!" + + "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", ), @@ -563,9 +563,9 @@ 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" + 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", ),