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