diff --git a/ai/__pycache__/__init__.cpython-311.pyc b/ai/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..4cb76c3 Binary files /dev/null and b/ai/__pycache__/__init__.cpython-311.pyc differ diff --git a/ai/__pycache__/local_ai_api.cpython-311.pyc b/ai/__pycache__/local_ai_api.cpython-311.pyc new file mode 100644 index 0000000..d147f65 Binary files /dev/null and b/ai/__pycache__/local_ai_api.cpython-311.pyc differ diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index bf43253..3ca28fe 100644 Binary files a/config/__pycache__/__init__.cpython-311.pyc and b/config/__pycache__/__init__.cpython-311.pyc differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 881731c..230f107 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 42d995d..f3a665e 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index e65b92e..25bcb75 100644 Binary files a/config/__pycache__/wsgi.cpython-311.pyc and b/config/__pycache__/wsgi.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 291d043..2a6d7a7 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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 +from pathlib import Path from dotenv import load_dotenv +load_dotenv() + BASE_DIR = Path(__file__).resolve().parent.parent -load_dotenv(BASE_DIR.parent / ".env") -SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "change-me") -DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true" +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&") -ALLOWED_HOSTS = [ - "127.0.0.1", - "localhost", - os.getenv("HOST_FQDN", ""), -] +DEBUG = os.environ.get("DEBUG", "True") == "True" -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 -] +ALLOWED_HOSTS = ["*"] -# 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 +# CSRF settings for Flatlogic Cloud +CSRF_TRUSTED_ORIGINS = ["https://*.flatlogic.app", "https://*.flatlogic.com"] INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'core', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "core", + "ai", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - # Disable X-Frame-Options middleware to allow Flatlogic preview iframes. - # 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -X_FRAME_OPTIONS = 'ALLOWALL' - -ROOT_URLCONF = 'config.urls' +ROOT_URLCONF = "config.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - # IMPORTANT: do not remove – injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp - 'core.context_processors.project_context', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BASE_DIR / "templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "core.context_processors.deployment_info", ], }, }, ] -WSGI_APPLICATION = 'config.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/5.2/ref/settings/#databases +WSGI_APPLICATION = "config.wsgi.application" DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': os.getenv('DB_NAME', ''), - 'USER': os.getenv('DB_USER', ''), - 'PASSWORD': os.getenv('DB_PASS', ''), - 'HOST': os.getenv('DB_HOST', '127.0.0.1'), - 'PORT': os.getenv('DB_PORT', '3306'), - 'OPTIONS': { - 'charset': 'utf8mb4', - }, - }, + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": os.environ.get("DB_NAME", "naims_db"), + "USER": os.environ.get("DB_USER", "naims_user"), + "PASSWORD": os.environ.get("DB_PASS", "naims_pass"), + "HOST": os.environ.get("DB_HOST", "127.0.0.1"), + "PORT": os.environ.get("DB_PORT", "3306"), + } } - -# Password validation -# https://docs.djangoproject.com/en/5.2/ref/settings/#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", }, ] - -# Internationalization -# https://docs.djangoproject.com/en/5.2/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" USE_I18N = True - USE_TZ = True +STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "staticfiles" +STATICFILES_DIRS = [BASE_DIR / "static"] -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/5.2/howto/static-files/ +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -STATIC_URL = 'static/' -# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS. -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' +LOGIN_REDIRECT_URL = "home" +LOGOUT_REDIRECT_URL = "home" diff --git a/config/urls.py b/config/urls.py index bcfc074..fecf250 100644 --- a/config/urls.py +++ b/config/urls.py @@ -21,9 +21,10 @@ from django.conf.urls.static import static urlpatterns = [ path("admin/", admin.site.urls), + path("accounts/", include("django.contrib.auth.urls")), path("", include("core.urls")), ] if settings.DEBUG: 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) \ No newline at end of file diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc index c26577c..a461877 100644 Binary files a/core/__pycache__/__init__.cpython-311.pyc and b/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 2964e11..2b503a8 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 45edf7a..aa3550c 100644 Binary files a/core/__pycache__/apps.cpython-311.pyc and b/core/__pycache__/apps.cpython-311.pyc differ diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 75bf223..17c1590 100644 Binary files a/core/__pycache__/context_processors.cpython-311.pyc and b/core/__pycache__/context_processors.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000..f3dbc08 Binary files /dev/null and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 18a063c..37ef93b 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index ebb8c6e..255a096 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 8d204fa..56d0307 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..1130bf2 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,24 @@ 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',) \ No newline at end of file diff --git a/core/context_processors.py b/core/context_processors.py index 0bf87c3..1d6c402 100644 --- a/core/context_processors.py +++ b/core/context_processors.py @@ -1,7 +1,7 @@ import os import time -def project_context(request): +def deployment_info(request): """ 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", ""), # Used for cache-busting static assets "deployment_timestamp": int(time.time()), - } + } \ No newline at end of file diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..0e562c7 --- /dev/null +++ b/core/forms.py @@ -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...'}), + } diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..d3a30a8 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -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'), + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..706c393 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc index 5b49a39..9c0eaf3 100644 Binary files a/core/migrations/__pycache__/__init__.cpython-311.pyc and b/core/migrations/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..ba0c825 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,48 @@ 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}" \ No newline at end of file diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..d0c6c48 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,123 @@ - - - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - {% load static %} - - {% block head %}{% endblock %} + + + {% block title %}NAIMS - Namibia Agricultural Information Management System{% endblock %} + + + + + + + + {% block extra_css %}{% endblock %} - - {% block content %}{% endblock %} - + - + {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} + +
+ {% endfor %} +
+ {% endif %} + +
+ {% block content %}{% endblock %} +
+ + + + + + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/core/templates/core/farmer_detail.html b/core/templates/core/farmer_detail.html new file mode 100644 index 0000000..df7b364 --- /dev/null +++ b/core/templates/core/farmer_detail.html @@ -0,0 +1,120 @@ +{% extends "base.html" %} + +{% block title %}{{ farmer.name }} Profile | NAIMS Namibia{% endblock %} + +{% block content %} +
+
+ +
+
+
+
+
+ {{ farmer.name|slice:":1" }} +
+

