remove wagtail and admin website, remove edit option, from hitung ayam, add create flock to settings
This commit is contained in:
1 parent
74dd3faf60
commit
30a736641f
39 files changed
+634
-819
No files matched your search
@@ -20,9 +20,6 @@ DB_NAME=db.sqlite3
|
||||
# DB_HOST=database
|
||||
# DB_PORT=5432
|
||||
|
||||
WAGTAIL_SITE_NAME=New Dashboard
|
||||
WAGTAILADMIN_BASE_URL=http://localhost:8000
|
||||
|
||||
API_PAGE_SIZE=50
|
||||
SESSION_COOKIE_SECURE=False
|
||||
CSRF_COOKIE_SECURE=False
|
||||
@@ -34,6 +31,10 @@ BOOTSTRAP_API_KEY=
|
||||
BOOTSTRAP_ADMIN_USER=admin
|
||||
BOOTSTRAP_ADMIN_PASSWORD=
|
||||
|
||||
# Optional staff account (leave BOOTSTRAP_STAFF_USER empty to skip)
|
||||
BOOTSTRAP_STAFF_USER=
|
||||
BOOTSTRAP_STAFF_PASSWORD=
|
||||
|
||||
# karung-web-admin (external)
|
||||
KARUNG_WEB_ADMIN_BASE_URL=http://127.0.0.1:5000
|
||||
KARUNG_WEB_ADMIN_TIMEOUT_SECONDS=30
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
|
||||
|
||||
from apps.accounts.models import ApiKey, User
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(DjangoUserAdmin):
|
||||
ordering = ("user_name",)
|
||||
list_display = ("user_name", "status", "created_at")
|
||||
search_fields = ("user_name",)
|
||||
fieldsets = (
|
||||
(None, {"fields": ("user_name", "password")}),
|
||||
("Permissions", {"fields": ("status", "groups", "user_permissions")}),
|
||||
)
|
||||
add_fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"classes": ("wide",),
|
||||
"fields": ("user_name", "password1", "password2", "status"),
|
||||
},
|
||||
),
|
||||
)
|
||||
filter_horizontal = ("groups", "user_permissions")
|
||||
|
||||
|
||||
@admin.register(ApiKey)
|
||||
class ApiKeyAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "prefix", "user", "is_active", "last_used_at", "created_at")
|
||||
list_filter = ("is_active",)
|
||||
search_fields = ("name", "prefix", "user__user_name")
|
||||
readonly_fields = ("prefix", "key_hash", "last_used_at", "created_at", "updated_at")
|
||||
@@ -1,8 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from apps.farms.models import Cycle, Flock, Kandang, Site
|
||||
|
||||
admin.site.register(Site)
|
||||
admin.site.register(Kandang)
|
||||
admin.site.register(Flock)
|
||||
admin.site.register(Cycle)
|
||||
@@ -0,0 +1,50 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def backfill_flock_kandang(apps, schema_editor):
|
||||
Flock = apps.get_model("farms", "Flock")
|
||||
for flock in Flock.objects.select_related("cycle").iterator():
|
||||
if flock.cycle_id and flock.kandang_id is None:
|
||||
flock.kandang_id = flock.cycle.kandang_id
|
||||
flock.save(update_fields=["kandang_id"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("farms", "0008_cycle_feed_initial_balance"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="flock",
|
||||
name="kandang",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="flocks",
|
||||
to="farms.kandang",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(backfill_flock_kandang, migrations.RunPython.noop),
|
||||
migrations.AlterField(
|
||||
model_name="flock",
|
||||
name="kandang",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="flocks",
|
||||
to="farms.kandang",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="flock",
|
||||
name="cycle",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="flocks",
|
||||
to="farms.cycle",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def remove_orphan_flocks(apps, schema_editor):
|
||||
Flock = apps.get_model("farms", "Flock")
|
||||
Flock.objects.filter(cycle_id__isnull=True).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("farms", "0009_flock_belongs_to_kandang"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(remove_orphan_flocks, migrations.RunPython.noop),
|
||||
migrations.RemoveField(
|
||||
model_name="flock",
|
||||
name="kandang",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="flock",
|
||||
name="cycle",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="flocks",
|
||||
to="farms.cycle",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,45 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def backfill_flock_kandang_from_cycle(apps, schema_editor):
|
||||
Flock = apps.get_model("farms", "Flock")
|
||||
for flock in Flock.objects.select_related("cycle").iterator():
|
||||
if flock.cycle_id and flock.kandang_id is None:
|
||||
flock.kandang_id = flock.cycle.kandang_id
|
||||
flock.save(update_fields=["kandang_id"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("farms", "0010_restore_flock_cycle_erd"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="flock",
|
||||
name="kandang",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="flocks",
|
||||
to="farms.kandang",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(backfill_flock_kandang_from_cycle, migrations.RunPython.noop),
|
||||
migrations.AlterField(
|
||||
model_name="flock",
|
||||
name="kandang",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="flocks",
|
||||
to="farms.kandang",
|
||||
),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="flock",
|
||||
name="cycle",
|
||||
),
|
||||
]
|
||||
@@ -2,13 +2,10 @@ from datetime import date
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
from wagtail.snippets.models import register_snippet
|
||||
|
||||
from apps.accounts.models import User
|
||||
|
||||
|
||||
@register_snippet
|
||||
class Site(models.Model):
|
||||
site_id = models.BigAutoField(primary_key=True)
|
||||
site_name = models.CharField(max_length=30)
|
||||
@@ -16,11 +13,6 @@ class Site(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
panels = [
|
||||
FieldPanel("site_name"),
|
||||
FieldPanel("user"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
db_table = "sites"
|
||||
ordering = ["site_name"]
|
||||
@@ -29,7 +21,6 @@ class Site(models.Model):
|
||||
return self.site_name
|
||||
|
||||
|
||||
@register_snippet
|
||||
class Kandang(models.Model):
|
||||
kandang_id = models.BigAutoField(primary_key=True)
|
||||
kandang_name = models.CharField(max_length=30)
|
||||
@@ -37,11 +28,6 @@ class Kandang(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
panels = [
|
||||
FieldPanel("kandang_name"),
|
||||
FieldPanel("site"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
db_table = "kandang"
|
||||
ordering = ["kandang_name"]
|
||||
@@ -50,23 +36,17 @@ class Kandang(models.Model):
|
||||
return self.kandang_name
|
||||
|
||||
|
||||
@register_snippet
|
||||
class Flock(models.Model):
|
||||
flock_id = models.BigAutoField(primary_key=True)
|
||||
flock_name = models.CharField(max_length=30)
|
||||
cycle = models.ForeignKey(
|
||||
"Cycle",
|
||||
kandang = models.ForeignKey(
|
||||
Kandang,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="flocks",
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
panels = [
|
||||
FieldPanel("flock_name"),
|
||||
FieldPanel("cycle"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
db_table = "flock"
|
||||
ordering = ["flock_name"]
|
||||
@@ -75,7 +55,6 @@ class Flock(models.Model):
|
||||
return self.flock_name
|
||||
|
||||
|
||||
@register_snippet
|
||||
class Cycle(models.Model):
|
||||
STATUS_ACTIVE = "active"
|
||||
STATUS_CLOSED = "closed"
|
||||
@@ -98,17 +77,6 @@ class Cycle(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
panels = [
|
||||
FieldPanel("kandang"),
|
||||
FieldPanel("total_days"),
|
||||
FieldPanel("start_date"),
|
||||
FieldPanel("end_date"),
|
||||
FieldPanel("doc_in_weight"),
|
||||
FieldPanel("doc_in_count"),
|
||||
FieldPanel("feed_initial_balance"),
|
||||
FieldPanel("status"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
db_table = "cycles"
|
||||
ordering = ["-start_date"]
|
||||
|
||||
@@ -21,15 +21,13 @@ class KandangSerializer(PkAsIdMixin, serializers.ModelSerializer):
|
||||
class FlockSerializer(PkAsIdMixin, serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Flock
|
||||
fields = ["id", "flock_name", "cycle", "created_at", "updated_at"]
|
||||
fields = ["id", "flock_name", "kandang", "created_at", "updated_at"]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
|
||||
class CycleSerializer(PkAsIdMixin, serializers.ModelSerializer):
|
||||
kandang = serializers.PrimaryKeyRelatedField(queryset=Kandang.objects.all())
|
||||
kandang_name = serializers.CharField(source="kandang.kandang_name", read_only=True)
|
||||
flock = serializers.SerializerMethodField()
|
||||
flock_name = serializers.SerializerMethodField()
|
||||
chick_in_weight = serializers.IntegerField(source="doc_in_weight")
|
||||
current_day = serializers.IntegerField(read_only=True)
|
||||
visible_through_date = serializers.DateField(read_only=True)
|
||||
@@ -40,8 +38,6 @@ class CycleSerializer(PkAsIdMixin, serializers.ModelSerializer):
|
||||
"id",
|
||||
"kandang",
|
||||
"kandang_name",
|
||||
"flock",
|
||||
"flock_name",
|
||||
"total_days",
|
||||
"current_day",
|
||||
"visible_through_date",
|
||||
@@ -57,26 +53,7 @@ class CycleSerializer(PkAsIdMixin, serializers.ModelSerializer):
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"kandang_name",
|
||||
"flock",
|
||||
"flock_name",
|
||||
"feed_initial_balance",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
def _first_flock(self, obj):
|
||||
return obj.flocks.order_by("flock_id").first()
|
||||
|
||||
def get_flock(self, obj):
|
||||
flock = self._first_flock(obj)
|
||||
return flock.pk if flock is not None else None
|
||||
|
||||
def get_flock_name(self, obj):
|
||||
flock = self._first_flock(obj)
|
||||
return flock.flock_name if flock is not None else None
|
||||
|
||||
def create(self, validated_data):
|
||||
kandang = validated_data["kandang"]
|
||||
cycle = Cycle.objects.create(**validated_data)
|
||||
Flock.objects.create(flock_name=kandang.kandang_name[:30], cycle=cycle)
|
||||
return cycle
|
||||
+52
-51
@@ -10,12 +10,37 @@ from apps.farms.models import Cycle, Flock, Kandang, Site
|
||||
class FarmAPITests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(user_name="tester", password="secret")
|
||||
self.admin = User.objects.create_user(
|
||||
user_name="admin",
|
||||
password="secret",
|
||||
status=User.STATUS_SUPERADMIN,
|
||||
)
|
||||
self.client = APIClient()
|
||||
self.client.force_authenticate(self.user)
|
||||
self.site = Site.objects.create(site_name="Sukawarna", user=self.user)
|
||||
self.kandang = Kandang.objects.create(kandang_name="Kandang 1", site=self.site)
|
||||
|
||||
def test_create_cycle_auto_creates_flock_and_returns_erd_fields(self):
|
||||
def test_create_flock_under_kandang_requires_superadmin(self):
|
||||
self.client.force_authenticate(self.admin)
|
||||
response = self.client.post(
|
||||
"/api/v1/flocks/",
|
||||
{"flock_name": "Lantai 1", "kandang": self.kandang.pk},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 201, response.data)
|
||||
self.assertEqual(response.data["flock_name"], "Lantai 1")
|
||||
self.assertEqual(response.data["kandang"], self.kandang.pk)
|
||||
|
||||
def test_staff_cannot_create_flock(self):
|
||||
self.client.force_authenticate(self.user)
|
||||
response = self.client.post(
|
||||
"/api/v1/flocks/",
|
||||
{"flock_name": "Lantai 1", "kandang": self.kandang.pk},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 403, response.data)
|
||||
|
||||
def test_create_cycle_without_flock(self):
|
||||
self.client.force_authenticate(self.user)
|
||||
start = date.today()
|
||||
response = self.client.post(
|
||||
"/api/v1/cycles/",
|
||||
@@ -31,56 +56,32 @@ class FarmAPITests(TestCase):
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 201, response.data)
|
||||
self.assertEqual(response.data["chick_in_weight"], 42)
|
||||
self.assertEqual(response.data["kandang_name"], "Kandang 1")
|
||||
self.assertEqual(response.data["flock_name"], "Kandang 1")
|
||||
self.assertIsNotNone(response.data["flock"])
|
||||
cycle = Cycle.objects.get(pk=response.data["id"])
|
||||
flock = Flock.objects.get(pk=response.data["flock"])
|
||||
self.assertEqual(cycle.doc_in_weight, 42)
|
||||
self.assertEqual(flock.cycle_id, cycle.pk)
|
||||
self.assertNotIn("flock", response.data)
|
||||
self.assertNotIn("flock_name", response.data)
|
||||
|
||||
def test_list_cycles_by_kandang(self):
|
||||
cycle = Cycle.objects.create(
|
||||
kandang=self.kandang,
|
||||
total_days=35,
|
||||
start_date=date.today(),
|
||||
end_date=date.today() + timedelta(days=34),
|
||||
doc_in_weight=40,
|
||||
doc_in_count=9000,
|
||||
status=Cycle.STATUS_ACTIVE,
|
||||
)
|
||||
flock = Flock.objects.create(flock_name="Flock A", cycle=cycle)
|
||||
response = self.client.get("/api/v1/cycles/", {"kandang_id": self.kandang.pk})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
results = response.data["results"] if isinstance(response.data, dict) else response.data
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0]["flock"], flock.pk)
|
||||
self.assertEqual(results[0]["kandang_name"], "Kandang 1")
|
||||
self.assertEqual(results[0]["flock_name"], "Flock A")
|
||||
def test_flock_persists_across_cycles(self):
|
||||
flock = Flock.objects.create(flock_name="Lantai 1", kandang=self.kandang)
|
||||
start = date.today()
|
||||
for offset in (0, 40):
|
||||
Cycle.objects.create(
|
||||
kandang=self.kandang,
|
||||
total_days=35,
|
||||
start_date=start + timedelta(days=offset),
|
||||
end_date=start + timedelta(days=offset + 34),
|
||||
doc_in_weight=40,
|
||||
doc_in_count=9000,
|
||||
status=Cycle.STATUS_ACTIVE,
|
||||
)
|
||||
self.assertEqual(Flock.objects.filter(kandang=self.kandang).count(), 1)
|
||||
self.assertEqual(flock.kandang_id, self.kandang.pk)
|
||||
|
||||
def test_list_flocks_by_cycle(self):
|
||||
cycle = Cycle.objects.create(
|
||||
kandang=self.kandang,
|
||||
total_days=35,
|
||||
start_date=date.today(),
|
||||
end_date=date.today() + timedelta(days=34),
|
||||
doc_in_weight=40,
|
||||
doc_in_count=9000,
|
||||
status=Cycle.STATUS_ACTIVE,
|
||||
)
|
||||
flock = Flock.objects.create(flock_name="Flock A", cycle=cycle)
|
||||
other_cycle = Cycle.objects.create(
|
||||
kandang=self.kandang,
|
||||
total_days=35,
|
||||
start_date=date.today(),
|
||||
end_date=date.today() + timedelta(days=34),
|
||||
doc_in_weight=40,
|
||||
doc_in_count=9000,
|
||||
status=Cycle.STATUS_ACTIVE,
|
||||
)
|
||||
Flock.objects.create(flock_name="Flock B", cycle=other_cycle)
|
||||
response = self.client.get("/api/v1/flocks/", {"cycle_id": cycle.pk})
|
||||
def test_list_flocks_by_kandang(self):
|
||||
self.client.force_authenticate(self.user)
|
||||
Flock.objects.create(flock_name="Lantai 1", kandang=self.kandang)
|
||||
Flock.objects.create(flock_name="Lantai 2", kandang=self.kandang)
|
||||
other_kandang = Kandang.objects.create(kandang_name="Kandang 2", site=self.site)
|
||||
Flock.objects.create(flock_name="Other", kandang=other_kandang)
|
||||
response = self.client.get("/api/v1/flocks/", {"kandang_id": self.kandang.pk})
|
||||
self.assertEqual(response.status_code, 200, response.data)
|
||||
results = response.data["results"] if isinstance(response.data, dict) else response.data
|
||||
self.assertEqual([r["id"] for r in results], [flock.pk])
|
||||
self.assertEqual(len(results), 2)
|
||||
@@ -56,19 +56,20 @@ class KandangViewSet(viewsets.ModelViewSet):
|
||||
|
||||
|
||||
class FlockViewSet(viewsets.ModelViewSet):
|
||||
queryset = Flock.objects.all()
|
||||
queryset = Flock.objects.select_related("kandang").all()
|
||||
serializer_class = FlockSerializer
|
||||
permission_classes = [IsSuperAdminForWrite]
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
cycle_id = self.request.query_params.get("cycle_id")
|
||||
if cycle_id:
|
||||
qs = qs.filter(cycle_id=cycle_id)
|
||||
kandang_id = self.request.query_params.get("kandang_id")
|
||||
if kandang_id:
|
||||
qs = qs.filter(kandang_id=kandang_id)
|
||||
return qs
|
||||
|
||||
|
||||
class CycleViewSet(viewsets.ModelViewSet):
|
||||
queryset = Cycle.objects.select_related("kandang").prefetch_related("flocks").all()
|
||||
queryset = Cycle.objects.select_related("kandang").all()
|
||||
serializer_class = CycleSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
@@ -5,62 +5,119 @@ from django.db import transaction
|
||||
from apps.accounts.models import ApiKey, User
|
||||
|
||||
|
||||
def ensure_bootstrap_user(
|
||||
*,
|
||||
user_name: str,
|
||||
password: str,
|
||||
status: str,
|
||||
is_staff: bool,
|
||||
is_superuser: bool,
|
||||
force_password: bool,
|
||||
stdout,
|
||||
) -> User:
|
||||
user, created = User.objects.get_or_create(
|
||||
user_name=user_name,
|
||||
defaults={
|
||||
"status": status,
|
||||
"is_staff": is_staff,
|
||||
"is_superuser": is_superuser,
|
||||
},
|
||||
)
|
||||
|
||||
if not created and user.status != status:
|
||||
raise CommandError(
|
||||
f"User {user_name!r} already exists with status {user.status!r}; "
|
||||
f"cannot bootstrap as {status!r}"
|
||||
)
|
||||
|
||||
should_set_password = created or not user.has_usable_password() or force_password
|
||||
if should_set_password:
|
||||
user.set_password(password)
|
||||
user.status = status
|
||||
user.is_staff = is_staff
|
||||
user.is_superuser = is_superuser
|
||||
user.save()
|
||||
action = "Created" if created else "Updated password for"
|
||||
stdout.write(f"{action} {status} user {user_name}")
|
||||
else:
|
||||
user.status = status
|
||||
user.is_staff = is_staff
|
||||
user.is_superuser = is_superuser
|
||||
user.save(update_fields=["status", "is_staff", "is_superuser", "updated_at"])
|
||||
stdout.write(f"User {user_name} already exists (password unchanged)")
|
||||
|
||||
return user
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Create production superadmin login only (no demo sites/cycles/data). "
|
||||
"Uses BOOTSTRAP_ADMIN_USER and BOOTSTRAP_ADMIN_PASSWORD from settings."
|
||||
"Create production login accounts only (no demo sites/cycles/data). "
|
||||
"Uses BOOTSTRAP_ADMIN_* and optional BOOTSTRAP_STAFF_* from settings."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--force-password",
|
||||
action="store_true",
|
||||
help="Reset password even when the admin user already has one",
|
||||
help="Reset password even when the user already has one",
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def handle(self, *args, **options):
|
||||
user_name = (settings.BOOTSTRAP_ADMIN_USER or "admin").strip()
|
||||
password = settings.BOOTSTRAP_ADMIN_PASSWORD or ""
|
||||
if not password:
|
||||
force_password = options["force_password"]
|
||||
|
||||
admin_user_name = (settings.BOOTSTRAP_ADMIN_USER or "admin").strip()
|
||||
admin_password = settings.BOOTSTRAP_ADMIN_PASSWORD or ""
|
||||
if not admin_password:
|
||||
raise CommandError(
|
||||
"BOOTSTRAP_ADMIN_PASSWORD is required. Set it in .env before running bootstrap_admin."
|
||||
)
|
||||
|
||||
user, created = User.objects.get_or_create(
|
||||
user_name=user_name,
|
||||
defaults={
|
||||
"status": User.STATUS_SUPERADMIN,
|
||||
"is_staff": True,
|
||||
"is_superuser": True,
|
||||
},
|
||||
admin = ensure_bootstrap_user(
|
||||
user_name=admin_user_name,
|
||||
password=admin_password,
|
||||
status=User.STATUS_SUPERADMIN,
|
||||
is_staff=True,
|
||||
is_superuser=True,
|
||||
force_password=force_password,
|
||||
stdout=self.stdout,
|
||||
)
|
||||
|
||||
should_set_password = created or not user.has_usable_password() or options["force_password"]
|
||||
if should_set_password:
|
||||
user.set_password(password)
|
||||
user.status = User.STATUS_SUPERADMIN
|
||||
user.is_staff = True
|
||||
user.is_superuser = True
|
||||
user.save()
|
||||
action = "Created" if created else "Updated password for"
|
||||
self.stdout.write(f"{action} superadmin {user_name}")
|
||||
else:
|
||||
user.status = User.STATUS_SUPERADMIN
|
||||
user.is_superuser = True
|
||||
user.save(update_fields=["status", "is_superuser", "updated_at"])
|
||||
self.stdout.write(f"Superadmin {user_name} already exists (password unchanged)")
|
||||
staff_user_name = (settings.BOOTSTRAP_STAFF_USER or "").strip()
|
||||
staff_password = settings.BOOTSTRAP_STAFF_PASSWORD or ""
|
||||
if staff_user_name:
|
||||
if staff_user_name == admin_user_name:
|
||||
raise CommandError(
|
||||
"BOOTSTRAP_STAFF_USER must differ from BOOTSTRAP_ADMIN_USER."
|
||||
)
|
||||
if not staff_password:
|
||||
raise CommandError(
|
||||
"BOOTSTRAP_STAFF_PASSWORD is required when BOOTSTRAP_STAFF_USER is set."
|
||||
)
|
||||
ensure_bootstrap_user(
|
||||
user_name=staff_user_name,
|
||||
password=staff_password,
|
||||
status=User.STATUS_ACTIVE,
|
||||
is_staff=True,
|
||||
is_superuser=False,
|
||||
force_password=force_password,
|
||||
stdout=self.stdout,
|
||||
)
|
||||
|
||||
bootstrap_key = settings.BOOTSTRAP_API_KEY
|
||||
if bootstrap_key:
|
||||
api_key, _ = ApiKey.generate(user, "bootstrap")
|
||||
api_key, _ = ApiKey.generate(admin, "bootstrap")
|
||||
api_key.prefix = bootstrap_key[:8]
|
||||
api_key.key_hash = ApiKey.hash_key(bootstrap_key)
|
||||
api_key.save(update_fields=["prefix", "key_hash", "updated_at"])
|
||||
self.stdout.write(f"Bootstrap API key installed (prefix={api_key.prefix})")
|
||||
|
||||
accounts = admin_user_name
|
||||
if staff_user_name:
|
||||
accounts = f"{admin_user_name}, {staff_user_name}"
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Bootstrap OK: login={user_name} (no demo farm data — use Pengaturan to add sites/kandang/cycles)"
|
||||
f"Bootstrap OK: login={accounts} "
|
||||
"(no demo farm data — use Pengaturan to add sites/kandang/cycles)"
|
||||
)
|
||||
)
|
||||
@@ -133,7 +133,7 @@ class Command(BaseCommand):
|
||||
real_by_date = self.load_real_rows(Path(iot_json))
|
||||
|
||||
cycles = Cycle.objects.filter(
|
||||
start_date=cycle_start, flocks__isnull=False
|
||||
start_date=cycle_start, kandang__flocks__isnull=False
|
||||
).distinct()
|
||||
if not cycles:
|
||||
raise CommandError(f"No cycles starting {cycle_start} with flocks")
|
||||
@@ -141,7 +141,7 @@ class Command(BaseCommand):
|
||||
seeded = 0
|
||||
for cycle in cycles:
|
||||
kandang_name = cycle.kandang.kandang_name
|
||||
for flock in cycle.flocks.order_by("flock_id"):
|
||||
for flock in cycle.kandang.flocks.order_by("flock_id"):
|
||||
profile = self.profile_for(kandang_name, flock.flock_name)
|
||||
rng = random.Random(options["jitter_seed"] + flock.pk)
|
||||
rows = self.build_flock_rows(flock, cycle, real_by_date, profile, rng)
|
||||
|
||||
@@ -206,8 +206,11 @@ class Command(BaseCommand):
|
||||
ChickenWeight.objects.filter(cycle=existing).delete()
|
||||
FeedSacks.objects.filter(cycle=existing).delete()
|
||||
ManualInput.objects.filter(cycle=existing).delete()
|
||||
IotPanel.objects.filter(flock__cycle=existing).delete()
|
||||
Flock.objects.filter(cycle=existing).delete()
|
||||
IotPanel.objects.filter(
|
||||
flock__kandang=kandang,
|
||||
date__gte=existing.start_date,
|
||||
date__lte=existing.end_date,
|
||||
).delete()
|
||||
existing.delete()
|
||||
self.stdout.write("Reset previous Kandang 1 cycle")
|
||||
|
||||
@@ -232,7 +235,7 @@ class Command(BaseCommand):
|
||||
|
||||
flocks_by_name: dict[str, Flock] = {}
|
||||
for name in FLOCK_NAMES:
|
||||
flock, _ = Flock.objects.get_or_create(flock_name=name, cycle=cycle)
|
||||
flock, _ = Flock.objects.get_or_create(flock_name=name, kandang=kandang)
|
||||
flocks_by_name[name] = flock
|
||||
|
||||
dates = [START_DATE + timedelta(days=i) for i in range(TOTAL_DAYS)]
|
||||
@@ -426,7 +429,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Seeded cycle={cycle.pk} site={site.pk} kandang={kandang.pk} "
|
||||
f"flocks={Flock.objects.filter(cycle=cycle).count()} "
|
||||
f"flocks={Flock.objects.filter(kandang=kandang).count()} "
|
||||
f"cc={len(cc_by_date)} cw={len(cw_by_date)} fs={len(fs_by_date)} "
|
||||
f"manual={len(manual_by_date)} kpi={KPI.objects.filter(cycle=cycle).count()} "
|
||||
f"iot_panel={panel_count} closeout_fcr={last_kpi.fcr if last_kpi else None} "
|
||||
|
||||
@@ -96,8 +96,11 @@ class Command(BaseCommand):
|
||||
ChickenWeight.objects.filter(cycle=existing).delete()
|
||||
FeedSacks.objects.filter(cycle=existing).delete()
|
||||
ManualInput.objects.filter(cycle=existing).delete()
|
||||
IotPanel.objects.filter(flock__cycle=existing).delete()
|
||||
Flock.objects.filter(cycle=existing).delete()
|
||||
IotPanel.objects.filter(
|
||||
flock__kandang=kandang,
|
||||
date__gte=existing.start_date,
|
||||
date__lte=existing.end_date,
|
||||
).delete()
|
||||
existing.delete()
|
||||
self.stdout.write("Reset previous Kandang 2 cycle")
|
||||
|
||||
@@ -122,7 +125,7 @@ class Command(BaseCommand):
|
||||
|
||||
flocks_by_name: dict[str, Flock] = {}
|
||||
for name in ("Lantai 1", "Lantai 2"):
|
||||
flock, _ = Flock.objects.get_or_create(flock_name=name, cycle=cycle)
|
||||
flock, _ = Flock.objects.get_or_create(flock_name=name, kandang=kandang)
|
||||
flocks_by_name[name] = flock
|
||||
|
||||
cc_by_date = {}
|
||||
@@ -288,7 +291,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Seeded cycle={cycle.pk} site={site.pk} kandang={kandang.pk} "
|
||||
f"flocks={Flock.objects.filter(cycle=cycle).count()} "
|
||||
f"flocks={Flock.objects.filter(kandang=kandang).count()} "
|
||||
f"cc={len(cc_by_date)} cw={len(cw_by_date)} fs={len(fs_by_date)} "
|
||||
f"manual={len(manual_by_date)} kpi={KPI.objects.filter(cycle=cycle).count()} "
|
||||
f"iot_panel={panel_count} closeout_fcr={last_kpi.fcr if last_kpi else None}"
|
||||
|
||||
@@ -9,6 +9,8 @@ from django.core.management import call_command
|
||||
@override_settings(
|
||||
BOOTSTRAP_ADMIN_USER="admin",
|
||||
BOOTSTRAP_ADMIN_PASSWORD="Pr04dm1n",
|
||||
BOOTSTRAP_STAFF_USER="",
|
||||
BOOTSTRAP_STAFF_PASSWORD="",
|
||||
)
|
||||
class BootstrapAdminTests(TestCase):
|
||||
def test_creates_superadmin_without_demo_data(self):
|
||||
@@ -23,3 +25,31 @@ class BootstrapAdminTests(TestCase):
|
||||
with self.settings(BOOTSTRAP_ADMIN_PASSWORD=""):
|
||||
with self.assertRaises(CommandError):
|
||||
call_command("bootstrap_admin")
|
||||
|
||||
@override_settings(
|
||||
BOOTSTRAP_STAFF_USER="staff",
|
||||
BOOTSTRAP_STAFF_PASSWORD="St4ff!",
|
||||
)
|
||||
def test_creates_staff_when_configured(self):
|
||||
call_command("bootstrap_admin")
|
||||
staff = User.objects.get(user_name="staff")
|
||||
self.assertEqual(staff.status, User.STATUS_ACTIVE)
|
||||
self.assertTrue(staff.is_staff)
|
||||
self.assertFalse(staff.is_superuser)
|
||||
self.assertTrue(staff.check_password("St4ff!"))
|
||||
|
||||
@override_settings(
|
||||
BOOTSTRAP_STAFF_USER="staff",
|
||||
BOOTSTRAP_STAFF_PASSWORD="",
|
||||
)
|
||||
def test_requires_staff_password_when_user_set(self):
|
||||
with self.assertRaises(CommandError):
|
||||
call_command("bootstrap_admin")
|
||||
|
||||
@override_settings(
|
||||
BOOTSTRAP_STAFF_USER="admin",
|
||||
BOOTSTRAP_STAFF_PASSWORD="St4ff!",
|
||||
)
|
||||
def test_rejects_staff_same_as_admin(self):
|
||||
with self.assertRaises(CommandError):
|
||||
call_command("bootstrap_admin")
|
||||
@@ -1,19 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from apps.operations.models import (
|
||||
AIInsight,
|
||||
ChickenCounting,
|
||||
ChickenWeight,
|
||||
FeedSacks,
|
||||
IotPanel,
|
||||
KPI,
|
||||
ManualInput,
|
||||
)
|
||||
|
||||
admin.site.register(KPI)
|
||||
admin.site.register(ManualInput)
|
||||
admin.site.register(ChickenCounting)
|
||||
admin.site.register(ChickenWeight)
|
||||
admin.site.register(FeedSacks)
|
||||
admin.site.register(IotPanel)
|
||||
admin.site.register(AIInsight)
|
||||
@@ -1,11 +1,8 @@
|
||||
from django.db import models
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
from wagtail.snippets.models import register_snippet
|
||||
|
||||
from apps.farms.models import Cycle, Flock
|
||||
|
||||
|
||||
@register_snippet
|
||||
class IotPanel(models.Model):
|
||||
panel_id = models.BigAutoField(primary_key=True)
|
||||
date = models.DateField()
|
||||
@@ -19,8 +16,6 @@ class IotPanel(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
panels = [FieldPanel("flock"), FieldPanel("date"), FieldPanel("average_temperature")]
|
||||
|
||||
class Meta:
|
||||
db_table = "iot_panel"
|
||||
ordering = ["-date"]
|
||||
@@ -31,7 +26,6 @@ class IotPanel(models.Model):
|
||||
]
|
||||
|
||||
|
||||
@register_snippet
|
||||
class AIInsight(models.Model):
|
||||
date = models.DateField()
|
||||
insight_text = models.TextField()
|
||||
@@ -42,14 +36,11 @@ class AIInsight(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
panels = [FieldPanel("cycle"), FieldPanel("date"), FieldPanel("insight_text"), FieldPanel("alert")]
|
||||
|
||||
class Meta:
|
||||
db_table = "ai_insight"
|
||||
ordering = ["-date"]
|
||||
|
||||
|
||||
@register_snippet
|
||||
class FeedSacks(models.Model):
|
||||
fs_id = models.BigAutoField(primary_key=True)
|
||||
date = models.DateField()
|
||||
@@ -63,8 +54,6 @@ class FeedSacks(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
panels = [FieldPanel("cycle"), FieldPanel("date"), FieldPanel("in_today"), FieldPanel("out_today")]
|
||||
|
||||
class Meta:
|
||||
db_table = "feed_sacks"
|
||||
ordering = ["-date"]
|
||||
@@ -74,7 +63,6 @@ class FeedSacks(models.Model):
|
||||
]
|
||||
|
||||
|
||||
@register_snippet
|
||||
class ChickenCounting(models.Model):
|
||||
cc_id = models.BigAutoField(primary_key=True)
|
||||
date = models.DateField()
|
||||
@@ -84,8 +72,6 @@ class ChickenCounting(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
panels = [FieldPanel("cycle"), FieldPanel("date"), FieldPanel("total_count")]
|
||||
|
||||
class Meta:
|
||||
db_table = "chicken_counting"
|
||||
ordering = ["-date"]
|
||||
@@ -94,7 +80,6 @@ class ChickenCounting(models.Model):
|
||||
]
|
||||
|
||||
|
||||
@register_snippet
|
||||
class ChickenWeight(models.Model):
|
||||
cw_id = models.BigAutoField(primary_key=True)
|
||||
date = models.DateField()
|
||||
@@ -108,13 +93,6 @@ class ChickenWeight(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
panels = [
|
||||
FieldPanel("cycle"),
|
||||
FieldPanel("date"),
|
||||
FieldPanel("average_weight"),
|
||||
FieldPanel("uniformity"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
db_table = "chicken_weight"
|
||||
ordering = ["-date"]
|
||||
@@ -123,7 +101,6 @@ class ChickenWeight(models.Model):
|
||||
]
|
||||
|
||||
|
||||
@register_snippet
|
||||
class ManualInput(models.Model):
|
||||
manual_id = models.BigAutoField(primary_key=True)
|
||||
date = models.DateField()
|
||||
@@ -149,8 +126,6 @@ class ManualInput(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
panels = [FieldPanel("cycle"), FieldPanel("date"), FieldPanel("age_manual"), FieldPanel("fcr_manual")]
|
||||
|
||||
class Meta:
|
||||
db_table = "manual_input"
|
||||
ordering = ["-date"]
|
||||
@@ -159,7 +134,6 @@ class ManualInput(models.Model):
|
||||
]
|
||||
|
||||
|
||||
@register_snippet
|
||||
class KPI(models.Model):
|
||||
kpi_id = models.BigAutoField(primary_key=True)
|
||||
date = models.DateField()
|
||||
@@ -187,8 +161,6 @@ class KPI(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
panels = [FieldPanel("cycle"), FieldPanel("date"), FieldPanel("age"), FieldPanel("fcr"), FieldPanel("eef")]
|
||||
|
||||
class Meta:
|
||||
db_table = "kpi"
|
||||
ordering = ["-date"]
|
||||
|
||||
@@ -19,7 +19,7 @@ class CycleContextMixin(serializers.Serializer):
|
||||
flock_id = serializers.SerializerMethodField()
|
||||
|
||||
def get_flock_id(self, obj):
|
||||
flock = obj.cycle.flocks.order_by("flock_id").first()
|
||||
flock = obj.cycle.kandang.flocks.order_by("flock_id").first()
|
||||
return flock.pk if flock is not None else None
|
||||
|
||||
|
||||
@@ -124,8 +124,7 @@ class IotPanelSerializer(PkAsIdMixin, serializers.ModelSerializer):
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
def get_kandang(self, obj):
|
||||
cycle = getattr(obj.flock, "cycle", None)
|
||||
return cycle.kandang_id if cycle is not None else None
|
||||
return obj.flock.kandang_id
|
||||
|
||||
|
||||
class AIInsightSerializer(PkAsIdMixin, CycleContextMixin, serializers.ModelSerializer):
|
||||
|
||||
@@ -11,7 +11,7 @@ import httpx
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.farms.models import Flock
|
||||
from apps.farms.models import Cycle, Flock
|
||||
from apps.operations.models import IotPanel
|
||||
|
||||
WIB = ZoneInfo("Asia/Jakarta")
|
||||
@@ -199,7 +199,7 @@ def sync_iot_panels(
|
||||
django_flocks: dict[str, Flock] = {}
|
||||
for external_id, django_id in flock_map.items():
|
||||
try:
|
||||
django_flocks[str(external_id)] = Flock.objects.select_related("cycle").get(pk=int(django_id))
|
||||
django_flocks[str(external_id)] = Flock.objects.select_related("kandang").get(pk=int(django_id))
|
||||
except Flock.DoesNotExist:
|
||||
continue
|
||||
|
||||
@@ -236,7 +236,16 @@ def sync_iot_panels(
|
||||
continue
|
||||
|
||||
snapshot_date = fetched_at.astimezone(WIB).date()
|
||||
age = cycle_age_for_date(flock.cycle.start_date, snapshot_date)
|
||||
cycle = (
|
||||
Cycle.objects.filter(
|
||||
kandang_id=flock.kandang_id,
|
||||
start_date__lte=snapshot_date,
|
||||
end_date__gte=snapshot_date,
|
||||
)
|
||||
.order_by("-start_date")
|
||||
.first()
|
||||
)
|
||||
age = cycle_age_for_date(cycle.start_date, snapshot_date) if cycle else 1
|
||||
fields = panel_fields_from_payload(payload_data, age)
|
||||
|
||||
IotPanel.objects.update_or_create(
|
||||
|
||||
@@ -26,7 +26,7 @@ class OperationsAPITests(TestCase):
|
||||
doc_in_count=10000,
|
||||
status=Cycle.STATUS_ACTIVE,
|
||||
)
|
||||
self.flock = Flock.objects.create(flock_name="Flock A", cycle=self.cycle)
|
||||
self.flock = Flock.objects.create(flock_name="Flock A", kandang=self.kandang)
|
||||
|
||||
def test_karungs_alias_lists_feed_sacks_with_date(self):
|
||||
FeedSacks.objects.create(
|
||||
@@ -65,7 +65,7 @@ class OperationsAPITests(TestCase):
|
||||
self.assertEqual(results[0]["date"], date.today().isoformat())
|
||||
|
||||
def test_panel_iot_filters_by_flock(self):
|
||||
flock_b = Flock.objects.create(flock_name="Flock B", cycle=self.cycle)
|
||||
flock_b = Flock.objects.create(flock_name="Flock B", kandang=self.kandang)
|
||||
row = IotPanel.objects.create(
|
||||
flock=self.flock,
|
||||
date=date.today(),
|
||||
@@ -379,7 +379,7 @@ class DashboardVisibilityTests(TestCase):
|
||||
self.assertEqual(response.data["date"], "2026-08-28")
|
||||
|
||||
def test_panel_iot_not_hidden_before_publish_time(self):
|
||||
flock = Flock.objects.create(flock_name="Flock A", cycle=self.cycle)
|
||||
flock = Flock.objects.create(flock_name="Flock A", kandang=self.kandang)
|
||||
IotPanel.objects.create(
|
||||
flock=flock,
|
||||
date=date(2026, 8, 28),
|
||||
|
||||
@@ -53,7 +53,7 @@ class CycleScopedViewSet(VisibilityFilteredMixin, viewsets.ModelViewSet):
|
||||
if params.get("cycle_id"):
|
||||
qs = qs.filter(cycle_id=params["cycle_id"])
|
||||
if params.get("flock_id"):
|
||||
qs = qs.filter(cycle__flocks__flock_id=params["flock_id"]).distinct()
|
||||
qs = qs.filter(cycle__kandang__flocks=params["flock_id"]).distinct()
|
||||
if params.get("date"):
|
||||
qs = qs.filter(date=params["date"])
|
||||
return self.filter_visible_through(qs)
|
||||
@@ -143,14 +143,23 @@ class FeedSacksViewSet(CycleScopedViewSet):
|
||||
|
||||
class IotPanelViewSet(viewsets.ModelViewSet):
|
||||
"""IoT panel is real-time — not subject to the 17:00 dashboard publish cutoff."""
|
||||
queryset = IotPanel.objects.select_related("flock", "flock__cycle").all()
|
||||
queryset = IotPanel.objects.select_related("flock", "flock__kandang").all()
|
||||
serializer_class = IotPanelSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
params = self.request.query_params
|
||||
if params.get("cycle_id"):
|
||||
qs = qs.filter(flock__cycle_id=params["cycle_id"])
|
||||
cycle_id = params.get("cycle_id")
|
||||
if cycle_id:
|
||||
try:
|
||||
cycle = Cycle.objects.get(pk=cycle_id)
|
||||
except Cycle.DoesNotExist:
|
||||
return qs.none()
|
||||
qs = qs.filter(
|
||||
flock__kandang_id=cycle.kandang_id,
|
||||
date__gte=cycle.start_date,
|
||||
date__lte=cycle.end_date,
|
||||
)
|
||||
if params.get("flock_id"):
|
||||
qs = qs.filter(flock_id=params["flock_id"])
|
||||
if params.get("date"):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Django settings for New Dashboard (ERD-aligned Wagtail + DRF)."""
|
||||
"""Django settings for New Dashboard (ERD-aligned DRF API)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -42,24 +42,6 @@ ALLOWED_HOSTS = env_list("ALLOWED_HOSTS", "localhost,127.0.0.1")
|
||||
CSRF_TRUSTED_ORIGINS = env_list("CSRF_TRUSTED_ORIGINS", "http://localhost:8000")
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"apps.accounts",
|
||||
"apps.farms",
|
||||
"apps.operations",
|
||||
"apps.jobs",
|
||||
"wagtail.contrib.forms",
|
||||
"wagtail.contrib.redirects",
|
||||
"wagtail.embeds",
|
||||
"wagtail.sites",
|
||||
"wagtail.users",
|
||||
"wagtail.snippets",
|
||||
"wagtail.documents",
|
||||
"wagtail.images",
|
||||
"wagtail.search",
|
||||
"wagtail.admin",
|
||||
"wagtail",
|
||||
"modelcluster",
|
||||
"taggit",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
@@ -68,6 +50,10 @@ INSTALLED_APPS = [
|
||||
"rest_framework",
|
||||
"corsheaders",
|
||||
"drf_spectacular",
|
||||
"apps.accounts",
|
||||
"apps.farms",
|
||||
"apps.operations",
|
||||
"apps.jobs",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -79,7 +65,6 @@ MIDDLEWARE = [
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"wagtail.contrib.redirects.middleware.RedirectMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "config.urls"
|
||||
@@ -142,10 +127,6 @@ MEDIA_ROOT = BASE_DIR / "media"
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
WAGTAIL_SITE_NAME = env("WAGTAIL_SITE_NAME", "New Dashboard")
|
||||
WAGTAILADMIN_BASE_URL = env("WAGTAILADMIN_BASE_URL", "http://localhost:8000")
|
||||
WAGTAIL_APPEND_SLASH = False
|
||||
|
||||
CORS_ALLOWED_ORIGINS = env_list("CORS_ALLOWED_ORIGINS", "http://localhost:3000")
|
||||
CORS_ALLOW_CREDENTIALS = env_bool("CORS_ALLOW_CREDENTIALS", True)
|
||||
|
||||
@@ -177,6 +158,8 @@ BOOTSTRAP_API_KEY = env("BOOTSTRAP_API_KEY", "") or ""
|
||||
|
||||
BOOTSTRAP_ADMIN_USER = env("BOOTSTRAP_ADMIN_USER", "admin") or "admin"
|
||||
BOOTSTRAP_ADMIN_PASSWORD = env("BOOTSTRAP_ADMIN_PASSWORD", "") or ""
|
||||
BOOTSTRAP_STAFF_USER = env("BOOTSTRAP_STAFF_USER", "") or ""
|
||||
BOOTSTRAP_STAFF_PASSWORD = env("BOOTSTRAP_STAFF_PASSWORD", "") or ""
|
||||
|
||||
KARUNG_WEB_ADMIN_BASE_URL = (
|
||||
env("KARUNG_WEB_ADMIN_BASE_URL", "http://127.0.0.1:5000") or ""
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||
from wagtail import urls as wagtail_urls
|
||||
from wagtail.admin import urls as wagtailadmin_urls
|
||||
from wagtail.documents import urls as wagtaildocs_urls
|
||||
|
||||
urlpatterns = [
|
||||
path("django-admin/", admin.site.urls),
|
||||
path("admin/", include(wagtailadmin_urls)),
|
||||
path("documents/", include(wagtaildocs_urls)),
|
||||
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path(
|
||||
"api/docs/",
|
||||
@@ -20,7 +13,6 @@ urlpatterns = [
|
||||
path("api/v1/", include("apps.accounts.urls")),
|
||||
path("api/v1/", include("apps.farms.urls")),
|
||||
path("api/v1/", include("apps.operations.urls")),
|
||||
path("", include(wagtail_urls)),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
Django>=5.1,<5.3
|
||||
wagtail>=6.3,<7.0
|
||||
djangorestframework>=3.15,<4.0
|
||||
drf-spectacular>=0.27,<1.0
|
||||
django-cors-headers>=4.4,<5.0
|
||||
@@ -7,5 +6,4 @@ python-dotenv>=1.0,<2.0
|
||||
gunicorn>=23.0,<24.0
|
||||
psycopg[binary]>=3.2,<4.0
|
||||
httpx>=0.27,<1.0
|
||||
Pillow>=10.0,<12.0
|
||||
openpyxl>=3.1,<4.0
|
||||
Reference in new issue
Block a user