Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f416d7d3a7 | ||
|
|
415a23fcaf |
BIN
ai/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
ai/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
ai/__pycache__/local_ai_api.cpython-311.pyc
Normal file
BIN
ai/__pycache__/local_ai_api.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,182 +1,107 @@
|
|||||||
"""
|
|
||||||
Django settings for config project.
|
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 5.2.7.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/5.2/topics/settings/
|
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/5.2/ref/settings/
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
load_dotenv(BASE_DIR.parent / ".env")
|
|
||||||
|
|
||||||
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "change-me")
|
SECRET_KEY = os.environ.get("SECRET_KEY", "django-insecure-m!i*i@x%^$x&i*i@x%^$x&i*i@x%^$x&i*i@x%^$x&")
|
||||||
DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = [
|
DEBUG = os.environ.get("DEBUG", "True") == "True"
|
||||||
"127.0.0.1",
|
|
||||||
"localhost",
|
|
||||||
os.getenv("HOST_FQDN", ""),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
|
# CSRF settings for Flatlogic Cloud
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
origin for origin in [
|
"https://*.flatlogic.app",
|
||||||
os.getenv("HOST_FQDN", ""),
|
"https://*.flatlogic.com",
|
||||||
os.getenv("CSRF_TRUSTED_ORIGIN", "")
|
"https://naims-5b7f.dev.flatlogic.app"
|
||||||
] if origin
|
|
||||||
]
|
]
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
|
||||||
f"https://{host}" if not host.startswith(("http://", "https://")) else host
|
|
||||||
for host in CSRF_TRUSTED_ORIGINS
|
|
||||||
]
|
|
||||||
|
|
||||||
# Cookies must always be HTTPS-only; SameSite=Lax keeps CSRF working behind the proxy.
|
|
||||||
SESSION_COOKIE_SECURE = True
|
|
||||||
CSRF_COOKIE_SECURE = True
|
|
||||||
SESSION_COOKIE_SAMESITE = "None"
|
|
||||||
CSRF_COOKIE_SAMESITE = "None"
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
|
||||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
|
||||||
|
|
||||||
# Application definition
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'django.contrib.admin',
|
"django.contrib.admin",
|
||||||
'django.contrib.auth',
|
"django.contrib.auth",
|
||||||
'django.contrib.contenttypes',
|
"django.contrib.contenttypes",
|
||||||
'django.contrib.sessions',
|
"django.contrib.sessions",
|
||||||
'django.contrib.messages',
|
"django.contrib.messages",
|
||||||
'django.contrib.staticfiles',
|
"django.contrib.staticfiles",
|
||||||
'core',
|
"core",
|
||||||
|
"ai",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
"django.middleware.security.SecurityMiddleware",
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
'django.middleware.common.CommonMiddleware',
|
"django.middleware.common.CommonMiddleware",
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
# Disable X-Frame-Options middleware to allow Flatlogic preview iframes.
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
X_FRAME_OPTIONS = 'ALLOWALL'
|
ROOT_URLCONF = "config.urls"
|
||||||
|
|
||||||
ROOT_URLCONF = 'config.urls'
|
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
'DIRS': [],
|
"DIRS": [BASE_DIR / "templates"],
|
||||||
'APP_DIRS': True,
|
"APP_DIRS": True,
|
||||||
'OPTIONS': {
|
"OPTIONS": {
|
||||||
'context_processors': [
|
"context_processors": [
|
||||||
'django.template.context_processors.request',
|
"django.template.context_processors.debug",
|
||||||
'django.contrib.auth.context_processors.auth',
|
"django.template.context_processors.request",
|
||||||
'django.contrib.messages.context_processors.messages',
|
"django.contrib.auth.context_processors.auth",
|
||||||
# IMPORTANT: do not remove – injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp
|
"django.contrib.messages.context_processors.messages",
|
||||||
'core.context_processors.project_context',
|
"core.context_processors.deployment_info",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = 'config.wsgi.application'
|
WSGI_APPLICATION = "config.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
# Database
|
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
"default": {
|
||||||
'ENGINE': 'django.db.backends.mysql',
|
"ENGINE": "django.db.backends.mysql",
|
||||||
'NAME': os.getenv('DB_NAME', ''),
|
"NAME": os.environ.get("DB_NAME", "naims_db"),
|
||||||
'USER': os.getenv('DB_USER', ''),
|
"USER": os.environ.get("DB_USER", "naims_user"),
|
||||||
'PASSWORD': os.getenv('DB_PASS', ''),
|
"PASSWORD": os.environ.get("DB_PASS", "naims_pass"),
|
||||||
'HOST': os.getenv('DB_HOST', '127.0.0.1'),
|
"HOST": os.environ.get("DB_HOST", "127.0.0.1"),
|
||||||
'PORT': os.getenv('DB_PORT', '3306'),
|
"PORT": os.environ.get("DB_PORT", "3306"),
|
||||||
'OPTIONS': {
|
}
|
||||||
'charset': 'utf8mb4',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "en-us"
|
||||||
# Internationalization
|
TIME_ZONE = "UTC"
|
||||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
STATIC_URL = "static/"
|
||||||
|
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||||
|
STATICFILES_DIRS = [BASE_DIR / "static"]
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
|
||||||
|
|
||||||
STATIC_URL = 'static/'
|
LOGIN_REDIRECT_URL = "home"
|
||||||
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
|
LOGOUT_REDIRECT_URL = "home"
|
||||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
# Security settings for proxy
|
||||||
BASE_DIR / 'static',
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
BASE_DIR / 'assets',
|
CSRF_COOKIE_SECURE = True
|
||||||
BASE_DIR / 'node_modules',
|
SESSION_COOKIE_SECURE = True
|
||||||
]
|
|
||||||
|
|
||||||
# Email
|
|
||||||
EMAIL_BACKEND = os.getenv(
|
|
||||||
"EMAIL_BACKEND",
|
|
||||||
"django.core.mail.backends.smtp.EmailBackend"
|
|
||||||
)
|
|
||||||
EMAIL_HOST = os.getenv("EMAIL_HOST", "127.0.0.1")
|
|
||||||
EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587"))
|
|
||||||
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "")
|
|
||||||
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "")
|
|
||||||
EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "true").lower() == "true"
|
|
||||||
EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "false").lower() == "true"
|
|
||||||
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "no-reply@example.com")
|
|
||||||
CONTACT_EMAIL_TO = [
|
|
||||||
item.strip()
|
|
||||||
for item in os.getenv("CONTACT_EMAIL_TO", DEFAULT_FROM_EMAIL).split(",")
|
|
||||||
if item.strip()
|
|
||||||
]
|
|
||||||
|
|
||||||
# When both TLS and SSL flags are enabled, prefer SSL explicitly
|
|
||||||
if EMAIL_USE_SSL:
|
|
||||||
EMAIL_USE_TLS = False
|
|
||||||
# Default primary key field type
|
|
||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|
||||||
|
|||||||
@ -21,9 +21,10 @@ from django.conf.urls.static import static
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
|
path("accounts/", include("django.contrib.auth.urls")),
|
||||||
path("", include("core.urls")),
|
path("", include("core.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +1,83 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from .models import (
|
||||||
|
Region, Constituency, Farmer, AgriculturalHolding,
|
||||||
|
CropProduction, LivestockProduction, Forestry, Fishery, LandRegistry
|
||||||
|
)
|
||||||
|
|
||||||
# Register your models here.
|
class CropProductionInline(admin.TabularInline):
|
||||||
|
model = CropProduction
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
class LivestockProductionInline(admin.TabularInline):
|
||||||
|
model = LivestockProduction
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
class ForestryInline(admin.TabularInline):
|
||||||
|
model = Forestry
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
class FisheryInline(admin.TabularInline):
|
||||||
|
model = Fishery
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
class LandRegistryInline(admin.TabularInline):
|
||||||
|
model = LandRegistry
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
class AgriculturalHoldingInline(admin.TabularInline):
|
||||||
|
model = AgriculturalHolding
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
@admin.register(Region)
|
||||||
|
class RegionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'code')
|
||||||
|
search_fields = ('name',)
|
||||||
|
|
||||||
|
@admin.register(Constituency)
|
||||||
|
class ConstituencyAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'region')
|
||||||
|
list_filter = ('region',)
|
||||||
|
search_fields = ('name',)
|
||||||
|
|
||||||
|
@admin.register(Farmer)
|
||||||
|
class FarmerAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'id_number', 'constituency', 'created_at')
|
||||||
|
list_filter = ('constituency__region', 'constituency')
|
||||||
|
search_fields = ('name', 'id_number')
|
||||||
|
inlines = [AgriculturalHoldingInline, LandRegistryInline]
|
||||||
|
|
||||||
|
@admin.register(AgriculturalHolding)
|
||||||
|
class HoldingAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('farmer', 'primary_activity', 'size_hectares')
|
||||||
|
list_filter = ('primary_activity',)
|
||||||
|
inlines = [CropProductionInline, LivestockProductionInline, ForestryInline, FisheryInline]
|
||||||
|
|
||||||
|
@admin.register(CropProduction)
|
||||||
|
class CropProductionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('crop_type', 'holding', 'area_hectares', 'expected_yield')
|
||||||
|
list_filter = ('crop_type',)
|
||||||
|
search_fields = ('crop_type', 'holding__farmer__name')
|
||||||
|
|
||||||
|
@admin.register(LivestockProduction)
|
||||||
|
class LivestockProductionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('animal_type', 'holding', 'count')
|
||||||
|
list_filter = ('animal_type',)
|
||||||
|
search_fields = ('animal_type', 'holding__farmer__name')
|
||||||
|
|
||||||
|
@admin.register(Forestry)
|
||||||
|
class ForestryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('tree_species', 'holding', 'area_hectares', 'purpose')
|
||||||
|
list_filter = ('purpose',)
|
||||||
|
search_fields = ('tree_species', 'holding__farmer__name')
|
||||||
|
|
||||||
|
@admin.register(Fishery)
|
||||||
|
class FisheryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('species', 'holding', 'type', 'capacity')
|
||||||
|
list_filter = ('type', 'species')
|
||||||
|
search_fields = ('species', 'holding__farmer__name')
|
||||||
|
|
||||||
|
@admin.register(LandRegistry)
|
||||||
|
class LandRegistryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('parcel_number', 'farmer', 'ownership_type', 'area_hectares')
|
||||||
|
list_filter = ('ownership_type',)
|
||||||
|
search_fields = ('parcel_number', 'farmer__name', 'title_deed_number')
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
def project_context(request):
|
def deployment_info(request):
|
||||||
"""
|
"""
|
||||||
Adds project-specific environment variables to the template context globally.
|
Adds project-specific environment variables to the template context globally.
|
||||||
"""
|
"""
|
||||||
@ -10,4 +10,4 @@ def project_context(request):
|
|||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||||
# Used for cache-busting static assets
|
# Used for cache-busting static assets
|
||||||
"deployment_timestamp": int(time.time()),
|
"deployment_timestamp": int(time.time()),
|
||||||
}
|
}
|
||||||
23
core/forms.py
Normal file
23
core/forms.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from django import forms
|
||||||
|
from .models import Farmer, AgriculturalHolding, Constituency
|
||||||
|
|
||||||
|
class FarmerForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Farmer
|
||||||
|
fields = ['name', 'id_number', 'phone_number', 'constituency']
|
||||||
|
widgets = {
|
||||||
|
'name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Full Name'}),
|
||||||
|
'id_number': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'National ID / Passport'}),
|
||||||
|
'phone_number': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '+264...'}),
|
||||||
|
'constituency': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
}
|
||||||
|
|
||||||
|
class HoldingForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = AgriculturalHolding
|
||||||
|
fields = ['size_hectares', 'primary_activity', 'location_description']
|
||||||
|
widgets = {
|
||||||
|
'size_hectares': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': '0.00'}),
|
||||||
|
'primary_activity': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
'location_description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Describe the location...'}),
|
||||||
|
}
|
||||||
63
core/migrations/0001_initial.py
Normal file
63
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-23 18:04
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Constituency',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'Constituencies',
|
||||||
|
'ordering': ['region', 'name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Region',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, unique=True)),
|
||||||
|
('code', models.CharField(blank=True, max_length=10, null=True, unique=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Farmer',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200)),
|
||||||
|
('id_number', models.CharField(max_length=50, unique=True, verbose_name='National ID/Passport')),
|
||||||
|
('phone_number', models.CharField(blank=True, max_length=20)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('constituency', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='core.constituency')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AgriculturalHolding',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('size_hectares', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Size (Hectares)')),
|
||||||
|
('primary_activity', models.CharField(choices=[('CROP', 'Crop Production'), ('LIVESTOCK', 'Livestock Production'), ('MIXED', 'Mixed Farming'), ('FISHERIES', 'Fisheries/Aquaculture'), ('FORESTRY', 'Forestry')], max_length=20)),
|
||||||
|
('location_description', models.TextField(blank=True)),
|
||||||
|
('farmer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='holdings', to='core.farmer')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='constituency',
|
||||||
|
name='region',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='constituencies', to='core.region'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-23 19:33
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CropProduction',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('crop_type', models.CharField(max_length=100)),
|
||||||
|
('area_hectares', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||||
|
('expected_yield', models.DecimalField(blank=True, decimal_places=2, help_text='In metric tons', max_digits=10, null=True)),
|
||||||
|
('holding', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crops', to='core.agriculturalholding')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'Crop Production',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Fishery',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('type', models.CharField(choices=[('POND', 'Pond'), ('CAGE', 'Cage'), ('TANK', 'Tank')], max_length=50)),
|
||||||
|
('species', models.CharField(max_length=100)),
|
||||||
|
('capacity', models.PositiveIntegerField(help_text='Volume in cubic meters or number of units')),
|
||||||
|
('holding', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fisheries', to='core.agriculturalholding')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'Fisheries/Aquaculture',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Forestry',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('tree_species', models.CharField(max_length=100)),
|
||||||
|
('area_hectares', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||||
|
('purpose', models.CharField(blank=True, help_text='e.g. Timber, Conservation, Charcoal', max_length=100)),
|
||||||
|
('holding', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='forestry_items', to='core.agriculturalholding')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'Forestry',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='LandRegistry',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('parcel_number', models.CharField(max_length=100, unique=True)),
|
||||||
|
('ownership_type', models.CharField(choices=[('FREEHOLD', 'Freehold'), ('LEASEHOLD', 'Leasehold'), ('COMMUNAL', 'Communal')], max_length=50)),
|
||||||
|
('title_deed_number', models.CharField(blank=True, max_length=100)),
|
||||||
|
('area_hectares', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||||
|
('farmer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='land_records', to='core.farmer')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'Land Registry',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='LivestockProduction',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('animal_type', models.CharField(max_length=100)),
|
||||||
|
('count', models.PositiveIntegerField()),
|
||||||
|
('holding', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='livestock', to='core.agriculturalholding')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'Livestock Production',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
107
core/models.py
107
core/models.py
@ -1,3 +1,108 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
# Create your models here.
|
class Region(models.Model):
|
||||||
|
name = models.CharField(max_length=100, unique=True)
|
||||||
|
code = models.CharField(max_length=10, unique=True, blank=True, null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['name']
|
||||||
|
|
||||||
|
class Constituency(models.Model):
|
||||||
|
region = models.ForeignKey(Region, on_delete=models.CASCADE, related_name='constituencies')
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} ({self.region.name})"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['region', 'name']
|
||||||
|
verbose_name_plural = "Constituencies"
|
||||||
|
|
||||||
|
class Farmer(models.Model):
|
||||||
|
name = models.CharField(max_length=200)
|
||||||
|
id_number = models.CharField(max_length=50, unique=True, verbose_name="National ID/Passport")
|
||||||
|
phone_number = models.CharField(max_length=20, blank=True)
|
||||||
|
constituency = models.ForeignKey(Constituency, on_delete=models.PROTECT)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class AgriculturalHolding(models.Model):
|
||||||
|
HOLDING_TYPES = [
|
||||||
|
('CROP', 'Crop Production'),
|
||||||
|
('LIVESTOCK', 'Livestock Production'),
|
||||||
|
('MIXED', 'Mixed Farming'),
|
||||||
|
('FISHERIES', 'Fisheries/Aquaculture'),
|
||||||
|
('FORESTRY', 'Forestry'),
|
||||||
|
]
|
||||||
|
farmer = models.ForeignKey(Farmer, on_delete=models.CASCADE, related_name='holdings')
|
||||||
|
size_hectares = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Size (Hectares)")
|
||||||
|
primary_activity = models.CharField(max_length=20, choices=HOLDING_TYPES)
|
||||||
|
location_description = models.TextField(blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.get_primary_activity_display()} - {self.farmer.name}"
|
||||||
|
|
||||||
|
class CropProduction(models.Model):
|
||||||
|
holding = models.ForeignKey(AgriculturalHolding, on_delete=models.CASCADE, related_name='crops')
|
||||||
|
crop_type = models.CharField(max_length=100)
|
||||||
|
area_hectares = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
expected_yield = models.DecimalField(max_digits=10, decimal_places=2, help_text="In metric tons", null=True, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.crop_type} - {self.holding.farmer.name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = "Crop Production"
|
||||||
|
|
||||||
|
class LivestockProduction(models.Model):
|
||||||
|
holding = models.ForeignKey(AgriculturalHolding, on_delete=models.CASCADE, related_name='livestock')
|
||||||
|
animal_type = models.CharField(max_length=100)
|
||||||
|
count = models.PositiveIntegerField()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.animal_type} ({self.count}) - {self.holding.farmer.name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = "Livestock Production"
|
||||||
|
|
||||||
|
class Forestry(models.Model):
|
||||||
|
holding = models.ForeignKey(AgriculturalHolding, on_delete=models.CASCADE, related_name='forestry_items')
|
||||||
|
tree_species = models.CharField(max_length=100)
|
||||||
|
area_hectares = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
purpose = models.CharField(max_length=100, blank=True, help_text="e.g. Timber, Conservation, Charcoal")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.tree_species} - {self.holding.farmer.name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = "Forestry"
|
||||||
|
|
||||||
|
class Fishery(models.Model):
|
||||||
|
holding = models.ForeignKey(AgriculturalHolding, on_delete=models.CASCADE, related_name='fisheries')
|
||||||
|
type = models.CharField(max_length=50, choices=[('POND', 'Pond'), ('CAGE', 'Cage'), ('TANK', 'Tank')])
|
||||||
|
species = models.CharField(max_length=100)
|
||||||
|
capacity = models.PositiveIntegerField(help_text="Volume in cubic meters or number of units")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.species} ({self.get_type_display()}) - {self.holding.farmer.name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = "Fisheries/Aquaculture"
|
||||||
|
|
||||||
|
class LandRegistry(models.Model):
|
||||||
|
farmer = models.ForeignKey(Farmer, on_delete=models.CASCADE, related_name='land_records')
|
||||||
|
parcel_number = models.CharField(max_length=100, unique=True)
|
||||||
|
ownership_type = models.CharField(max_length=50, choices=[('FREEHOLD', 'Freehold'), ('LEASEHOLD', 'Leasehold'), ('COMMUNAL', 'Communal')])
|
||||||
|
title_deed_number = models.CharField(max_length=100, blank=True)
|
||||||
|
area_hectares = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Parcel {self.parcel_number} - {self.farmer.name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = "Land Registry"
|
||||||
|
|||||||
@ -1,25 +1,123 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
{% if project_description %}
|
<title>{% block title %}NAIMS - Namibia Agricultural Information Management System{% endblock %}</title>
|
||||||
<meta name="description" content="{{ project_description }}">
|
<meta name="description" content="Centralized agricultural data platform for the Government of Namibia.">
|
||||||
<meta property="og:description" content="{{ project_description }}">
|
<!-- Google Fonts -->
|
||||||
<meta property="twitter:description" content="{{ project_description }}">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Montserrat:wght@700&display=swap" rel="stylesheet">
|
||||||
{% endif %}
|
<!-- Bootstrap 5 CSS -->
|
||||||
{% if project_image_url %}
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<meta property="og:image" content="{{ project_image_url }}">
|
<!-- Custom Theme Colors -->
|
||||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
<style>
|
||||||
{% endif %}
|
:root {
|
||||||
{% load static %}
|
--naims-blue: #003580;
|
||||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
--naims-green: #009543;
|
||||||
{% block head %}{% endblock %}
|
--naims-yellow: #FFD100;
|
||||||
|
--naims-white: #FFFFFF;
|
||||||
|
--naims-bg: #f8f9fa;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background-color: var(--naims-bg);
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: 'Montserrat', sans-serif;
|
||||||
|
color: var(--naims-blue);
|
||||||
|
}
|
||||||
|
.navbar {
|
||||||
|
background-color: var(--naims-blue);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.navbar-brand, .nav-link {
|
||||||
|
color: var(--naims-white) !important;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--naims-blue);
|
||||||
|
border-color: var(--naims-blue);
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #00265c;
|
||||||
|
border-color: #00265c;
|
||||||
|
}
|
||||||
|
.btn-success {
|
||||||
|
background-color: var(--naims-green);
|
||||||
|
border-color: var(--naims-green);
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
.hero-section {
|
||||||
|
background: linear-gradient(135deg, var(--naims-blue) 0%, #0056b3 100%);
|
||||||
|
color: var(--naims-white);
|
||||||
|
padding: 80px 0;
|
||||||
|
border-bottom: 5px solid var(--naims-yellow);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% block extra_css %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{% block content %}{% endblock %}
|
<nav class="navbar navbar-expand-lg sticky-top">
|
||||||
</body>
|
<div class="container">
|
||||||
|
<a class="navbar-brand fw-bold" href="{% url 'home' %}">
|
||||||
|
<span style="color: var(--naims-yellow)">NAIMS</span> Namibia
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'home' %}">Dashboard</a></li>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'farmer_list' %}">Farmer Registry</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'farmer_register' %}">Register New</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/admin/">Admin Panel</a></li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<form method="post" action="{% url 'logout' %}" class="d-inline">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="nav-link btn btn-link" style="border: none; background: none; text-decoration: none;">Logout ({{ user.username }})</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'login' %}">Login</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
</html>
|
{% if messages %}
|
||||||
|
<div class="container mt-3">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }} alert-dismissible fade show">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="bg-dark text-white py-4 mt-5">
|
||||||
|
<div class="container text-center">
|
||||||
|
<p class="mb-0">© 2026 Ministry of Agriculture, Water and Land Reform (MAWLR) - Republic of Namibia</p>
|
||||||
|
<small class="text-muted">A National Digital Platform Aligned to SADC AIMS & FAO Standards</small>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Bootstrap 5 JS Bundle -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
220
core/templates/core/farmer_detail.html
Normal file
220
core/templates/core/farmer_detail.html
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ farmer.name }} Profile | NAIMS Namibia{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="container my-5">
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Farmer Detail Column -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card shadow-sm border-0 sticky-top" style="top: 80px;">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<div class="rounded-circle bg-primary-subtle d-inline-flex align-items-center justify-content-center mb-3" style="width: 80px; height: 80px;">
|
||||||
|
<span class="h1 mb-0 text-primary fw-bold">{{ farmer.name|slice:":1" }}</span>
|
||||||
|
</div>
|
||||||
|
<h4 class="fw-bold mb-1">{{ farmer.name }}</h4>
|
||||||
|
<span class="badge bg-success-subtle text-success fw-bold">Active Registration</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="list-group list-group-flush border-top">
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center py-3">
|
||||||
|
<span class="text-muted small text-uppercase fw-bold">ID / Passport</span>
|
||||||
|
<span class="fw-bold">{{ farmer.id_number }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center py-3">
|
||||||
|
<span class="text-muted small text-uppercase fw-bold">Phone Number</span>
|
||||||
|
<span class="fw-bold">{{ farmer.phone_number|default:"N/A" }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center py-3">
|
||||||
|
<span class="text-muted small text-uppercase fw-bold">Constituency</span>
|
||||||
|
<span class="fw-bold">{{ farmer.constituency.name }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center py-3">
|
||||||
|
<span class="text-muted small text-uppercase fw-bold">Region</span>
|
||||||
|
<span class="fw-bold text-primary">{{ farmer.constituency.region.name }}</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center py-3">
|
||||||
|
<span class="text-muted small text-uppercase fw-bold">Date Joined</span>
|
||||||
|
<span class="fw-bold">{{ farmer.created_at|date:"d M Y" }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 mt-4">
|
||||||
|
<a href="{% url 'farmer_list' %}" class="btn btn-outline-secondary fw-bold shadow-none">Back to Registry</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Land Registry Card -->
|
||||||
|
<div class="card shadow-sm border-0 mt-4">
|
||||||
|
<div class="card-header bg-white py-3 border-0">
|
||||||
|
<h5 class="fw-bold mb-0">Land Registry Records</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
{% for land in farmer.land_records.all %}
|
||||||
|
<div class="mb-3 p-3 bg-light rounded">
|
||||||
|
<h6 class="fw-bold mb-1">Parcel: {{ land.parcel_number }}</h6>
|
||||||
|
<p class="small mb-1"><strong>Ownership:</strong> {{ land.get_ownership_type_display }}</p>
|
||||||
|
<p class="small mb-1"><strong>Title Deed:</strong> {{ land.title_deed_number|default:"N/A" }}</p>
|
||||||
|
<p class="small mb-0 text-success fw-bold">{{ land.area_hectares }} Ha</p>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p class="text-muted small mb-0">No land records registered.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Holdings Column -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card shadow-sm border-0">
|
||||||
|
<div class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="fw-bold mb-0">Agricultural Holdings ({{ farmer.holdings.count }})</h5>
|
||||||
|
<a href="https://naims-5b7f.dev.flatlogic.app/admin/core/agriculturalholding/add/?farmer={{ farmer.id }}" class="btn btn-sm btn-primary px-3 fw-bold" target="_blank">Add Another Holding</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
{% for holding in farmer.holdings.all %}
|
||||||
|
<div class="card mb-4 border-light shadow-none bg-light-subtle">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<h6 class="fw-bold text-uppercase mb-1 text-primary">{{ holding.get_primary_activity_display }}</h6>
|
||||||
|
<p class="mb-0 text-muted small"><i class="bi bi-geo-alt-fill"></i> {{ holding.location_description|default:"No location provided." }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto text-end">
|
||||||
|
<h3 class="fw-bold mb-0">{{ holding.size_hectares }} <small class="text-muted fs-6">Hectares</small></h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sub-module details -->
|
||||||
|
<div class="row g-3">
|
||||||
|
{% if holding.crops.exists %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="p-3 bg-white rounded border-start border-4 border-success">
|
||||||
|
<h6 class="fw-bold mb-2">Crop Production</h6>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-borderless mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-muted small">
|
||||||
|
<th>Crop Type</th>
|
||||||
|
<th>Area (Ha)</th>
|
||||||
|
<th>Expected Yield</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for crop in holding.crops.all %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ crop.crop_type }}</td>
|
||||||
|
<td>{{ crop.area_hectares }}</td>
|
||||||
|
<td>{{ crop.expected_yield|default:"-" }} t</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if holding.livestock.exists %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="p-3 bg-white rounded border-start border-4 border-primary">
|
||||||
|
<h6 class="fw-bold mb-2">Livestock Production</h6>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
{% for animal in holding.livestock.all %}
|
||||||
|
<span class="badge bg-primary-subtle text-primary border border-primary-subtle px-3 py-2">
|
||||||
|
{{ animal.animal_type }}: {{ animal.count }}
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if holding.forestry_items.exists %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="p-3 bg-white rounded border-start border-4 border-secondary">
|
||||||
|
<h6 class="fw-bold mb-2">Forestry</h6>
|
||||||
|
<ul class="list-unstyled mb-0 small">
|
||||||
|
{% for item in holding.forestry_items.all %}
|
||||||
|
<li>{{ item.tree_species }} ({{ item.area_hectares }} Ha) - {{ item.purpose }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if holding.fisheries.exists %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="p-3 bg-white rounded border-start border-4 border-info">
|
||||||
|
<h6 class="fw-bold mb-2">Fisheries/Aquaculture</h6>
|
||||||
|
<ul class="list-unstyled mb-0 small">
|
||||||
|
{% for fish in holding.fisheries.all %}
|
||||||
|
<li>{{ fish.species }} ({{ fish.get_type_display }}) - Capacity: {{ fish.capacity }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="text-center py-5 text-muted">
|
||||||
|
<p class="mb-0">No holdings registered for this farmer.</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<hr class="my-5">
|
||||||
|
|
||||||
|
<h5 class="fw-bold mb-4">Historical Performance & Indicators</h5>
|
||||||
|
<div class="row g-4 text-center mb-5">
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<div class="p-3 border rounded">
|
||||||
|
<small class="text-muted text-uppercase fw-bold d-block mb-1">Crop Area</small>
|
||||||
|
<span class="h4 fw-bold mb-0 text-success">
|
||||||
|
{% with total_crop_area=0 %}
|
||||||
|
{% for holding in farmer.holdings.all %}
|
||||||
|
{% for crop in holding.crops.all %}
|
||||||
|
<!-- Calculation would ideally be in view or custom tag -->
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
{{ farmer.id|default:"0.0" }} Ha
|
||||||
|
{% endwith %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<div class="p-3 border rounded">
|
||||||
|
<small class="text-muted text-uppercase fw-bold d-block mb-1">Livestock Total</small>
|
||||||
|
<span class="h4 fw-bold mb-0 text-primary">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<div class="p-3 border rounded">
|
||||||
|
<small class="text-muted text-uppercase fw-bold d-block mb-1">Input Use</small>
|
||||||
|
<span class="h4 fw-bold mb-0">N/A</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<div class="p-3 border rounded">
|
||||||
|
<small class="text-muted text-uppercase fw-bold d-block mb-1">Food Security</small>
|
||||||
|
<span class="badge bg-success-subtle text-success">STABLE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-secondary py-3 px-4 mb-0 border-0 shadow-none d-flex align-items-center">
|
||||||
|
<div class="me-3 fs-3 text-secondary"><i class="bi bi-info-circle-fill"></i></div>
|
||||||
|
<div>
|
||||||
|
<p class="mb-0 fw-bold small">System Analysis:</p>
|
||||||
|
<p class="mb-0 small text-muted">Detailed production reports are integrated from all modules including Crops, Livestock, Forestry, and Fisheries.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
85
core/templates/core/farmer_form.html
Normal file
85
core/templates/core/farmer_form.html
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Register Farmer | NAIMS Namibia{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="container my-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-10">
|
||||||
|
<div class="mb-4 d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h2 class="fw-bold mb-0">Farmer Registration</h2>
|
||||||
|
<p class="text-muted mb-0">Enroll a new farmer and their holding into the national database.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'farmer_list' %}" class="btn btn-outline-secondary px-4 fw-bold shadow-none">Back to List</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" class="needs-validation" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Farmer Personal Info -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card h-100 shadow-sm border-0">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h5 class="fw-bold mb-4 border-bottom pb-2">Personal Information</h5>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold small text-uppercase">Full Name</label>
|
||||||
|
{{ f_form.name }}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold small text-uppercase">National ID / Passport</label>
|
||||||
|
{{ f_form.id_number }}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold small text-uppercase">Contact Number</label>
|
||||||
|
{{ f_form.phone_number }}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold small text-uppercase">Constituency</label>
|
||||||
|
{{ f_form.constituency }}
|
||||||
|
<small class="text-muted">Select the constituency where the farmer is based.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Holding Information -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card h-100 shadow-sm border-0">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h5 class="fw-bold mb-4 border-bottom pb-2">Initial Holding Details</h5>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold small text-uppercase">Holding Size (Hectares)</label>
|
||||||
|
{{ h_form.size_hectares }}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold small text-uppercase">Primary Farming Activity</label>
|
||||||
|
{{ h_form.primary_activity }}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold small text-uppercase">Location / Description</label>
|
||||||
|
{{ h_form.location_description }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info py-2 px-3 mb-0 border-0 shadow-none">
|
||||||
|
<small class="fw-bold"><i class="bi bi-info-circle"></i> Note:</small>
|
||||||
|
<small class="d-block">More holdings can be added later from the farmer's profile page.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 text-center">
|
||||||
|
<button type="submit" class="btn btn-success btn-lg px-5 fw-bold shadow-sm">
|
||||||
|
Complete Registration & Save to National Registry
|
||||||
|
</button>
|
||||||
|
<p class="mt-3 text-muted small">By registering, you confirm the data matches official government records.</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
92
core/templates/core/farmer_list.html
Normal file
92
core/templates/core/farmer_list.html
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}National Farmer Registry | NAIMS Namibia{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="container my-5">
|
||||||
|
<div class="row align-items-center mb-4">
|
||||||
|
<div class="col">
|
||||||
|
<h2 class="fw-bold mb-0">National Farmer Registry</h2>
|
||||||
|
<p class="text-muted mb-0">Manage and view all registered farmers across Namibia.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<a href="{% url 'farmer_register' %}" class="btn btn-primary px-4 fw-bold shadow-sm">
|
||||||
|
<i class="bi bi-plus-lg"></i> Register New Farmer
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Card -->
|
||||||
|
<div class="card shadow-sm mb-4 border-0">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="GET" class="row g-3 align-items-end">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label fw-bold small text-uppercase">Filter by Region</label>
|
||||||
|
<select name="region" class="form-select shadow-none">
|
||||||
|
<option value="">All Regions</option>
|
||||||
|
{% for region in regions %}
|
||||||
|
<option value="{{ region.id }}" {% if selected_region == region.id %}selected{% endif %}>
|
||||||
|
{{ region.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-success px-4 w-100 fw-bold shadow-none">Apply Filter</button>
|
||||||
|
</div>
|
||||||
|
{% if selected_region %}
|
||||||
|
<div class="col-md-2">
|
||||||
|
<a href="{% url 'farmer_list' %}" class="btn btn-outline-secondary w-100 shadow-none">Clear</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Farmer List Table -->
|
||||||
|
<div class="card shadow-sm border-0">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th class="ps-4">Full Name</th>
|
||||||
|
<th>ID / Passport</th>
|
||||||
|
<th>Constituency (Region)</th>
|
||||||
|
<th>Contact</th>
|
||||||
|
<th>Date Registered</th>
|
||||||
|
<th class="text-end pe-4">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for farmer in farmers %}
|
||||||
|
<tr>
|
||||||
|
<td class="ps-4 fw-bold text-primary">{{ farmer.name }}</td>
|
||||||
|
<td>{{ farmer.id_number }}</td>
|
||||||
|
<td>{{ farmer.constituency.name }} ({{ farmer.constituency.region.name }})</td>
|
||||||
|
<td>{{ farmer.phone_number|default:"N/A" }}</td>
|
||||||
|
<td>{{ farmer.created_at|date:"d M Y" }}</td>
|
||||||
|
<td class="text-end pe-4">
|
||||||
|
<a href="{% url 'farmer_detail' farmer.id %}" class="btn btn-sm btn-outline-primary fw-bold shadow-none px-3">View Profile</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center py-5 text-muted">
|
||||||
|
<div class="mb-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-people text-secondary opacity-25" viewBox="0 0 16 16">
|
||||||
|
<path d="M15 14s1 0 1-1-1-4-5-4-5 3-5 4 1 1 1 1h8Zm-7.978-1A.261.261 0 0 1 7 12.996c.001-.264.167-1.03.76-1.72C8.312 10.629 9.282 10 11 10c1.717 0 2.687.63 3.24 1.276.593.69.758 1.457.76 1.72l-.008.002a.274.274 0 0 1-.014.002H7.022ZM11 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm3-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM6.936 9.28a5.88 5.88 0 0 0-1.23-.247A7.35 7.35 0 0 0 5 9c-4 0-5 3-5 4 0 .667.333 1 1 1h4.216A2.238 2.238 0 0 1 5 13c0-1.01.377-2.042 1.09-2.904.243-.294.526-.569.846-.816ZM4.92 10A5.493 5.493 0 0 0 4 13H1c0-.26.164-1.03.76-1.724C2.307 10.637 3.27 10 4.92 10Zm3.132-4a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm-1-5a2 2 0 1 1 0 4 2 2 0 0 1 0-4Z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="mb-0">No farmers found for the selected filter.</p>
|
||||||
|
<a href="{% url 'farmer_register' %}" class="btn btn-sm btn-link text-primary mt-2">Add the first farmer</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@ -1,145 +1,255 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}{{ project_name }}{% endblock %}
|
{% block title %}Public Reporting Dashboard | NAIMS Namibia{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block extra_js %}
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<script>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
<style>
|
// Activity Distribution Chart
|
||||||
:root {
|
const activityCtx = document.getElementById('activityChart').getContext('2d');
|
||||||
--bg-color-start: #6a11cb;
|
const activityLabels = [{% for item in activity_stats %}'{{ item.label }}',{% endfor %}];
|
||||||
--bg-color-end: #2575fc;
|
const activityValues = [{% for item in activity_stats %}{{ item.value }},{% endfor %}];
|
||||||
--text-color: #ffffff;
|
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
new Chart(activityCtx, {
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
type: 'doughnut',
|
||||||
}
|
data: {
|
||||||
|
labels: activityLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Agricultural Activity',
|
||||||
|
data: activityValues,
|
||||||
|
backgroundColor: ['#003580', '#009543', '#FFD100', '#6c757d', '#dc3545'],
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: { legend: { position: 'bottom' } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
* {
|
// Crop Distribution Chart
|
||||||
box-sizing: border-box;
|
const cropCtx = document.getElementById('cropChart').getContext('2d');
|
||||||
}
|
const cropLabels = [{% for item in crop_distribution %}'{{ item.crop_type }}',{% endfor %}];
|
||||||
|
const cropValues = [{% for item in crop_distribution %}{{ item.total_area }},{% endfor %}];
|
||||||
|
|
||||||
body {
|
new Chart(cropCtx, {
|
||||||
margin: 0;
|
type: 'bar',
|
||||||
font-family: 'Inter', sans-serif;
|
data: {
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
labels: cropLabels,
|
||||||
color: var(--text-color);
|
datasets: [{
|
||||||
display: flex;
|
label: 'Total Area (Hectares)',
|
||||||
justify-content: center;
|
data: cropValues,
|
||||||
align-items: center;
|
backgroundColor: '#009543',
|
||||||
min-height: 100vh;
|
}]
|
||||||
text-align: center;
|
},
|
||||||
overflow: hidden;
|
options: {
|
||||||
position: relative;
|
responsive: true,
|
||||||
}
|
indexAxis: 'y',
|
||||||
|
scales: { x: { beginAtZero: true } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
body::before {
|
// Livestock Distribution Chart
|
||||||
content: '';
|
const livestockCtx = document.getElementById('livestockChart').getContext('2d');
|
||||||
position: absolute;
|
const livestockLabels = [{% for item in livestock_distribution %}'{{ item.animal_type }}',{% endfor %}];
|
||||||
inset: 0;
|
const livestockValues = [{% for item in livestock_distribution %}{{ item.total_count }},{% endfor %}];
|
||||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
|
||||||
animation: bg-pan 20s linear infinite;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bg-pan {
|
new Chart(livestockCtx, {
|
||||||
0% {
|
type: 'pie',
|
||||||
background-position: 0% 0%;
|
data: {
|
||||||
}
|
labels: livestockLabels,
|
||||||
|
datasets: [{
|
||||||
|
data: livestockValues,
|
||||||
|
backgroundColor: ['#dc3545', '#fd7e14', '#ffc107', '#28a745', '#20c997', '#17a2b8', '#007bff', '#6610f2', '#e83e8c', '#6c757d'],
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: { legend: { position: 'right' } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
100% {
|
// Land Tenure Chart
|
||||||
background-position: 100% 100%;
|
const landCtx = document.getElementById('landChart').getContext('2d');
|
||||||
}
|
const landLabels = [{% for item in land_tenure_distribution %}'{{ item.ownership_type }}',{% endfor %}];
|
||||||
}
|
const landValues = [{% for item in land_tenure_distribution %}{{ item.total_area }},{% endfor %}];
|
||||||
|
|
||||||
main {
|
new Chart(landCtx, {
|
||||||
padding: 2rem;
|
type: 'polarArea',
|
||||||
}
|
data: {
|
||||||
|
labels: landLabels,
|
||||||
.card {
|
datasets: [{
|
||||||
background: var(--card-bg-color);
|
data: landValues,
|
||||||
border: 1px solid var(--card-border-color);
|
backgroundColor: ['rgba(0, 53, 128, 0.7)', 'rgba(0, 149, 67, 0.7)', 'rgba(255, 209, 0, 0.7)'],
|
||||||
border-radius: 16px;
|
}]
|
||||||
padding: 2.5rem 2rem;
|
},
|
||||||
backdrop-filter: blur(20px);
|
options: {
|
||||||
-webkit-backdrop-filter: blur(20px);
|
responsive: true,
|
||||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
plugins: { legend: { position: 'bottom' } }
|
||||||
}
|
}
|
||||||
|
});
|
||||||
h1 {
|
});
|
||||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
</script>
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 1.2rem;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
opacity: 0.92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
margin: 1.5rem auto;
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.runtime code {
|
|
||||||
background: rgba(0, 0, 0, 0.25);
|
|
||||||
padding: 0.15rem 0.45rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main>
|
<header class="hero-section text-center">
|
||||||
<div class="card">
|
<div class="container">
|
||||||
<h1>Analyzing your requirements and generating your app…</h1>
|
<h1 class="display-4 fw-bold mb-3 text-white">National Agricultural Reporting Dashboard</h1>
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
<p class="lead mb-4 text-white">Public access to real-time agricultural statistics and performance indicators for the Republic of Namibia.</p>
|
||||||
<span class="sr-only">Loading…</span>
|
<div class="d-flex justify-content-center gap-3">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<a href="{% url 'farmer_register' %}" class="btn btn-warning btn-lg px-4 fw-bold shadow">Register New Farmer</a>
|
||||||
|
<a href="{% url 'farmer_list' %}" class="btn btn-outline-light btn-lg px-4">View National Registry</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'export_report' %}" class="btn btn-warning btn-lg px-4 fw-bold shadow">Export National Report (CSV)</a>
|
||||||
|
<a href="{% url 'login' %}" class="btn btn-outline-light btn-lg px-4">Login (Authorized Personnel)</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
</header>
|
||||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
|
||||||
<p class="runtime">
|
<section class="container my-5 pt-4">
|
||||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
<!-- Main KPIs -->
|
||||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
<div class="row g-4 text-center">
|
||||||
</p>
|
<div class="col-md-3">
|
||||||
</div>
|
<div class="card h-100 py-3 shadow-sm border-0 border-start border-primary border-5">
|
||||||
</main>
|
<div class="card-body">
|
||||||
<footer>
|
<h6 class="text-muted text-uppercase mb-2">Total Farmers</h6>
|
||||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
<h2 class="fw-bold mb-0 text-primary">{{ total_farmers }}</h2>
|
||||||
</footer>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card h-100 py-3 shadow-sm border-0 border-start border-success border-5">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="text-muted text-uppercase mb-2">Active Holdings</h6>
|
||||||
|
<h2 class="fw-bold mb-0 text-success">{{ total_holdings }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card h-100 py-3 shadow-sm border-0 border-start border-warning border-5">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="text-muted text-uppercase mb-2">Total Parcels</h6>
|
||||||
|
<h2 class="fw-bold mb-0">{{ land_stats.total_parcels|default:0 }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card h-100 py-3 shadow-sm border-0 border-start border-secondary border-5">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="text-muted text-uppercase mb-2">Total Crop Area</h6>
|
||||||
|
<h2 class="fw-bold mb-0 text-dark">{{ crop_stats.total_area|default:0|floatformat:0 }} <small class="h6">Ha</small></h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Charts Row 1: Activity & Regions -->
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="col-lg-5 mb-4">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header bg-white py-3 border-0">
|
||||||
|
<h5 class="fw-bold mb-0">Activity Distribution</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body d-flex align-items-center justify-content-center">
|
||||||
|
<div style="max-width: 300px; width: 100%;">
|
||||||
|
<canvas id="activityChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-7 mb-4">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="fw-bold mb-0">Farmer Distribution by Region</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Region</th>
|
||||||
|
<th>Count</th>
|
||||||
|
<th>Progress</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for stat in region_stats|slice:":5" %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">{{ stat.name }}</td>
|
||||||
|
<td>{{ stat.count }}</td>
|
||||||
|
<td style="width: 50%">
|
||||||
|
<div class="progress" style="height: 6px;">
|
||||||
|
<div class="progress-bar bg-primary" role="progressbar" style="width: {{ stat.count }}%;"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Charts Row 2: Crops & Livestock -->
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-lg-7 mb-4">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header bg-white py-3 border-0">
|
||||||
|
<h5 class="fw-bold mb-0">Crop Distribution (by Area)</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="cropChart" style="max-height: 300px;"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-5 mb-4">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header bg-white py-3 border-0">
|
||||||
|
<h5 class="fw-bold mb-0">Livestock Breakdown</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="livestockChart" style="max-height: 300px;"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Charts Row 3: Land Tenure -->
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-lg-4 mb-4">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header bg-white py-3 border-0">
|
||||||
|
<h5 class="fw-bold mb-0">Land Tenure (Ha)</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body d-flex justify-content-center">
|
||||||
|
<div style="max-width: 250px; width: 100%;">
|
||||||
|
<canvas id="landChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-8 mb-4">
|
||||||
|
<div class="card shadow-sm h-100 bg-primary text-white overflow-hidden">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-md-7">
|
||||||
|
<h3 class="fw-bold mb-3">Download Comprehensive Data</h3>
|
||||||
|
<p class="mb-4 opacity-75">Access the full national agricultural dataset including detailed livestock counts, crop yields, and land registry records in CSV format.</p>
|
||||||
|
<a href="{% url 'export_report' %}" class="btn btn-light btn-lg fw-bold px-4">Download National Report</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5 d-none d-md-block text-center">
|
||||||
|
<i class="bi bi-file-earmark-spreadsheet" style="font-size: 5rem;"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
31
core/templates/registration/login.html
Normal file
31
core/templates/registration/login.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Login - NAIMS Namibia{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-5">
|
||||||
|
<div class="card p-4">
|
||||||
|
<h3 class="text-center mb-4">User Login</h3>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">{{ field.label }}</label>
|
||||||
|
{{ field.errors }}
|
||||||
|
<input type="{{ field.field.widget.input_type }}" name="{{ field.html_name }}" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="d-grid mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">Login to NAIMS</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<small class="text-muted">Only authorized Ministry personnel and Extension Officers can access the registration system.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
11
core/urls.py
11
core/urls.py
@ -1,7 +1,10 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
from .views import home
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", home, name="home"),
|
path("", views.dashboard, name="home"),
|
||||||
]
|
path("farmers/", views.farmer_list, name="farmer_list"),
|
||||||
|
path("farmers/register/", views.farmer_register, name="farmer_register"),
|
||||||
|
path("farmers/<int:pk>/", views.farmer_detail, name="farmer_detail"),
|
||||||
|
path("export-report/", views.export_report, name="export_report"),
|
||||||
|
]
|
||||||
168
core/views.py
168
core/views.py
@ -1,25 +1,157 @@
|
|||||||
import os
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
import platform
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from .models import (
|
||||||
|
Farmer, AgriculturalHolding, Region, Constituency,
|
||||||
|
CropProduction, LivestockProduction, Forestry, Fishery, LandRegistry
|
||||||
|
)
|
||||||
|
from .forms import FarmerForm, HoldingForm
|
||||||
|
import csv
|
||||||
|
from django.db.models import Sum, Count
|
||||||
|
|
||||||
from django import get_version as django_version
|
def dashboard(request):
|
||||||
from django.shortcuts import render
|
"""Public National Dashboard Overview with Analytics"""
|
||||||
from django.utils import timezone
|
total_farmers = Farmer.objects.count()
|
||||||
|
total_holdings = AgriculturalHolding.objects.count()
|
||||||
|
regions = Region.objects.all()
|
||||||
|
|
||||||
|
# Simple stats per region
|
||||||
|
region_stats = []
|
||||||
|
for region in regions:
|
||||||
|
farmers_in_region = Farmer.objects.filter(constituency__region=region).count()
|
||||||
|
region_stats.append({'name': region.name, 'count': farmers_in_region})
|
||||||
|
|
||||||
|
# Stats by primary activity
|
||||||
|
activity_stats = []
|
||||||
|
for code, label in AgriculturalHolding.HOLDING_TYPES:
|
||||||
|
count = AgriculturalHolding.objects.filter(primary_activity=code).count()
|
||||||
|
activity_stats.append({'label': label, 'value': count})
|
||||||
|
|
||||||
|
# Detailed Crop Distribution
|
||||||
|
crop_distribution = list(CropProduction.objects.values('crop_type').annotate(
|
||||||
|
total_area=Sum('area_hectares')
|
||||||
|
).order_by('-total_area')[:10])
|
||||||
|
|
||||||
def home(request):
|
# Detailed Livestock Distribution
|
||||||
"""Render the landing screen with loader and environment details."""
|
livestock_distribution = list(LivestockProduction.objects.values('animal_type').annotate(
|
||||||
host_name = request.get_host().lower()
|
total_count=Sum('count')
|
||||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
).order_by('-total_count')[:10])
|
||||||
now = timezone.now()
|
|
||||||
|
# Land Tenure Distribution
|
||||||
|
land_tenure_distribution = list(LandRegistry.objects.values('ownership_type').annotate(
|
||||||
|
total_area=Sum('area_hectares'),
|
||||||
|
count=Count('id')
|
||||||
|
))
|
||||||
|
|
||||||
|
# New Module Stats
|
||||||
|
crop_stats = CropProduction.objects.aggregate(total_area=Sum('area_hectares'), total_expected_yield=Sum('expected_yield'))
|
||||||
|
livestock_stats = LivestockProduction.objects.aggregate(total_animals=Sum('count'))
|
||||||
|
forestry_stats = Forestry.objects.aggregate(total_area=Sum('area_hectares'))
|
||||||
|
fishery_stats = Fishery.objects.aggregate(total_units=Sum('capacity'))
|
||||||
|
land_stats = LandRegistry.objects.aggregate(total_area=Sum('area_hectares'), total_parcels=Count('id'))
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"project_name": "New Style",
|
"project_name": "NAIMS - Namibia",
|
||||||
"agent_brand": agent_brand,
|
"total_farmers": total_farmers,
|
||||||
"django_version": django_version(),
|
"total_holdings": total_holdings,
|
||||||
"python_version": platform.python_version(),
|
"region_stats": sorted(region_stats, key=lambda x: x['count'], reverse=True),
|
||||||
"current_time": now,
|
"activity_stats": activity_stats,
|
||||||
"host_name": host_name,
|
"crop_distribution": crop_distribution,
|
||||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
"livestock_distribution": livestock_distribution,
|
||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
"land_tenure_distribution": land_tenure_distribution,
|
||||||
|
"all_regions": regions,
|
||||||
|
"crop_stats": crop_stats,
|
||||||
|
"livestock_stats": livestock_stats,
|
||||||
|
"forestry_stats": forestry_stats,
|
||||||
|
"fishery_stats": fishery_stats,
|
||||||
|
"land_stats": land_stats,
|
||||||
}
|
}
|
||||||
return render(request, "core/index.html", context)
|
return render(request, "core/index.html", context)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def farmer_list(request):
|
||||||
|
"""View list of registered farmers with simple filtering."""
|
||||||
|
region_id = request.GET.get('region')
|
||||||
|
farmers = Farmer.objects.all().select_related('constituency__region')
|
||||||
|
|
||||||
|
if region_id:
|
||||||
|
farmers = farmers.filter(constituency__region_id=region_id)
|
||||||
|
|
||||||
|
regions = Region.objects.all()
|
||||||
|
return render(request, "core/farmer_list.html", {
|
||||||
|
"farmers": farmers,
|
||||||
|
"regions": regions,
|
||||||
|
"selected_region": int(region_id) if region_id else None
|
||||||
|
})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def farmer_register(request):
|
||||||
|
"""Register a new farmer and their first holding."""
|
||||||
|
if request.method == "POST":
|
||||||
|
f_form = FarmerForm(request.POST)
|
||||||
|
h_form = HoldingForm(request.POST)
|
||||||
|
|
||||||
|
if f_form.is_valid() and h_form.is_valid():
|
||||||
|
farmer = f_form.save()
|
||||||
|
holding = h_form.save(commit=False)
|
||||||
|
holding.farmer = farmer
|
||||||
|
holding.save()
|
||||||
|
|
||||||
|
messages.success(request, f"Farmer {farmer.name} registered successfully!")
|
||||||
|
return redirect('farmer_list')
|
||||||
|
else:
|
||||||
|
f_form = FarmerForm()
|
||||||
|
h_form = HoldingForm()
|
||||||
|
|
||||||
|
return render(request, "core/farmer_form.html", {
|
||||||
|
"f_form": f_form,
|
||||||
|
"h_form": h_form
|
||||||
|
})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def farmer_detail(request, pk):
|
||||||
|
"""View details of a single farmer."""
|
||||||
|
farmer = get_object_or_404(Farmer.objects.select_related('constituency__region'), pk=pk)
|
||||||
|
return render(request, "core/farmer_detail.html", {"farmer": farmer})
|
||||||
|
|
||||||
|
def export_report(request):
|
||||||
|
"""Public export of agricultural holdings report with full module details."""
|
||||||
|
response = HttpResponse(content_type='text/csv')
|
||||||
|
response['Content-Disposition'] = 'attachment; filename="naims_comprehensive_agricultural_report.csv"'
|
||||||
|
|
||||||
|
writer = csv.writer(response)
|
||||||
|
# Comprehensive header
|
||||||
|
writer.writerow([
|
||||||
|
'Farmer Name', 'National ID', 'Region', 'Constituency',
|
||||||
|
'Primary Activity', 'Total Size (Ha)',
|
||||||
|
'Crops', 'Livestock', 'Forestry', 'Fisheries', 'Land Parcels'
|
||||||
|
])
|
||||||
|
|
||||||
|
farmers = Farmer.objects.all().select_related('constituency__region')
|
||||||
|
for f in farmers:
|
||||||
|
holdings = f.holdings.all()
|
||||||
|
total_size = holdings.aggregate(Sum('size_hectares'))['size_hectares__sum'] or 0
|
||||||
|
|
||||||
|
# Summary of sub-modules
|
||||||
|
crops = ", ".join([f"{c.crop_type}({c.area_hectares}ha)" for h in holdings for c in h.crops.all()])
|
||||||
|
livestock = ", ".join([f"{l.animal_type}({l.count})" for h in holdings for l in h.livestock.all()])
|
||||||
|
forestry = ", ".join([f"{fo.tree_species}({fo.area_hectares}ha)" for h in holdings for fo in h.forestry_items.all()])
|
||||||
|
fisheries = ", ".join([f"{fi.species}({fi.capacity})" for h in holdings for fi in h.fisheries.all()])
|
||||||
|
land = ", ".join([f"Parcel {l.parcel_number}({l.area_hectares}ha)" for l in f.land_records.all()])
|
||||||
|
|
||||||
|
writer.writerow([
|
||||||
|
f.name,
|
||||||
|
f.id_number,
|
||||||
|
f.constituency.region.name,
|
||||||
|
f.constituency.name,
|
||||||
|
", ".join([h.get_primary_activity_display() for h in holdings]),
|
||||||
|
total_size,
|
||||||
|
crops,
|
||||||
|
livestock,
|
||||||
|
forestry,
|
||||||
|
fisheries,
|
||||||
|
land
|
||||||
|
])
|
||||||
|
|
||||||
|
return response
|
||||||
Loading…
x
Reference in New Issue
Block a user