{{ farmer.name }}

+ Active Registration +
+ +
    +
  • + ID / Passport + {{ farmer.id_number }} +
  • +
  • + Phone Number + {{ farmer.phone_number|default:"N/A" }} +
  • +
  • + Constituency + {{ farmer.constituency.name }} +
  • +
  • + Region + {{ farmer.constituency.region.name }} +
  • +
  • + Date Joined + {{ farmer.created_at|date:"d M Y" }} +
  • +
+ + +
+
+
+ + +
+
+
+
Agricultural Holdings ({{ farmer.holdings.count }})
+ Add Another Holding +
+
+ {% for holding in farmer.holdings.all %} +
+
+
+
+
{{ holding.get_primary_activity_display }}
+

{{ holding.location_description|default:"No location provided." }}

+
+
+

{{ holding.size_hectares }} Hectares

+
+
+
+
+ {% empty %} +
+

No holdings registered for this farmer.

+
+ {% endfor %} + +
+ +
Historical Performance & Indicators
+
+
+
+ Crop Output + 0.0 t +
+
+
+
+ Livestock + 0 +
+
+
+
+ Input Use + N/A +
+
+
+
+ Food Security + STABLE +
+
+
+ +
+
+
+

System Analysis:

+

Detailed production reports are expected during the next harvest assessment cycle (July 2026).

+
+
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/farmer_form.html b/core/templates/core/farmer_form.html new file mode 100644 index 0000000..25aab01 --- /dev/null +++ b/core/templates/core/farmer_form.html @@ -0,0 +1,85 @@ +{% extends "base.html" %} + +{% block title %}Register Farmer | NAIMS Namibia{% endblock %} + +{% block content %} +
+
+
+
+
+

Farmer Registration

+

Enroll a new farmer and their holding into the national database.

+
+ Back to List +
+ +
+ {% csrf_token %} +
+ +
+
+
+
Personal Information
+ +
+ + {{ f_form.name }} +
+
+ + {{ f_form.id_number }} +
+
+ + {{ f_form.phone_number }} +
+
+ + {{ f_form.constituency }} + Select the constituency where the farmer is based. +
+
+
+
+ + +
+
+
+
Initial Holding Details
+ +
+ + {{ h_form.size_hectares }} +
+
+ + {{ h_form.primary_activity }} +
+
+ + {{ h_form.location_description }} +
+ +
+ Note: + More holdings can be added later from the farmer's profile page. +
+
+
+
+
+ +
+ +

By registering, you confirm the data matches official government records.

