This commit is contained in:
Flatlogic Bot 2026-02-23 18:57:12 +00:00
parent fcc05abc6e
commit 415a23fcaf
31 changed files with 911 additions and 324 deletions

Binary file not shown.

Binary file not shown.

View File

@ -1,182 +1,98 @@
"""
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", ""),
]
CSRF_TRUSTED_ORIGINS = [ ALLOWED_HOSTS = ["*"]
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. # CSRF settings for Flatlogic Cloud
SESSION_COOKIE_SECURE = True CSRF_TRUSTED_ORIGINS = ["https://*.flatlogic.app", "https://*.flatlogic.com"]
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 = [
BASE_DIR / 'static',
BASE_DIR / 'assets',
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'

View File

@ -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.

View File

@ -1,3 +1,24 @@
from django.contrib import admin from django.contrib import admin
from .models import Region, Constituency, Farmer, AgriculturalHolding
# Register your models here. @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')
@admin.register(AgriculturalHolding)
class HoldingAdmin(admin.ModelAdmin):
list_display = ('farmer', 'primary_activity', 'size_hectares')
list_filter = ('primary_activity',)

View File

@ -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
View 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...'}),
}

View 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'),
),
]

View File

@ -1,3 +1,48 @@
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}"

View File

@ -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">&copy; 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>

View File

@ -0,0 +1,120 @@
{% 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>
</div>
<!-- Holdings Column -->
<div class="col-lg-8">
<div class="card shadow-sm border-0 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">Agricultural Holdings ({{ farmer.holdings.count }})</h5>
<a href="#" class="btn btn-sm btn-primary px-3 fw-bold">Add Another Holding</a>
</div>
<div class="card-body p-4">
{% for holding in farmer.holdings.all %}
<div class="card mb-3 border-light shadow-none bg-light-subtle">
<div class="card-body">
<div class="row align-items-center">
<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>
</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 Output</small>
<span class="h4 fw-bold mb-0 text-success">0.0 t</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</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 expected during the next harvest assessment cycle (July 2026).</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endblock %}

View 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 %}

View 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 %}

View File

@ -1,145 +1,158 @@
{% 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> const ctx = document.getElementById('activityChart').getContext('2d');
:root { const labels = [{% for item in activity_stats %}'{{ item.label }}',{% endfor %}];
--bg-color-start: #6a11cb; const values = [{% for item in activity_stats %}{{ item.value }},{% endfor %}];
--bg-color-end: #2575fc;
--text-color: #ffffff; new Chart(ctx, {
--card-bg-color: rgba(255, 255, 255, 0.01); type: 'doughnut',
--card-border-color: rgba(255, 255, 255, 0.1); data: {
} labels: labels,
datasets: [{
* { label: 'Agricultural Activity Distribution',
box-sizing: border-box; data: values,
} backgroundColor: [
'#003580', '#009543', '#FFD100', '#6c757d', '#dc3545'
body { ],
margin: 0; borderWidth: 1
font-family: 'Inter', sans-serif; }]
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); },
color: var(--text-color); options: {
display: flex; responsive: true,
justify-content: center; plugins: {
align-items: center; legend: {
min-height: 100vh; position: 'bottom',
text-align: center; }
overflow: hidden; }
position: relative; }
} });
});
body::before { </script>
content: '';
position: absolute;
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>");
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
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 %}
<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> <div class="row g-4 text-center">
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code> <!-- Stats Cards -->
</p> <div class="col-md-3">
</div> <div class="card h-100 py-3 shadow-sm" style="border-left: 5px solid var(--naims-blue);">
</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" style="border-left: 5px solid var(--naims-green);">
<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" style="border-left: 5px solid var(--naims-yellow);">
<div class="card-body">
<h6 class="text-muted text-uppercase mb-2">National Regions</h6>
<h2 class="fw-bold mb-0">14</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card h-100 py-3 shadow-sm" style="border-left: 5px solid #6c757d;">
<div class="card-body">
<h6 class="text-muted text-uppercase mb-2">System Status</h6>
<h2 class="fw-bold mb-0 text-muted">ONLINE</h2>
</div>
</div>
</div>
</div>
<div class="row mt-5">
<!-- Activity Chart -->
<div class="col-lg-5 mb-4 mb-lg-0">
<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: 350px; width: 100%;">
<canvas id="activityChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Region Distribution Table -->
<div class="col-lg-7">
<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>
{% if user.is_authenticated %}
<a href="{% url 'farmer_list' %}" class="btn btn-sm btn-outline-primary">Registry View</a>
{% endif %}
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Region</th>
<th>Registered Farmers</th>
<th>Status</th>
<th>Progress</th>
</tr>
</thead>
<tbody>
{% for stat in region_stats %}
<tr>
<td class="fw-bold">{{ stat.name }}</td>
<td>{{ stat.count }}</td>
<td><span class="badge bg-success-subtle text-success">Verified</span></td>
<td style="width: 30%">
{% with stat.count|default:0 as cnt %}
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-primary" role="progressbar" style="width: {% if total_farmers > 0 %}{{ cnt|add:0|floatformat:0 }}{% else %}0{% endif %}%;"></div>
</div>
{% endwith %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center py-4 text-muted">No data available yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="mt-3">
<a href="{% url 'export_report' %}" class="btn btn-outline-success w-100 fw-bold">Download Full Regional Performance Report (CSV)</a>
</div>
</div>
</div>
</div>
</div>
</section>
{% endblock %} {% endblock %}

View 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 %}

View File

@ -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"),
]

View File

@ -1,25 +1,101 @@
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
from .forms import FarmerForm, HoldingForm
import csv
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()
def home(request):
"""Render the landing screen with loader and environment details.""" # Simple stats per region
host_name = request.get_host().lower() region_stats = []
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" for region in regions:
now = timezone.now() 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})
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, "all_regions": regions,
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
} }
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."""
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="naims_agricultural_holdings_report.csv"'
writer = csv.writer(response)
writer.writerow(['Farmer Name', 'Region', 'Constituency', 'Primary Activity', 'Size (Hectares)'])
holdings = AgriculturalHolding.objects.select_related('farmer__constituency__region').all()
for h in holdings:
writer.writerow([
h.farmer.name,
h.farmer.constituency.region.name,
h.farmer.constituency.name,
h.get_primary_activity_display(),
h.size_hectares
])
return response