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 (
+
+ )
+}
+
+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 (
-
@@ -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