diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..0eeacd6e1 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,16 @@ + + +- [ ] The project passes automated tests (build, linting, etc.). +- [ ] You updated the project's documentation with new changes. +- [ ] You've [linked](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) any issue this PR closes +- [ ] You reviewed your own PR and made sure there's no test/debug code or any obvious mistakes. + +Make sure that the code wasn't copied from elsewhere (check one): + +- [ ] This is your own original code +- [ ] You have made sure that we have permission to use the copied code and that we follow its licensing + +_Add a description of your changes, including visual aid and useful links_ diff --git a/.github/workflows/canopeum_backend.yml b/.github/workflows/canopeum_backend_pr_validation.yml similarity index 69% rename from .github/workflows/canopeum_backend.yml rename to .github/workflows/canopeum_backend_pr_validation.yml index 998dcd612..10fe0e073 100644 --- a/.github/workflows/canopeum_backend.yml +++ b/.github/workflows/canopeum_backend_pr_validation.yml @@ -1,4 +1,4 @@ -name: canopeum_backend +name: Backend PR validation on: push: @@ -6,14 +6,14 @@ on: - main paths: - "canopeum_backend/**" - - ".github/workflows/canopeum_backend.yml" + - ".github/workflows/canopeum_backend_pr_validation.yml" pull_request: branches: - main - production paths: - "canopeum_backend/**" - - ".github/workflows/canopeum_backend.yml" + - ".github/workflows/canopeum_backend_pr_validation.yml" env: # Since the Django mypy extention RUNS the config file, we need a non-empty secret to avoid @@ -21,6 +21,19 @@ env: SECRET_KEY_DJANGO_CANOPEUM: mypy-ext jobs: + django-test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: canopeum_backend + steps: + - uses: actions/checkout@v4 + - name: Install uv using the standalone installer + run: curl -LsSf https://astral.sh/uv/install.sh | sh + - run: uv sync --locked --extra dev + - run: echo "$PWD/.venv/bin" >> $GITHUB_PATH + - name: Run Django Tests + run: python manage.py test mypy: runs-on: ubuntu-latest defaults: diff --git a/.github/workflows/canopeum_frontend.yml b/.github/workflows/canopeum_frontend_pr_validation.yml similarity index 80% rename from .github/workflows/canopeum_frontend.yml rename to .github/workflows/canopeum_frontend_pr_validation.yml index d76ffa34f..e3563610e 100644 --- a/.github/workflows/canopeum_frontend.yml +++ b/.github/workflows/canopeum_frontend_pr_validation.yml @@ -1,4 +1,4 @@ -name: canopeum_frontend +name: Frontend PR validation on: push: @@ -6,14 +6,14 @@ on: - main paths: - "canopeum_frontend/**" - - ".github/workflows/canopeum_frontend.yml" + - ".github/workflows/canopeum_frontend_pr_validation.yml" pull_request: branches: - main - production paths: - "canopeum_frontend/**" - - ".github/workflows/canopeum_frontend.yml" + - ".github/workflows/canopeum_frontend_pr_validation.yml" jobs: Lint: diff --git a/canopeum_backend/canopeum_backend/management/commands/initialize_database.py b/canopeum_backend/canopeum_backend/management/commands/initialize_database.py index fc3d20310..648120c19 100644 --- a/canopeum_backend/canopeum_backend/management/commands/initialize_database.py +++ b/canopeum_backend/canopeum_backend/management/commands/initialize_database.py @@ -1,6 +1,7 @@ # flake8: noqa: S311 -- Accept random int generation for database seeding import random +from collections.abc import Iterable from datetime import timedelta from pathlib import Path @@ -17,6 +18,7 @@ Batch, Batchfertilizer, BatchSpecies, + BatchSponsor, Comment, Contact, Coordinate, @@ -28,6 +30,7 @@ Role, Site, Siteadmin, + Sitetreespecies, Sitetype, SitetypeInternationalization, TreespeciestypeInternationalization, @@ -35,6 +38,10 @@ User, ) +seeding_images_path = ( + Path(canopeum_backend.settings.BASE_DIR) / "canopeum_backend" / "seeding" / "images" +) + tree_types = [ ["Balsam Fir", "Sapin baumier"], ["Black Maple", "Érable noir"], @@ -192,74 +199,126 @@ def create_batch_species_for_batch(batch): "Eight Batch", ] -sponsors = [ - "GreenGrow Solutions", - "ArborWorks", - "Evergreen Eco", - "Rooted Reforestation", - "TreeTech Innovations", - "Forest Guardians", - "LeafyLife Planting", - "EcoArbor", - "Sapling Services", - "Birch & Pine Planters", - "Foliage Force", - "Canopy Creations", - "GrowGreen Reforestation", +sponsor_names = [ + "Green Earth Initiative", + "EcoRoots Corporation", + "Sustainable Growth Group", + "Forest Futures Fund", + "Green Horizons Alliance", + "Tree of Life Foundation", + "Evergreen Solutions", + "Leaf Legacy Group", + "Planet Guardians Inc.", + "Roots of Tomorrow", + "Nature's Canopy Collective", + "Oxygen for All Co.", + "Verdant Ventures", + "ForestFlow Enterprises", + "Seedling Sustainability Co.", + "Pure Green Partners", + "Branch Out Initiative", + "Nature Nurturers", + "Earthwise Ecosystems", + "Renewed Forest Foundation", + "EcoSphere Solutions", + "TreeMendously Green", + "Reforest Co.", + "Grow Green Initiative", + "Earthshade Sponsors", + "GreenBreathe Fund", + "Flourishing Forest Foundation", + "Sapling Supporters Co.", + "Global Green Guardians", "Woodland Warriors", - "BranchOut Planting", - "EcoTree Partners", - "Grove Guardians", - "Sustainable Sowers", - "TimberTech Planting", - "Nature's Nurseries", - "GreenThumb Reforestation", - "Forest Frontiers", - "Arbor Alliance", - "TreeTrek Planters", - "RootRise Reforestation", - "LeafLegacy Planting", - "EcoRoot Reforestation", - "TrunkTrack Planters", - "WoodWise Reforestation", - "Boreal Bloom Planters", + "Sprout Sponsors", + "Evergreen Tomorrow Foundation", + "Roots & Shoots Initiative", + "TreeTop Trust", + "LeafLife Partners", + "The Canopy Collective", + "EcoLeaf Sponsorship", + "Pure Planet Patrons", + "Oxygen Origins Fund", + "The GreenWay Project", + "TreeTrail Trust", + "ThriveGreen Group", + "ForestFront Alliance", + "ReTree Sponsors", + "EcoPledge Foundation", + "Verdant Visions Fund", + "BranchRoots Collective", + "PlanetGreen Partnerships", + "SustainSeed Co.", + "NatureSprout Network", + "GreenFuture Trust", + "TreeLife Alliance", ] -def get_sponsors(): - number_of_sponsors = random.randint(1, 5) - random.shuffle(sponsors) - return sponsors[:number_of_sponsors] +def create_sponsor_for_batch(): + image_file_name = f"batch_logo{random.randint(1, 7)}.png" + with Path.open(seeding_images_path / image_file_name, "rb") as img_file: + django_file = File(img_file) -def get_sponsor(): - index = random.randint(0, len(sponsors) - 1) - return sponsors[index] + asset = Asset() + asset.asset.save(image_file_name, django_file, save=True) + + return BatchSponsor.objects.create( + name=sponsor_names.pop(random.randint(0, len(sponsor_names) - 1)), + url="https://uilogos.co/", + logo=asset, + ) + + +def create_species_for_site(site: Site, batches: Iterable[Batch]): + already_added_tree_type: dict[int, Sitetreespecies] = {} + for batch in batches: + for batch_specie in BatchSpecies.objects.filter(batch=batch): + quantity = batch_specie.quantity + # Add more to the site's quantity than the batches' quantity + # so they don't appear at 100%. Except Canopeum, let's use it as a 100% example + if site.name != "Canopeum": + quantity += random.randint(0, 50) + if batch_specie.tree_type.pk in already_added_tree_type: + site_tree_specie = already_added_tree_type[batch_specie.tree_type.pk] + site_tree_specie.quantity += quantity + site_tree_specie.save() + else: + site_tree_specie = Sitetreespecies.objects.create( + site=site, tree_type=batch_specie.tree_type, quantity=quantity + ) + already_added_tree_type[batch_specie.tree_type.pk] = site_tree_specie def create_batches_for_site(site): num_batches = random.randint(3, 8) for i in range(num_batches): number_of_seed = random.randint(50, 200) - plant_count = random.randint(0, number_of_seed) - survived_count = random.randint(0, plant_count) + survived_count = random.randint(100, 200) + replace_count = random.randint(0, 50) + + sponsor = create_sponsor_for_batch() + batch = Batch.objects.create( name=batch_names[i - 1], site=site, size=random.randint(20, 150), - sponsor=get_sponsor(), + sponsor=sponsor, soil_condition="Good", - plant_count=plant_count, survived_count=survived_count, - replace_count=plant_count - survived_count, + replace_count=replace_count, total_number_seed=number_of_seed, total_propagation=random.randint(0, number_of_seed), ) create_batch_species_for_batch(batch) - Batchfertilizer.objects.create( - batch=batch, - fertilizer_type=Fertilizertype.objects.first(), - ) + fertilizer_type = Fertilizertype.objects.first() + if fertilizer_type is not None: + Batchfertilizer.objects.create( + batch=batch, + fertilizer_type=fertilizer_type, + ) + yield batch class Command(BaseCommand): @@ -315,8 +374,7 @@ def handle(self, *args, **kwargs): self.create_roles() self.create_users() - self.create_canopeum_site() - self.create_other_sites() + self.create_sites() self.create_siteadmins() self.stdout.write(self.style.SUCCESS("Data Generated")) @@ -382,9 +440,6 @@ def create_site_types(self): ) def create_assets(self): - seeding_images_path = ( - Path(canopeum_backend.settings.BASE_DIR) / "canopeum_backend" / "seeding" / "images" - ) image_file_names = ( "site_img1.png", "site_img2.jpg", @@ -445,8 +500,9 @@ def create_users(self): role=Role.objects.get(name="User"), ) - def create_canopeum_site(self): - site = Site.objects.create( + def create_sites(self): + # Canopeum's site + site1 = Site.objects.create( name="Canopeum", is_public=True, site_type=Sitetype.objects.get( @@ -466,7 +522,7 @@ def create_canopeum_site(self): visitor_count=100, contact=Contact.objects.create( email="info@canopeum.com", - phone="+1 (514) 741-5008", + phone="+1 514 741-5008", address="721 Walker avenue, Office 200 Montréal, QC H4C 2H5", ), image=Asset.objects.first(), @@ -477,15 +533,16 @@ def create_canopeum_site(self): link="https://www.canopeum-pos.com", ), ) - create_batches_for_site(site) + batches = create_batches_for_site(site1) + create_species_for_site(site1, batches) post = Post.objects.create( - site=site, + site=site1, body="The season is officially started; " + "new plants are starting to grow and our volunteers are very dedicated!", share_count=5, ) post.media.add(*Asset.objects.filter(asset__contains="canopeum_post_img")) - create_posts_for_site(site) + create_posts_for_site(site1) Comment.objects.create( body="Wow, I'm very excited to join the team!", user=User.objects.get(email="tyrion@lannister.com"), @@ -496,8 +553,8 @@ def create_canopeum_site(self): user=User.objects.get(email="normal@user.com"), post=post, ) + # end of Canopeum's site - def create_other_sites(self): site_2 = Site.objects.create( name="Maple Grove Retreat", is_public=True, @@ -530,7 +587,8 @@ def create_other_sites(self): link="https://www.maplegroveretreat.com/events/maple-syrup-festival", ), ) - create_batches_for_site(site_2) + batches = create_batches_for_site(site_2) + create_species_for_site(site_2, batches) create_posts_for_site(site_2) site_3 = Site.objects.create( @@ -566,7 +624,8 @@ def create_other_sites(self): link="https://www.lakesideoasis.com/winter-getaway", ), ) - create_batches_for_site(site_3) + batches = create_batches_for_site(site_3) + create_species_for_site(site_3, batches) create_posts_for_site(site_3) site_4 = Site.objects.create( @@ -602,7 +661,8 @@ def create_other_sites(self): link="https://www.evergreentrail.com/guided-walks", ), ) - create_batches_for_site(site_4) + batches = create_batches_for_site(site_4) + create_species_for_site(site_4, batches) create_posts_for_site(site_4) def create_siteadmins(self): diff --git a/canopeum_backend/canopeum_backend/media/.gitkeep b/canopeum_backend/canopeum_backend/media/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/canopeum_backend/canopeum_backend/migrations/0001_initial.py b/canopeum_backend/canopeum_backend/migrations/0001_initial.py index 73936bcd2..365ee675b 100644 --- a/canopeum_backend/canopeum_backend/migrations/0001_initial.py +++ b/canopeum_backend/canopeum_backend/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.3 on 2024-05-28 19:03 +# Generated by Django 5.1 on 2024-09-30 16:29 import canopeum_backend.models import django.contrib.auth.models @@ -33,20 +33,6 @@ class Migration(migrations.Migration): ('asset', models.FileField(upload_to=canopeum_backend.models.upload_to)), ], ), - migrations.CreateModel( - name='Batch', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(blank=True, null=True)), - ('updated_at', models.DateTimeField(blank=True, null=True)), - ('name', models.TextField(blank=True, null=True)), - ('sponsor', 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)), - ], - ), migrations.CreateModel( name='Contact', fields=[ @@ -71,12 +57,6 @@ class Migration(migrations.Migration): ('address', models.TextField(blank=True, null=True)), ], ), - migrations.CreateModel( - name='Fertilizertype', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ], - ), migrations.CreateModel( name='FertilizertypeInternationalization', fields=[ @@ -93,12 +73,6 @@ class Migration(migrations.Migration): ('fr', models.TextField(blank=True, db_column='FR', null=True)), ], ), - migrations.CreateModel( - name='Mulchlayertype', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ], - ), migrations.CreateModel( name='MulchlayertypeInternationalization', fields=[ @@ -173,30 +147,27 @@ class Migration(migrations.Migration): ], ), migrations.CreateModel( - name='Batchfertilizer', + name='BatchSponsor', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('batch', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.batch')), - ('fertilizer_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.fertilizertype')), + ('name', models.TextField()), + ('url', models.TextField()), + ('logo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.asset')), ], ), - migrations.AddField( - model_name='fertilizertype', - name='name', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.fertilizertypeinternationalization'), - ), migrations.CreateModel( - name='Batchmulchlayer', + name='Fertilizertype', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('batch', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.batch')), - ('mulch_layer_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.mulchlayertype')), + ('name', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.fertilizertypeinternationalization')), ], ), - migrations.AddField( - model_name='mulchlayertype', - name='name', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.mulchlayertypeinternationalization'), + migrations.CreateModel( + name='Mulchlayertype', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.mulchlayertypeinternationalization')), + ], ), migrations.CreateModel( name='Like', @@ -252,10 +223,23 @@ class Migration(migrations.Migration): name='site', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.site'), ), - migrations.AddField( - model_name='batch', - name='site', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.site'), + migrations.CreateModel( + name='Batch', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True, null=True)), + ('name', models.TextField(blank=True, null=True)), + ('size', models.IntegerField(blank=True, null=True)), + ('soil_condition', models.TextField(blank=True, null=True)), + ('survived_count', models.IntegerField(blank=True, null=True)), + ('replace_count', models.IntegerField(blank=True, null=True)), + ('total_number_seed', models.IntegerField(blank=True, null=True)), + ('total_propagation', models.IntegerField(blank=True, null=True)), + ('image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.asset')), + ('sponsor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.batchsponsor')), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.site')), + ], ), migrations.CreateModel( name='Siteadmin', @@ -283,61 +267,95 @@ class Migration(migrations.Migration): name='Treetype', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.treespeciestypeinternationalization')), + ('name', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.treespeciestypeinternationalization')), ], ), migrations.CreateModel( - name='Sitetreespecies', + name='UserInvitation', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('quantity', models.IntegerField(blank=True, null=True)), + ('code', models.CharField(max_length=64, unique=True)), + ('expires_at', models.DateTimeField(default=canopeum_backend.models.one_week_from_today)), + ('email', models.EmailField(max_length=254)), + ('assigned_to_sites', models.ManyToManyField(to='canopeum_backend.site')), + ], + ), + migrations.CreateModel( + name='Widget', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.TextField(blank=True, null=True)), + ('body', models.TextField(blank=True, null=True)), ('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.site')), - ('tree_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.treetype')), ], ), migrations.CreateModel( - name='BatchSupportedSpecies', + name='Batchfertilizer', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('batch', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.batch')), - ('tree_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.treetype')), + ('batch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.batch')), + ('fertilizer_type', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='canopeum_backend.fertilizertype')), ], + options={ + 'constraints': [models.UniqueConstraint(fields=('batch', 'fertilizer_type'), name='unique_fertilizer_per_batch')], + }, ), migrations.CreateModel( - name='BatchSpecies', + name='Batchmulchlayer', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('quantity', models.IntegerField(blank=True, null=True)), - ('batch', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.batch')), - ('tree_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.treetype')), + ('batch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.batch')), + ('mulch_layer_type', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.mulchlayertype')), ], + options={ + 'constraints': [models.UniqueConstraint(fields=('batch', 'mulch_layer_type'), name='unique_mulch_layer_per_batch')], + }, ), migrations.CreateModel( - name='BatchSeed', + name='Sitetreespecies', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.IntegerField()), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.site')), + ('tree_type', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.treetype')), + ], + options={ + 'constraints': [models.UniqueConstraint(fields=('site', 'tree_type'), name='unique_tree_species_per_site')], + }, + ), + migrations.CreateModel( + name='BatchSupportedSpecies', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('quantity', models.IntegerField(blank=True, null=True)), - ('batch', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.batch')), - ('tree_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.treetype')), + ('batch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.batch')), + ('tree_type', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.treetype')), ], + options={ + 'constraints': [models.UniqueConstraint(fields=('batch', 'tree_type'), name='unique_supported_species_per_batch')], + }, ), migrations.CreateModel( - name='UserInvitation', + name='BatchSpecies', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=64, unique=True)), - ('expires_at', models.DateTimeField(default=canopeum_backend.models.one_week_from_today)), - ('email', models.EmailField(max_length=254)), - ('assigned_to_sites', models.ManyToManyField(to='canopeum_backend.site')), + ('quantity', models.IntegerField()), + ('batch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.batch')), + ('tree_type', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.treetype')), ], + options={ + 'constraints': [models.UniqueConstraint(fields=('batch', 'tree_type'), name='unique_species_per_batch')], + }, ), migrations.CreateModel( - name='Widget', + name='BatchSeed', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.TextField(blank=True, null=True)), - ('body', models.TextField(blank=True, null=True)), - ('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.site')), + ('quantity', models.IntegerField()), + ('batch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='canopeum_backend.batch')), + ('tree_type', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.treetype')), ], + options={ + 'constraints': [models.UniqueConstraint(fields=('batch', 'tree_type'), name='unique_seed_per_batch')], + }, ), ] diff --git a/canopeum_backend/canopeum_backend/migrations/0002_batch_image_batch_plant_count_batch_replace_count_and_more.py b/canopeum_backend/canopeum_backend/migrations/0002_batch_image_batch_plant_count_batch_replace_count_and_more.py deleted file mode 100644 index 3ca48751e..000000000 --- a/canopeum_backend/canopeum_backend/migrations/0002_batch_image_batch_plant_count_batch_replace_count_and_more.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.0.3 on 2024-06-19 20:22 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('canopeum_backend', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='batch', - name='image', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='canopeum_backend.asset'), - ), - migrations.AddField( - model_name='batch', - name='plant_count', - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name='batch', - name='replace_count', - field=models.IntegerField(blank=True, null=True), - ), - migrations.AddField( - model_name='batch', - name='survived_count', - field=models.IntegerField(blank=True, null=True), - ), - ] diff --git a/canopeum_backend/canopeum_backend/migrations/0003_unique_constraints_for_batch_and_site.py b/canopeum_backend/canopeum_backend/migrations/0003_unique_constraints_for_batch_and_site.py deleted file mode 100644 index e2995facd..000000000 --- a/canopeum_backend/canopeum_backend/migrations/0003_unique_constraints_for_batch_and_site.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 5.1 on 2024-09-19 17:02 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('canopeum_backend', '0002_batch_image_batch_plant_count_batch_replace_count_and_more'), - ] - - operations = [ - migrations.AddConstraint( - model_name='batchfertilizer', - constraint=models.UniqueConstraint(fields=('batch', 'fertilizer_type'), name='unique_fertilizer_per_batch'), - ), - migrations.AddConstraint( - model_name='batchmulchlayer', - constraint=models.UniqueConstraint(fields=('batch', 'mulch_layer_type'), name='unique_mulch_layer_per_batch'), - ), - migrations.AddConstraint( - model_name='batchseed', - constraint=models.UniqueConstraint(fields=('batch', 'tree_type'), name='unique_seed_per_batch'), - ), - migrations.AddConstraint( - model_name='batchspecies', - constraint=models.UniqueConstraint(fields=('batch', 'tree_type'), name='unique_species_per_batch'), - ), - migrations.AddConstraint( - model_name='batchsupportedspecies', - constraint=models.UniqueConstraint(fields=('batch', 'tree_type'), name='unique_supported_species_per_batch'), - ), - migrations.AddConstraint( - model_name='sitetreespecies', - constraint=models.UniqueConstraint(fields=('site', 'tree_type'), name='unique_tree_species_per_site'), - ), - ] diff --git a/canopeum_backend/canopeum_backend/migrations/0004_remove_setting_blank_with_date_auto.py b/canopeum_backend/canopeum_backend/migrations/0004_remove_setting_blank_with_date_auto.py deleted file mode 100644 index 36934708b..000000000 --- a/canopeum_backend/canopeum_backend/migrations/0004_remove_setting_blank_with_date_auto.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.1 on 2024-09-23 20:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('canopeum_backend', '0003_unique_constraints_for_batch_and_site'), - ] - - operations = [ - migrations.AlterField( - model_name='batch', - name='created_at', - field=models.DateTimeField(auto_now_add=True, null=True), - ), - migrations.AlterField( - model_name='batch', - name='updated_at', - field=models.DateTimeField(auto_now=True, null=True), - ), - ] diff --git a/canopeum_backend/canopeum_backend/models.py b/canopeum_backend/canopeum_backend/models.py index 56a879c53..7dc62d509 100644 --- a/canopeum_backend/canopeum_backend/models.py +++ b/canopeum_backend/canopeum_backend/models.py @@ -70,133 +70,6 @@ class Asset(models.Model): asset = models.FileField(upload_to=upload_to, null=False) -class Batch(models.Model): - site = models.ForeignKey("Site", models.CASCADE, blank=True, null=True) - created_at = models.DateTimeField(auto_now_add=True, null=True) - updated_at = models.DateTimeField(auto_now=True, null=True) - name = models.TextField(blank=True, null=True) - sponsor = models.TextField(blank=True, null=True) - size = models.IntegerField(blank=True, null=True) - soil_condition = models.TextField(blank=True, null=True) - plant_count = models.IntegerField(blank=True, null=True) - survived_count = models.IntegerField(blank=True, null=True) - replace_count = models.IntegerField(blank=True, null=True) - total_number_seed = models.IntegerField(blank=True, null=True) - total_propagation = models.IntegerField(blank=True, null=True) - image = models.ForeignKey(Asset, models.DO_NOTHING, blank=True, null=True) - - def add_fertilizer_by_id(self, pk: int): - fertilizer_type = Fertilizertype.objects.get(pk=pk) - return Batchfertilizer.objects.create(fertilizer_type=fertilizer_type, batch=self) - - def add_mulch_by_id(self, pk: int): - mulch_layer_type = Mulchlayertype.objects.get(pk=pk) - return Batchmulchlayer.objects.create(mulch_layer_type=mulch_layer_type, batch=self) - - def add_seed_by_id(self, pk: int, quantity: int): - tree_type = Treetype.objects.get(pk=pk) - return BatchSeed.objects.create(tree_type=tree_type, quantity=quantity, batch=self) - - def add_specie_by_id(self, pk: int, quantity: int): - tree_type = Treetype.objects.get(pk=pk) - return BatchSpecies.objects.create(tree_type=tree_type, quantity=quantity, batch=self) - - def add_supported_specie_by_id(self, pk: int): - tree_type = Treetype.objects.get(pk=pk) - return BatchSupportedSpecies.objects.create(tree_type=tree_type, batch=self) - - -class FertilizertypeInternationalization(models.Model): - en = models.TextField(db_column="EN", blank=True, null=True) - fr = models.TextField(db_column="FR", blank=True, null=True) - - -class Fertilizertype(models.Model): - name = models.ForeignKey( - FertilizertypeInternationalization, models.DO_NOTHING, blank=True, null=True - ) - - -class Batchfertilizer(models.Model): - batch = models.ForeignKey(Batch, models.CASCADE, blank=True, null=True) - fertilizer_type = models.ForeignKey(Fertilizertype, models.DO_NOTHING, blank=True, null=True) - - class Meta: - constraints = ( - models.UniqueConstraint( - fields=["batch", "fertilizer_type"], name="unique_fertilizer_per_batch" - ), - ) - - -class MulchlayertypeInternationalization(models.Model): - en = models.TextField(db_column="EN", blank=True, null=True) - fr = models.TextField(db_column="FR", blank=True, null=True) - - -class Mulchlayertype(models.Model): - name = models.ForeignKey( - MulchlayertypeInternationalization, models.DO_NOTHING, blank=True, null=True - ) - - -class Batchmulchlayer(models.Model): - batch = models.ForeignKey(Batch, models.CASCADE, blank=True, null=True) - mulch_layer_type = models.ForeignKey(Mulchlayertype, models.DO_NOTHING, blank=True, null=True) - - class Meta: - constraints = ( - models.UniqueConstraint( - fields=["batch", "mulch_layer_type"], name="unique_mulch_layer_per_batch" - ), - ) - - -class TreespeciestypeInternationalization(models.Model): - en = models.TextField(db_column="EN", blank=True, null=True) - fr = models.TextField(db_column="FR", blank=True, null=True) - - -class Treetype(models.Model): - name = models.ForeignKey( - TreespeciestypeInternationalization, models.DO_NOTHING, blank=True, null=True - ) - - -class BatchSpecies(models.Model): - batch = models.ForeignKey(Batch, models.CASCADE, blank=True, null=True) - tree_type = models.ForeignKey(Treetype, models.DO_NOTHING, blank=True, null=True) - quantity = models.IntegerField(blank=True, null=True) - - class Meta: - constraints = ( - models.UniqueConstraint(fields=["batch", "tree_type"], name="unique_species_per_batch"), - ) - - -class BatchSeed(models.Model): - batch = models.ForeignKey(Batch, models.CASCADE, blank=True, null=True) - tree_type = models.ForeignKey(Treetype, models.DO_NOTHING, blank=True, null=True) - quantity = models.IntegerField(blank=True, null=True) - - class Meta: - constraints = ( - models.UniqueConstraint(fields=["batch", "tree_type"], name="unique_seed_per_batch"), - ) - - -class BatchSupportedSpecies(models.Model): - batch = models.ForeignKey(Batch, models.CASCADE, blank=True, null=True) - tree_type = models.ForeignKey(Treetype, models.DO_NOTHING, blank=True, null=True) - - class Meta: - constraints = ( - models.UniqueConstraint( - fields=["batch", "tree_type"], name="unique_supported_species_per_batch" - ), - ) - - class Contact(models.Model): address = models.TextField(blank=True, null=True) email = models.EmailField(blank=True, null=True) @@ -272,7 +145,7 @@ def delete(self, using=None, keep_parents=False): class Site(models.Model): name = models.TextField() is_public = models.BooleanField(blank=False, null=False, default=False) - site_type = models.ForeignKey("Sitetype", models.DO_NOTHING, blank=True, null=True) + site_type = models.ForeignKey(Sitetype, models.DO_NOTHING, blank=True, null=True) coordinate = models.ForeignKey(Coordinate, models.SET_NULL, blank=True, null=True) description = models.TextField(blank=True, null=True) size = models.TextField(blank=True, null=True) @@ -283,6 +156,21 @@ class Site(models.Model): announcement = models.ForeignKey(Announcement, models.SET_NULL, blank=True, null=True) image = models.ForeignKey(Asset, models.SET_NULL, blank=True, null=True) + def get_plant_count(self) -> int: + site_species = Sitetreespecies.objects.filter(site=self) + return sum(specie.quantity for specie in site_species) + + def get_sponsor_progress(self) -> float: + total_plant_count = self.get_plant_count() + if total_plant_count == 0: + return 0 + + batches = Batch.objects.filter(site=self) + sponsored_plant_count = sum(batch.plant_count() for batch in batches) + + # Note: We don't cap the progress at 100% so it's obvious if there's a data issue + return sponsored_plant_count / total_plant_count * 100 + @override def delete(self, using=None, keep_parents=False): # Coordinate @@ -304,6 +192,138 @@ def delete(self, using=None, keep_parents=False): return super().delete(using, keep_parents) +class BatchSponsor(models.Model): + name = models.TextField() + url = models.TextField() + logo = models.ForeignKey(Asset, models.CASCADE) + + +class Batch(models.Model): + site = models.ForeignKey(Site, models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True, null=True) + updated_at = models.DateTimeField(auto_now=True, null=True) + name = models.TextField(blank=True, null=True) + sponsor = models.ForeignKey(BatchSponsor, models.CASCADE) + size = models.IntegerField(blank=True, null=True) + soil_condition = models.TextField(blank=True, null=True) + survived_count = models.IntegerField(blank=True, null=True) + replace_count = models.IntegerField(blank=True, null=True) + total_number_seed = models.IntegerField(blank=True, null=True) + total_propagation = models.IntegerField(blank=True, null=True) + image = models.ForeignKey(Asset, models.DO_NOTHING, blank=True, null=True) + + def add_fertilizer_by_id(self, pk: int): + fertilizer_type = Fertilizertype.objects.get(pk=pk) + return Batchfertilizer.objects.create(fertilizer_type=fertilizer_type, batch=self) + + def add_mulch_by_id(self, pk: int): + mulch_layer_type = Mulchlayertype.objects.get(pk=pk) + return Batchmulchlayer.objects.create(mulch_layer_type=mulch_layer_type, batch=self) + + def add_seed_by_id(self, pk: int, quantity: int): + tree_type = Treetype.objects.get(pk=pk) + return BatchSeed.objects.create(tree_type=tree_type, quantity=quantity, batch=self) + + def add_specie_by_id(self, pk: int, quantity: int): + tree_type = Treetype.objects.get(pk=pk) + return BatchSpecies.objects.create(tree_type=tree_type, quantity=quantity, batch=self) + + def add_supported_specie_by_id(self, pk: int): + tree_type = Treetype.objects.get(pk=pk) + return BatchSupportedSpecies.objects.create(tree_type=tree_type, batch=self) + + def plant_count(self) -> int: + batch_species = BatchSpecies.objects.filter(batch=self) + return sum(specie.quantity for specie in batch_species) + + +class FertilizertypeInternationalization(models.Model): + en = models.TextField(db_column="EN", blank=True, null=True) + fr = models.TextField(db_column="FR", blank=True, null=True) + + +class Fertilizertype(models.Model): + name = models.ForeignKey(FertilizertypeInternationalization, models.DO_NOTHING) + + +class Batchfertilizer(models.Model): + batch = models.ForeignKey(Batch, models.CASCADE) + fertilizer_type = models.ForeignKey(Fertilizertype, models.RESTRICT) + + class Meta: + constraints = ( + models.UniqueConstraint( + fields=["batch", "fertilizer_type"], name="unique_fertilizer_per_batch" + ), + ) + + +class MulchlayertypeInternationalization(models.Model): + en = models.TextField(db_column="EN", blank=True, null=True) + fr = models.TextField(db_column="FR", blank=True, null=True) + + +class Mulchlayertype(models.Model): + name = models.ForeignKey( + MulchlayertypeInternationalization, models.DO_NOTHING, blank=True, null=True + ) + + +class Batchmulchlayer(models.Model): + batch = models.ForeignKey(Batch, models.CASCADE) + mulch_layer_type = models.ForeignKey(Mulchlayertype, models.DO_NOTHING) + + class Meta: + constraints = ( + models.UniqueConstraint( + fields=["batch", "mulch_layer_type"], name="unique_mulch_layer_per_batch" + ), + ) + + +class TreespeciestypeInternationalization(models.Model): + en = models.TextField(db_column="EN", blank=True, null=True) + fr = models.TextField(db_column="FR", blank=True, null=True) + + +class Treetype(models.Model): + name = models.ForeignKey(TreespeciestypeInternationalization, models.DO_NOTHING) + + +class BatchSpecies(models.Model): + batch = models.ForeignKey(Batch, models.CASCADE) + tree_type = models.ForeignKey(Treetype, models.DO_NOTHING) + quantity = models.IntegerField() + + class Meta: + constraints = ( + models.UniqueConstraint(fields=["batch", "tree_type"], name="unique_species_per_batch"), + ) + + +class BatchSeed(models.Model): + batch = models.ForeignKey(Batch, models.CASCADE) + tree_type = models.ForeignKey(Treetype, models.DO_NOTHING) + quantity = models.IntegerField() + + class Meta: + constraints = ( + models.UniqueConstraint(fields=["batch", "tree_type"], name="unique_seed_per_batch"), + ) + + +class BatchSupportedSpecies(models.Model): + batch = models.ForeignKey(Batch, models.CASCADE) + tree_type = models.ForeignKey(Treetype, models.DO_NOTHING) + + class Meta: + constraints = ( + models.UniqueConstraint( + fields=["batch", "tree_type"], name="unique_supported_species_per_batch" + ), + ) + + # 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 @@ -318,10 +338,11 @@ def delete(self, using=None, keep_parents=False): class Post(models.Model): - site = models.ForeignKey("Site", models.CASCADE, blank=False, null=False) + site = models.ForeignKey(Site, models.CASCADE, 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) + # TODO(NicolasDontigny): Add created by user? # created_by = models.ForeignKey(User, models.DO_NOTHING, blank=True, null=True) media = models.ManyToManyField(Asset, through=PostAsset, blank=True) @@ -359,9 +380,9 @@ class SiteFollower(models.Model): class Sitetreespecies(models.Model): - site = models.ForeignKey(Site, models.CASCADE, blank=True, null=True) - tree_type = models.ForeignKey("Treetype", models.DO_NOTHING, blank=True, null=True) - quantity = models.IntegerField(blank=True, null=True) + site = models.ForeignKey(Site, models.CASCADE) + tree_type = models.ForeignKey(Treetype, models.DO_NOTHING) + quantity = models.IntegerField() class Meta: constraints = ( diff --git a/canopeum_backend/canopeum_backend/seeding/images/batch_logo1.png b/canopeum_backend/canopeum_backend/seeding/images/batch_logo1.png new file mode 100644 index 000000000..1279a5434 Binary files /dev/null and b/canopeum_backend/canopeum_backend/seeding/images/batch_logo1.png differ diff --git a/canopeum_backend/canopeum_backend/seeding/images/batch_logo2.png b/canopeum_backend/canopeum_backend/seeding/images/batch_logo2.png new file mode 100644 index 000000000..ecaa86089 Binary files /dev/null and b/canopeum_backend/canopeum_backend/seeding/images/batch_logo2.png differ diff --git a/canopeum_backend/canopeum_backend/seeding/images/batch_logo3.png b/canopeum_backend/canopeum_backend/seeding/images/batch_logo3.png new file mode 100644 index 000000000..87ca73583 Binary files /dev/null and b/canopeum_backend/canopeum_backend/seeding/images/batch_logo3.png differ diff --git a/canopeum_backend/canopeum_backend/seeding/images/batch_logo4.png b/canopeum_backend/canopeum_backend/seeding/images/batch_logo4.png new file mode 100644 index 000000000..a3ac4489a Binary files /dev/null and b/canopeum_backend/canopeum_backend/seeding/images/batch_logo4.png differ diff --git a/canopeum_backend/canopeum_backend/seeding/images/batch_logo5.png b/canopeum_backend/canopeum_backend/seeding/images/batch_logo5.png new file mode 100644 index 000000000..7f774741c Binary files /dev/null and b/canopeum_backend/canopeum_backend/seeding/images/batch_logo5.png differ diff --git a/canopeum_backend/canopeum_backend/seeding/images/batch_logo6.png b/canopeum_backend/canopeum_backend/seeding/images/batch_logo6.png new file mode 100644 index 000000000..e7fb0bd7e Binary files /dev/null and b/canopeum_backend/canopeum_backend/seeding/images/batch_logo6.png differ diff --git a/canopeum_backend/canopeum_backend/seeding/images/batch_logo7.png b/canopeum_backend/canopeum_backend/seeding/images/batch_logo7.png new file mode 100644 index 000000000..70d78b54b Binary files /dev/null and b/canopeum_backend/canopeum_backend/seeding/images/batch_logo7.png differ diff --git a/canopeum_backend/canopeum_backend/serializers.py b/canopeum_backend/canopeum_backend/serializers.py index 211bbe910..47572696e 100644 --- a/canopeum_backend/canopeum_backend/serializers.py +++ b/canopeum_backend/canopeum_backend/serializers.py @@ -2,6 +2,7 @@ # pyright: reportIncompatibleVariableOverride=false import random +from collections.abc import Mapping from decimal import Decimal from typing import Any @@ -19,6 +20,7 @@ Batchmulchlayer, BatchSeed, BatchSpecies, + BatchSponsor, BatchSupportedSpecies, Comment, Contact, @@ -340,6 +342,39 @@ def get_sites(self, obj): return SiteNameSerializer(sites_list, many=True).data +class BatchSponsorSerializer(serializers.ModelSerializer[BatchSponsor]): + logo = AssetSerializer() + + class Meta: + model = BatchSponsor + fields = "__all__" + + def create(self, validated_data): + logo_data = validated_data.pop("logo") + logo_serializer = AssetSerializer(data=logo_data) + logo_serializer.is_valid() + created_logo = logo_serializer.save() + + return BatchSponsor.objects.create(**validated_data, logo=created_logo) + + def update(self, instance, validated_data: Mapping[str, Any]): + instance.name = validated_data.get("name", instance.name) + instance.url = validated_data.get("url", instance.url) + logo_data = validated_data.get("logo") + if logo_data is not None: + logo_serializer = AssetSerializer(data=logo_data) + logo_serializer.is_valid() + old_logo_asset_to_delete = Asset.objects.get(pk=instance.logo.pk) + instance.logo = logo_serializer.save() + if old_logo_asset_to_delete is not None: + # TODO(NicolasDontigny): The old image file is not deleted from the media folder; + # Figure out if that is something we want to do + old_logo_asset_to_delete.delete() + + instance.save() + return instance + + class SiteSocialSerializer(serializers.ModelSerializer[Site]): site_type = SiteTypeSerializer() contact = ContactSerializer() @@ -363,147 +398,40 @@ class Meta: "widget", ) - def get_sponsors(self, obj) -> list[str]: - return self.context.get("sponsors", list[str]()) # type: ignore[no-any-return] + @extend_schema_field(BatchSponsorSerializer(many=True)) + def get_sponsors(self, obj: Site): + batches = Batch.objects.filter(site=obj) + + sponsors = [batch.sponsor for batch in batches if batch.sponsor is not None] + return BatchSponsorSerializer(sponsors, many=True).data @extend_schema_field(WidgetSerializer(many=True)) def get_widget(self, obj): return WidgetSerializer(obj.widget_set.all(), many=True).data -class BatchfertilizerSerializer(serializers.ModelSerializer[Batchfertilizer]): - id = serializers.SerializerMethodField() - en = serializers.SerializerMethodField() - fr = serializers.SerializerMethodField() - - class Meta: - model = Batchfertilizer - fields = ("id", "en", "fr") - - def get_id(self, obj: Batchfertilizer): - return FertilizerTypeSerializer(obj.fertilizer_type).data.get("id", None) - - def get_en(self, obj: Batchfertilizer): - return ( - InternationalizationSerializer(obj.fertilizer_type.name).data.get("en", None) - if obj.fertilizer_type - else None - ) - - def get_fr(self, obj: Batchfertilizer): - return ( - InternationalizationSerializer(obj.fertilizer_type.name).data.get("fr", None) - if obj.fertilizer_type - else None - ) - - -class BatchMulchLayerSerializer(serializers.ModelSerializer[Batchmulchlayer]): - id = serializers.SerializerMethodField() - en = serializers.SerializerMethodField() - fr = serializers.SerializerMethodField() - - class Meta: - model = Mulchlayertype - fields = ("id", "en", "fr") - - def get_id(self, obj: Batchmulchlayer): - return MulchLayerTypeSerializer(obj.mulch_layer_type).data.get("id", None) - - def get_en(self, obj: Batchmulchlayer): - return ( - InternationalizationSerializer(obj.mulch_layer_type.name).data.get("en", None) - if obj.mulch_layer_type - else None - ) - - def get_fr(self, obj: Batchmulchlayer): - return ( - InternationalizationSerializer(obj.mulch_layer_type.name).data.get("fr", None) - if obj.mulch_layer_type - else None - ) - - -class BatchSupportedSpeciesSerializer(serializers.ModelSerializer[BatchSupportedSpecies]): - id = serializers.SerializerMethodField() - en = serializers.SerializerMethodField() - fr = serializers.SerializerMethodField() - - class Meta: - model = BatchSupportedSpecies - fields = ("id", "en", "fr") - - def get_id(self, obj: BatchSupportedSpecies): - return TreeTypeSerializer(obj.tree_type).data.get("id", None) - - def get_en(self, obj: BatchSupportedSpecies): - return ( - InternationalizationSerializer(obj.tree_type.name).data.get("en", None) - if obj.tree_type - else None - ) - - def get_fr(self, obj: BatchSupportedSpecies): - return ( - InternationalizationSerializer(obj.tree_type.name).data.get("fr", None) - if obj.tree_type - else None - ) - - class BatchSeedSerializer(serializers.ModelSerializer[BatchSeed]): - id = serializers.SerializerMethodField() - en = serializers.SerializerMethodField() - fr = serializers.SerializerMethodField() + tree_type = serializers.SerializerMethodField() class Meta: model = BatchSeed - fields = ("id", "quantity", "en", "fr") - - def get_id(self, obj: BatchSeed): - return TreeTypeSerializer(obj.tree_type).data.get("id", None) + fields = ("id", "quantity", "tree_type") - def get_en(self, obj: BatchSeed): - return ( - InternationalizationSerializer(obj.tree_type.name).data.get("en", None) - if obj.tree_type - else None - ) - - def get_fr(self, obj: BatchSeed): - return ( - InternationalizationSerializer(obj.tree_type.name).data.get("fr", None) - if obj.tree_type - else None - ) + @extend_schema_field(TreeTypeSerializer) + def get_tree_type(self, obj: BatchSeed): + return TreeTypeSerializer(obj.tree_type).data class BatchSpeciesSerializer(serializers.ModelSerializer[BatchSpecies]): - id = serializers.SerializerMethodField() - en = serializers.SerializerMethodField() - fr = serializers.SerializerMethodField() + tree_type = serializers.SerializerMethodField() class Meta: model = BatchSpecies - fields = ("id", "quantity", "en", "fr") - - def get_id(self, obj: BatchSpecies): - return TreeTypeSerializer(obj.tree_type).data.get("id", None) + fields = ("id", "quantity", "tree_type") - def get_en(self, obj: BatchSpecies): - return ( - InternationalizationSerializer(obj.tree_type.name).data.get("en", None) - if obj.tree_type - else None - ) - - def get_fr(self, obj: BatchSpecies): - return ( - InternationalizationSerializer(obj.tree_type.name).data.get("fr", None) - if obj.tree_type - else None - ) + @extend_schema_field(TreeTypeSerializer) + def get_tree_type(self, obj: BatchSpecies): + return TreeTypeSerializer(obj.tree_type).data class BatchDetailSerializer(serializers.ModelSerializer[Batch]): @@ -512,6 +440,7 @@ class BatchDetailSerializer(serializers.ModelSerializer[Batch]): supported_species = serializers.SerializerMethodField() seeds = serializers.SerializerMethodField() species = serializers.SerializerMethodField() + sponsor = serializers.SerializerMethodField() # HACK to allow handling the image with a AssetSerializer separately # TODO: Figure out how to feed the image directly to BatchDetailSerializer image = AssetSerializer(required=False) @@ -520,25 +449,43 @@ class Meta: model = Batch fields = "__all__" - @extend_schema_field(BatchfertilizerSerializer(many=True)) - def get_fertilizers(self, obj): - return BatchfertilizerSerializer(obj.batchfertilizer_set.all(), many=True).data - - @extend_schema_field(BatchMulchLayerSerializer(many=True)) - def get_mulch_layers(self, obj): - return BatchMulchLayerSerializer(obj.batchmulchlayer_set.all(), many=True).data - - @extend_schema_field(BatchSupportedSpeciesSerializer(many=True)) - def get_supported_species(self, obj): - return BatchSupportedSpeciesSerializer(obj.batchsupportedspecies_set.all(), many=True).data + @extend_schema_field(FertilizerTypeSerializer(many=True)) + def get_fertilizers(self, obj: Batch): + batch_fertilizers = Batchfertilizer.objects.filter(batch=obj) + fertilizer_types = [ + batch_fertilizer.fertilizer_type for batch_fertilizer in batch_fertilizers + ] + + return FertilizerTypeSerializer(fertilizer_types, many=True).data + + @extend_schema_field(MulchLayerTypeSerializer(many=True)) + def get_mulch_layers(self, obj: Batch): + batch_mulch_layers = Batchmulchlayer.objects.filter(batch=obj) + mulch_layer_types = [ + batch_mulch_layer.mulch_layer_type for batch_mulch_layer in batch_mulch_layers + ] + return MulchLayerTypeSerializer(mulch_layer_types, many=True).data + + @extend_schema_field(TreeTypeSerializer(many=True)) + def get_supported_species(self, obj: Batch): + batch_supported_species_list = BatchSupportedSpecies.objects.filter(batch=obj) + supported_species_types = [ + batch_supported_species.tree_type + for batch_supported_species in batch_supported_species_list + ] + return TreeTypeSerializer(supported_species_types, many=True).data @extend_schema_field(BatchSeedSerializer(many=True)) def get_seeds(self, obj): - return BatchSeedSerializer(obj.batchseed_set.all(), many=True).data + return BatchSeedSerializer(BatchSeed.objects.filter(batch=obj), many=True).data @extend_schema_field(BatchSpeciesSerializer(many=True)) def get_species(self, obj): - return BatchSpeciesSerializer(obj.batchspecies_set.all(), many=True).data + return BatchSpeciesSerializer(BatchSpecies.objects.filter(batch=obj), many=True).data + + @extend_schema_field(BatchSponsorSerializer) + def get_sponsor(self, obj): + return BatchSponsorSerializer(BatchSponsor.objects.get(batch=obj)).data class SiteAdminSerializer(serializers.ModelSerializer[Siteadmin]): @@ -596,10 +543,9 @@ class SiteSummarySerializer(serializers.ModelSerializer[Site]): site_type = SiteTypeSerializer() coordinate = CoordinatesSerializer() plant_count = serializers.SerializerMethodField() + sponsor_progress = serializers.SerializerMethodField() survived_count = serializers.SerializerMethodField() propagation_count = serializers.SerializerMethodField() - progress = serializers.SerializerMethodField() - sponsors = serializers.SerializerMethodField() admins = SiteAdminSerializer(source="siteadmin_set", many=True) batches = serializers.SerializerMethodField() @@ -611,17 +557,19 @@ class Meta: "coordinate", "site_type", "plant_count", + "sponsor_progress", "survived_count", "propagation_count", "visitor_count", - "sponsors", - "progress", "admins", "batches", ) - def get_plant_count(self, obj) -> int: - return random.randint(100, 200) # noqa: S311 + def get_plant_count(self, obj: Site) -> int: + return obj.get_plant_count() + + def get_sponsor_progress(self, obj: Site) -> float: + return obj.get_sponsor_progress() def get_survived_count(self, obj) -> int: return random.randint(50, 100) # noqa: S311 @@ -629,13 +577,6 @@ def get_survived_count(self, obj) -> int: def get_propagation_count(self, obj) -> int: return random.randint(5, 50) # noqa: S311 - def get_progress(self, obj) -> float: - return random.randint(0, 10000) / 100 # noqa: S311 - - def get_sponsors(self, obj) -> list[str]: - batches = Batch.objects.filter(site=obj) - return [batch.sponsor for batch in batches if batch.sponsor] - @extend_schema_field(BatchDetailSerializer(many=True)) def get_batches(self, obj): batches = obj.batch_set.all().order_by("-updated_at") @@ -646,9 +587,9 @@ class SiteSummaryDetailSerializer(serializers.ModelSerializer[Site]): site_type = SiteTypeSerializer() coordinate = CoordinatesSerializer() plant_count = serializers.SerializerMethodField() + sponsor_progress = serializers.SerializerMethodField() survived_count = serializers.SerializerMethodField() propagation_count = serializers.SerializerMethodField() - progress = serializers.SerializerMethodField() sponsors = serializers.SerializerMethodField() admins = SiteAdminSerializer(source="siteadmin_set", many=True) batches = serializers.SerializerMethodField() @@ -662,18 +603,21 @@ class Meta: "coordinate", "site_type", "plant_count", + "sponsor_progress", "survived_count", "propagation_count", "visitor_count", "sponsors", - "progress", "admins", "batches", "weather", ) - def get_plant_count(self, obj) -> int: - return random.randint(100, 200) # noqa: S311 + def get_plant_count(self, obj: Site) -> int: + return obj.get_plant_count() + + def get_sponsor_progress(self, obj: Site) -> float: + return obj.get_sponsor_progress() def get_survived_count(self, obj) -> int: return random.randint(50, 100) # noqa: S311 @@ -681,12 +625,11 @@ def get_survived_count(self, obj) -> int: def get_propagation_count(self, obj) -> int: return random.randint(5, 50) # noqa: S311 - def get_progress(self, obj) -> float: - return random.randint(0, 10000) / 100 # noqa: S311 - - def get_sponsors(self, obj) -> list[str]: + @extend_schema_field(BatchSponsorSerializer(many=True)) + def get_sponsors(self, obj): batches = Batch.objects.filter(site=obj) - return [batch.sponsor for batch in batches if batch.sponsor] + sponsors = [batch.sponsor for batch in batches if batch.sponsor] + return BatchSponsorSerializer(sponsors, many=True).data @extend_schema_field(WeatherSerializer) def get_weather(self, obj): diff --git a/canopeum_backend/canopeum_backend/settings.py b/canopeum_backend/canopeum_backend/settings.py index 51b55f053..346616b36 100644 --- a/canopeum_backend/canopeum_backend/settings.py +++ b/canopeum_backend/canopeum_backend/settings.py @@ -11,6 +11,7 @@ """ import os +import sys from datetime import timedelta from pathlib import Path @@ -170,17 +171,24 @@ def get_secret(key: str, default: str): # Database # https://docs.djangoproject.com/en/5.0/ref/settings/#databases - DATABASES = { - "default": dj_database_url.parse( - "mysql://canopeum_user:{}@{}:{}/canopeum_db".format( - get_secret("MYSQL_PASSWORD_CANOPEUM", ""), - get_secret("MYSQL_HOST_CANOPEUM", "localhost"), - get_secret("MYSQL_PORT_CANOPEUM", "3308"), - ), - conn_max_age=600, - conn_health_checks=True, - ) + "default": ( + # If running Django tests, use an in-memory database, + # which is faster and requires less setup + # Using conditional instead of TEST dictionary because USER can't be overriden + # https://docs.djangoproject.com/en/5.1/topics/testing/overview/#the-test-database + {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"} + if sys.argv[1] == "test" + else dj_database_url.parse( + "mysql://canopeum_user:{}@{}:{}/canopeum_db".format( + get_secret("MYSQL_PASSWORD_CANOPEUM", ""), + get_secret("MYSQL_HOST_CANOPEUM", "localhost"), + get_secret("MYSQL_PORT_CANOPEUM", "3308"), + ), + conn_max_age=600, + conn_health_checks=True, + ) + ), } # Password validation diff --git a/canopeum_backend/canopeum_backend/views.py b/canopeum_backend/canopeum_backend/views.py index dbc92deba..db353e28e 100644 --- a/canopeum_backend/canopeum_backend/views.py +++ b/canopeum_backend/canopeum_backend/views.py @@ -68,6 +68,7 @@ AnnouncementSerializer, AssetSerializer, BatchDetailSerializer, + BatchSponsorSerializer, ChangePasswordSerializer, CommentSerializer, ContactSerializer, @@ -556,10 +557,7 @@ def get(self, request: Request, siteId): self.check_object_permissions(request, site) - batches = Batch.objects.filter(site=siteId) - sponsors = [batch.sponsor for batch in batches] - - serializer = SiteSocialSerializer(site, context={"sponsors": sponsors}) + serializer = SiteSocialSerializer(site) return Response(serializer.data) @@ -834,7 +832,14 @@ def delete(self, request: Request, postId): "properties": { "site": {"type": "number"}, "name": {"type": "string", "nullable": True}, - "sponsor": {"type": "string", "nullable": True}, + "sponsorName": {"type": "string", "nullable": True}, + "sponsorWebsiteUrl": {"type": "string", "nullable": True}, + # TODO(NicolasDontigny): sponsorLogo should be in the sponsor object, but the generated + # typescript api does not correctly type it as a FileParameter type + # unless it is a root key + # Raise the issue upstream, OR it should be fixed when we figure out how to serialize + # multipart/form-data + "sponsorLogo": {"type": "string", "format": "binary", "nullable": True}, "size": {"type": "number", "nullable": True}, "soilCondition": {"type": "string", "nullable": True}, "plantCount": {"type": "number", "nullable": True}, @@ -909,12 +914,26 @@ def post(self, request: Request): else: image = asset_serializer.save() + sponsor_data = { + "name": request.data.get("sponsor_name"), + "url": request.data.get("sponsor_website_url"), + "logo": { + "asset": request.data.get("sponsor_logo"), + }, + } + sponsor_serializer = BatchSponsorSerializer(data=sponsor_data) + sponsor = None + if not sponsor_serializer.is_valid(): + errors.append(sponsor_serializer.errors) + else: + sponsor = sponsor_serializer.save() + batch_serializer = BatchDetailSerializer(data=request.data) if not batch_serializer.is_valid(): errors.append(batch_serializer.errors) else: site = Site.objects.get(pk=request.data.get("site", "")) - batch = batch_serializer.save(site=site, image=image) + batch = batch_serializer.save(site=site, image=image, sponsor=sponsor) for fertilizer_id in parsed_fertilizer_ids: batch.add_fertilizer_by_id(fertilizer_id) @@ -975,11 +994,30 @@ def patch(self, request: Request, batchId): # else: # image = asset_serializer.save() + sponsor = None + sponsor_data = { + "name": request.data.get("sponsor_name"), + "url": request.data.get("sponsor_website_url"), + } + if request.data.get("sponsor_logo") is not None: + sponsor_data["logo"] = { + "asset": request.data.get("sponsor_logo"), + } + sponsor_serializer = BatchSponsorSerializer( + batch.sponsor, + data=sponsor_data, + partial=True, + ) + if not sponsor_serializer.is_valid(): + errors.append(sponsor_serializer.errors) + else: + sponsor = sponsor_serializer.save() + batch_serializer = BatchDetailSerializer(batch, data=request.data, partial=True) if not batch_serializer.is_valid(): errors.append(batch_serializer.errors) else: - batch = batch_serializer.save() + batch = batch_serializer.save(sponsor=sponsor) # Less efficient, but so much easier to just remove all then recreate mappings. Batchfertilizer.objects.filter(batch=batch).delete() diff --git a/canopeum_backend/test.py b/canopeum_backend/test.py new file mode 100644 index 000000000..ff81fdd16 --- /dev/null +++ b/canopeum_backend/test.py @@ -0,0 +1,21 @@ +# This should preferably go in a test folder. But we only have a single test for now +# Tests are auto-discovered as long as they're named test*.py +from io import StringIO + +from django.core.management import call_command +from django.test import TestCase as DBTestCase + + +class PendingMigrationsTests(DBTestCase): + def test_no_pending_migrations(self): + out = StringIO() + try: + # https://docs.djangoproject.com/en/5.1/ref/django-admin/#cmdoption-makemigrations-check + call_command( + "makemigrations", + "--check", + stdout=out, + stderr=StringIO(), + ) + except SystemExit: + raise AssertionError("Pending migrations:\n" + out.getvalue()) from None diff --git a/canopeum_frontend/.eslintrc.cjs b/canopeum_frontend/.eslintrc.cjs index aac20e8eb..4004c4e5c 100644 --- a/canopeum_frontend/.eslintrc.cjs +++ b/canopeum_frontend/.eslintrc.cjs @@ -72,4 +72,20 @@ module.exports = { // https://github.com/cartant/eslint-plugin-etc/issues/63 'etc/no-implicit-any-catch': 'off', }, + overrides: [ + { + files: 'src/locale/**/*.ts', + rules: { + // We prefer avoiding line-breaks in translation files + 'max-len': 'off', + // Imports across languages to use the "satisfies" keyword on object literals + // We need to apply it directly on object literals to check for excess properties + // https://www.typescriptlang.org/docs/handbook/2/objects.html#excess-property-checks + 'no-autofix/no-relative-import-paths/no-relative-import-paths': 'off', + // i18next uses snake_case for special handling + // https://www.i18next.com/translation-function/plurals#singular-plural + camelcase: 'off', + }, + }, + ], } diff --git a/canopeum_frontend/package-lock.json b/canopeum_frontend/package-lock.json index 20a14f787..82188175d 100644 --- a/canopeum_frontend/package-lock.json +++ b/canopeum_frontend/package-lock.json @@ -721,9 +721,9 @@ "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.0.tgz", - "integrity": "sha512-SHetuSLvJDzuNbOdtPVbq6yMMMlLoW5Q94uDqJZqy50gcmAjxFkVqmzqSGEFq9gT2iMuIeKV1PXVWmvUhuZLlQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0" @@ -760,15 +760,15 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.1.tgz", - "integrity": "sha512-dEPNKzBPU+vFPGa+z3axPRn8XVDetYORmDC0wAiej+TNcOZE70ZMJa0X7JdeoM6q/nWTMZeLpN/fTnD9o8MQBA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz", + "integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==", "license": "MIT", "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.0", + "@emotion/utils": "^1.4.1", "csstype": "^3.0.2" } }, @@ -817,9 +817,9 @@ } }, "node_modules/@emotion/utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.0.tgz", - "integrity": "sha512-spEnrA1b6hDR/C68lC2M7m6ALPUHZC0lIY7jAS/B/9DuuO1ZP04eov8SMv/6fwRd8pzmsn2AuJEznRREWlQrlQ==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz", + "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==", "license": "MIT" }, "node_modules/@emotion/weak-memoize": { @@ -1865,14 +1865,14 @@ } }, "node_modules/@mui/private-theming": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.0.tgz", - "integrity": "sha512-+L5qccs4gwsR0r1dgjqhN24QEQRkqIbfOdxILyMbMkuI50x6wNyt9XrV+J3WtjtZTMGJCrUa5VmZBE6OEPGPWA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.1.tgz", + "integrity": "sha512-JlrjIdhyZUtewtdAuUsvi3ZnO0YS49IW4Mfz19ZWTlQ0sDGga6LNPVwHClWr2/zJK2we2BQx9/i8M32rgKuzrg==", "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.25.6", - "@mui/utils": "^6.1.0", + "@mui/utils": "^6.1.1", "prop-types": "^15.8.1" }, "engines": { @@ -1893,14 +1893,14 @@ } }, "node_modules/@mui/private-theming/node_modules/@mui/utils": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.0.tgz", - "integrity": "sha512-oT8ZzMISRUhTVpdbYzY0CgrCBb3t/YEdcaM13tUnuTjZ15pdA6g5lx15ZJUdgYXV6PbJdw7tDQgMEr4uXK5TXQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.1.tgz", + "integrity": "sha512-HlRrgdJSPbYDXPpoVMWZV8AE7WcFtAk13rWNWAEVWKSanzBBkymjz3km+Th/Srowsh4pf1fTSP1B0L116wQBYw==", "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.25.6", - "@mui/types": "^7.2.16", + "@mui/types": "^7.2.17", "@types/prop-types": "^15.7.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -1924,9 +1924,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.0.tgz", - "integrity": "sha512-MZ+vtaCkjamrT41+b0Er9OMenjAtP/32+L6fARL9/+BZKuV2QbR3q3TmavT2x0NhDu35IM03s4yKqj32Ziqnyg==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.1.tgz", + "integrity": "sha512-HJyIoMpFb11fnHuRtUILOXgq6vj4LhIlE8maG4SwP/W+E5sa7HFexhnB3vOMT7bKys4UKNxhobC8jwWxYilGsA==", "license": "MIT", "peer": true, "dependencies": { @@ -1958,17 +1958,17 @@ } }, "node_modules/@mui/system": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.0.tgz", - "integrity": "sha512-NumkGDqT6EdXfcoFLYQ+M4XlTW5hH3+aK48xAbRqKPXJfxl36CBt4DLduw/Voa5dcayGus9T6jm1AwU2hoJ5hQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.1.tgz", + "integrity": "sha512-PaYsCz2tUOcpu3T0okDEsSuP/yCDIj9JZ4Tox1JovRSKIjltHpXPsXZSGr3RiWdtM1MTQMFMCZzu0+CKbyy+Kw==", "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.25.6", - "@mui/private-theming": "^6.1.0", - "@mui/styled-engine": "^6.1.0", - "@mui/types": "^7.2.16", - "@mui/utils": "^6.1.0", + "@mui/private-theming": "^6.1.1", + "@mui/styled-engine": "^6.1.1", + "@mui/types": "^7.2.17", + "@mui/utils": "^6.1.1", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1999,14 +1999,14 @@ } }, "node_modules/@mui/system/node_modules/@mui/utils": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.0.tgz", - "integrity": "sha512-oT8ZzMISRUhTVpdbYzY0CgrCBb3t/YEdcaM13tUnuTjZ15pdA6g5lx15ZJUdgYXV6PbJdw7tDQgMEr4uXK5TXQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.1.tgz", + "integrity": "sha512-HlRrgdJSPbYDXPpoVMWZV8AE7WcFtAk13rWNWAEVWKSanzBBkymjz3km+Th/Srowsh4pf1fTSP1B0L116wQBYw==", "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.25.6", - "@mui/types": "^7.2.16", + "@mui/types": "^7.2.17", "@types/prop-types": "^15.7.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -2030,9 +2030,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.16", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.16.tgz", - "integrity": "sha512-qI8TV3M7ShITEEc8Ih15A2vLzZGLhD+/UPNwck/hcls2gwg7dyRjNGXcQYHKLB5Q7PuTRfrTkAoPa2VV1s67Ag==", + "version": "7.2.17", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.17.tgz", + "integrity": "sha512-oyumoJgB6jDV8JFzRqjBo2daUuHpzDjoO/e3IrRhhHo/FxJlaVhET6mcNrKHUq2E+R+q3ql0qAtvQ4rfWHhAeQ==", "license": "MIT", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -2317,9 +2317,9 @@ } }, "node_modules/@oclif/plugin-help/node_modules/@oclif/core": { - "version": "4.0.22", - "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.0.22.tgz", - "integrity": "sha512-aXM2O4g7f+kPNzhhOfqGOVRVYDxTVrH7Y720MuH0Twq5WHMxI4XwntnyBaRscoCPG6FWhItZLtiZxsvaUdupGg==", + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.0.23.tgz", + "integrity": "sha512-wDl/eis7XDIM1pQWUGKLB+EQKJO9UrjaQ5NcwIbz7GW0gWuJfo9QAK75csgNUN/9Pbok9Ryt+sJgogS4RCIp5g==", "dev": true, "license": "MIT", "dependencies": { @@ -2335,6 +2335,7 @@ "is-wsl": "^2.2.0", "lilconfig": "^3.1.2", "minimatch": "^9.0.5", + "semver": "^7.6.3", "string-width": "^4.2.3", "supports-color": "^8", "widest-line": "^3.1.0", @@ -2345,6 +2346,19 @@ "node": ">=18.0.0" } }, + "node_modules/@oclif/plugin-help/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@phenomnomnominal/tsquery": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@phenomnomnominal/tsquery/-/tsquery-5.0.1.tgz", @@ -2457,6 +2471,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -2470,6 +2485,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -2483,6 +2499,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2496,6 +2513,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2509,6 +2527,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2522,6 +2541,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2535,6 +2555,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2548,6 +2569,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2561,6 +2583,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2574,6 +2597,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2587,6 +2611,7 @@ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2600,6 +2625,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2613,6 +2639,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2626,6 +2653,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2639,6 +2667,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2652,6 +2681,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2875,9 +2905,9 @@ "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.7", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", - "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "version": "4.17.9", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.9.tgz", + "integrity": "sha512-w9iWudx1XWOHW5lQRS9iKpK/XuRhnN+0T7HvdCCd802FYkT1AMTnxndJHGrNJwRoRHkslGr4S29tjm1cT7x/7w==", "license": "MIT" }, "node_modules/@types/mapbox__point-geometry": { @@ -2907,9 +2937,9 @@ } }, "node_modules/@types/node": { - "version": "20.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", - "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", + "version": "20.16.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.9.tgz", + "integrity": "sha512-rkvIVJxsOfBejxK7I0FO5sa2WxFmJCzoDwcd88+fq/CUfynNywTo/1/T6hyFz22CyztsnLS9nVlHOnTI36RH5w==", "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -2941,9 +2971,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.7.tgz", - "integrity": "sha512-KUnDCJF5+AiZd8owLIeVHqmW9yM4sqmDVf2JRJiBMFkGvkoZ4/WyV2lL4zVsoinmRS/W3FeEdZLEWFRofnT2FQ==", + "version": "18.3.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.9.tgz", + "integrity": "sha512-+BpAVyTpJkNWWSSnaLBk6ePpHLOGJKnEQNbINNovPWzvEUyAe3e+/d494QdEh71RekM/qV7lw6jzf1HGrJyAtQ==", "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -3392,16 +3422,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz", - "integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz", + "integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.6.0", - "@typescript-eslint/types": "8.6.0", - "@typescript-eslint/typescript-estree": "8.6.0" + "@typescript-eslint/scope-manager": "8.7.0", + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/typescript-estree": "8.7.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3415,14 +3445,14 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz", - "integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz", + "integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.6.0", - "@typescript-eslint/visitor-keys": "8.6.0" + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/visitor-keys": "8.7.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3433,9 +3463,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz", - "integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz", + "integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==", "dev": true, "license": "MIT", "engines": { @@ -3447,14 +3477,14 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz", - "integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz", + "integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.6.0", - "@typescript-eslint/visitor-keys": "8.6.0", + "@typescript-eslint/types": "8.7.0", + "@typescript-eslint/visitor-keys": "8.7.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3476,13 +3506,13 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz", - "integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz", + "integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.6.0", + "@typescript-eslint/types": "8.7.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -4132,9 +4162,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "dev": true, "funding": [ { @@ -4152,8 +4182,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" }, @@ -4263,9 +4293,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001660", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", - "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==", + "version": "1.0.30001664", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz", + "integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==", "dev": true, "funding": [ { @@ -5143,9 +5173,9 @@ } }, "node_modules/dom-lib": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dom-lib/-/dom-lib-3.3.1.tgz", - "integrity": "sha512-N2mpo8qQmB9wIMZJVjER+BSh4GJiZZ7S6EjnMtyETcXo90hpITUDXpUhqOcfXZ2ZefytuYYKTZMp3CGR2X+tDA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/dom-lib/-/dom-lib-3.3.2.tgz", + "integrity": "sha512-ux0wcf6lggOCcJ6O3Q3mewbCOM/CL9f6+NXmxaWsF0/AKCvFNbfdmmqNnMG7cMVupCr9VeFEYWspSAD9WT/6gA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0" @@ -5202,9 +5232,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.24", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.24.tgz", - "integrity": "sha512-0x0wLCmpdKFCi9ulhvYZebgcPmHTkFVUfU2wzDykadkslKwT4oAmDTHEKLnlrDsMGZe4B+ksn8quZfZjYsBetA==", + "version": "1.5.29", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.29.tgz", + "integrity": "sha512-PF8n2AlIhCKXQ+gTpiJi0VhcHDb69kYX4MtCiivctc2QD3XuNZ/XIOlbGzt7WAjjEev0TtaH6Cu3arZExm5DOw==", "dev": true, "license": "ISC" }, @@ -5825,9 +5855,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.11.0.tgz", - "integrity": "sha512-gbBE5Hitek/oG6MUVj6sFuzEjA/ClzNflVrLovHi/JgLdC7fiN5gLAY1WIPW1a0V5I999MnsrvVrCOGmmVqDBQ==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, "license": "MIT", "dependencies": { @@ -6177,9 +6207,9 @@ } }, "node_modules/eslint-plugin-n": { - "version": "17.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.10.2.tgz", - "integrity": "sha512-e+s4eAf5NtJaxPhTNu3qMO0Iz40WANS93w9LQgYcvuljgvDmWi/a3rh+OrNyMHeng6aOWGJO0rCg5lH4zi8yTw==", + "version": "17.10.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.10.3.tgz", + "integrity": "sha512-ySZBfKe49nQZWR1yFaA0v/GsH6Fgp8ah6XV0WDz6CN8WO0ek4McMzb7A2xnf4DCYV43frjCygvb9f/wx7UUxRw==", "dev": true, "license": "MIT", "optional": true, @@ -7576,9 +7606,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", - "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz", + "integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==", "dev": true, "license": "MIT" }, @@ -9703,9 +9733,9 @@ } }, "node_modules/maplibre-gl": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.0.tgz", - "integrity": "sha512-hkt7je7NxiMQE8EpCxLWP8t6tkK6SkrMe0hIBjYd4Ar/Q7BOCILxthGmGnU993Mwmkvs2mGiXnVUSOK12DeCzg==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz", + "integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==", "license": "BSD-3-Clause", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", @@ -11212,6 +11242,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "1.0.5" }, @@ -11243,9 +11274,9 @@ } }, "node_modules/rsuite": { - "version": "5.70.2", - "resolved": "https://registry.npmjs.org/rsuite/-/rsuite-5.70.2.tgz", - "integrity": "sha512-0VZxc/kzeVIEd7PNobguqHUrf5VYYovKXkYg+GiE4yp+HADuWj/1JqugzTAcxk/uxSLIwO+VFgYISi4DJf3/fg==", + "version": "5.70.3", + "resolved": "https://registry.npmjs.org/rsuite/-/rsuite-5.70.3.tgz", + "integrity": "sha512-gRLq3Uy4nAcCKoa5QpydvWDrsV/WYiTcoPytnQLo4LOtmOfTaMsehK2Dz+h2iiz6Ro8Oo69p+JTQa0TdQ3sl8w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.1", @@ -12805,9 +12836,9 @@ } }, "node_modules/vite": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", - "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/canopeum_frontend/src/App.scss b/canopeum_frontend/src/App.scss index 3159719e8..ee7c01904 100644 --- a/canopeum_frontend/src/App.scss +++ b/canopeum_frontend/src/App.scss @@ -4,7 +4,11 @@ $primary: #007E51; $secondary: #F18200; $border-radius: 0.5rem; $link-decoration: none; +/* TODO: We should reference palette, not direct colors +https://github.com/BesLogic/releaf-canopeum/issues/249 */ $green: #06C270; +$lightgreen: #E8F3E9; +$cream: #FFFAF5; $font-family-sans-serif: -apple-system, BlinkMacSystemFont, 'Lato', Roboto, 'Helvetica Neue', Arial, sans-serif; $font-family-monospace: 'Helvetica Neue', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; diff --git a/canopeum_frontend/src/assets/styles/DropdownMenu.scss b/canopeum_frontend/src/assets/styles/DropdownMenu.scss new file mode 100644 index 000000000..d23c62d7a --- /dev/null +++ b/canopeum_frontend/src/assets/styles/DropdownMenu.scss @@ -0,0 +1,8 @@ +.rs-dropdown-item-submenu:hover > .rs-dropdown-item-toggle { + background-color: $lightgreen; + color: inherit; +} + +.rs-dropdown-item.rs-dropdown-item-focus { + background-color: $lightgreen; +} diff --git a/canopeum_frontend/src/assets/styles/GlobalStyles.scss b/canopeum_frontend/src/assets/styles/GlobalStyles.scss index 07275c15b..564028e12 100644 --- a/canopeum_frontend/src/assets/styles/GlobalStyles.scss +++ b/canopeum_frontend/src/assets/styles/GlobalStyles.scss @@ -5,8 +5,7 @@ @import "./Icons.scss"; @import "./Navbar.scss"; @import "./Transitions.scss"; - -$cream: #FFFAF5; +@import "./DropdownMenu.scss"; $small-width: 540px; $medium-width: 720px; @@ -16,6 +15,7 @@ $xxlarge-width: 1600px; ::-webkit-scrollbar { width: 10px; + height: 10px; } ::-webkit-scrollbar-track { @@ -49,11 +49,15 @@ body { } .lightgreen-color { - color: #E8F3E9; + color: $lightgreen; } .bg-lightgreen { - background: #E8F3E9; + background: $lightgreen; +} + +.cream-color { + color: $cream; } .bg-cream { diff --git a/canopeum_frontend/src/components/CreatePostWidget.tsx b/canopeum_frontend/src/components/CreatePostWidget.tsx index 54301bde5..562ffe575 100644 --- a/canopeum_frontend/src/components/CreatePostWidget.tsx +++ b/canopeum_frontend/src/components/CreatePostWidget.tsx @@ -164,7 +164,7 @@ const CreatePostWidget = ({ siteId, addNewPost }: Props) => {
{postBodyNumberOfWords}/{MAXIMUM_WORDS_PER_POST} - {translate('social.comments.words', { count: MAXIMUM_WORDS_PER_POST })} + {translate('social.comments.word', { count: MAXIMUM_WORDS_PER_POST })}
diff --git a/canopeum_frontend/src/components/MainLayout.tsx b/canopeum_frontend/src/components/MainLayout.tsx index d4e942054..916532bd9 100644 --- a/canopeum_frontend/src/components/MainLayout.tsx +++ b/canopeum_frontend/src/components/MainLayout.tsx @@ -72,8 +72,6 @@ const MainLayout = () => { } path='*' /> - } path='/home' /> - {/* The following routes are accessible to Visitors without any authentication */} } path='/sites/:siteId/social' /> } path='/posts/:postId' /> diff --git a/canopeum_frontend/src/components/analytics/AnalyticsSiteHeader.tsx b/canopeum_frontend/src/components/analytics/AnalyticsSiteHeader.tsx index a352d9284..75f41e49c 100644 --- a/canopeum_frontend/src/components/analytics/AnalyticsSiteHeader.tsx +++ b/canopeum_frontend/src/components/analytics/AnalyticsSiteHeader.tsx @@ -2,18 +2,17 @@ import './AnalyticsSiteHeader.scss' import { useContext } from 'react' import { useTranslation } from 'react-i18next' -import { Link } from 'react-router-dom' import SiteCountBadge from '@components/analytics/SiteCountBadge' import SiteSponsorProgress from '@components/analytics/SiteSponsorProgress' +import BatchSponsorLogo from '@components/batches/BatchSponsorLogo' import { LanguageContext } from '@components/context/LanguageContext' import CustomIconBadge from '@components/CustomIconBadge' -import { appRoutes } from '@constants/routes.constant' import { getImageNameByWMOCategories } from '@constants/weatherImageMap' -import type { SiteDetailSummary } from '@services/api' +import type { SiteSummaryDetail } from '@services/api' type Props = { - readonly siteSummary: SiteDetailSummary, + readonly siteSummary: SiteSummaryDetail, } const AnalyticsSiteHeader = ({ siteSummary }: Props) => { @@ -52,20 +51,12 @@ const AnalyticsSiteHeader = ({ siteSummary }: Props) => {
-
-
-

{siteSummary.name}

-
- - {translateValue(siteSummary.siteType)} -
+
+

{siteSummary.name}

+
+ + {translateValue(siteSummary.siteType)}
- - - -
{ />
-
-
- group - {translate('analyticsSite.sponsors')}: +
+ group + {translate('analyticsSite.sponsors')}: +
{siteSummary.sponsors.map(sponsor => ( - {sponsor} + ))}
- +
diff --git a/canopeum_frontend/src/components/analytics/BatchActions.tsx b/canopeum_frontend/src/components/analytics/BatchActions.tsx new file mode 100644 index 000000000..e7eaf5b0e --- /dev/null +++ b/canopeum_frontend/src/components/analytics/BatchActions.tsx @@ -0,0 +1,103 @@ +import { useContext, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Dropdown, Popover, Whisper } from 'rsuite' +import type { OverlayTriggerHandle } from 'rsuite/esm/internals/Picker' + +import EditBatchModal from '@components/analytics/batch-modal/EditBatchModal' +import { SnackbarContext } from '@components/context/SnackbarContext' +import ConfirmationDialog from '@components/dialogs/ConfirmationDialog' +import useApiClient from '@hooks/ApiClientHook' +import type { BatchDetail } from '@services/api' + +type Props = { + readonly onEdit: () => void, + readonly onDelete: () => void, + readonly batchDetail: BatchDetail, +} + +const BatchActions = ({ onEdit, onDelete, batchDetail }: Props) => { + const { t: translate } = useTranslation() + const { openAlertSnackbar } = useContext(SnackbarContext) + const { getApiClient } = useApiClient() + + const whisperRef = useRef(null) + + const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false) + const [batchToEdit, setBatchToEdit] = useState(null) + + const deleteBatch = async () => { + whisperRef.current?.close() + try { + await getApiClient().batchClient.delete(batchDetail.id) + openAlertSnackbar( + translate('analyticsSite.delete-batch.success', { batchName: batchDetail.name }), + ) + onDelete() + } catch { + openAlertSnackbar( + translate('analyticsSite.delete-batch.error', { batchName: batchDetail.name }), + { severity: 'error' }, + ) + } + } + + const handleConfirmDeleteClose = (proceed: boolean) => { + setConfirmDeleteOpen(false) + if (proceed) void deleteBatch() + } + + return ( + <> + + + setBatchToEdit(batchDetail)}> + {translate('analyticsSite.edit-batch')} + + setConfirmDeleteOpen(true)}> + {translate('analyticsSite.delete-batch.title')} + + + + } + trigger='click' + > + + + {batchToEdit && ( + { + setBatchToEdit(null) + if (reason === 'edit') onEdit() + }} + /> + )} + + {translate('analyticsSite.delete-batch.message', { + batchName: batchDetail.name, + })} + + + ) +} + +export default BatchActions diff --git a/canopeum_frontend/src/components/analytics/BatchTable.tsx b/canopeum_frontend/src/components/analytics/BatchTable.tsx index d58fe405a..c91589e08 100644 --- a/canopeum_frontend/src/components/analytics/BatchTable.tsx +++ b/canopeum_frontend/src/components/analytics/BatchTable.tsx @@ -2,7 +2,8 @@ import { useCallback, useContext, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import EditBatchModal from '@components/analytics/batch-modal/EditBatchModal' +import BatchActions from '@components/analytics/BatchActions' +import BatchSponsorLogo from '@components/batches/BatchSponsorLogo' import { LanguageContext } from '@components/context/LanguageContext' import useApiClient from '@hooks/ApiClientHook' import type { BatchDetail } from '@services/api' @@ -23,7 +24,6 @@ const BatchTable = (props: Props) => { const { getApiClient } = useApiClient() const [batches, setBatches] = useState(props.batches) - const [batchToEdit, setBatchToEdit] = useState(null) const fetchBatch = useCallback( async (siteId: number) => { @@ -66,14 +66,15 @@ const BatchTable = (props: Props) => { scope='col' style={{ width: '17.5rem' }} > - {batch.name} - +
+ {batch.name} + + setBatches(previous => previous.filter(b => b.id !== batch.id))} + onEdit={() => void fetchBatch(props.siteId)} + /> +
))} @@ -90,7 +91,7 @@ const BatchTable = (props: Props) => { key={`batch-${batch.id}-sponsor`} style={{ borderColor: cellBorderColor }} > - {batch.sponsor} + ))} @@ -108,9 +109,9 @@ const BatchTable = (props: Props) => { style={{ borderColor: cellBorderColor }} >
    - {batch.species.map(type => ( -
  • - {translateValue(type)} x {type.quantity} + {batch.species.map(batchSpecies => ( +
  • + {translateValue(batchSpecies.treeType)} x {batchSpecies.quantity}
  • ))}
@@ -299,8 +300,8 @@ const BatchTable = (props: Props) => { >
    {batch.seeds.map(batchSeed => ( -
  • - {translateValue(batchSeed)} x {batchSeed.quantity} +
  • + {translateValue(batchSeed.treeType)} x {batchSeed.quantity}
  • ))}
@@ -309,15 +310,6 @@ const BatchTable = (props: Props) => { - {batchToEdit && ( - { - setBatchToEdit(null) - if (reason === 'edit') void fetchBatch(props.siteId) - }} - /> - )}
) } diff --git a/canopeum_frontend/src/components/analytics/FertilizersSelector.tsx b/canopeum_frontend/src/components/analytics/FertilizersSelector.tsx index d254e7467..61b427d55 100644 --- a/canopeum_frontend/src/components/analytics/FertilizersSelector.tsx +++ b/canopeum_frontend/src/components/analytics/FertilizersSelector.tsx @@ -4,13 +4,13 @@ import { useTranslation } from 'react-i18next' import OptionQuantitySelector, { type SelectorOption, type SelectorOptionQuantity } from '@components/analytics/OptionQuantitySelector' import { LanguageContext } from '@components/context/LanguageContext' import useApiClient from '@hooks/ApiClientHook' -import { Batchfertilizer, type FertilizerType } from '@services/api' +import { FertilizerType } from '@services/api' import { notEmpty } from '@utils/arrayUtils' type Props = { readonly fertilizers?: FertilizerType[], // Make sure that onChange is included in a useCallback if part of a component - readonly onChange: (selectedFertilizers: Batchfertilizer[]) => void, + readonly onChange: (selectedFertilizers: FertilizerType[]) => void, } const FertilizersSelector = ({ onChange, fertilizers }: Props) => { @@ -66,7 +66,7 @@ const FertilizersSelector = ({ onChange, fertilizers }: Props) => { const matchingFertilizer = availableFertilizers.get(optionQuantity.option.value) if (!matchingFertilizer) return null - return new Batchfertilizer({ + return new FertilizerType({ ...matchingFertilizer, }) }) diff --git a/canopeum_frontend/src/components/analytics/ImageUpload.scss b/canopeum_frontend/src/components/analytics/ImageUpload.scss new file mode 100644 index 000000000..8fdd95c93 --- /dev/null +++ b/canopeum_frontend/src/components/analytics/ImageUpload.scss @@ -0,0 +1,11 @@ +@import '../../App.scss'; + +.upload-button { + background-color: var(--bs-body-bg); +} + +.upload-instruction { + background-color: var(--bs-body-bg); + border-radius: $border-radius; + padding: 0 0.5rem; +} diff --git a/canopeum_frontend/src/components/analytics/ImageUpload.tsx b/canopeum_frontend/src/components/analytics/ImageUpload.tsx index 3fa831b59..bdc3ca3d6 100644 --- a/canopeum_frontend/src/components/analytics/ImageUpload.tsx +++ b/canopeum_frontend/src/components/analytics/ImageUpload.tsx @@ -1,10 +1,13 @@ /* eslint-disable jsx-a11y/no-noninteractive-element-interactions -- custom input label */ +import './ImageUpload.scss' + import type { ChangeEvent, DragEvent } from 'react' import { useTranslation } from 'react-i18next' import UploadIcon from '@assets/icons/upload.svg' type Props = { + readonly id: string, readonly imageUrl?: string, readonly onChange: (file: File) => void, } @@ -14,7 +17,7 @@ const supportedFileTypes = [ 'image/jpeg', ] -const ImageUpload = ({ onChange, imageUrl }: Props) => { +const ImageUpload = ({ id, onChange, imageUrl }: Props) => { const { t } = useTranslation() const handleFileChange = ( @@ -35,10 +38,10 @@ const ImageUpload = ({ onChange, imageUrl }: Props) => { } return ( -
+
handleFileChange(event)} type='file' /> diff --git a/canopeum_frontend/src/components/analytics/MulchLayersSelector.tsx b/canopeum_frontend/src/components/analytics/MulchLayersSelector.tsx index 03459b38f..8d0cdae84 100644 --- a/canopeum_frontend/src/components/analytics/MulchLayersSelector.tsx +++ b/canopeum_frontend/src/components/analytics/MulchLayersSelector.tsx @@ -4,14 +4,13 @@ import { useTranslation } from 'react-i18next' import OptionQuantitySelector, { type SelectorOption, type SelectorOptionQuantity } from '@components/analytics/OptionQuantitySelector' import { LanguageContext } from '@components/context/LanguageContext' import useApiClient from '@hooks/ApiClientHook' -import type { MulchLayerType } from '@services/api' -import { BatchMulchLayer } from '@services/api' +import { MulchLayerType } from '@services/api' import { notEmpty } from '@utils/arrayUtils' type Props = { readonly mulchLayers?: MulchLayerType[], // Make sure that onChange is included in a useCallback if part of a component - readonly onChange: (selectedMulchLayers: BatchMulchLayer[]) => void, + readonly onChange: (selectedMulchLayers: MulchLayerType[]) => void, } const MulchLayersSelector = ({ onChange, mulchLayers }: Props) => { @@ -67,7 +66,7 @@ const MulchLayersSelector = ({ onChange, mulchLayers }: Props) => { const matchingMulchLayer = availableMulchLayers.get(optionQuantity.option.value) if (!matchingMulchLayer) return null - return new BatchMulchLayer({ + return new MulchLayerType({ ...matchingMulchLayer, }) }) diff --git a/canopeum_frontend/src/components/analytics/SiteAdminTabs.scss b/canopeum_frontend/src/components/analytics/SiteAdminTabs.scss new file mode 100644 index 000000000..e4710aea2 --- /dev/null +++ b/canopeum_frontend/src/components/analytics/SiteAdminTabs.scss @@ -0,0 +1,11 @@ +@import '../../App.scss'; + +.admin-site-tabs.nav .nav-link { + color: $cream; + opacity: 60%; + + &.active { + opacity: 100%; + border-bottom: 2px solid $cream; + } +} diff --git a/canopeum_frontend/src/components/analytics/SiteAdminTabs.tsx b/canopeum_frontend/src/components/analytics/SiteAdminTabs.tsx new file mode 100644 index 000000000..25f51a084 --- /dev/null +++ b/canopeum_frontend/src/components/analytics/SiteAdminTabs.tsx @@ -0,0 +1,51 @@ +import './SiteAdminTabs.scss' + +import { useTranslation } from 'react-i18next' +import { Link, matchPath, useLocation } from 'react-router-dom' + +import { appRoutes } from '@constants/routes.constant' + +type Props = { + readonly siteId: number, +} + +const SiteAdminTabs = ({ siteId }: Props) => { + const { t: translate } = useTranslation() + const location = useLocation() + + const isAnalyticsActive = matchPath(appRoutes.site(siteId), location.pathname) + const isSocialActive = matchPath(appRoutes.siteSocial(siteId), location.pathname) + + return ( +
+
    +
  • + + {translate('analyticsSite.site-tabs.analytics')} + +
  • +
  • + + {translate('analyticsSite.site-tabs.social')} + +
  • +
+
+ ) +} + +export default SiteAdminTabs diff --git a/canopeum_frontend/src/components/analytics/SiteSuccessRatesChart.tsx b/canopeum_frontend/src/components/analytics/SiteSuccessRatesChart.tsx index 978848907..6f8d8db87 100644 --- a/canopeum_frontend/src/components/analytics/SiteSuccessRatesChart.tsx +++ b/canopeum_frontend/src/components/analytics/SiteSuccessRatesChart.tsx @@ -22,7 +22,7 @@ const buildChartOptions = (siteSummaries: SiteSummary[]) => { // total-functions/no-partial-division -- length checked above options.average = siteSummaries.reduce( - (accumulator, current) => accumulator + current.progress, + (accumulator, current) => accumulator + current.sponsorProgress, 0, ) / siteSummaries.length @@ -34,10 +34,10 @@ const buildChartOptions = (siteSummaries: SiteSummary[]) => { // However, bars will appear really thin, so we use stacked bars to stack // 0-height bars on top of each other. const strackedSerie = Array.from({ length: siteSummaries.length }).fill(0) - strackedSerie[siteIndex] = site.progress + strackedSerie[siteIndex] = site.sponsorProgress options.colors.push( - site.progress > options.average + site.sponsorProgress > options.average ? 'var(--bs-primary)' : 'var(--bs-secondary)', ) diff --git a/canopeum_frontend/src/components/analytics/SiteSummaryCard.tsx b/canopeum_frontend/src/components/analytics/SiteSummaryCard.tsx index 1985f7b63..c97c0dbb7 100644 --- a/canopeum_frontend/src/components/analytics/SiteSummaryCard.tsx +++ b/canopeum_frontend/src/components/analytics/SiteSummaryCard.tsx @@ -115,7 +115,7 @@ const SiteSummaryCard = ({ site, admins, onSiteChange, onSiteEdit }: Props) => {
- +
diff --git a/canopeum_frontend/src/components/analytics/SupportSpeciesSelector.tsx b/canopeum_frontend/src/components/analytics/SupportSpeciesSelector.tsx index dc665efd4..4faaa709d 100644 --- a/canopeum_frontend/src/components/analytics/SupportSpeciesSelector.tsx +++ b/canopeum_frontend/src/components/analytics/SupportSpeciesSelector.tsx @@ -4,14 +4,13 @@ import { useTranslation } from 'react-i18next' import OptionQuantitySelector, { type SelectorOption, type SelectorOptionQuantity } from '@components/analytics/OptionQuantitySelector' import { LanguageContext } from '@components/context/LanguageContext' import useApiClient from '@hooks/ApiClientHook' -import type { TreeType } from '@services/api' -import { BatchSupportedSpecies } from '@services/api' +import { TreeType } from '@services/api' import { notEmpty } from '@utils/arrayUtils' type Props = { - readonly species?: BatchSupportedSpecies[], + readonly species?: TreeType[], // Make sure that onChange is included in a useCallback if part of a component - readonly onChange: (selectedSpecies: BatchSupportedSpecies[]) => void, + readonly onChange: (selectedSpecies: TreeType[]) => void, } const SupportSpeciesSelector = ({ onChange, species }: Props) => { @@ -65,7 +64,7 @@ const SupportSpeciesSelector = ({ onChange, species }: Props) => { const matchingSpecie = availableSpecies.get(optionQuantity.option.value) if (!matchingSpecie) return null - return new BatchSupportedSpecies({ + return new TreeType({ ...matchingSpecie, }) }) diff --git a/canopeum_frontend/src/components/analytics/batch-modal/BatchForm.tsx b/canopeum_frontend/src/components/analytics/batch-modal/BatchForm.tsx new file mode 100644 index 000000000..b05d3aa7c --- /dev/null +++ b/canopeum_frontend/src/components/analytics/batch-modal/BatchForm.tsx @@ -0,0 +1,314 @@ +import { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { type BatchFormDto, DEFAULT_BATCH_FORM_DTO, transformToEditBatchDto } from '@components/analytics/batch-modal/batchModal.model' +import FertilizersSelector from '@components/analytics/FertilizersSelector' +import ImageUpload from '@components/analytics/ImageUpload' +import MulchLayersSelector from '@components/analytics/MulchLayersSelector' +import SupportSpeciesSelector from '@components/analytics/SupportSpeciesSelector' +import TreeSpeciesSelector from '@components/analytics/TreeSpeciesSelector' +import type { BatchDetail } from '@services/api' +import { getApiBaseUrl } from '@services/apiSettings' +import { floorNumberValue } from '@utils/formUtils' + +type Props = { + readonly initialBatch?: BatchDetail, + readonly handleBatchChange: (batchFormDto: BatchFormDto) => void, +} + +const BatchForm = ({ handleBatchChange, initialBatch }: Props) => { + const { t } = useTranslation() + + const [batch, setBatch] = useState(DEFAULT_BATCH_FORM_DTO) + const [batchImageURL, setBatchImageURL] = useState() + const [sponsorLogoUrl, setSponsorLogoUrl] = useState() + + useEffect(() => { + if (!initialBatch) return + + setBatch(transformToEditBatchDto(initialBatch)) + setSponsorLogoUrl(`${getApiBaseUrl()}${initialBatch.sponsor.logo.asset}`) + + if (!initialBatch.image) return + + setBatchImageURL(`${getApiBaseUrl()}${initialBatch.image.asset}`) + }, [initialBatch]) + + useEffect(() => handleBatchChange(batch), [batch, handleBatchChange]) + + const onImageUpload = (file: File) => { + setBatch(value => ({ ...value, image: file })) + setBatchImageURL(URL.createObjectURL(file)) + } + + const onSponsorLogoUpload = (file: File) => { + setBatch(value => ({ + ...value, + sponsor: { + ...value.sponsor, + logo: file, + }, + })) + setSponsorLogoUrl(URL.createObjectURL(file)) + } + + return ( +
+
+
+ + setBatch(value => ({ ...value, name: event.target.value }))} + type='text' + value={batch.name} + /> +
+ +
+ + + setBatch(value => ({ + ...value, + sponsor: { ...value.sponsor, name: event.target.value }, + }))} + type='text' + value={batch.sponsor?.name} + /> +
+ +
+ + + setBatch(value => ({ + ...value, + sponsor: { ...value.sponsor, url: event.target.value }, + }))} + type='text' + value={batch.sponsor?.url} + /> +
+ +
+ + +
+ +
+ +
+ + setBatch(value => ({ + ...value, + size: Number.parseInt(event.target.value, 10), + }))} + type='number' + value={floorNumberValue(batch.size)} + /> + + {t('analyticsSite.batch-modal.feet-squared')} + +
+
+ +
+ + setBatch(current => ({ + ...current, + species, + })), + [], + )} + species={batch.species} + /> +
+ +
+ + setBatch(value => ({ ...value, soilCondition: event.target.value }))} + type='text' + value={batch.soilCondition} + /> +
+ + + setBatch(current => ({ + ...current, + fertilizers, + })), + [], + )} + /> + + + setBatch(current => ({ + ...current, + mulchLayers, + })), + [], + )} + /> + + + setBatch(current => ({ + ...current, + supportedSpecies, + })), + [], + )} + species={batch.supportedSpecies} + /> + +
+ + + setBatch(value => ({ + ...value, + plantCount: Number.parseInt(event.target.value, 10), + }))} + type='number' + value={floorNumberValue(batch.plantCount)} + /> +
+ +
+ + + setBatch(value => ({ + ...value, + survivedCount: Number.parseInt(event.target.value, 10), + }))} + type='number' + value={floorNumberValue(batch.survivedCount)} + /> +
+ +
+ + + setBatch(value => ({ + ...value, + replaceCount: Number.parseInt(event.target.value, 10), + }))} + type='number' + value={floorNumberValue(batch.replaceCount)} + /> +
+ +
+ + setBatch(current => ({ + ...current, + seeds: species, + })), + [], + )} + species={batch.seeds} + /> +
+ +
+ + + setBatch(value => ({ + ...value, + totalNumberSeed: Number.parseInt(event.target.value, 10), + }))} + type='number' + value={floorNumberValue(batch.totalNumberSeed)} + /> +
+ +
+ + + setBatch(value => ({ + ...value, + totalPropagation: Number.parseInt(event.target.value, 10), + }))} + type='number' + value={floorNumberValue(batch.totalPropagation)} + /> +
+ +
+ + +
+
+
+ ) +} + +export default BatchForm diff --git a/canopeum_frontend/src/components/analytics/batch-modal/CreateBatchModal.tsx b/canopeum_frontend/src/components/analytics/batch-modal/CreateBatchModal.tsx index 1bba0f4e3..b4846bdc9 100644 --- a/canopeum_frontend/src/components/analytics/batch-modal/CreateBatchModal.tsx +++ b/canopeum_frontend/src/components/analytics/batch-modal/CreateBatchModal.tsx @@ -1,70 +1,28 @@ -/* eslint-disable max-lines -- disable max-lines */ import { Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material' -import { useCallback, useContext, useState } from 'react' +import { useContext, useState } from 'react' import { useTranslation } from 'react-i18next' -import FertilizersSelector from '@components/analytics/FertilizersSelector' -import ImageUpload from '@components/analytics/ImageUpload' -import MulchLayersSelector from '@components/analytics/MulchLayersSelector' -import SupportSpeciesSelector from '@components/analytics/SupportSpeciesSelector' -import TreeSpeciesSelector from '@components/analytics/TreeSpeciesSelector' +import BatchForm from '@components/analytics/batch-modal/BatchForm' +import { type BatchFormDto, DEFAULT_BATCH_FORM_DTO } from '@components/analytics/batch-modal/batchModal.model' import { SnackbarContext } from '@components/context/SnackbarContext' import useApiClient from '@hooks/ApiClientHook' -import type { Batchfertilizer, BatchMulchLayer, BatchSupportedSpecies, Seeds, SiteSummary, Species } from '@services/api' +import type { SiteSummaryDetail } from '@services/api' import { assetFormatter } from '@utils/assetFormatter' -import { floorNumberValue } from '@utils/formUtils' type Props = { readonly open: boolean, - readonly site: SiteSummary, + readonly site: SiteSummaryDetail, readonly handleClose: (reason?: 'create') => void, } -type CreateBatchDto = { - siteId: number, - name?: string, - sponsor?: string, - size?: number, - soilCondition?: string, - plantCount?: number, - survivedCount?: number, - replaceCount?: number, - totalNumberSeed?: number, - totalPropagation?: number, - image?: File, - fertilizers: Batchfertilizer[], - mulchLayers: BatchMulchLayer[], - seeds: Seeds[], - species: Species[], - supportedSpecies: BatchSupportedSpecies[], -} - -const defaultCreateBatch: CreateBatchDto = { - siteId: 0, - name: undefined, - size: undefined, - soilCondition: undefined, - sponsor: undefined, - supportedSpecies: [], - plantCount: undefined, - survivedCount: undefined, - replaceCount: undefined, - totalNumberSeed: undefined, - totalPropagation: undefined, - image: undefined, - fertilizers: [], - mulchLayers: [], - seeds: [], - species: [], -} - const CreateBatchModal = ({ open, site, handleClose }: Props) => { const { t } = useTranslation() const { getApiClient } = useApiClient() const { openAlertSnackbar } = useContext(SnackbarContext) - const [batch, setBatch] = useState(defaultCreateBatch) - const [batchImageURL, setBatchImageURL] = useState() + const [batch, setBatch] = useState(DEFAULT_BATCH_FORM_DTO) + + const handleBatchChange = (batchFormDto: BatchFormDto) => setBatch(batchFormDto) const handleSubmitBatch = async () => { const { @@ -85,6 +43,10 @@ const CreateBatchModal = ({ open, site, handleClose }: Props) => { image, } = batch + const sponsorLogoImage = sponsor?.logo + ? await assetFormatter(sponsor.logo) + : undefined + const batchImage = image ? await assetFormatter(image) : undefined @@ -93,7 +55,9 @@ const CreateBatchModal = ({ open, site, handleClose }: Props) => { await getApiClient().batchClient.create( site.id, name, - sponsor, + sponsor?.name, + sponsor?.url, + sponsorLogoImage, size, soilCondition, plantCount, @@ -114,32 +78,21 @@ const CreateBatchModal = ({ open, site, handleClose }: Props) => { { severity: 'error' }, ) - return + return false } + openAlertSnackbar( t('analyticsSite.batch-modal.feedback.create-success'), ) - resetBatch() handleClose('create') - } - - const onImageUpload = (file: File) => { - setBatch(value => ({ ...value, image: file })) - setBatchImageURL(URL.createObjectURL(file)) - } - const onClose = () => { - resetBatch() - handleClose() + return true } - const resetBatch = () => { - setBatch({ ...defaultCreateBatch }) - setBatchImageURL(undefined) - } + const handleCancel = () => handleClose() return ( - +
{t('analyticsSite.batch-modal.create-title')} @@ -147,236 +100,13 @@ const CreateBatchModal = ({ open, site, handleClose }: Props) => { -
-
-
- - setBatch(value => ({ ...value, name: event.target.value }))} - type='text' - value={batch.name} - /> -
- -
- - setBatch(value => ({ ...value, sponsor: event.target.value }))} - type='text' - value={batch.sponsor} - /> -
- -
- -
- - setBatch(value => ({ - ...value, - size: Number.parseInt(event.target.value, 10), - }))} - type='number' - value={floorNumberValue(batch.size)} - /> - - {t('analyticsSite.batch-modal.feet-squared')} - -
-
- -
- - setBatch(current => ({ - ...current, - species, - })), - [], - )} - species={batch.species} - /> -
- -
- - - setBatch(value => ({ ...value, soilCondition: event.target.value }))} - type='text' - value={batch.soilCondition} - /> -
- - - setBatch(current => ({ - ...current, - fertilizers, - })), - [], - )} - /> - - - setBatch(current => ({ - ...current, - mulchLayers, - })), - [], - )} - /> - - - setBatch(current => ({ - ...current, - supportedSpecies, - })), - [], - )} - species={batch.supportedSpecies} - /> - -
- - - setBatch(value => ({ - ...value, - plantCount: Number.parseInt(event.target.value, 10), - }))} - type='number' - value={floorNumberValue(batch.plantCount)} - /> -
- -
- - - setBatch(value => ({ - ...value, - survivedCount: Number.parseInt(event.target.value, 10), - }))} - type='number' - value={floorNumberValue(batch.survivedCount)} - /> -
- -
- - - setBatch(value => ({ - ...value, - replaceCount: Number.parseInt(event.target.value, 10), - }))} - type='number' - value={floorNumberValue(batch.replaceCount)} - /> -
- -
- - setBatch(current => ({ - ...current, - seeds: species, - })), - [], - )} - species={batch.seeds} - /> -
- -
- - - setBatch(value => ({ - ...value, - totalNumberSeed: Number.parseInt(event.target.value, 10), - }))} - type='number' - value={floorNumberValue(batch.totalNumberSeed)} - /> -
- -
- - - setBatch(value => ({ - ...value, - totalPropagation: Number.parseInt(event.target.value, 10), - }))} - type='number' - value={floorNumberValue(batch.totalPropagation)} - /> -
- -
- - -
-
-
+
diff --git a/canopeum_frontend/src/components/social/SiteSocialHeader.tsx b/canopeum_frontend/src/components/social/SiteSocialHeader.tsx index 513b456cf..f77d7cc40 100644 --- a/canopeum_frontend/src/components/social/SiteSocialHeader.tsx +++ b/canopeum_frontend/src/components/social/SiteSocialHeader.tsx @@ -3,6 +3,7 @@ import './SiteSocialHeader.scss' import { useContext, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' +import BatchSponsorLogo from '@components/batches/BatchSponsorLogo' import { AuthenticationContext } from '@components/context/AuthenticationContext' import { LanguageContext } from '@components/context/LanguageContext' import ToggleSwitch from '@components/inputs/ToggleSwitch' @@ -131,12 +132,12 @@ const SiteSocialHeader = ({ site, viewMode }: Props) => { {translate('social.site-social-header.sponsors')}:
- {site.sponsors.map(sponsorName => ( + {site.sponsors.map(sponsor => (
- {sponsorName} +
))}
diff --git a/canopeum_frontend/src/locale/en/analytics.json b/canopeum_frontend/src/locale/en/analytics.json deleted file mode 100644 index 148dc564d..000000000 --- a/canopeum_frontend/src/locale/en/analytics.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "title": "Manage my Sites", - "create-site": "Create a New Site", - "edit-site-info": "Edit Site Information", - "average": "average", - "batches_one": "batch", - "batches_other": "batches", - "sufficient": "sufficient", - "insufficient": "insufficient", - "table-row-1": "batch name / ID", - "table-row-2": "sponsor", - "table-row-3": "species", - "table-row-4": "size of land planted", - "table-row-5": "soil condition", - "table-row-6": "fertilizers", - "table-row-7": "mulch layers", - "table-row-8": "supported species", - "table-row-9": "number of plants", - "table-row-10": "how many survived", - "table-row-11": "how many replaced", - "table-row-12": "amount of seeds collected in area", - "table-row-13": "types of seeds collected", - "last-update": "Last Update", - "visitors": "Visitors", - "sponsored": "Sponsored", - "unnamed-site": "Unnamed site", - "site-save-success": "Site saved successfully", - "site-save-error": "Error saving Site", - "site-deleted": "Site {{siteName}} was successfully deleted.", - "site-deleted-error": "There was a problem while trying to delete the site {{siteName}}...", - "success-rate-chart": { - "title": "Average Annual Success Rate Per Site" - }, - "batches": { - "batch-tracking": "Batch Tracking" - }, - "site-summary": { - "planted": "Planted", - "survived": "Survived", - "propagation": "Propagation", - "sponsored": "Sponsored", - "no-admins": "No Admins", - "visitors": "Visitors", - "unknown": "Unknown", - "admins-saved": "Admins saved successfully for {{siteName}}!", - "delete-site-confirmation-title": "Delete Site", - "delete-site-confirmation-message": "Are you sure you want to delete the site {{siteName}}?" - }, - "site-modal": { - "site-name": "site name", - "site-type": "site type", - "site-image": "project image or enterprise logo", - "site-gps-coordinates": "GPS coordinates", - "site-dms-latitude": "DMS latitude", - "site-dms-longitude": "DMS longitude", - "site-presentation": "brief presentation", - "site-size": "size of the land", - "site-tree-species": "Number of Trees per Species", - "site-research-partner": "research partnership?", - "site-map-visibility": "visibility on the map", - "yes": "yes", - "no": "no", - "visible": "visible", - "invisible": "invisible" - }, - "image-upload": "or drop an image to upload" -} diff --git a/canopeum_frontend/src/locale/en/analytics.ts b/canopeum_frontend/src/locale/en/analytics.ts new file mode 100644 index 000000000..d57b10200 --- /dev/null +++ b/canopeum_frontend/src/locale/en/analytics.ts @@ -0,0 +1,65 @@ +export default { + title: 'Manage my Sites', + 'create-site': 'Create a New Site', + 'edit-site-info': 'Edit Site Information', + average: 'average', + batch_one: 'batch', + batch_other: 'batches', + sufficient: 'sufficient', + insufficient: 'insufficient', + 'table-row-1': 'batch name / ID', + 'table-row-2': 'sponsor', + 'table-row-3': 'species', + 'table-row-4': 'size of land planted', + 'table-row-5': 'soil condition', + 'table-row-6': 'fertilizers', + 'table-row-7': 'mulch layers', + 'table-row-8': 'supported species', + 'table-row-9': 'number of plants', + 'table-row-10': 'how many survived', + 'table-row-11': 'how many replaced', + 'table-row-12': 'amount of seeds collected in area', + 'table-row-13': 'types of seeds collected', + 'last-update': 'Last Update', + visitors: 'Visitors', + sponsored: 'Sponsored', + 'unnamed-site': 'Unnamed site', + 'site-save-success': 'Site saved successfully', + 'site-save-error': 'Error saving Site', + 'success-rate-chart': { + title: 'Average Annual Success Rate Per Site', + }, + 'batch-tracking': 'Batch Tracking', + 'site-summary': { + planted: 'Planted', + survived: 'Survived', + propagation: 'Propagation', + sponsored: 'Sponsored', + 'no-admins': 'No Admins', + visitors: 'Visitors', + unknown: 'Unknown', + 'admins-saved': 'Admins saved successfully for {{siteName}}!', + 'delete-site-confirmation-title': 'Delete Site', + 'delete-site-confirmation-message': 'Are you sure you want to delete the site {{siteName}}?', + 'site-deleted': 'Site {{siteName}} was successfully deleted.', + 'site-deleted-error': 'There was a problem while trying to delete the site {{siteName}}...', + }, + 'site-modal': { + 'site-name': 'site name', + 'site-type': 'site type', + 'site-image': 'project image or enterprise logo', + 'site-gps-coordinates': 'GPS coordinates', + 'site-dms-latitude': 'DMS latitude', + 'site-dms-longitude': 'DMS longitude', + 'site-presentation': 'brief presentation', + 'site-size': 'size of the land', + 'site-tree-species': 'Number of Trees per Species', + 'site-research-partner': 'research partnership?', + 'site-map-visibility': 'visibility on the map', + yes: 'yes', + no: 'no', + visible: 'visible', + invisible: 'invisible', + }, + 'image-upload': 'or drop an image to upload', +} diff --git a/canopeum_frontend/src/locale/en/analyticsSite.json b/canopeum_frontend/src/locale/en/analyticsSite.json deleted file mode 100644 index 1638db5df..000000000 --- a/canopeum_frontend/src/locale/en/analyticsSite.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "location": "Location", - "batch-tracking": "Batch Tracking", - "social-page": "Social Page", - "sponsors": "Sponsors", - "add-new-batch": "Add a New Batch", - "batch-modal": { - "create-title": "Create a New Batch", - "edit-title": "Edit a Batch", - "name-label": "Batch Name / ID", - "sponsor-label": "Sponsor", - "size-label": "Size of the land", - "feet-squared": "ft²", - "number-of-trees-label": "Number of Trees per Species", - "soil-condition-label": "Soil condition", - "fertilizers-label": "Fertilizers", - "mulch-layers-label": "Mulch layers", - "support-species-label": "Support Species", - "total-number-of-plants-label": "Total Number of plants", - "survived-label": "How many survived", - "replaced-label": "How many replaced", - "seeds-per-species-label": "Number of Seeds Collected per Species", - "total-seeds-label": "Total number of seeds collected", - "propagation-label": "Total Propagation", - "images-label": "Image", - "feedback": { - "create-success": "Batch created successfully", - "create-error": "Error creating batch", - "edit-success": "Batch edited successfully", - "edit-error": "Error editing batch" - } - } -} diff --git a/canopeum_frontend/src/locale/en/analyticsSite.ts b/canopeum_frontend/src/locale/en/analyticsSite.ts new file mode 100644 index 000000000..5b7bba338 --- /dev/null +++ b/canopeum_frontend/src/locale/en/analyticsSite.ts @@ -0,0 +1,46 @@ +export default { + location: 'Location', + 'batch-tracking': 'Batch Tracking', + 'site-tabs': { + analytics: 'Analytics', + social: 'Social', + }, + 'social-page': 'Social Page', + sponsors: 'Sponsors', + 'add-new-batch': 'Add a New Batch', + 'edit-batch': 'Edit Batch', + 'delete-batch': { + title: 'Delete Batch', + message: 'Are you sure you want to delete {{batchName}} batch?', + success: 'Batch {{batchName}} was successfully deleted.', + error: 'There was a problem while trying to delete the batch {{batchName}}...', + }, + 'batch-modal': { + 'create-title': 'Create a New Batch', + 'edit-title': 'Edit a Batch', + 'name-label': 'Batch Name / ID', + 'sponsor-name-label': 'Sponsor', + 'sponsor-website-url-label': 'Sponsor website URL', + 'sponsor-logo-label': 'Sponsor logo', + 'size-label': 'Size of the land', + 'feet-squared': 'ft²', + 'number-of-trees-label': 'Number of Trees per Species', + 'soil-condition-label': 'Soil condition', + 'fertilizers-label': 'Fertilizers', + 'mulch-layers-label': 'Mulch layers', + 'support-species-label': 'Support Species', + 'total-number-of-plants-label': 'Total Number of plants', + 'survived-label': 'How many survived', + 'replaced-label': 'How many replaced', + 'seeds-per-species-label': 'Number of Seeds Collected per Species', + 'total-seeds-label': 'Total number of seeds collected', + 'propagation-label': 'Total Propagation', + 'images-label': 'Image', + feedback: { + 'create-success': 'Batch created successfully', + 'create-error': 'Error creating batch', + 'edit-success': 'Batch edited successfully', + 'edit-error': 'Error editing batch', + }, + }, +} diff --git a/canopeum_frontend/src/locale/en/auth.json b/canopeum_frontend/src/locale/en/auth.json deleted file mode 100644 index 77f69d9ee..000000000 --- a/canopeum_frontend/src/locale/en/auth.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "log-in-header-text": "Log In to Your Account", - "sign-up-header-text": "Create Your Account", - "username-label": "Username", - "username-error-required": "Please enter a username", - "email-label": "Email Address", - "email-error-required": "Please enter an email address", - "email-error-format": "Please enter a valid email address", - "password-label": "Password", - "password-error-required": "Please enter a password", - "password-error-format": "Password is invalid. It must contain minimum 8 characters, and at least 1 letter and 1 number.", - "create-password-label": "Create Password", - "password-confirmation-label": "Re-enter Password", - "password-confirmation-error-required": "Please re-enter your password", - "password-error-must-match": "Passwords do not match", - "log-in": "Log In", - "sign-up": "Sign Up", - "create-account": "Create Account", - "already-have-an-account": "Already have an account?", - "back-to-map": "Back to the map", - "log-in-error": "Error while trying to log in", - "sign-up-error": "Error while trying to sign up", - "code-invalid": "Your invitation link is invalid. Please contact the person who invited you to ask for a new link.", - "invitation-expired": "Your invitation is expired. Please contact the person who invited you to ask for a new link." -} diff --git a/canopeum_frontend/src/locale/en/auth.ts b/canopeum_frontend/src/locale/en/auth.ts new file mode 100644 index 000000000..791c64385 --- /dev/null +++ b/canopeum_frontend/src/locale/en/auth.ts @@ -0,0 +1,28 @@ +export default { + 'log-in-header-text': 'Log In to Your Account', + 'sign-up-header-text': 'Create Your Account', + 'username-label': 'Username', + 'username-error-required': 'Please enter a username', + 'email-label': 'Email Address', + 'email-error-required': 'Please enter an email address', + 'email-error-format': 'Please enter a valid email address', + 'password-label': 'Password', + 'password-error-required': 'Please enter a password', + 'password-error-format': + 'Password is invalid. It must contain minimum 8 characters, and at least 1 letter and 1 number.', + 'create-password-label': 'Create Password', + 'password-confirmation-label': 'Re-enter Password', + 'password-confirmation-error-required': 'Please re-enter your password', + 'password-error-must-match': 'Passwords do not match', + 'log-in': 'Log In', + 'sign-up': 'Sign Up', + 'create-account': 'Create Account', + 'already-have-an-account': 'Already have an account?', + 'back-to-map': 'Back to the map', + 'log-in-error': 'Error while trying to log in', + 'sign-up-error': 'Error while trying to sign up', + 'code-invalid': + 'Your invitation link is invalid. Please contact the person who invited you to ask for a new link.', + 'invitation-expired': + 'Your invitation is expired. Please contact the person who invited you to ask for a new link.', +} diff --git a/canopeum_frontend/src/locale/en/errors.json b/canopeum_frontend/src/locale/en/errors.json deleted file mode 100644 index ce89652d2..000000000 --- a/canopeum_frontend/src/locale/en/errors.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "email-taken": "There is already a user using this email.", - "current-password-invalid": "The entered current password is invalid.", - "password-error-must-match": "Passwords do not match", - "email-invalid": "Invalid email format (e.g. john.doe@contoso.com)", - "url-invalid": "Invalid URL format (e.g. https://www.contoso.com)", - "phone-invalid": "Invalid phone number format (e.g. +1 123 456 7890)" -} diff --git a/canopeum_frontend/src/locale/en/errors.ts b/canopeum_frontend/src/locale/en/errors.ts new file mode 100644 index 000000000..4aff41889 --- /dev/null +++ b/canopeum_frontend/src/locale/en/errors.ts @@ -0,0 +1,8 @@ +export default { + 'email-taken': 'There is already a user using this email.', + 'current-password-invalid': 'The entered current password is invalid.', + 'password-error-must-match': 'Passwords do not match', + 'email-invalid': 'Invalid email format (e.g. john.doe@contoso.com)', + 'url-invalid': 'Invalid URL format (e.g. https://www.contoso.com)', + 'phone-invalid': 'Invalid phone number format (e.g. +1 123 456 7890)', +} diff --git a/canopeum_frontend/src/locale/en/generic.json b/canopeum_frontend/src/locale/en/generic.json deleted file mode 100644 index e10a35620..000000000 --- a/canopeum_frontend/src/locale/en/generic.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "cancel": "Cancel", - "delete": "Delete", - "ok": "Ok", - "submit": "Submit", - "subscribe": "Subscribe", - "copied-clipboard": "Copied to clipboard", - "error-default": "Oooops! There was a problem while loading the data.", - "upload": "Upload", - "edit": "Edit" -} diff --git a/canopeum_frontend/src/locale/en/generic.ts b/canopeum_frontend/src/locale/en/generic.ts new file mode 100644 index 000000000..741737230 --- /dev/null +++ b/canopeum_frontend/src/locale/en/generic.ts @@ -0,0 +1,11 @@ +export default { + cancel: 'Cancel', + delete: 'Delete', + ok: 'Ok', + submit: 'Submit', + subscribe: 'Subscribe', + 'copied-clipboard': 'Copied to clipboard', + 'error-default': 'Oooops! There was a problem while loading the data.', + upload: 'Upload', + edit: 'Edit', +} diff --git a/canopeum_frontend/src/locale/en/home.json b/canopeum_frontend/src/locale/en/home.json deleted file mode 100644 index ad6e2c605..000000000 --- a/canopeum_frontend/src/locale/en/home.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Hello {{username}},", - "subtitle": "Here's the latest news of all organizations you followed:", - "no-news": "You have no followed content. Please visit a site's social page to follow it and subscribe to its news." -} diff --git a/canopeum_frontend/src/locale/en/home.ts b/canopeum_frontend/src/locale/en/home.ts new file mode 100644 index 000000000..b6ce37b2e --- /dev/null +++ b/canopeum_frontend/src/locale/en/home.ts @@ -0,0 +1,6 @@ +export default { + title: 'Hello {{username}},', + subtitle: "Here's the latest news of all organizations you followed:", + 'no-news': + "You have no followed content. Please visit a site's social page to follow it and subscribe to its news.", +} diff --git a/canopeum_frontend/src/locale/en/index.ts b/canopeum_frontend/src/locale/en/index.ts index 48761110d..5057e5124 100644 --- a/canopeum_frontend/src/locale/en/index.ts +++ b/canopeum_frontend/src/locale/en/index.ts @@ -1,27 +1,25 @@ -import analyticsJSON from './analytics.json' -import analyticsSiteJSON from './analyticsSite.json' -import authJSON from './auth.json' -import errorsJSON from './errors.json' -import genericJSON from './generic.json' -import homeJSON from './home.json' -import navbarJSON from './navbar.json' -import postsJSON from './posts.json' -import settingsJSON from './settings.json' -import socialJSON from './social.json' +import analytics from './analytics' +import analyticsSite from './analyticsSite' +import auth from './auth' +import errors from './errors' +import generic from './generic' +import home from './home' +import navbar from './navbar' +import posts from './posts' +import settings from './settings' +import social from './social' -const enJSON = { +export default { translation: { - analytics: { ...analyticsJSON }, - analyticsSite: { ...analyticsSiteJSON }, - auth: { ...authJSON }, - errors: { ...errorsJSON }, - generic: { ...genericJSON }, - home: { ...homeJSON }, - navbar: { ...navbarJSON }, - posts: { ...postsJSON }, - settings: { ...settingsJSON }, - social: { ...socialJSON }, + analytics, + analyticsSite, + auth, + errors, + generic, + home, + navbar, + posts, + settings, + social, }, } - -export default enJSON diff --git a/canopeum_frontend/src/locale/en/navbar.json b/canopeum_frontend/src/locale/en/navbar.json deleted file mode 100644 index 64a5697b4..000000000 --- a/canopeum_frontend/src/locale/en/navbar.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "home": "Home", - "sites": "Sites", - "map": "Map", - "utilities": "Utilities", - "settings": "Settings", - "log-in": "Log In" -} diff --git a/canopeum_frontend/src/locale/en/navbar.ts b/canopeum_frontend/src/locale/en/navbar.ts new file mode 100644 index 000000000..df80dfef8 --- /dev/null +++ b/canopeum_frontend/src/locale/en/navbar.ts @@ -0,0 +1,8 @@ +export default { + home: 'Home', + sites: 'Sites', + map: 'Map', + utilities: 'Utilities', + settings: 'Settings', + 'log-in': 'Log In', +} diff --git a/canopeum_frontend/src/locale/en/posts.json b/canopeum_frontend/src/locale/en/posts.json deleted file mode 100644 index e11e31ad6..000000000 --- a/canopeum_frontend/src/locale/en/posts.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "back-to-social": "Back to the social page", - "error-loading-post": "There was a problem while trying to fetch the post.", - "error-loading-posts": "There was a problem while trying to fetch the posts." -} diff --git a/canopeum_frontend/src/locale/en/posts.ts b/canopeum_frontend/src/locale/en/posts.ts new file mode 100644 index 000000000..1346a551b --- /dev/null +++ b/canopeum_frontend/src/locale/en/posts.ts @@ -0,0 +1,5 @@ +export default { + 'back-to-social': 'Back to the social page', + 'error-loading-post': 'There was a problem while trying to fetch the post.', + 'error-loading-posts': 'There was a problem while trying to fetch the posts.', +} diff --git a/canopeum_frontend/src/locale/en/settings.json b/canopeum_frontend/src/locale/en/settings.json deleted file mode 100644 index b7976ea00..000000000 --- a/canopeum_frontend/src/locale/en/settings.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "tabs": { - "edit-profile": "Edit My Profile", - "manage-admins": "Manage Administrators", - "terms-and-policies": "Terms & Policies", - "logout": "Log Out" - }, - "edit-profile": { - "title": "Edit My Profile", - "profile-saved": "Profile successfully saved!", - "current-password": "Current Password", - "new-password": "New Password", - "new-password-confirmation": "Confirm New Password", - "current-password-invalid": "The entered current password is invalid.", - "save-profile-error": "There was a problem while saving your profile." - }, - "manage-admins": { - "title": "Manage Administrators", - "invite-admin": "Invite an Administrator", - "generate-link": "Generate Link", - "copy-link": "Copy Link", - "assign-to-label": "Assign to", - "generate-link-error": "There was a problem while trying to generate the link.", - "copy-link-message": "Please copy and share this link with the owner of the following email address:" - }, - "terms-and-policies": { - "title": "Terms & Policies" - } -} diff --git a/canopeum_frontend/src/locale/en/settings.ts b/canopeum_frontend/src/locale/en/settings.ts new file mode 100644 index 000000000..9419252e3 --- /dev/null +++ b/canopeum_frontend/src/locale/en/settings.ts @@ -0,0 +1,30 @@ +export default { + tabs: { + 'edit-profile': 'Edit My Profile', + 'manage-admins': 'Manage Administrators', + 'terms-and-policies': 'Terms & Policies', + logout: 'Log Out', + }, + 'edit-profile': { + title: 'Edit My Profile', + 'profile-saved': 'Profile successfully saved!', + 'current-password': 'Current Password', + 'new-password': 'New Password', + 'new-password-confirmation': 'Confirm New Password', + 'current-password-invalid': 'The entered current password is invalid.', + 'save-profile-error': 'There was a problem while saving your profile.', + }, + 'manage-admins': { + title: 'Manage Administrators', + 'invite-admin': 'Invite an Administrator', + 'generate-link': 'Generate Link', + 'copy-link': 'Copy Link', + 'assign-to-label': 'Assign to', + 'generate-link-error': 'There was a problem while trying to generate the link.', + 'copy-link-message': + 'Please copy and share this link with the owner of the following email address:', + }, + 'terms-and-policies': { + title: 'Terms & Policies', + }, +} diff --git a/canopeum_frontend/src/locale/en/social.json b/canopeum_frontend/src/locale/en/social.json deleted file mode 100644 index 2950283c2..000000000 --- a/canopeum_frontend/src/locale/en/social.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "site-social-header": { - "follow": "Follow", - "unfollow": "Unfollow", - "sponsors": "Sponsors", - "public": "Public" - }, - "comments": { - "leave-a-comment": "Leave a Comment", - "words_one": "word", - "words_other": "words", - "comments": "Comments", - "send": "Send", - "comment-body-required": "Your comment cannot be empty.", - "comment-body-max-chars_one": "Your comment cannot contain more than {{count}} words.", - "comment-body-max-chars_other": "Your comment cannot contain more than {{count}} words.", - "comment-deletion-error": "There was a problem while trying to delete the comment.", - "comment-deletion-confirm-title": "Delete Comment", - "comment-deletion-confirm-self": "Are you sure you want to delete your comment? Doing so will permanently delete this comment; this action is irreversible.", - "comment-deletion-confirm-other": "Are you sure you want to delete this comment by {{author}}? Doing so will permanently delete this comment; this action is irreversible." - }, - "share-dialog": { - "title": "Share Post", - "message": "To share this post, simply copy this link and paste it on your favorite social media platforms, or send to a friend!", - "copy-link": "Copy Link" - }, - "posts": { - "post-body-required": "You must enter a message in order to publish your post." - }, - "widgets": { - "create": "add a new widget", - "submit": "submit", - "cancel": "cancel", - "max-word": "100 words maximum", - "title": "widget title" - }, - "contact": { - "title": "Contact", - "address": "Address", - "phone": "Phone", - "email": "Email", - "feedback": { - "edit-success": "Contact edited successfully", - "edit-error": "Error editing contact" - } - }, - "announcement": { - "title": "Announcement", - "body": "Body", - "link": "Link", - "feedback": { - "edit-success": "Announcement edited successfully", - "edit-error": "Error editing announcement" - } - } -} diff --git a/canopeum_frontend/src/locale/en/social.ts b/canopeum_frontend/src/locale/en/social.ts new file mode 100644 index 000000000..0d9299c17 --- /dev/null +++ b/canopeum_frontend/src/locale/en/social.ts @@ -0,0 +1,59 @@ +export default { + 'site-social-header': { + follow: 'Follow', + unfollow: 'Unfollow', + sponsors: 'Sponsors', + public: 'Public', + }, + comments: { + 'leave-a-comment': 'Leave a Comment', + word_one: 'word', + word_other: 'words', + comments: 'Comments', + send: 'Send', + 'comment-body-required': 'Your comment cannot be empty.', + 'comment-body-max-chars_one': 'Your comment cannot contain more than {{count}} words.', + 'comment-body-max-chars_other': 'Your comment cannot contain more than {{count}} words.', + 'comment-deletion-error': 'There was a problem while trying to delete the comment.', + 'comment-deletion-confirm-title': 'Delete Comment', + 'comment-deletion-confirm-self': + 'Are you sure you want to delete your comment? Doing so will permanently delete this comment; this action is irreversible.', + 'comment-deletion-confirm-other': + 'Are you sure you want to delete this comment by {{author}}? Doing so will permanently delete this comment; this action is irreversible.', + }, + 'share-dialog': { + title: 'Share Post', + message: + 'To share this post, simply copy this link and paste it on your favorite social media platforms, or send to a friend!', + 'copy-link': 'Copy Link', + }, + posts: { + 'post-body-required': 'You must enter a message in order to publish your post.', + }, + widgets: { + create: 'add a new widget', + submit: 'submit', + cancel: 'cancel', + 'max-word': '100 words maximum', + title: 'widget title', + }, + contact: { + title: 'Contact', + address: 'Address', + phone: 'Phone', + email: 'Email', + feedback: { + 'edit-success': 'Contact edited successfully', + 'edit-error': 'Error editing contact', + }, + }, + announcement: { + title: 'Announcement', + body: 'Body', + link: 'Link', + feedback: { + 'edit-success': 'Announcement edited successfully', + 'edit-error': 'Error editing announcement', + }, + }, +} diff --git a/canopeum_frontend/src/locale/fr/analytics.json b/canopeum_frontend/src/locale/fr/analytics.json deleted file mode 100644 index 95a045b71..000000000 --- a/canopeum_frontend/src/locale/fr/analytics.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "title": "Gérer mes Sites", - "create-site": "Créer un Nouveau Site", - "edit-site-info": "Modifier les données d'un Site", - "average": "moyenne", - "batches_one": "lot", - "batches_other": "lots", - "sufficient": "suffisant", - "insufficient": "insuffisant", - "table-row-1": "nom du lot / ID", - "table-row-2": "sponsor", - "table-row-3": "espèces", - "table-row-4": "taille du terrain planté", - "table-row-5": "état du sol", - "table-row-6": "engrais", - "table-row-7": "couches de paillis", - "table-row-8": "espèces supportées", - "table-row-9": "nombre de plants", - "table-row-10": "combien ont survécu", - "table-row-11": "combien ont été remplacés", - "table-row-12": "quantité de graines collectées dans la zone", - "table-row-13": "types de graines collectées", - "last-update": "Dernière Mise à Jour", - "visitors": "Visiteurs", - "sponsored": "Sponsorisé", - "no-admins": "Pas d'administrateurs", - "unknown": "Inconnu", - "admins-saved": "Administrateurs sauvegardés avec succès pour {{siteName}}!", - "unnamed-site": "Site sans nom", - "site-save-success": "Site saved successfully", - "site-save-error": "Error saving Site", - "site-deleted": "Le site {{siteName}} a été supprimé avec succès.", - "site-deleted-error": "Un problème est survenu lors de la tentative de suppression du site {{siteName}}...", - "success-rate-chart": { - "title": "Taux De Réussite Annuel Moyen Par Site" - }, - "batches": { - "batch-tracking": "Suivi des Lots" - }, - "site-summary": { - "planted": "Planté", - "survived": "Survécu", - "propagation": "Propagation", - "sponsored": "Sponsorisé", - "no-admins": "Pas d'administrateurs", - "visitors": "Visiteurs", - "unknown": "Inconnu", - "admins-saved": "Administrateurs sauvegardés avec succès pour {{siteName}}!", - "delete-site-confirmation-title": "Suppression du Site", - "delete-site-confirmation-message": "Etes vous sur de vouloir supprimer le site {{siteName}}?" - }, - "site-modal": { - "site-name": "nom du site", - "site-type": "type de site", - "site-image": "image du projet ou logo de l'entreprise", - "site-gps-coordinates": "coordonnées GPS", - "site-dms-latitude": "latitude DMS", - "site-dms-longitude": "longitude DMS", - "site-presentation": "présentation brève", - "site-size": "taille du terrain", - "site-tree-species": "nombre d'arbres par espèce", - "site-research-partner": "partenariat de recherche?", - "site-map-visibility": "visibilité sur la carte", - "yes": "oui", - "no": "non", - "visible": "visible", - "invisible": "invisible" - }, - "image-upload": "ou déposez une image à téléverser" -} diff --git a/canopeum_frontend/src/locale/fr/analytics.ts b/canopeum_frontend/src/locale/fr/analytics.ts new file mode 100644 index 000000000..6d8d4ba78 --- /dev/null +++ b/canopeum_frontend/src/locale/fr/analytics.ts @@ -0,0 +1,68 @@ +import type Shape from '../en/analytics' + +export default { + title: 'Gérer mes Sites', + 'create-site': 'Créer un Nouveau Site', + 'edit-site-info': "Modifier les données d'un Site", + average: 'moyenne', + batch_one: 'lot', + batch_other: 'lots', + sufficient: 'suffisant', + insufficient: 'insuffisant', + 'table-row-1': 'nom du lot / ID', + 'table-row-2': 'sponsor', + 'table-row-3': 'espèces', + 'table-row-4': 'taille du terrain planté', + 'table-row-5': 'état du sol', + 'table-row-6': 'engrais', + 'table-row-7': 'couches de paillis', + 'table-row-8': 'espèces supportées', + 'table-row-9': 'nombre de plants', + 'table-row-10': 'combien ont survécu', + 'table-row-11': 'combien ont été remplacés', + 'table-row-12': 'quantité de graines collectées dans la zone', + 'table-row-13': 'types de graines collectées', + 'last-update': 'Dernière Mise à Jour', + visitors: 'Visiteurs', + sponsored: 'Sponsorisé', + 'unnamed-site': 'Site sans nom', + 'site-save-success': 'Site saved successfully', + 'site-save-error': 'Error saving Site', + 'success-rate-chart': { + title: 'Taux De Réussite Annuel Moyen Par Site', + }, + 'batch-tracking': 'Suivi des Lots', + 'site-summary': { + planted: 'Planté', + survived: 'Survécu', + propagation: 'Propagation', + sponsored: 'Sponsorisé', + 'no-admins': "Pas d'administrateurs", + visitors: 'Visiteurs', + unknown: 'Inconnu', + 'admins-saved': 'Administrateurs sauvegardés avec succès pour {{siteName}}!', + 'delete-site-confirmation-title': 'Suppression du Site', + 'delete-site-confirmation-message': 'Êtes vous sur de vouloir supprimer le site {{siteName}}?', + 'site-deleted': 'Le site {{siteName}} a été supprimé avec succès.', + 'site-deleted-error': + 'Un problème est survenu lors de la tentative de suppression du site {{siteName}}...', + }, + 'site-modal': { + 'site-name': 'nom du site', + 'site-type': 'type de site', + 'site-image': "image du projet ou logo de l'entreprise", + 'site-gps-coordinates': 'coordonnées GPS', + 'site-dms-latitude': 'latitude DMS', + 'site-dms-longitude': 'longitude DMS', + 'site-presentation': 'présentation brève', + 'site-size': 'taille du terrain', + 'site-tree-species': "nombre d'arbres par espèce", + 'site-research-partner': 'partenariat de recherche?', + 'site-map-visibility': 'visibilité sur la carte', + yes: 'oui', + no: 'non', + visible: 'visible', + invisible: 'invisible', + }, + 'image-upload': 'ou déposez une image à téléverser', +} satisfies typeof Shape diff --git a/canopeum_frontend/src/locale/fr/analyticsSite.json b/canopeum_frontend/src/locale/fr/analyticsSite.json deleted file mode 100644 index e8141f672..000000000 --- a/canopeum_frontend/src/locale/fr/analyticsSite.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "location": "Localisation", - "batch-tracking": "Suivi des lots", - "social-page": "Page Sociale", - "sponsors": "Sponsors", - "add-new-batch": "Ajouter un Nouveau Lot", - "batch-modal": { - "create-title": "Créer un Nouveau Lot", - "edit-title": "Modifier un Lot", - "name-label": "Nom / ID du Lot", - "sponsor-label": "Sponsor", - "size-label": "Taille du terrain", - "feet-squared": "pi²", - "number-of-trees-label": "Nombre d'arbres par espèce", - "soil-condition-label": "État du sol", - "fertilizers-label": "Engrais", - "mulch-layers-label": "Couches de paillis", - "support-species-label": "Espèces de support", - "total-number-of-plants-label": "Nombre total de plantes", - "survived-label": "Combien ont survécu", - "replaced-label": "Combien ont été remplacés", - "seeds-per-species-label": "Nombre de graines collectées par espèce", - "total-seeds-label": "Nombre total de graines collectées", - "propagation-label": "Propagation totale", - "images-label": "Image", - "feedback": { - "create-success": "Lot créé avec succès", - "create-error": "Échec de la création du lot", - "edit-success": "Lot modifié avec succès", - "edit-error": "Échec de la modification du lot" - } - } -} diff --git a/canopeum_frontend/src/locale/fr/analyticsSite.ts b/canopeum_frontend/src/locale/fr/analyticsSite.ts new file mode 100644 index 000000000..bafbee9b9 --- /dev/null +++ b/canopeum_frontend/src/locale/fr/analyticsSite.ts @@ -0,0 +1,48 @@ +import type Shape from '../en/analyticsSite' + +export default { + location: 'Localisation', + 'batch-tracking': 'Suivi des lots', + 'site-tabs': { + analytics: 'Analytique', + social: 'Social', + }, + 'social-page': 'Page Sociale', + sponsors: 'Sponsors', + 'add-new-batch': 'Ajouter un Nouveau Lot', + 'edit-batch': 'Modifier un Lot', + 'delete-batch': { + title: 'Suppression du Lot', + message: 'Êtes vous sur de vouloir supprimer le lot {{batchName}}?', + success: 'Lot {{batchName}} supprimé avec succès.', + error: 'Un problème est survenu lors de la tentative de suppression du lot {{batchName}}...', + }, + 'batch-modal': { + 'create-title': 'Créer un Nouveau Lot', + 'edit-title': 'Modifier un Lot', + 'name-label': 'Nom / ID du Lot', + 'sponsor-name-label': 'Sponsor', + 'sponsor-website-url-label': 'Site web du sponsor', + 'sponsor-logo-label': 'Logo du sponsor', + 'size-label': 'Taille du terrain', + 'feet-squared': 'pi²', + 'number-of-trees-label': "Nombre d'arbres par espèce", + 'soil-condition-label': 'État du sol', + 'fertilizers-label': 'Engrais', + 'mulch-layers-label': 'Couches de paillis', + 'support-species-label': 'Espèces de support', + 'total-number-of-plants-label': 'Nombre total de plantes', + 'survived-label': 'Combien ont survécu', + 'replaced-label': 'Combien ont été remplacés', + 'seeds-per-species-label': 'Nombre de graines collectées par espèce', + 'total-seeds-label': 'Nombre total de graines collectées', + 'propagation-label': 'Propagation totale', + 'images-label': 'Image', + feedback: { + 'create-success': 'Lot créé avec succès', + 'create-error': 'Échec de la création du lot', + 'edit-success': 'Lot modifié avec succès', + 'edit-error': 'Échec de la modification du lot', + }, + }, +} satisfies typeof Shape diff --git a/canopeum_frontend/src/locale/fr/auth.json b/canopeum_frontend/src/locale/fr/auth.json deleted file mode 100644 index 24965f173..000000000 --- a/canopeum_frontend/src/locale/fr/auth.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "log-in-header-text": "Connectez-vous à votre compte", - "sign-up-header-text": "Créez votre compte", - "username-label": "Nom d'utilisateur", - "username-error-required": " Veuillez entrer un nom d'utilisateur", - "email-label": "Adresse courriel", - "email-error-required": "Veuillez entrer une adresse courriel", - "email-error-format": "Veuillez entrer une adresse courriel valide", - "password-label": "Mot de passe", - "password-error-required": "Veuillez entrer un mot de passe", - "password-error-format": "Le mot de passe n'est pas valide. Il doit contenir au minimum 8 caractères, et au moins 1 lettre et 1 chiffre.", - "create-password-label": "Créez votre mot de passe", - "password-confirmation-label": "Entrez à nouveau le mot de passe", - "password-confirmation-error-required": "Veuillez entrer à nouveau le mot de passe", - "password-error-must-match": "Passwords do not match", - "log-in": "Se Connecter", - "sign-up": "S'inscrire", - "create-account": "Créer mon compte", - "already-have-an-account": "Vous avez déjà un compte?", - "back-to-map": "Retourner à la carte", - "log-in-error": "Erreur lors de la tentative de connexion", - "sign-up-error": "Erreur lors de la tentative d'inscription", - "code-invalid": "Votre lien d'invitation est invalide. Veuillez contacter la personne qui vous a invité pour demander un nouveau lien.", - "invitation-expired": "Votre invitation est expirée. Veuillez contacter la personne qui vous a invité pour demander un nouveau lien." -} diff --git a/canopeum_frontend/src/locale/fr/auth.ts b/canopeum_frontend/src/locale/fr/auth.ts new file mode 100644 index 000000000..240c4fa22 --- /dev/null +++ b/canopeum_frontend/src/locale/fr/auth.ts @@ -0,0 +1,30 @@ +import type Shape from '../en/auth' + +export default { + 'log-in-header-text': 'Connectez-vous à votre compte', + 'sign-up-header-text': 'Créez votre compte', + 'username-label': "Nom d'utilisateur", + 'username-error-required': " Veuillez entrer un nom d'utilisateur", + 'email-label': 'Adresse courriel', + 'email-error-required': 'Veuillez entrer une adresse courriel', + 'email-error-format': 'Veuillez entrer une adresse courriel valide', + 'password-label': 'Mot de passe', + 'password-error-required': 'Veuillez entrer un mot de passe', + 'password-error-format': + "Le mot de passe n'est pas valide. Il doit contenir au minimum 8 caractères, et au moins 1 lettre et 1 chiffre.", + 'create-password-label': 'Créez votre mot de passe', + 'password-confirmation-label': 'Entrez à nouveau le mot de passe', + 'password-confirmation-error-required': 'Veuillez entrer à nouveau le mot de passe', + 'password-error-must-match': 'Passwords do not match', + 'log-in': 'Se Connecter', + 'sign-up': "S'inscrire", + 'create-account': 'Créer mon compte', + 'already-have-an-account': 'Vous avez déjà un compte?', + 'back-to-map': 'Retourner à la carte', + 'log-in-error': 'Erreur lors de la tentative de connexion', + 'sign-up-error': "Erreur lors de la tentative d'inscription", + 'code-invalid': + "Votre lien d'invitation est invalide. Veuillez contacter la personne qui vous a invité pour demander un nouveau lien.", + 'invitation-expired': + 'Votre invitation est expirée. Veuillez contacter la personne qui vous a invité pour demander un nouveau lien.', +} satisfies typeof Shape diff --git a/canopeum_frontend/src/locale/fr/errors.json b/canopeum_frontend/src/locale/fr/errors.json deleted file mode 100644 index 3763b752f..000000000 --- a/canopeum_frontend/src/locale/fr/errors.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "email-taken": "Il y a déjà un utilisateur qui utilise cette adresse courriel.", - "current-password-invalid": "Le mot de passe actuel saisi est invalide.", - "password-error-must-match": "Les mots de passe ne correspondent pas.", - "email-invalid": "Format d'adresse courriel invalide (exemple: john.doe@contoso.com)", - "url-invalid": "Format d'URL invalide (exemple: https://www.contoso.com)", - "phone-invalid": "Format de numéro de téléphone invalide (exemple: +1 123 456 7890)" -} diff --git a/canopeum_frontend/src/locale/fr/errors.ts b/canopeum_frontend/src/locale/fr/errors.ts new file mode 100644 index 000000000..cbdd96975 --- /dev/null +++ b/canopeum_frontend/src/locale/fr/errors.ts @@ -0,0 +1,10 @@ +import type Shape from '../en/errors' + +export default { + 'email-taken': 'Il y a déjà un utilisateur qui utilise cette adresse courriel.', + 'current-password-invalid': 'Le mot de passe actuel saisi est invalide.', + 'password-error-must-match': 'Les mots de passe ne correspondent pas.', + 'email-invalid': "Format d'adresse courriel invalide (exemple: john.doe@contoso.com)", + 'url-invalid': "Format d'URL invalide (exemple: https://www.contoso.com)", + 'phone-invalid': 'Format de numéro de téléphone invalide (exemple: +1 123 456 7890)', +} satisfies typeof Shape diff --git a/canopeum_frontend/src/locale/fr/generic.json b/canopeum_frontend/src/locale/fr/generic.json deleted file mode 100644 index 95b19aebf..000000000 --- a/canopeum_frontend/src/locale/fr/generic.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "cancel": "Annuler", - "delete": "Supprimer", - "ok": "Ok", - "submit": "Soumettre", - "subscribe": "S'abonner", - "copied-clipboard": "Copié dans le presse-papier", - "error-default": "Oooops! Une erreur est survenue lors du chargement des données.", - "upload": "Téléverser", - "edit": "Modifier" -} diff --git a/canopeum_frontend/src/locale/fr/generic.ts b/canopeum_frontend/src/locale/fr/generic.ts new file mode 100644 index 000000000..31e70e613 --- /dev/null +++ b/canopeum_frontend/src/locale/fr/generic.ts @@ -0,0 +1,13 @@ +import type Shape from '../en/generic' + +export default { + cancel: 'Annuler', + delete: 'Supprimer', + ok: 'Ok', + submit: 'Soumettre', + subscribe: "S'abonner", + 'copied-clipboard': 'Copié dans le presse-papier', + 'error-default': 'Oooops! Une erreur est survenue lors du chargement des données.', + upload: 'Téléverser', + edit: 'Modifier', +} satisfies typeof Shape diff --git a/canopeum_frontend/src/locale/fr/home.json b/canopeum_frontend/src/locale/fr/home.json deleted file mode 100644 index 6affc5340..000000000 --- a/canopeum_frontend/src/locale/fr/home.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Bonjour {{username}},", - "subtitle": "Voici les dernières nouvelles de toutes les organisations que vous avez suivies:", - "no-news": "Vous n'avez aucun contenu suivi. Veuillez visiter la page sociale d'un site pour le suivre et vous abonner à ses nouvelles." -} diff --git a/canopeum_frontend/src/locale/fr/home.ts b/canopeum_frontend/src/locale/fr/home.ts new file mode 100644 index 000000000..32780ad24 --- /dev/null +++ b/canopeum_frontend/src/locale/fr/home.ts @@ -0,0 +1,8 @@ +import type Shape from '../en/home' + +export default { + title: 'Bonjour {{username}},', + subtitle: 'Voici les dernières nouvelles de toutes les organisations que vous avez suivies:', + 'no-news': + "Vous n'avez aucun contenu suivi. Veuillez visiter la page sociale d'un site pour le suivre et vous abonner à ses nouvelles.", +} satisfies typeof Shape diff --git a/canopeum_frontend/src/locale/fr/index.ts b/canopeum_frontend/src/locale/fr/index.ts index a54d29c7f..12dcad2a8 100644 --- a/canopeum_frontend/src/locale/fr/index.ts +++ b/canopeum_frontend/src/locale/fr/index.ts @@ -1,27 +1,26 @@ -import analyticsJSON from './analytics.json' -import analyticsSiteJSON from './analyticsSite.json' -import authJSON from './auth.json' -import errorsJSON from './errors.json' -import genericJSON from './generic.json' -import homeJSON from './home.json' -import navbarJSON from './navbar.json' -import postsJSON from './posts.json' -import settingsJSON from './settings.json' -import socialJSON from './social.json' +import type Shape from '../en' +import analytics from './analytics' +import analyticsSite from './analyticsSite' +import auth from './auth' +import errors from './errors' +import generic from './generic' +import home from './home' +import navbar from './navbar' +import posts from './posts' +import settings from './settings' +import social from './social' -const frJSON = { +export default { translation: { - analytics: { ...analyticsJSON }, - analyticsSite: { ...analyticsSiteJSON }, - auth: { ...authJSON }, - errors: { ...errorsJSON }, - generic: { ...genericJSON }, - home: { ...homeJSON }, - navbar: { ...navbarJSON }, - posts: { ...postsJSON }, - settings: { ...settingsJSON }, - social: { ...socialJSON }, + analytics, + analyticsSite, + auth, + errors, + generic, + home, + navbar, + posts, + settings, + social, }, -} - -export default frJSON +} satisfies typeof Shape diff --git a/canopeum_frontend/src/locale/fr/navbar.json b/canopeum_frontend/src/locale/fr/navbar.json deleted file mode 100644 index 0f953dc45..000000000 --- a/canopeum_frontend/src/locale/fr/navbar.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "home": "Accueil", - "sites": "Sites", - "map": "Carte", - "utilities": "Utilitaires", - "settings": "Paramètres", - "log-in": "Se Connecter" -} diff --git a/canopeum_frontend/src/locale/fr/navbar.ts b/canopeum_frontend/src/locale/fr/navbar.ts new file mode 100644 index 000000000..468367a17 --- /dev/null +++ b/canopeum_frontend/src/locale/fr/navbar.ts @@ -0,0 +1,10 @@ +import type Shape from '../en/navbar' + +export default { + home: 'Accueil', + sites: 'Sites', + map: 'Carte', + utilities: 'Utilitaires', + settings: 'Paramètres', + 'log-in': 'Se Connecter', +} satisfies typeof Shape diff --git a/canopeum_frontend/src/locale/fr/posts.json b/canopeum_frontend/src/locale/fr/posts.json deleted file mode 100644 index d97c1df70..000000000 --- a/canopeum_frontend/src/locale/fr/posts.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "back-to-social": "Retour vers la page sociale", - "error-loading-post": "Un problème est survenu lors de la tentative de récupération du message.", - "error-loading-posts": "Un problème est survenu lors de la tentative de récupération des messages." -} diff --git a/canopeum_frontend/src/locale/fr/posts.ts b/canopeum_frontend/src/locale/fr/posts.ts new file mode 100644 index 000000000..96a1f48de --- /dev/null +++ b/canopeum_frontend/src/locale/fr/posts.ts @@ -0,0 +1,8 @@ +import type Shape from '../en/posts' + +export default { + 'back-to-social': 'Retour vers la page sociale', + 'error-loading-post': 'Un problème est survenu lors de la tentative de récupération du message.', + 'error-loading-posts': + 'Un problème est survenu lors de la tentative de récupération des messages.', +} satisfies typeof Shape diff --git a/canopeum_frontend/src/locale/fr/settings.json b/canopeum_frontend/src/locale/fr/settings.json deleted file mode 100644 index 3eea91eda..000000000 --- a/canopeum_frontend/src/locale/fr/settings.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "tabs": { - "edit-profile": "Modifier Mon Profil", - "manage-admins": "Gérer les Administrateurs", - "terms-and-policies": "Conditions & Politiques", - "logout": "Se Déconnecter" - }, - "edit-profile": { - "title": "Modifier Mon Profil", - "profile-saved": "Profil sauvegardé avec succès!", - "current-password": "Mot de passe actuel", - "new-password": "Nouveau mot de passe", - "new-password-confirmation": "Confirmation du nouveau mot de passe", - "current-password-invalid": "Le mot de passe actuel entré est invalide.", - "save-profile-error": "Un problème est survenu lors de la sauvegarde de votre profil." - }, - "manage-admins": { - "title": "Gérer les Administrateurs", - "invite-admin": "Inviter un Administrateur", - "generate-link": "Générer un Lien", - "copy-link": "Copier le Lien", - "assign-to-label": "Assigner à", - "generate-link-error": "Une erreur est survenue en essayant de générer le lien.", - "copy-link-message": "Veuillez copier et partager ce lien avec le propriétaire de l'adresse courriel suivante:" - }, - "terms-and-policies": { - "title": "Conditions & Politiques" - } -} diff --git a/canopeum_frontend/src/locale/fr/settings.ts b/canopeum_frontend/src/locale/fr/settings.ts new file mode 100644 index 000000000..8d063a25f --- /dev/null +++ b/canopeum_frontend/src/locale/fr/settings.ts @@ -0,0 +1,32 @@ +import type Shape from '../en/settings' + +export default { + tabs: { + 'edit-profile': 'Modifier Mon Profil', + 'manage-admins': 'Gérer les Administrateurs', + 'terms-and-policies': 'Conditions & Politiques', + logout: 'Se Déconnecter', + }, + 'edit-profile': { + title: 'Modifier Mon Profil', + 'profile-saved': 'Profil sauvegardé avec succès!', + 'current-password': 'Mot de passe actuel', + 'new-password': 'Nouveau mot de passe', + 'new-password-confirmation': 'Confirmation du nouveau mot de passe', + 'current-password-invalid': 'Le mot de passe actuel entré est invalide.', + 'save-profile-error': 'Un problème est survenu lors de la sauvegarde de votre profil.', + }, + 'manage-admins': { + title: 'Gérer les Administrateurs', + 'invite-admin': 'Inviter un Administrateur', + 'generate-link': 'Générer un Lien', + 'copy-link': 'Copier le Lien', + 'assign-to-label': 'Assigner à', + 'generate-link-error': 'Une erreur est survenue en essayant de générer le lien.', + 'copy-link-message': + "Veuillez copier et partager ce lien avec le propriétaire de l'adresse courriel suivante:", + }, + 'terms-and-policies': { + title: 'Conditions & Politiques', + }, +} satisfies typeof Shape diff --git a/canopeum_frontend/src/locale/fr/social.json b/canopeum_frontend/src/locale/fr/social.json deleted file mode 100644 index 3a138ec7a..000000000 --- a/canopeum_frontend/src/locale/fr/social.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "site-social-header": { - "follow": "Suivre", - "unfollow": "Ne plus suivre", - "sponsors": "Commanditaires", - "public": "Publique" - }, - "comments": { - "leave-a-comment": "Laisser un Commentaire", - "words_one": "mot", - "words_other": "mots", - "comments": "Commentaires", - "send": "Envoyer", - "comment-body-required": "Votre commentaire ne peut pas être vide.", - "comment-body-max-chars_one": "Votre commentaire ne peut pas contenir plus de {{count}} mots.", - "comment-body-max-chars_other": "Votre commentaire ne peut pas contenir plus de {{count}} mots.", - "comment-deletion-error": "Un problème est survenu lors de la suppression du commentaire.", - "comment-deletion-confirm-title": "Supprimer le Commentaire", - "comment-deletion-confirm-self": "Êtes-vous sûr de vouloir supprimer votre commentaire? Cela supprimera définitivement le commentaire; cette action est irréversible.", - "comment-deletion-confirm-other": "Êtes-vous sûr de vouloir supprimer ce commentaire de {{author}}? Cela supprimera définitivement le commentaire; cette action est irréversible." - }, - "share-dialog": { - "title": "Partager le Message", - "message": "Pour partager ce message, copiez simplement ce lien et collez-le sur vos plateformes de réseaux sociaux préférées, ou envoyez-le à un ami!", - "copy-link": "Copier le lien" - }, - "posts": { - "post-body-required": "Vous devez saisir un message pour pouvoir publier votre message." - }, - "widgets": { - "create": "ajouter un nouveau widget", - "submit": "soumettre", - "cancel": "annuler", - "max-word": "100 mots maximum", - "title": "titre du widget" - }, - "contact": { - "title": "Contact", - "address": "Adresse", - "phone": "Téléphone", - "email": "Email", - "feedback": { - "edit-success": "Contact modifié avec succès", - "edit-error": "Erreur lors de la modification du contact" - } - }, - "announcement": { - "title": "Annonce", - "body": "Corps", - "link": "Lien", - "feedback": { - "edit-success": "Annonce modifiée avec succès", - "edit-error": "Erreur lors de la modification de l'annonce" - } - } -} diff --git a/canopeum_frontend/src/locale/fr/social.ts b/canopeum_frontend/src/locale/fr/social.ts new file mode 100644 index 000000000..db6cc3171 --- /dev/null +++ b/canopeum_frontend/src/locale/fr/social.ts @@ -0,0 +1,62 @@ +import type Shape from '../en/social' + +export default { + 'site-social-header': { + follow: 'Suivre', + unfollow: 'Ne plus suivre', + sponsors: 'Commanditaires', + public: 'Publique', + }, + comments: { + 'leave-a-comment': 'Laisser un Commentaire', + word_one: 'mot', + word_other: 'mots', + comments: 'Commentaires', + send: 'Envoyer', + 'comment-body-required': 'Votre commentaire ne peut pas être vide.', + 'comment-body-max-chars_one': 'Votre commentaire ne peut pas contenir plus de {{count}} mots.', + 'comment-body-max-chars_other': + 'Votre commentaire ne peut pas contenir plus de {{count}} mots.', + 'comment-deletion-error': 'Un problème est survenu lors de la suppression du commentaire.', + 'comment-deletion-confirm-title': 'Supprimer le Commentaire', + 'comment-deletion-confirm-self': + 'Êtes-vous sûr de vouloir supprimer votre commentaire? Cela supprimera définitivement le commentaire; cette action est irréversible.', + 'comment-deletion-confirm-other': + 'Êtes-vous sûr de vouloir supprimer ce commentaire de {{author}}? Cela supprimera définitivement le commentaire; cette action est irréversible.', + }, + 'share-dialog': { + title: 'Partager le Message', + message: + 'Pour partager ce message, copiez simplement ce lien et collez-le sur vos plateformes de réseaux sociaux préférées, ou envoyez-le à un ami!', + 'copy-link': 'Copier le lien', + }, + posts: { + 'post-body-required': 'Vous devez saisir un message pour pouvoir publier votre message.', + }, + widgets: { + create: 'ajouter un nouveau widget', + submit: 'soumettre', + cancel: 'annuler', + 'max-word': '100 mots maximum', + title: 'titre du widget', + }, + contact: { + title: 'Contact', + address: 'Adresse', + phone: 'Téléphone', + email: 'Email', + feedback: { + 'edit-success': 'Contact modifié avec succès', + 'edit-error': 'Erreur lors de la modification du contact', + }, + }, + announcement: { + title: 'Annonce', + body: 'Corps', + link: 'Lien', + feedback: { + 'edit-success': 'Annonce modifiée avec succès', + 'edit-error': "Erreur lors de la modification de l'annonce", + }, + }, +} satisfies typeof Shape diff --git a/canopeum_frontend/src/locale/index.ts b/canopeum_frontend/src/locale/index.ts index 8e6c51223..9be07116f 100644 --- a/canopeum_frontend/src/locale/index.ts +++ b/canopeum_frontend/src/locale/index.ts @@ -1,9 +1,10 @@ -import enJSON from './en/index' -import frJSON from './fr/index' +import enLang from './en/index' +import frLang from './fr/index' -const resources = { - en: { ...enJSON }, - fr: { ...frJSON }, +// NOTE: Object shape consistency is checked in each export rather than only here +// because we want to check for excess properties. +// https://www.typescriptlang.org/docs/handbook/2/objects.html#excess-property-checks +export default { + en: enLang, + fr: frLang, } - -export default resources diff --git a/canopeum_frontend/src/pages/Analytics.tsx b/canopeum_frontend/src/pages/Analytics.tsx index b3b3c0e5e..4622816e7 100644 --- a/canopeum_frontend/src/pages/Analytics.tsx +++ b/canopeum_frontend/src/pages/Analytics.tsx @@ -160,7 +160,7 @@ const Analytics = () => { {site.batches.length}{' '} - {translate('analytics.batches', { count: site.batches.length })} + {translate('analytics.batch', { count: site.batches.length })} @@ -223,7 +223,7 @@ const Analytics = () => {
-
{translate('analytics.batches.batch-tracking')}
+
{translate('analytics.batch-tracking')}
Filters Go Here
diff --git a/canopeum_frontend/src/pages/AnalyticsSite.tsx b/canopeum_frontend/src/pages/AnalyticsSite.tsx index 53f149061..d7143bbed 100644 --- a/canopeum_frontend/src/pages/AnalyticsSite.tsx +++ b/canopeum_frontend/src/pages/AnalyticsSite.tsx @@ -6,9 +6,10 @@ import LoadingPage from './LoadingPage' import AnalyticsSiteHeader from '@components/analytics/AnalyticsSiteHeader' import CreateBatchModal from '@components/analytics/batch-modal/CreateBatchModal' import BatchTable from '@components/analytics/BatchTable' +import SiteAdminTabs from '@components/analytics/SiteAdminTabs' import { LanguageContext } from '@components/context/LanguageContext' import useApiClient from '@hooks/ApiClientHook' -import type { SiteDetailSummary } from '@services/api' +import type { SiteSummaryDetail } from '@services/api' const AnalyticsSite = () => { const { t: translate } = useTranslation<'analytics'>() @@ -16,7 +17,7 @@ const AnalyticsSite = () => { const { formatDate } = useContext(LanguageContext) const { getApiClient } = useApiClient() - const [siteSummary, setSiteSummary] = useState() + const [siteSummary, setSiteSummary] = useState() const [lastModifiedBatchDate, setLastModifiedBatchDate] = useState() const [isCreateBatchOpen, setIsCreateBatchOpen] = useState(false) @@ -61,6 +62,8 @@ const AnalyticsSite = () => { return (
+ +
diff --git a/canopeum_frontend/src/pages/SiteSocialPage.tsx b/canopeum_frontend/src/pages/SiteSocialPage.tsx index 86cbbba44..e0478162b 100644 --- a/canopeum_frontend/src/pages/SiteSocialPage.tsx +++ b/canopeum_frontend/src/pages/SiteSocialPage.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next' import { useParams } from 'react-router-dom' import LoadingPage from './LoadingPage' +import SiteAdminTabs from '@components/analytics/SiteAdminTabs' import { AuthenticationContext } from '@components/context/AuthenticationContext' import CreatePostWidget from '@components/CreatePostWidget' import AnnouncementCard from '@components/social/AnnouncementCard' @@ -130,6 +131,12 @@ const SiteSocialPage = () => { ref={scrollableContainerRef} >
+ {viewMode === 'admin' && ( +
+ +
+ )} +
diff --git a/canopeum_frontend/src/services/api.ts b/canopeum_frontend/src/services/api.ts index b561f01fc..6422223af 100644 --- a/canopeum_frontend/src/services/api.ts +++ b/canopeum_frontend/src/services/api.ts @@ -140,7 +140,9 @@ export class BatchClient { create( site?: number | undefined, name?: string | null | undefined, - sponsor?: string | null | undefined, + sponsorName?: string | null | undefined, + sponsorWebsiteUrl?: string | null | undefined, + sponsorLogo?: FileParameter | null | undefined, size?: number | null | undefined, soilCondition?: string | null | undefined, plantCount?: number | null | undefined, @@ -167,8 +169,18 @@ export class BatchClient { if (name !== null && name !== undefined) { content_.append('name', name.toString()) } - if (sponsor !== null && sponsor !== undefined) { - content_.append('sponsor', sponsor.toString()) + if (sponsorName !== null && sponsorName !== undefined) { + content_.append('sponsorName', sponsorName.toString()) + } + if (sponsorWebsiteUrl !== null && sponsorWebsiteUrl !== undefined) { + content_.append('sponsorWebsiteUrl', sponsorWebsiteUrl.toString()) + } + if (sponsorLogo !== null && sponsorLogo !== undefined) { + content_.append( + 'sponsorLogo', + sponsorLogo.data, + sponsorLogo.fileName ? sponsorLogo.fileName : 'sponsorLogo', + ) } if (size !== null && size !== undefined) { content_.append('size', size.toString()) @@ -264,7 +276,9 @@ export class BatchClient { update( batchId: number, name?: string | null | undefined, - sponsor?: string | null | undefined, + sponsorName?: string | null | undefined, + sponsorWebsiteUrl?: string | null | undefined, + sponsorLogo?: FileParameter | null | undefined, size?: number | null | undefined, soilCondition?: string | null | undefined, plantCount?: number | null | undefined, @@ -289,8 +303,18 @@ export class BatchClient { if (name !== null && name !== undefined) { content_.append('name', name.toString()) } - if (sponsor !== null && sponsor !== undefined) { - content_.append('sponsor', sponsor.toString()) + if (sponsorName !== null && sponsorName !== undefined) { + content_.append('sponsorName', sponsorName.toString()) + } + if (sponsorWebsiteUrl !== null && sponsorWebsiteUrl !== undefined) { + content_.append('sponsorWebsiteUrl', sponsorWebsiteUrl.toString()) + } + if (sponsorLogo !== null && sponsorLogo !== undefined) { + content_.append( + 'sponsorLogo', + sponsorLogo.data, + sponsorLogo.fileName ? sponsorLogo.fileName : 'sponsorLogo', + ) } if (size !== null && size !== undefined) { content_.append('size', size.toString()) @@ -1178,7 +1202,7 @@ export class SiteClient { return Promise.resolve(null as any) } - summary(siteId: number): Promise { + summary(siteId: number): Promise { let url_ = this.baseUrl + '/analytics/sites/{siteId}/summary' if (siteId === undefined || siteId === null) { throw new Error("The parameter 'siteId' must be defined.") @@ -1198,7 +1222,7 @@ export class SiteClient { }) } - protected processSummary(response: Response): Promise { + protected processSummary(response: Response): Promise { const status = response.status let _headers: any = {} if (response.headers && response.headers.forEach) { @@ -1210,7 +1234,7 @@ export class SiteClient { let resultData200 = _responseText === '' ? null : JSON.parse(_responseText, this.jsonParseReviver) - result200 = SiteDetailSummary.fromJS(resultData200) + result200 = SiteSummaryDetail.fromJS(resultData200) return result200 }) } else if (status !== 200 && status !== 204) { @@ -1223,7 +1247,7 @@ export class SiteClient { ) }) } - return Promise.resolve(null as any) + return Promise.resolve(null as any) } map(): Promise { @@ -3222,24 +3246,23 @@ export interface IAsset { export class BatchDetail implements IBatchDetail { readonly id!: number - readonly fertilizers!: Batchfertilizer[] - readonly mulchLayers!: BatchMulchLayer[] - readonly supportedSpecies!: BatchSupportedSpecies[] + readonly fertilizers!: FertilizerType[] + readonly mulchLayers!: MulchLayerType[] + readonly supportedSpecies!: TreeType[] readonly seeds!: BatchSeed[] readonly species!: BatchSpecies[] + readonly sponsor!: BatchSponsor image?: Asset - createdAt?: Date | undefined - updatedAt?: Date | undefined + readonly createdAt!: Date | undefined + readonly updatedAt!: Date | undefined name?: string | undefined - sponsor?: string | undefined size?: number | undefined soilCondition?: string | undefined - plantCount?: number | undefined survivedCount?: number | undefined replaceCount?: number | undefined totalNumberSeed?: number | undefined totalPropagation?: number | undefined - site?: number | undefined; + site!: number; [key: string]: any @@ -3257,6 +3280,7 @@ export class BatchDetail implements IBatchDetail { this.supportedSpecies = [] this.seeds = [] this.species = [] + this.sponsor = new BatchSponsor() } } @@ -3271,19 +3295,19 @@ export class BatchDetail implements IBatchDetail { if (Array.isArray(_data['fertilizers'])) { ;( this).fertilizers = [] as any for (let item of _data['fertilizers']) { - ;( this).fertilizers!.push(Batchfertilizer.fromJS(item)) + ;( this).fertilizers!.push(FertilizerType.fromJS(item)) } } if (Array.isArray(_data['mulchLayers'])) { ;( this).mulchLayers = [] as any for (let item of _data['mulchLayers']) { - ;( this).mulchLayers!.push(BatchMulchLayer.fromJS(item)) + ;( this).mulchLayers!.push(MulchLayerType.fromJS(item)) } } if (Array.isArray(_data['supportedSpecies'])) { ;( this).supportedSpecies = [] as any for (let item of _data['supportedSpecies']) { - ;( this).supportedSpecies!.push(BatchSupportedSpecies.fromJS(item)) + ;( this).supportedSpecies!.push(TreeType.fromJS(item)) } } if (Array.isArray(_data['seeds'])) { @@ -3298,18 +3322,19 @@ export class BatchDetail implements IBatchDetail { ;( this).species!.push(BatchSpecies.fromJS(item)) } } + ;( this).sponsor = _data['sponsor'] + ? BatchSponsor.fromJS(_data['sponsor']) + : new BatchSponsor() this.image = _data['image'] ? Asset.fromJS(_data['image']) : undefined - this.createdAt = _data['createdAt'] + ;( this).createdAt = _data['createdAt'] ? new Date(_data['createdAt'].toString()) : undefined - this.updatedAt = _data['updatedAt'] + ;( this).updatedAt = _data['updatedAt'] ? new Date(_data['updatedAt'].toString()) : undefined this.name = _data['name'] - this.sponsor = _data['sponsor'] this.size = _data['size'] this.soilCondition = _data['soilCondition'] - this.plantCount = _data['plantCount'] this.survivedCount = _data['survivedCount'] this.replaceCount = _data['replaceCount'] this.totalNumberSeed = _data['totalNumberSeed'] @@ -3363,14 +3388,13 @@ export class BatchDetail implements IBatchDetail { data['species'].push(item.toJSON()) } } + data['sponsor'] = this.sponsor ? this.sponsor.toJSON() : undefined data['image'] = this.image ? this.image.toJSON() : undefined data['createdAt'] = this.createdAt ? this.createdAt.toISOString() : undefined data['updatedAt'] = this.updatedAt ? this.updatedAt.toISOString() : undefined data['name'] = this.name - data['sponsor'] = this.sponsor data['size'] = this.size data['soilCondition'] = this.soilCondition - data['plantCount'] = this.plantCount data['survivedCount'] = this.survivedCount data['replaceCount'] = this.replaceCount data['totalNumberSeed'] = this.totalNumberSeed @@ -3382,92 +3406,31 @@ export class BatchDetail implements IBatchDetail { export interface IBatchDetail { id: number - fertilizers: Batchfertilizer[] - mulchLayers: BatchMulchLayer[] - supportedSpecies: BatchSupportedSpecies[] + fertilizers: FertilizerType[] + mulchLayers: MulchLayerType[] + supportedSpecies: TreeType[] seeds: BatchSeed[] species: BatchSpecies[] + sponsor: BatchSponsor image?: Asset - createdAt?: Date | undefined - updatedAt?: Date | undefined + createdAt: Date | undefined + updatedAt: Date | undefined name?: string | undefined - sponsor?: string | undefined size?: number | undefined soilCondition?: string | undefined - plantCount?: number | undefined survivedCount?: number | undefined replaceCount?: number | undefined totalNumberSeed?: number | undefined totalPropagation?: number | undefined - site?: number | undefined - - [key: string]: any -} - -export class BatchMulchLayer implements IBatchMulchLayer { - readonly id!: number - readonly en!: string - readonly fr!: string; - - [key: string]: any - - constructor(data?: IBatchMulchLayer) { - if (data) { - for (var property in data) { - if (data.hasOwnProperty(property)) { - ;( this)[property] = ( data)[property] - } - } - } - } - - init(_data?: any) { - if (_data) { - for (var property in _data) { - if (_data.hasOwnProperty(property)) { - this[property] = _data[property] - } - } - ;( this).id = _data['id'] - ;( this).en = _data['en'] - ;( this).fr = _data['fr'] - } - } - - static fromJS(data: any): BatchMulchLayer { - data = typeof data === 'object' ? data : {} - let result = new BatchMulchLayer() - result.init(data) - return result - } - - toJSON(data?: any) { - data = typeof data === 'object' ? data : {} - for (var property in this) { - if (this.hasOwnProperty(property)) { - data[property] = this[property] - } - } - data['id'] = this.id - data['en'] = this.en - data['fr'] = this.fr - return data - } -} - -export interface IBatchMulchLayer { - id: number - en: string - fr: string + site: number [key: string]: any } export class BatchSeed implements IBatchSeed { readonly id!: number - quantity?: number | undefined - readonly en!: string - readonly fr!: string; + quantity!: number + readonly treeType!: TreeType; [key: string]: any @@ -3479,6 +3442,9 @@ export class BatchSeed implements IBatchSeed { } } } + if (!data) { + this.treeType = new TreeType() + } } init(_data?: any) { @@ -3490,8 +3456,9 @@ export class BatchSeed implements IBatchSeed { } ;( this).id = _data['id'] this.quantity = _data['quantity'] - ;( this).en = _data['en'] - ;( this).fr = _data['fr'] + ;( this).treeType = _data['treeType'] + ? TreeType.fromJS(_data['treeType']) + : new TreeType() } } @@ -3511,25 +3478,23 @@ export class BatchSeed implements IBatchSeed { } data['id'] = this.id data['quantity'] = this.quantity - data['en'] = this.en - data['fr'] = this.fr + data['treeType'] = this.treeType ? this.treeType.toJSON() : undefined return data } } export interface IBatchSeed { - quantity?: number | undefined - en: string - fr: string + id: number + quantity: number + treeType: TreeType [key: string]: any } export class BatchSpecies implements IBatchSpecies { readonly id!: number - quantity?: number | undefined - readonly en!: string - readonly fr!: string; + quantity!: number + readonly treeType!: TreeType; [key: string]: any @@ -3541,6 +3506,9 @@ export class BatchSpecies implements IBatchSpecies { } } } + if (!data) { + this.treeType = new TreeType() + } } init(_data?: any) { @@ -3552,8 +3520,9 @@ export class BatchSpecies implements IBatchSpecies { } ;( this).id = _data['id'] this.quantity = _data['quantity'] - ;( this).en = _data['en'] - ;( this).fr = _data['fr'] + ;( this).treeType = _data['treeType'] + ? TreeType.fromJS(_data['treeType']) + : new TreeType() } } @@ -3573,87 +3542,28 @@ export class BatchSpecies implements IBatchSpecies { } data['id'] = this.id data['quantity'] = this.quantity - data['en'] = this.en - data['fr'] = this.fr + data['treeType'] = this.treeType ? this.treeType.toJSON() : undefined return data } } export interface IBatchSpecies { - quantity?: number | undefined - en: string - fr: string - - [key: string]: any -} - -export class BatchSupportedSpecies implements IBatchSupportedSpecies { - readonly id!: number - readonly en!: string - readonly fr!: string; - - [key: string]: any - - constructor(data?: IBatchSupportedSpecies) { - if (data) { - for (var property in data) { - if (data.hasOwnProperty(property)) { - ;( this)[property] = ( data)[property] - } - } - } - } - - init(_data?: any) { - if (_data) { - for (var property in _data) { - if (_data.hasOwnProperty(property)) { - this[property] = _data[property] - } - } - ;( this).id = _data['id'] - ;( this).en = _data['en'] - ;( this).fr = _data['fr'] - } - } - - static fromJS(data: any): BatchSupportedSpecies { - data = typeof data === 'object' ? data : {} - let result = new BatchSupportedSpecies() - result.init(data) - return result - } - - toJSON(data?: any) { - data = typeof data === 'object' ? data : {} - for (var property in this) { - if (this.hasOwnProperty(property)) { - data[property] = this[property] - } - } - data['id'] = this.id - data['en'] = this.en - data['fr'] = this.fr - return data - } -} - -export interface IBatchSupportedSpecies { id: number - en: string - fr: string + quantity: number + treeType: TreeType [key: string]: any } -export class Batchfertilizer implements IBatchfertilizer { +export class BatchSponsor implements IBatchSponsor { readonly id!: number - readonly en!: string - readonly fr!: string; + logo!: Asset + name!: string + url!: string; [key: string]: any - constructor(data?: IBatchfertilizer) { + constructor(data?: IBatchSponsor) { if (data) { for (var property in data) { if (data.hasOwnProperty(property)) { @@ -3661,6 +3571,9 @@ export class Batchfertilizer implements IBatchfertilizer { } } } + if (!data) { + this.logo = new Asset() + } } init(_data?: any) { @@ -3671,14 +3584,15 @@ export class Batchfertilizer implements IBatchfertilizer { } } ;( this).id = _data['id'] - ;( this).en = _data['en'] - ;( this).fr = _data['fr'] + this.logo = _data['logo'] ? Asset.fromJS(_data['logo']) : new Asset() + this.name = _data['name'] + this.url = _data['url'] } } - static fromJS(data: any): Batchfertilizer { + static fromJS(data: any): BatchSponsor { data = typeof data === 'object' ? data : {} - let result = new Batchfertilizer() + let result = new BatchSponsor() result.init(data) return result } @@ -3691,16 +3605,18 @@ export class Batchfertilizer implements IBatchfertilizer { } } data['id'] = this.id - data['en'] = this.en - data['fr'] = this.fr + data['logo'] = this.logo ? this.logo.toJSON() : undefined + data['name'] = this.name + data['url'] = this.url return data } } -export interface IBatchfertilizer { +export interface IBatchSponsor { id: number - en: string - fr: string + logo: Asset + name: string + url: string [key: string]: any } @@ -5192,145 +5108,6 @@ export interface ISiteAdmin { [key: string]: any } -export class SiteDetailSummary implements ISiteDetailSummary { - readonly id!: number - name!: string - coordinate!: Coordinates - siteType!: SiteType - readonly plantCount!: number - readonly survivedCount!: number - readonly propagationCount!: number - visitorCount?: number | undefined - readonly sponsors!: string[] - readonly progress!: number - admins!: SiteAdmin[] - readonly batches!: BatchDetail[] - readonly weather!: Weather; - - [key: string]: any - - constructor(data?: ISiteDetailSummary) { - if (data) { - for (var property in data) { - if (data.hasOwnProperty(property)) { - ;( this)[property] = ( data)[property] - } - } - } - if (!data) { - this.coordinate = new Coordinates() - this.siteType = new SiteType() - this.sponsors = [] - this.admins = [] - this.batches = [] - this.weather = new Weather() - } - } - - init(_data?: any) { - if (_data) { - for (var property in _data) { - if (_data.hasOwnProperty(property)) { - this[property] = _data[property] - } - } - ;( this).id = _data['id'] - this.name = _data['name'] - this.coordinate = _data['coordinate'] - ? Coordinates.fromJS(_data['coordinate']) - : new Coordinates() - this.siteType = _data['siteType'] ? SiteType.fromJS(_data['siteType']) : new SiteType() - ;( this).plantCount = _data['plantCount'] - ;( this).survivedCount = _data['survivedCount'] - ;( this).propagationCount = _data['propagationCount'] - this.visitorCount = _data['visitorCount'] - if (Array.isArray(_data['sponsors'])) { - ;( this).sponsors = [] as any - for (let item of _data['sponsors']) { - ;( this).sponsors!.push(item) - } - } - ;( this).progress = _data['progress'] - if (Array.isArray(_data['admins'])) { - this.admins = [] as any - for (let item of _data['admins']) { - this.admins!.push(SiteAdmin.fromJS(item)) - } - } - if (Array.isArray(_data['batches'])) { - ;( this).batches = [] as any - for (let item of _data['batches']) { - ;( this).batches!.push(BatchDetail.fromJS(item)) - } - } - ;( this).weather = _data['weather'] ? Weather.fromJS(_data['weather']) : new Weather() - } - } - - static fromJS(data: any): SiteDetailSummary { - data = typeof data === 'object' ? data : {} - let result = new SiteDetailSummary() - result.init(data) - return result - } - - toJSON(data?: any) { - data = typeof data === 'object' ? data : {} - for (var property in this) { - if (this.hasOwnProperty(property)) { - data[property] = this[property] - } - } - data['id'] = this.id - data['name'] = this.name - data['coordinate'] = this.coordinate ? this.coordinate.toJSON() : undefined - data['siteType'] = this.siteType ? this.siteType.toJSON() : undefined - data['plantCount'] = this.plantCount - data['survivedCount'] = this.survivedCount - data['propagationCount'] = this.propagationCount - data['visitorCount'] = this.visitorCount - if (Array.isArray(this.sponsors)) { - data['sponsors'] = [] - for (let item of this.sponsors) { - data['sponsors'].push(item) - } - } - data['progress'] = this.progress - if (Array.isArray(this.admins)) { - data['admins'] = [] - for (let item of this.admins) { - data['admins'].push(item.toJSON()) - } - } - if (Array.isArray(this.batches)) { - data['batches'] = [] - for (let item of this.batches) { - data['batches'].push(item.toJSON()) - } - } - data['weather'] = this.weather ? this.weather.toJSON() : undefined - return data - } -} - -export interface ISiteDetailSummary { - id: number - name: string - coordinate: Coordinates - siteType: SiteType - plantCount: number - survivedCount: number - propagationCount: number - visitorCount?: number | undefined - sponsors: string[] - progress: number - admins: SiteAdmin[] - batches: BatchDetail[] - weather: Weather - - [key: string]: any -} - export class SiteMap implements ISiteMap { readonly id!: number name!: string @@ -5531,7 +5308,7 @@ export class SiteSocial implements ISiteSocial { description?: string | undefined contact!: Contact announcement!: Announcement - readonly sponsors!: string[] + readonly sponsors!: BatchSponsor[] readonly widget!: Widget[]; [key: string]: any @@ -5574,7 +5351,7 @@ export class SiteSocial implements ISiteSocial { if (Array.isArray(_data['sponsors'])) { ;( this).sponsors = [] as any for (let item of _data['sponsors']) { - ;( this).sponsors!.push(item) + ;( this).sponsors!.push(BatchSponsor.fromJS(item)) } } if (Array.isArray(_data['widget'])) { @@ -5611,7 +5388,7 @@ export class SiteSocial implements ISiteSocial { if (Array.isArray(this.sponsors)) { data['sponsors'] = [] for (let item of this.sponsors) { - data['sponsors'].push(item) + data['sponsors'].push(item.toJSON()) } } if (Array.isArray(this.widget)) { @@ -5633,7 +5410,7 @@ export interface ISiteSocial { description?: string | undefined contact: Contact announcement: Announcement - sponsors: string[] + sponsors: BatchSponsor[] widget: Widget[] [key: string]: any @@ -5645,17 +5422,137 @@ export class SiteSummary implements ISiteSummary { coordinate!: Coordinates siteType!: SiteType readonly plantCount!: number + readonly sponsorProgress!: number readonly survivedCount!: number readonly propagationCount!: number visitorCount?: number | undefined - readonly sponsors!: string[] - readonly progress!: number admins!: SiteAdmin[] readonly batches!: BatchDetail[]; [key: string]: any constructor(data?: ISiteSummary) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) { + ;( this)[property] = ( data)[property] + } + } + } + if (!data) { + this.coordinate = new Coordinates() + this.siteType = new SiteType() + this.admins = [] + this.batches = [] + } + } + + init(_data?: any) { + if (_data) { + for (var property in _data) { + if (_data.hasOwnProperty(property)) { + this[property] = _data[property] + } + } + ;( this).id = _data['id'] + this.name = _data['name'] + this.coordinate = _data['coordinate'] + ? Coordinates.fromJS(_data['coordinate']) + : new Coordinates() + this.siteType = _data['siteType'] ? SiteType.fromJS(_data['siteType']) : new SiteType() + ;( this).plantCount = _data['plantCount'] + ;( this).sponsorProgress = _data['sponsorProgress'] + ;( this).survivedCount = _data['survivedCount'] + ;( this).propagationCount = _data['propagationCount'] + this.visitorCount = _data['visitorCount'] + if (Array.isArray(_data['admins'])) { + this.admins = [] as any + for (let item of _data['admins']) { + this.admins!.push(SiteAdmin.fromJS(item)) + } + } + if (Array.isArray(_data['batches'])) { + ;( this).batches = [] as any + for (let item of _data['batches']) { + ;( this).batches!.push(BatchDetail.fromJS(item)) + } + } + } + } + + static fromJS(data: any): SiteSummary { + data = typeof data === 'object' ? data : {} + let result = new SiteSummary() + result.init(data) + return result + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {} + for (var property in this) { + if (this.hasOwnProperty(property)) { + data[property] = this[property] + } + } + data['id'] = this.id + data['name'] = this.name + data['coordinate'] = this.coordinate ? this.coordinate.toJSON() : undefined + data['siteType'] = this.siteType ? this.siteType.toJSON() : undefined + data['plantCount'] = this.plantCount + data['sponsorProgress'] = this.sponsorProgress + data['survivedCount'] = this.survivedCount + data['propagationCount'] = this.propagationCount + data['visitorCount'] = this.visitorCount + if (Array.isArray(this.admins)) { + data['admins'] = [] + for (let item of this.admins) { + data['admins'].push(item.toJSON()) + } + } + if (Array.isArray(this.batches)) { + data['batches'] = [] + for (let item of this.batches) { + data['batches'].push(item.toJSON()) + } + } + return data + } +} + +export interface ISiteSummary { + id: number + name: string + coordinate: Coordinates + siteType: SiteType + plantCount: number + sponsorProgress: number + survivedCount: number + propagationCount: number + visitorCount?: number | undefined + admins: SiteAdmin[] + batches: BatchDetail[] + + [key: string]: any +} + +export class SiteSummaryDetail implements ISiteSummaryDetail { + readonly id!: number + name!: string + coordinate!: Coordinates + siteType!: SiteType + readonly plantCount!: number + readonly sponsorProgress!: number + readonly survivedCount!: number + readonly propagationCount!: number + visitorCount?: number | undefined + readonly sponsors!: BatchSponsor[] + admins!: SiteAdmin[] + readonly batches!: BatchDetail[] + readonly weather!: Weather; + + [key: string]: any + + constructor(data?: ISiteSummaryDetail) { if (data) { for (var property in data) { if (data.hasOwnProperty(property)) { @@ -5669,6 +5566,7 @@ export class SiteSummary implements ISiteSummary { this.sponsors = [] this.admins = [] this.batches = [] + this.weather = new Weather() } } @@ -5686,16 +5584,16 @@ export class SiteSummary implements ISiteSummary { : new Coordinates() this.siteType = _data['siteType'] ? SiteType.fromJS(_data['siteType']) : new SiteType() ;( this).plantCount = _data['plantCount'] + ;( this).sponsorProgress = _data['sponsorProgress'] ;( this).survivedCount = _data['survivedCount'] ;( this).propagationCount = _data['propagationCount'] this.visitorCount = _data['visitorCount'] if (Array.isArray(_data['sponsors'])) { ;( this).sponsors = [] as any for (let item of _data['sponsors']) { - ;( this).sponsors!.push(item) + ;( this).sponsors!.push(BatchSponsor.fromJS(item)) } } - ;( this).progress = _data['progress'] if (Array.isArray(_data['admins'])) { this.admins = [] as any for (let item of _data['admins']) { @@ -5708,12 +5606,13 @@ export class SiteSummary implements ISiteSummary { ;( this).batches!.push(BatchDetail.fromJS(item)) } } + ;( this).weather = _data['weather'] ? Weather.fromJS(_data['weather']) : new Weather() } } - static fromJS(data: any): SiteSummary { + static fromJS(data: any): SiteSummaryDetail { data = typeof data === 'object' ? data : {} - let result = new SiteSummary() + let result = new SiteSummaryDetail() result.init(data) return result } @@ -5730,16 +5629,16 @@ export class SiteSummary implements ISiteSummary { data['coordinate'] = this.coordinate ? this.coordinate.toJSON() : undefined data['siteType'] = this.siteType ? this.siteType.toJSON() : undefined data['plantCount'] = this.plantCount + data['sponsorProgress'] = this.sponsorProgress data['survivedCount'] = this.survivedCount data['propagationCount'] = this.propagationCount data['visitorCount'] = this.visitorCount if (Array.isArray(this.sponsors)) { data['sponsors'] = [] for (let item of this.sponsors) { - data['sponsors'].push(item) + data['sponsors'].push(item.toJSON()) } } - data['progress'] = this.progress if (Array.isArray(this.admins)) { data['admins'] = [] for (let item of this.admins) { @@ -5752,23 +5651,25 @@ export class SiteSummary implements ISiteSummary { data['batches'].push(item.toJSON()) } } + data['weather'] = this.weather ? this.weather.toJSON() : undefined return data } } -export interface ISiteSummary { +export interface ISiteSummaryDetail { id: number name: string coordinate: Coordinates siteType: SiteType plantCount: number + sponsorProgress: number survivedCount: number propagationCount: number visitorCount?: number | undefined - sponsors: string[] - progress: number + sponsors: BatchSponsor[] admins: SiteAdmin[] batches: BatchDetail[] + weather: Weather [key: string]: any } @@ -5834,7 +5735,7 @@ export interface ISiteType { export class Sitetreespecies implements ISitetreespecies { readonly id!: number - quantity?: number | undefined + quantity!: number readonly en!: string readonly fr!: string; @@ -5888,7 +5789,7 @@ export class Sitetreespecies implements ISitetreespecies { export interface ISitetreespecies { id: number - quantity?: number | undefined + quantity: number en: string fr: string diff --git a/docker-compose.yml b/docker-compose.yml index 4edc6e215..dceace555 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,4 @@ +version: '3.7' services: mysql: hostname: mysql