Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,107 +1,182 @@
|
|||||||
import os
|
"""
|
||||||
|
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
|
from pathlib import Path
|
||||||
|
import os
|
||||||
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.environ.get("SECRET_KEY", "django-insecure-m!i*i@x%^$x&i*i@x%^$x&i*i@x%^$x&i*i@x%^$x&")
|
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "change-me")
|
||||||
|
DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
|
||||||
|
|
||||||
DEBUG = os.environ.get("DEBUG", "True") == "True"
|
ALLOWED_HOSTS = [
|
||||||
|
"127.0.0.1",
|
||||||
ALLOWED_HOSTS = ["*"]
|
"localhost",
|
||||||
|
os.getenv("HOST_FQDN", ""),
|
||||||
# CSRF settings for Flatlogic Cloud
|
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
|
||||||
"https://*.flatlogic.app",
|
|
||||||
"https://*.flatlogic.com",
|
|
||||||
"https://naims-5b7f.dev.flatlogic.app"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
|
origin for origin in [
|
||||||
|
os.getenv("HOST_FQDN", ""),
|
||||||
|
os.getenv("CSRF_TRUSTED_ORIGIN", "")
|
||||||
|
] 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',
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
# Disable X-Frame-Options middleware to allow Flatlogic preview iframes.
|
||||||
|
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = "config.urls"
|
X_FRAME_OPTIONS = 'ALLOWALL'
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'config.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
"DIRS": [BASE_DIR / "templates"],
|
'DIRS': [],
|
||||||
"APP_DIRS": True,
|
'APP_DIRS': True,
|
||||||
"OPTIONS": {
|
'OPTIONS': {
|
||||||
"context_processors": [
|
'context_processors': [
|
||||||
"django.template.context_processors.debug",
|
'django.template.context_processors.request',
|
||||||
"django.template.context_processors.request",
|
'django.contrib.auth.context_processors.auth',
|
||||||
"django.contrib.auth.context_processors.auth",
|
'django.contrib.messages.context_processors.messages',
|
||||||
"django.contrib.messages.context_processors.messages",
|
# IMPORTANT: do not remove – injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp
|
||||||
"core.context_processors.deployment_info",
|
'core.context_processors.project_context',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
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.environ.get("DB_NAME", "naims_db"),
|
'NAME': os.getenv('DB_NAME', ''),
|
||||||
"USER": os.environ.get("DB_USER", "naims_user"),
|
'USER': os.getenv('DB_USER', ''),
|
||||||
"PASSWORD": os.environ.get("DB_PASS", "naims_pass"),
|
'PASSWORD': os.getenv('DB_PASS', ''),
|
||||||
"HOST": os.environ.get("DB_HOST", "127.0.0.1"),
|
'HOST': os.getenv('DB_HOST', '127.0.0.1'),
|
||||||
"PORT": os.environ.get("DB_PORT", "3306"),
|
'PORT': os.getenv('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"
|
|
||||||
TIME_ZONE = "UTC"
|
# Internationalization
|
||||||
|
# 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"]
|
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||||
|
|
||||||
LOGIN_REDIRECT_URL = "home"
|
STATIC_URL = 'static/'
|
||||||
LOGOUT_REDIRECT_URL = "home"
|
# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS.
|
||||||
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||||
|
|
||||||
# Security settings for proxy
|
STATICFILES_DIRS = [
|
||||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
BASE_DIR / 'static',
|
||||||
CSRF_COOKIE_SECURE = True
|
BASE_DIR / 'assets',
|
||||||
SESSION_COOKIE_SECURE = True
|
BASE_DIR / 'node_modules',
|
||||||
|
]
|
||||||
|
|
||||||
|
# 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,7 +21,6 @@ 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")),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,83 +1,3 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import (
|
|
||||||
Region, Constituency, Farmer, AgriculturalHolding,
|
|
||||||
CropProduction, LivestockProduction, Forestry, Fishery, LandRegistry
|
|
||||||
)
|
|
||||||
|
|
||||||
class CropProductionInline(admin.TabularInline):
|
# Register your models here.
|
||||||
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 deployment_info(request):
|
def project_context(request):
|
||||||
"""
|
"""
|
||||||
Adds project-specific environment variables to the template context globally.
|
Adds project-specific environment variables to the template context globally.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
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...'}),
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
# 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',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
107
core/models.py
107
core/models.py
@ -1,108 +1,3 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
class Region(models.Model):
|
# Create your models here.
|
||||||
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,123 +1,25 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
||||||
<title>{% block title %}NAIMS - Namibia Agricultural Information Management System{% endblock %}</title>
|
{% if project_description %}
|
||||||
<meta name="description" content="Centralized agricultural data platform for the Government of Namibia.">
|
<meta name="description" content="{{ project_description }}">
|
||||||
<!-- Google Fonts -->
|
<meta property="og: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">
|
<meta property="twitter:description" content="{{ project_description }}">
|
||||||
<!-- Bootstrap 5 CSS -->
|
{% endif %}
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
{% if project_image_url %}
|
||||||
<!-- Custom Theme Colors -->
|
<meta property="og:image" content="{{ project_image_url }}">
|
||||||
<style>
|
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||||
:root {
|
{% endif %}
|
||||||
--naims-blue: #003580;
|
{% load static %}
|
||||||
--naims-green: #009543;
|
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||||
--naims-yellow: #FFD100;
|
{% block head %}{% endblock %}
|
||||||
--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>
|
||||||
<nav class="navbar navbar-expand-lg sticky-top">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{% 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 %}
|
{% 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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@ -1,220 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
{% 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,255 +1,145 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Public Reporting Dashboard | NAIMS Namibia{% endblock %}
|
{% block title %}{{ project_name }}{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block head %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<script>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||||
// Activity Distribution Chart
|
<style>
|
||||||
const activityCtx = document.getElementById('activityChart').getContext('2d');
|
:root {
|
||||||
const activityLabels = [{% for item in activity_stats %}'{{ item.label }}',{% endfor %}];
|
--bg-color-start: #6a11cb;
|
||||||
const activityValues = [{% for item in activity_stats %}{{ item.value }},{% endfor %}];
|
--bg-color-end: #2575fc;
|
||||||
|
--text-color: #ffffff;
|
||||||
new Chart(activityCtx, {
|
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||||
type: 'doughnut',
|
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||||
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
|
* {
|
||||||
const cropCtx = document.getElementById('cropChart').getContext('2d');
|
box-sizing: border-box;
|
||||||
const cropLabels = [{% for item in crop_distribution %}'{{ item.crop_type }}',{% endfor %}];
|
|
||||||
const cropValues = [{% for item in crop_distribution %}{{ item.total_area }},{% endfor %}];
|
|
||||||
|
|
||||||
new Chart(cropCtx, {
|
|
||||||
type: 'bar',
|
|
||||||
data: {
|
|
||||||
labels: cropLabels,
|
|
||||||
datasets: [{
|
|
||||||
label: 'Total Area (Hectares)',
|
|
||||||
data: cropValues,
|
|
||||||
backgroundColor: '#009543',
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
indexAxis: 'y',
|
|
||||||
scales: { x: { beginAtZero: true } }
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Livestock Distribution Chart
|
body {
|
||||||
const livestockCtx = document.getElementById('livestockChart').getContext('2d');
|
margin: 0;
|
||||||
const livestockLabels = [{% for item in livestock_distribution %}'{{ item.animal_type }}',{% endfor %}];
|
font-family: 'Inter', sans-serif;
|
||||||
const livestockValues = [{% for item in livestock_distribution %}{{ item.total_count }},{% endfor %}];
|
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||||
|
color: var(--text-color);
|
||||||
new Chart(livestockCtx, {
|
display: flex;
|
||||||
type: 'pie',
|
justify-content: center;
|
||||||
data: {
|
align-items: center;
|
||||||
labels: livestockLabels,
|
min-height: 100vh;
|
||||||
datasets: [{
|
text-align: center;
|
||||||
data: livestockValues,
|
overflow: hidden;
|
||||||
backgroundColor: ['#dc3545', '#fd7e14', '#ffc107', '#28a745', '#20c997', '#17a2b8', '#007bff', '#6610f2', '#e83e8c', '#6c757d'],
|
position: relative;
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
plugins: { legend: { position: 'right' } }
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Land Tenure Chart
|
body::before {
|
||||||
const landCtx = document.getElementById('landChart').getContext('2d');
|
content: '';
|
||||||
const landLabels = [{% for item in land_tenure_distribution %}'{{ item.ownership_type }}',{% endfor %}];
|
position: absolute;
|
||||||
const landValues = [{% for item in land_tenure_distribution %}{{ item.total_area }},{% endfor %}];
|
inset: 0;
|
||||||
|
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>");
|
||||||
new Chart(landCtx, {
|
animation: bg-pan 20s linear infinite;
|
||||||
type: 'polarArea',
|
z-index: -1;
|
||||||
data: {
|
|
||||||
labels: landLabels,
|
|
||||||
datasets: [{
|
|
||||||
data: landValues,
|
|
||||||
backgroundColor: ['rgba(0, 53, 128, 0.7)', 'rgba(0, 149, 67, 0.7)', 'rgba(255, 209, 0, 0.7)'],
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
plugins: { legend: { position: 'bottom' } }
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
@keyframes bg-pan {
|
||||||
</script>
|
0% {
|
||||||
|
background-position: 0% 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: 100% 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg-color);
|
||||||
|
border: 1px solid var(--card-border-color);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 2.5rem 2rem;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
||||||
|
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 %}
|
||||||
<header class="hero-section text-center">
|
<main>
|
||||||
<div class="container">
|
<div class="card">
|
||||||
<h1 class="display-4 fw-bold mb-3 text-white">National Agricultural Reporting Dashboard</h1>
|
<h1>Analyzing your requirements and generating your app…</h1>
|
||||||
<p class="lead mb-4 text-white">Public access to real-time agricultural statistics and performance indicators for the Republic of Namibia.</p>
|
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||||
<div class="d-flex justify-content-center gap-3">
|
<span class="sr-only">Loading…</span>
|
||||||
{% 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>
|
||||||
|
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
||||||
|
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
||||||
|
<p class="runtime">
|
||||||
|
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
||||||
|
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</main>
|
||||||
|
<footer>
|
||||||
<section class="container my-5 pt-4">
|
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||||
<!-- Main KPIs -->
|
</footer>
|
||||||
<div class="row g-4 text-center">
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="card h-100 py-3 shadow-sm border-0 border-start border-primary border-5">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="text-muted text-uppercase mb-2">Total Farmers</h6>
|
|
||||||
<h2 class="fw-bold mb-0 text-primary">{{ total_farmers }}</h2>
|
|
||||||
</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 %}
|
||||||
@ -1,31 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@ -1,10 +1,7 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from . import views
|
|
||||||
|
from .views import home
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.dashboard, name="home"),
|
path("", home, 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,157 +1,25 @@
|
|||||||
from django.shortcuts import render, redirect, get_object_or_404
|
import os
|
||||||
from django.contrib.auth.decorators import login_required
|
import platform
|
||||||
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
|
|
||||||
|
|
||||||
def dashboard(request):
|
from django import get_version as django_version
|
||||||
"""Public National Dashboard Overview with Analytics"""
|
from django.shortcuts import render
|
||||||
total_farmers = Farmer.objects.count()
|
from django.utils import timezone
|
||||||
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
|
def home(request):
|
||||||
activity_stats = []
|
"""Render the landing screen with loader and environment details."""
|
||||||
for code, label in AgriculturalHolding.HOLDING_TYPES:
|
host_name = request.get_host().lower()
|
||||||
count = AgriculturalHolding.objects.filter(primary_activity=code).count()
|
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||||
activity_stats.append({'label': label, 'value': count})
|
now = timezone.now()
|
||||||
|
|
||||||
# Detailed Crop Distribution
|
|
||||||
crop_distribution = list(CropProduction.objects.values('crop_type').annotate(
|
|
||||||
total_area=Sum('area_hectares')
|
|
||||||
).order_by('-total_area')[:10])
|
|
||||||
|
|
||||||
# Detailed Livestock Distribution
|
|
||||||
livestock_distribution = list(LivestockProduction.objects.values('animal_type').annotate(
|
|
||||||
total_count=Sum('count')
|
|
||||||
).order_by('-total_count')[:10])
|
|
||||||
|
|
||||||
# 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": "NAIMS - Namibia",
|
"project_name": "New Style",
|
||||||
"total_farmers": total_farmers,
|
"agent_brand": agent_brand,
|
||||||
"total_holdings": total_holdings,
|
"django_version": django_version(),
|
||||||
"region_stats": sorted(region_stats, key=lambda x: x['count'], reverse=True),
|
"python_version": platform.python_version(),
|
||||||
"activity_stats": activity_stats,
|
"current_time": now,
|
||||||
"crop_distribution": crop_distribution,
|
"host_name": host_name,
|
||||||
"livestock_distribution": livestock_distribution,
|
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||||
"land_tenure_distribution": land_tenure_distribution,
|
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||||
"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