+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/farmer_list.html b/core/templates/core/farmer_list.html new file mode 100644 index 0000000..49979c5 --- /dev/null +++ b/core/templates/core/farmer_list.html @@ -0,0 +1,92 @@ +{% extends "base.html" %} + +{% block title %}National Farmer Registry | NAIMS Namibia{% endblock %} + +{% block content %} +
+
+
+

National Farmer Registry

+

Manage and view all registered farmers across Namibia.

+
+ +
+ + +
+
+
+
+ + +
+
+ +
+ {% if selected_region %} +
+ Clear +
+ {% endif %} +
+
+
+ + +
+
+
+ + + + + + + + + + + + + {% for farmer in farmers %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
Full NameID / PassportConstituency (Region)ContactDate RegisteredActions
{{ farmer.name }}{{ farmer.id_number }}{{ farmer.constituency.name }} ({{ farmer.constituency.region.name }}){{ farmer.phone_number|default:"N/A" }}{{ farmer.created_at|date:"d M Y" }} + View Profile +
+
+ + + +
+

No farmers found for the selected filter.

+ Add the first farmer +
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..96e1b11 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,158 @@ {% extends "base.html" %} -{% block title %}{{ project_name }}{% endblock %} +{% block title %}Public Reporting Dashboard | NAIMS Namibia{% endblock %} -{% block head %} - - - - +{% block extra_js %} + + {% endblock %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+
+

National Agricultural Reporting Dashboard

+

Public access to real-time agricultural statistics and performance indicators for the Republic of Namibia.

+
+ {% if user.is_authenticated %} + Register New Farmer + View National Registry + {% else %} + Export National Report (CSV) + Login (Authorized Personnel) + {% endif %} +
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- + + +
+
+ +
+
+
+
Total Farmers
+

{{ total_farmers }}

+
+
+
+
+
+
+
Active Holdings
+

{{ total_holdings }}

+
+
+
+
+
+
+
National Regions
+

14

+
+
+
+
+
+
+
System Status
+

ONLINE

+
+
+
+
+ +
+ +
+
+
+
Activity Distribution
+
+
+
+ +
+
+
+
+ + +
+
+
+
Farmer Distribution by Region
+ {% if user.is_authenticated %} + Registry View + {% endif %} +
+
+
+ + + + + + + + + + + {% for stat in region_stats %} + + + + + + + {% empty %} + + + + {% endfor %} + +
RegionRegistered FarmersStatusProgress
{{ stat.name }}{{ stat.count }}Verified + {% with stat.count|default:0 as cnt %} +
+
+
+ {% endwith %} +
No data available yet.
+
+ +
+
+
+
+
{% endblock %} \ No newline at end of file diff --git a/core/templates/registration/login.html b/core/templates/registration/login.html new file mode 100644 index 0000000..7861165 --- /dev/null +++ b/core/templates/registration/login.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} + +{% block title %}Login - NAIMS Namibia{% endblock %} + +{% block content %} +
+
+
+
+

User Login

+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field.errors }} + +
+ {% endfor %} +
+ +
+
+
+ Only authorized Ministry personnel and Extension Officers can access the registration system. +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 6299e3d..582cff1 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,10 @@ from django.urls import path - -from .views import home +from . import views 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//", views.farmer_detail, name="farmer_detail"), + path("export-report/", views.export_report, name="export_report"), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index c9aed12..611b85e 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,101 @@ -import os -import platform +from django.shortcuts import render, redirect, get_object_or_404 +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 -from django.shortcuts import render -from django.utils import timezone - - -def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() +def dashboard(request): + """Public National Dashboard Overview with Analytics""" + total_farmers = Farmer.objects.count() + total_holdings = AgriculturalHolding.objects.count() + regions = Region.objects.all() + + # Simple stats per region + region_stats = [] + for region in regions: + farmers_in_region = Farmer.objects.filter(constituency__region=region).count() + region_stats.append({'name': region.name, 'count': farmers_in_region}) + + # Stats by primary activity + activity_stats = [] + for code, label in AgriculturalHolding.HOLDING_TYPES: + count = AgriculturalHolding.objects.filter(primary_activity=code).count() + activity_stats.append({'label': label, 'value': count}) context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + "project_name": "NAIMS - Namibia", + "total_farmers": total_farmers, + "total_holdings": total_holdings, + "region_stats": sorted(region_stats, key=lambda x: x['count'], reverse=True), + "activity_stats": activity_stats, + "all_regions": regions, } 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 \ No newline at end of file