migrate site id to varchar instead of int
This commit is contained in:
1 parent
61f954a3bd
commit
36639d8c38
16 files changed
+448
-41
No files matched your search
@@ -30,22 +30,32 @@ class VisibilityScopeTests(TestCase):
|
||||
self.active_a = ActiveSite.objects.create(
|
||||
code="a",
|
||||
name="Site A",
|
||||
source_site_id="site-a-0001",
|
||||
api_base_url="https://a.example/api/v1",
|
||||
managed_by=self.gm,
|
||||
)
|
||||
self.active_b = ActiveSite.objects.create(
|
||||
code="b",
|
||||
name="Site B",
|
||||
source_site_id="site-b-0001",
|
||||
api_base_url="https://b.example/api/v1",
|
||||
managed_by=self.gm_other,
|
||||
)
|
||||
self.site = Site.objects.create(
|
||||
site_name="Site A", user=self.gm, active_site=self.active_a
|
||||
site_id="site-a-0001",
|
||||
site_name="Site A",
|
||||
user=self.gm,
|
||||
active_site=self.active_a,
|
||||
)
|
||||
self.site_other = Site.objects.create(
|
||||
site_name="Site B", user=self.gm_other, active_site=self.active_b
|
||||
site_id="site-b-0001",
|
||||
site_name="Site B",
|
||||
user=self.gm_other,
|
||||
active_site=self.active_b,
|
||||
)
|
||||
self.orphan = Site.objects.create(
|
||||
site_id="orphan-seed-0001", site_name="Orphan Seed", user=self.gm
|
||||
)
|
||||
self.orphan = Site.objects.create(site_name="Orphan Seed", user=self.gm)
|
||||
Kandang.objects.create(kandang_name="K1", site=self.site)
|
||||
Kandang.objects.create(kandang_name="K2", site=self.site_other)
|
||||
|
||||
@@ -95,6 +105,7 @@ class VisibilityScopeTests(TestCase):
|
||||
ActiveSite.objects.create(
|
||||
code="orphan-seed",
|
||||
name="Orphan Seed",
|
||||
source_site_id="orphan-seed-0001",
|
||||
api_base_url="https://orphan.example/api/v1",
|
||||
api_key="k",
|
||||
managed_by=None,
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
# Convert HQ sites.site_id from bigint to farm-matching slug (varchar).
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def _unique_slug(candidate: str, used: set[str]) -> str:
|
||||
final = candidate[:64]
|
||||
n = 0
|
||||
while final in used or not final:
|
||||
n += 1
|
||||
suffix = f"-{n}"
|
||||
final = f"{candidate[: 64 - len(suffix)]}{suffix}"
|
||||
used.add(final)
|
||||
return final
|
||||
|
||||
|
||||
def forwards_convert_site_pk(apps, schema_editor):
|
||||
from django.utils.text import slugify
|
||||
|
||||
connection = schema_editor.connection
|
||||
Site = apps.get_model("farms", "Site")
|
||||
Kandang = apps.get_model("farms", "Kandang")
|
||||
ActiveSite = apps.get_model("sync", "ActiveSite")
|
||||
|
||||
sites = list(
|
||||
Site.objects.all().values(
|
||||
"site_id",
|
||||
"site_name",
|
||||
"user_id",
|
||||
"active_site_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
)
|
||||
kandangs = list(
|
||||
Kandang.objects.all().values(
|
||||
"kandang_id",
|
||||
"kandang_name",
|
||||
"feed_in_button_urls",
|
||||
"site_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
)
|
||||
active_source = {
|
||||
row.pk: str(row.source_site_id).strip()
|
||||
for row in ActiveSite.objects.all()
|
||||
if str(row.source_site_id or "").strip()
|
||||
}
|
||||
|
||||
used: set[str] = set()
|
||||
mapping: dict[int, str] = {}
|
||||
for row in sites:
|
||||
old = row["site_id"]
|
||||
candidate = active_source.get(row["active_site_id"]) or ""
|
||||
if not candidate:
|
||||
base = slugify(row["site_name"] or "")[:40] or "site"
|
||||
candidate = f"{base}-legacy-{old}"
|
||||
mapping[old] = _unique_slug(candidate, used)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
if connection.vendor == "sqlite":
|
||||
cursor.execute("PRAGMA foreign_keys=OFF")
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE sites_new (
|
||||
site_id varchar(64) NOT NULL PRIMARY KEY,
|
||||
site_name varchar(30) NOT NULL,
|
||||
user_id bigint NOT NULL REFERENCES user_access (user_id)
|
||||
DEFERRABLE INITIALLY DEFERRED,
|
||||
active_site_id bigint UNIQUE REFERENCES active_sites (active_site_id)
|
||||
DEFERRABLE INITIALLY DEFERRED,
|
||||
created_at datetime NOT NULL,
|
||||
updated_at datetime NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
for row in sites:
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO sites_new
|
||||
(site_id, site_name, user_id, active_site_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
[
|
||||
mapping[row["site_id"]],
|
||||
row["site_name"],
|
||||
row["user_id"],
|
||||
row["active_site_id"],
|
||||
row["created_at"],
|
||||
row["updated_at"],
|
||||
],
|
||||
)
|
||||
cursor.execute("DROP TABLE sites")
|
||||
cursor.execute("ALTER TABLE sites_new RENAME TO sites")
|
||||
cursor.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_sites_user_id ON sites (user_id)"
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE kandang_new (
|
||||
kandang_id integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
kandang_name varchar(30) NOT NULL,
|
||||
feed_in_button_urls text NOT NULL DEFAULT '[]',
|
||||
site_id varchar(64) NOT NULL REFERENCES sites (site_id)
|
||||
DEFERRABLE INITIALLY DEFERRED,
|
||||
created_at datetime NOT NULL,
|
||||
updated_at datetime NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
import json
|
||||
|
||||
for row in kandangs:
|
||||
urls = row["feed_in_button_urls"]
|
||||
if not isinstance(urls, str):
|
||||
urls = json.dumps(urls if urls is not None else [])
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO kandang_new
|
||||
(kandang_id, kandang_name, feed_in_button_urls, site_id,
|
||||
created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
[
|
||||
row["kandang_id"],
|
||||
row["kandang_name"],
|
||||
urls,
|
||||
mapping[row["site_id"]],
|
||||
row["created_at"],
|
||||
row["updated_at"],
|
||||
],
|
||||
)
|
||||
cursor.execute("DROP TABLE kandang")
|
||||
cursor.execute("ALTER TABLE kandang_new RENAME TO kandang")
|
||||
cursor.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_kandang_site_id ON kandang (site_id)"
|
||||
)
|
||||
cursor.execute("PRAGMA foreign_keys=ON")
|
||||
elif connection.vendor == "postgresql":
|
||||
cursor.execute(
|
||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS site_id_new varchar(64)"
|
||||
)
|
||||
for old, new in mapping.items():
|
||||
cursor.execute(
|
||||
"UPDATE sites SET site_id_new = %s WHERE site_id = %s",
|
||||
[new, old],
|
||||
)
|
||||
cursor.execute(
|
||||
"ALTER TABLE kandang ADD COLUMN IF NOT EXISTS site_id_new varchar(64)"
|
||||
)
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE kandang AS k
|
||||
SET site_id_new = s.site_id_new
|
||||
FROM sites AS s
|
||||
WHERE k.site_id = s.site_id
|
||||
"""
|
||||
)
|
||||
cursor.execute(
|
||||
"""
|
||||
ALTER TABLE kandang DROP CONSTRAINT IF EXISTS kandang_site_id_fkey
|
||||
"""
|
||||
)
|
||||
# Django default name variants
|
||||
cursor.execute(
|
||||
"""
|
||||
DO $$
|
||||
DECLARE r RECORD;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT conname FROM pg_constraint
|
||||
WHERE conrelid = 'kandang'::regclass
|
||||
AND contype = 'f'
|
||||
AND pg_get_constraintdef(oid) ILIKE '%%site_id%%'
|
||||
LOOP
|
||||
EXECUTE format('ALTER TABLE kandang DROP CONSTRAINT %I', r.conname);
|
||||
END LOOP;
|
||||
END $$;
|
||||
"""
|
||||
)
|
||||
cursor.execute("ALTER TABLE kandang DROP COLUMN site_id")
|
||||
cursor.execute(
|
||||
"ALTER TABLE kandang RENAME COLUMN site_id_new TO site_id"
|
||||
)
|
||||
cursor.execute("ALTER TABLE sites DROP CONSTRAINT sites_pkey")
|
||||
cursor.execute("ALTER TABLE sites DROP COLUMN site_id")
|
||||
cursor.execute("ALTER TABLE sites RENAME COLUMN site_id_new TO site_id")
|
||||
cursor.execute("ALTER TABLE sites ALTER COLUMN site_id SET NOT NULL")
|
||||
cursor.execute("ALTER TABLE sites ADD PRIMARY KEY (site_id)")
|
||||
cursor.execute("ALTER TABLE kandang ALTER COLUMN site_id SET NOT NULL")
|
||||
cursor.execute(
|
||||
"""
|
||||
ALTER TABLE kandang
|
||||
ADD CONSTRAINT kandang_site_id_fkey
|
||||
FOREIGN KEY (site_id) REFERENCES sites (site_id) ON DELETE CASCADE
|
||||
"""
|
||||
)
|
||||
cursor.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_kandang_site_id ON kandang (site_id)"
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"sites.site_id slug migration unsupported for {connection.vendor}"
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("farms", "0018_rename_city_site_to_active_site"),
|
||||
("sync", "0007_uniq_source_site_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.SeparateDatabaseAndState(
|
||||
database_operations=[
|
||||
migrations.RunPython(forwards_convert_site_pk, migrations.RunPython.noop),
|
||||
],
|
||||
state_operations=[
|
||||
migrations.AlterField(
|
||||
model_name="site",
|
||||
name="site_id",
|
||||
field=models.SlugField(max_length=64, primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="kandang",
|
||||
name="site",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="kandangs",
|
||||
to="farms.site",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,14 +1,42 @@
|
||||
from datetime import date
|
||||
import secrets
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
|
||||
from apps.accounts.models import User
|
||||
|
||||
|
||||
def site_name_slug(site_name: str) -> str:
|
||||
"""Slug portion of a site id (no numeric suffix)."""
|
||||
code = slugify(str(site_name or "").strip())[:55]
|
||||
if not code:
|
||||
raise ValidationError({"site_name": "Nama site tidak valid untuk dijadikan id."})
|
||||
return code
|
||||
|
||||
|
||||
def site_id_from_name(site_name: str, *, numeric_suffix: int | None = None) -> str:
|
||||
"""Site PK: `{name-slug}-{4-digit number}`, matching farm dashboard identity."""
|
||||
base = site_name_slug(site_name)
|
||||
if numeric_suffix is None:
|
||||
numeric_suffix = secrets.randbelow(10_000)
|
||||
return f"{base}-{numeric_suffix:04d}"
|
||||
|
||||
|
||||
def allocate_site_id(site_name: str) -> str:
|
||||
"""Pick an unused site_id for HQ-only creates (seeds / manual)."""
|
||||
for _ in range(32):
|
||||
candidate = site_id_from_name(site_name)
|
||||
if not Site.objects.filter(pk=candidate).exists():
|
||||
return candidate
|
||||
raise ValidationError({"site_name": "Gagal membuat id site unik — coba lagi."})
|
||||
|
||||
|
||||
class Site(models.Model):
|
||||
site_id = models.BigAutoField(primary_key=True)
|
||||
# Same shape as farm dashboard Site.site_id; when linked, equals ActiveSite.source_site_id.
|
||||
site_id = models.SlugField(primary_key=True, max_length=64)
|
||||
site_name = models.CharField(max_length=30)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sites")
|
||||
# Linked when GM/GM Admin adds the site dashboard to their Pengaturan view list.
|
||||
@@ -29,6 +57,11 @@ class Site(models.Model):
|
||||
def __str__(self) -> str:
|
||||
return self.site_name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.site_id:
|
||||
self.site_id = allocate_site_id(self.site_name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Kandang(models.Model):
|
||||
kandang_id = models.BigAutoField(primary_key=True)
|
||||
|
||||
@@ -4,7 +4,9 @@ from apps.accounts.serializers import PkAsIdMixin
|
||||
from apps.farms.models import Cycle, Flock, Kandang, Site
|
||||
|
||||
|
||||
class SiteSerializer(PkAsIdMixin, serializers.ModelSerializer):
|
||||
class SiteSerializer(serializers.ModelSerializer):
|
||||
# Slug PK (matches farm site_id); do not use IntegerField PkAsIdMixin.
|
||||
id = serializers.CharField(source="pk", read_only=True)
|
||||
owner_name = serializers.CharField(source="user.display_name", read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -2,27 +2,56 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from apps.farms.models import Site
|
||||
from django.db import transaction
|
||||
from django.utils.text import slugify
|
||||
|
||||
from apps.farms.models import Kandang, Site
|
||||
from apps.sync.models import ActiveSite
|
||||
|
||||
|
||||
def desired_hq_site_id(active_site: ActiveSite) -> str:
|
||||
"""HQ Site PK should match the farm site slug when known."""
|
||||
source = str(active_site.source_site_id or "").strip()
|
||||
if source:
|
||||
return source[:64]
|
||||
code = slugify(str(active_site.code or "").strip())[:64]
|
||||
if code:
|
||||
return code
|
||||
raise ValueError("ActiveSite needs source_site_id or code to create an HQ site.")
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def ensure_hq_site_for_active_site(active_site: ActiveSite) -> Site:
|
||||
"""
|
||||
Ensure the active-site registry entry has an HQ Site for farm visibility.
|
||||
|
||||
Prefers an existing linked site, then an unlinked site owned by the
|
||||
registrar with a matching name (so seeded Sukawarna attaches when
|
||||
registered), otherwise creates a new Site.
|
||||
Site.site_id matches ActiveSite.source_site_id (farm Site.site_id) when set.
|
||||
Prefers an existing linked site, then a site already keyed by that id, then an
|
||||
unlinked site owned by the registrar with a matching name (re-keyed if needed),
|
||||
otherwise creates a new Site.
|
||||
"""
|
||||
existing = Site.objects.filter(active_site=active_site).first()
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
owner = active_site.managed_by
|
||||
if owner is None:
|
||||
raise ValueError("ActiveSite.managed_by is required to create an HQ site.")
|
||||
|
||||
desired_id = desired_hq_site_id(active_site)
|
||||
site_name = (active_site.name or active_site.code or "Site")[:30]
|
||||
|
||||
existing = Site.objects.filter(active_site=active_site).first()
|
||||
if existing is not None:
|
||||
if existing.pk == desired_id:
|
||||
return existing
|
||||
return _rekey_site(existing, desired_id=desired_id, site_name=site_name, owner=owner)
|
||||
|
||||
by_pk = Site.objects.filter(pk=desired_id).first()
|
||||
if by_pk is not None:
|
||||
by_pk.active_site = active_site
|
||||
by_pk.user = owner
|
||||
if by_pk.site_name != site_name:
|
||||
by_pk.site_name = site_name
|
||||
by_pk.save(update_fields=["active_site", "user", "site_name", "updated_at"])
|
||||
return by_pk
|
||||
|
||||
orphan = (
|
||||
Site.objects.filter(
|
||||
user=owner, active_site__isnull=True, site_name__iexact=site_name
|
||||
@@ -31,7 +60,6 @@ def ensure_hq_site_for_active_site(active_site: ActiveSite) -> Site:
|
||||
.first()
|
||||
)
|
||||
if orphan is None and active_site.code:
|
||||
# e.g. code "sukawarna" ↔ site_name "Sukawarna"
|
||||
orphan = (
|
||||
Site.objects.filter(
|
||||
user=owner, active_site__isnull=True, site_name__iexact=active_site.code
|
||||
@@ -40,10 +68,45 @@ def ensure_hq_site_for_active_site(active_site: ActiveSite) -> Site:
|
||||
.first()
|
||||
)
|
||||
if orphan is not None:
|
||||
orphan.active_site = active_site
|
||||
orphan.save(update_fields=["active_site", "updated_at"])
|
||||
return orphan
|
||||
if orphan.pk == desired_id:
|
||||
orphan.active_site = active_site
|
||||
orphan.save(update_fields=["active_site", "updated_at"])
|
||||
return orphan
|
||||
return _rekey_site(
|
||||
orphan,
|
||||
desired_id=desired_id,
|
||||
site_name=site_name,
|
||||
owner=owner,
|
||||
active_site=active_site,
|
||||
)
|
||||
|
||||
return Site.objects.create(
|
||||
site_name=site_name, user=owner, active_site=active_site
|
||||
site_id=desired_id,
|
||||
site_name=site_name,
|
||||
user=owner,
|
||||
active_site=active_site,
|
||||
)
|
||||
|
||||
|
||||
def _rekey_site(
|
||||
old: Site,
|
||||
*,
|
||||
desired_id: str,
|
||||
site_name: str,
|
||||
owner,
|
||||
active_site: ActiveSite | None = None,
|
||||
) -> Site:
|
||||
"""Move an HQ site (and its kandangs) onto the farm slug primary key."""
|
||||
link = active_site if active_site is not None else old.active_site
|
||||
new_site = Site.objects.create(
|
||||
site_id=desired_id,
|
||||
site_name=site_name or old.site_name,
|
||||
user=owner,
|
||||
active_site=None,
|
||||
)
|
||||
Kandang.objects.filter(site=old).update(site=new_site)
|
||||
old_active = old.active_site
|
||||
old.delete()
|
||||
new_site.active_site = link or old_active
|
||||
new_site.save(update_fields=["active_site", "updated_at"])
|
||||
return new_site
|
||||
@@ -12,14 +12,19 @@ from apps.sync.models import ActiveSite
|
||||
|
||||
def _gm_visible_site(*, owner: User, site_name: str, code: str) -> Site:
|
||||
"""GM scope only sees sites linked to an ActiveSite they manage."""
|
||||
site_id = f"{code}-0001"
|
||||
active_site = ActiveSite.objects.create(
|
||||
code=code,
|
||||
name=site_name,
|
||||
source_site_id=site_id,
|
||||
api_base_url=f"https://{code}.example/api/v1",
|
||||
managed_by=owner,
|
||||
)
|
||||
return Site.objects.create(
|
||||
site_name=site_name, user=owner, active_site=active_site
|
||||
site_id=site_id,
|
||||
site_name=site_name,
|
||||
user=owner,
|
||||
active_site=active_site,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,25 @@ from rest_framework.test import APIClient
|
||||
from apps.accounts.models import User
|
||||
from apps.farms.models import Cycle, Flock, Kandang, Site
|
||||
from apps.operations.models import ChickenWeight, FeedSacks, IotPanel
|
||||
from apps.sync.models import ActiveSite
|
||||
|
||||
|
||||
def _linked_site(user: User, *, site_name: str = "Sukawarna", code: str = "sukawarna") -> Site:
|
||||
"""GM-visible site linked to an ActiveSite (Pengaturan registry)."""
|
||||
site_id = f"{code}-0001"
|
||||
active = ActiveSite.objects.create(
|
||||
code=code,
|
||||
name=site_name,
|
||||
source_site_id=site_id,
|
||||
api_base_url=f"https://{code}.example/api/v1",
|
||||
managed_by=user,
|
||||
)
|
||||
return Site.objects.create(
|
||||
site_id=site_id,
|
||||
site_name=site_name,
|
||||
user=user,
|
||||
active_site=active,
|
||||
)
|
||||
|
||||
|
||||
@override_settings(DASHBOARD_PUBLISH_HOUR=0)
|
||||
@@ -15,7 +34,7 @@ class OperationsAPITests(TestCase):
|
||||
self.user = User.objects.create_user(user_name="tester", password="secret")
|
||||
self.client = APIClient()
|
||||
self.client.force_authenticate(self.user)
|
||||
site = Site.objects.create(site_name="Sukawarna", user=self.user)
|
||||
site = _linked_site(self.user)
|
||||
self.kandang = Kandang.objects.create(kandang_name="Kandang 1", site=site)
|
||||
self.cycle = Cycle.objects.create(
|
||||
kandang=self.kandang,
|
||||
@@ -314,7 +333,7 @@ class DashboardVisibilityTests(TestCase):
|
||||
self.user = User.objects.create_user(user_name="tester", password="secret")
|
||||
self.client = APIClient()
|
||||
self.client.force_authenticate(self.user)
|
||||
site = Site.objects.create(site_name="Sukawarna", user=self.user)
|
||||
site = _linked_site(self.user)
|
||||
self.kandang = Kandang.objects.create(kandang_name="Kandang 1", site=site)
|
||||
self.cycle = Cycle.objects.create(
|
||||
kandang=self.kandang,
|
||||
|
||||
@@ -15,6 +15,24 @@ from apps.operations.services.chicken_counting_edge import (
|
||||
location_prefix_for_kandang,
|
||||
sync_chicken_counting_for_cycle,
|
||||
)
|
||||
from apps.sync.models import ActiveSite
|
||||
|
||||
|
||||
def _linked_site(user: User, *, site_name: str = "Sukawarna", code: str = "sukawarna") -> Site:
|
||||
site_id = f"{code}-0001"
|
||||
active = ActiveSite.objects.create(
|
||||
code=code,
|
||||
name=site_name,
|
||||
source_site_id=site_id,
|
||||
api_base_url=f"https://{code}.example/api/v1",
|
||||
managed_by=user,
|
||||
)
|
||||
return Site.objects.create(
|
||||
site_id=site_id,
|
||||
site_name=site_name,
|
||||
user=user,
|
||||
active_site=active,
|
||||
)
|
||||
|
||||
DB_HISTORY_SAMPLE = [
|
||||
{"date": "2026-09-02", "location": "K1-L1", "total": 14591},
|
||||
@@ -133,7 +151,7 @@ class ChickenCountingSyncApiTests(TestCase):
|
||||
self.user = User.objects.create_user(user_name="tester", password="secret")
|
||||
self.client = APIClient()
|
||||
self.client.force_authenticate(self.user)
|
||||
site = Site.objects.create(site_name="Sukawarna", user=self.user)
|
||||
site = _linked_site(self.user)
|
||||
self.kandang = Kandang.objects.create(kandang_name="Kandang 1", site=site)
|
||||
self.cycle = Cycle.objects.create(
|
||||
kandang=self.kandang,
|
||||
|
||||
@@ -20,7 +20,7 @@ class ActiveSite(models.Model):
|
||||
blank=True,
|
||||
default="",
|
||||
db_index=True,
|
||||
help_text="Farm-location Site.site_id (slug) from the registering dashboard",
|
||||
help_text="Farm-location Site.site_id (slug); equals HQ sites.site_id when claimed",
|
||||
)
|
||||
api_base_url = models.URLField(
|
||||
max_length=255,
|
||||
|
||||
@@ -149,6 +149,7 @@ class ClaimActiveSiteTests(TestCase):
|
||||
self.active = ActiveSite.objects.create(
|
||||
code="sukawarna",
|
||||
name="Sukawarna",
|
||||
source_site_id="sukawarna-0001",
|
||||
api_base_url="https://sukawarna.example/api/v1",
|
||||
api_key="site-secret",
|
||||
managed_by=None,
|
||||
@@ -164,7 +165,18 @@ class ClaimActiveSiteTests(TestCase):
|
||||
self.assertEqual(response.status_code, 200, response.data)
|
||||
self.active.refresh_from_db()
|
||||
self.assertEqual(self.active.managed_by_id, self.gm.pk)
|
||||
self.assertTrue(Site.objects.filter(active_site=self.active, user=self.gm).exists())
|
||||
hq = Site.objects.get(active_site=self.active, user=self.gm)
|
||||
self.assertEqual(hq.pk, "sukawarna-0001")
|
||||
self.assertEqual(hq.site_id, self.active.source_site_id)
|
||||
|
||||
def test_claimed_site_api_id_is_farm_slug_string(self):
|
||||
self.client.force_authenticate(self.gm)
|
||||
self.client.post("/api/v1/active-sites/", {"code": "sukawarna"}, format="json")
|
||||
listed = self.client.get("/api/v1/sites/")
|
||||
self.assertEqual(listed.status_code, 200)
|
||||
row = next(r for r in listed.data["results"] if r["site_name"] == "Sukawarna")
|
||||
self.assertEqual(row["id"], "sukawarna-0001")
|
||||
self.assertIsInstance(row["id"], str)
|
||||
|
||||
def test_claim_unknown_code_404(self):
|
||||
self.client.force_authenticate(self.gm)
|
||||
@@ -191,7 +203,10 @@ class ClaimActiveSiteTests(TestCase):
|
||||
self.active.source_site_id = "sukawarna-0001"
|
||||
self.active.save(update_fields=["managed_by", "source_site_id"])
|
||||
hq_site = Site.objects.create(
|
||||
site_name="Sukawarna", user=self.gm, active_site=self.active
|
||||
site_id="sukawarna-0001",
|
||||
site_name="Sukawarna",
|
||||
user=self.gm,
|
||||
active_site=self.active,
|
||||
)
|
||||
self.client.force_authenticate(self.gm)
|
||||
response = self.client.delete(f"/api/v1/active-sites/{self.active.pk}/")
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
-- is_active true, is_superuser false. Product roles never use is_staff.)
|
||||
--
|
||||
-- user_access → sites → kandang → cycles
|
||||
-- (sites.site_id = farm slug = active_sites.source_site_id when linked)
|
||||
-- kandang → flock → iot_panel
|
||||
-- user_access → api_keys
|
||||
-- user_access → active_sites (managed_by)
|
||||
@@ -103,10 +104,12 @@ CREATE INDEX IF NOT EXISTS idx_active_sites_managed_by_id ON active_sites (manag
|
||||
CREATE INDEX IF NOT EXISTS idx_active_sites_source_site_id ON active_sites (source_site_id);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4. Sites
|
||||
-- 4. Sites (HQ farm tree)
|
||||
-- site_id is the farm dashboard Site.site_id slug (e.g. sukawarna-4821).
|
||||
-- When linked, it equals active_sites.source_site_id.
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS sites (
|
||||
site_id BIGSERIAL PRIMARY KEY,
|
||||
site_id VARCHAR(64) PRIMARY KEY,
|
||||
site_name VARCHAR(30) NOT NULL,
|
||||
user_id BIGINT NOT NULL REFERENCES user_access (user_id) ON DELETE CASCADE,
|
||||
active_site_id BIGINT UNIQUE REFERENCES active_sites (active_site_id) ON DELETE SET NULL,
|
||||
@@ -123,7 +126,7 @@ CREATE TABLE IF NOT EXISTS kandang (
|
||||
kandang_id BIGSERIAL PRIMARY KEY,
|
||||
kandang_name VARCHAR(30) NOT NULL,
|
||||
feed_in_button_urls JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
site_id BIGINT NOT NULL REFERENCES sites (site_id) ON DELETE CASCADE,
|
||||
site_id VARCHAR(64) NOT NULL REFERENCES sites (site_id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user