├── base ├── __init__.py ├── migrations │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-313.pyc │ │ ├── 0001_initial.cpython-313.pyc │ │ ├── 0004_department.cpython-313.pyc │ │ ├── 0003_alter_organization_options_and_more.cpython-313.pyc │ │ └── 0002_organization_location_source_and_more.cpython-313.pyc │ ├── 0003_alter_organization_options_and_more.py │ ├── 0004_department.py │ ├── 0002_organization_location_source_and_more.py │ └── 0001_initial.py ├── tests.py ├── __pycache__ │ ├── apps.cpython-313.pyc │ ├── urls.cpython-313.pyc │ ├── admin.cpython-313.pyc │ ├── forms.cpython-313.pyc │ ├── models.cpython-313.pyc │ ├── views.cpython-313.pyc │ └── __init__.cpython-313.pyc ├── apps.py ├── templates │ ├── departments_dropdown.html │ ├── geofence_violations.html │ ├── location_history.html │ ├── base.html │ ├── login.html │ ├── edit.html │ ├── register.html │ ├── list.html │ ├── dashboard.html │ └── detail.html ├── utils.py ├── urls.py ├── models.py ├── forms.py ├── admin.py └── views.py ├── geo_test ├── __init__.py ├── __pycache__ │ ├── urls.cpython-313.pyc │ ├── wsgi.cpython-313.pyc │ ├── __init__.cpython-313.pyc │ └── settings.cpython-313.pyc ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── manage.py └── README.md /base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /geo_test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /base/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /base/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /base/__pycache__/apps.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melau-eddy/Geofence-Hr/HEAD/base/__pycache__/apps.cpython-313.pyc -------------------------------------------------------------------------------- /base/__pycache__/urls.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melau-eddy/Geofence-Hr/HEAD/base/__pycache__/urls.cpython-313.pyc -------------------------------------------------------------------------------- /base/__pycache__/admin.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melau-eddy/Geofence-Hr/HEAD/base/__pycache__/admin.cpython-313.pyc -------------------------------------------------------------------------------- /base/__pycache__/forms.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melau-eddy/Geofence-Hr/HEAD/base/__pycache__/forms.cpython-313.pyc -------------------------------------------------------------------------------- /base/__pycache__/models.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melau-eddy/Geofence-Hr/HEAD/base/__pycache__/models.cpython-313.pyc -------------------------------------------------------------------------------- /base/__pycache__/views.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melau-eddy/Geofence-Hr/HEAD/base/__pycache__/views.cpython-313.pyc -------------------------------------------------------------------------------- /base/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melau-eddy/Geofence-Hr/HEAD/base/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /geo_test/__pycache__/urls.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melau-eddy/Geofence-Hr/HEAD/geo_test/__pycache__/urls.cpython-313.pyc -------------------------------------------------------------------------------- /geo_test/__pycache__/wsgi.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melau-eddy/Geofence-Hr/HEAD/geo_test/__pycache__/wsgi.cpython-313.pyc -------------------------------------------------------------------------------- /geo_test/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melau-eddy/Geofence-Hr/HEAD/geo_test/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /geo_test/__pycache__/settings.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melau-eddy/Geofence-Hr/HEAD/geo_test/__pycache__/settings.cpython-313.pyc -------------------------------------------------------------------------------- /base/migrations/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melau-eddy/Geofence-Hr/HEAD/base/migrations/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /base/migrations/__pycache__/0001_initial.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melau-eddy/Geofence-Hr/HEAD/base/migrations/__pycache__/0001_initial.cpython-313.pyc -------------------------------------------------------------------------------- /base/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BaseConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'base' 7 | -------------------------------------------------------------------------------- /base/migrations/__pycache__/0004_department.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melau-eddy/Geofence-Hr/HEAD/base/migrations/__pycache__/0004_department.cpython-313.pyc -------------------------------------------------------------------------------- /base/templates/departments_dropdown.html: -------------------------------------------------------------------------------- 1 | 2 | {% for department in departments %} 3 | 4 | {% endfor %} -------------------------------------------------------------------------------- /base/migrations/__pycache__/0003_alter_organization_options_and_more.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melau-eddy/Geofence-Hr/HEAD/base/migrations/__pycache__/0003_alter_organization_options_and_more.cpython-313.pyc -------------------------------------------------------------------------------- /base/migrations/__pycache__/0002_organization_location_source_and_more.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melau-eddy/Geofence-Hr/HEAD/base/migrations/__pycache__/0002_organization_location_source_and_more.cpython-313.pyc -------------------------------------------------------------------------------- /base/utils.py: -------------------------------------------------------------------------------- 1 | from django.contrib.gis.geos import Point 2 | from geopy.distance import geodesic 3 | 4 | def check_geofence(lat, lng, org_point, radius_meters): 5 | """Check if a point is within the geofence radius""" 6 | org_coords = (org_point.y, org_point.x) 7 | point_coords = (lat, lng) 8 | distance = geodesic(org_coords, point_coords).meters 9 | return distance <= radius_meters -------------------------------------------------------------------------------- /geo_test/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for geo_test project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'geo_test.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /geo_test/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for geo_test project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'geo_test.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'geo_test.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /geo_test/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for geo_test project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/4.2/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.contrib import admin 18 | from django.urls import path, include 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | path('', include('base.urls')), 23 | ] 24 | -------------------------------------------------------------------------------- /base/migrations/0003_alter_organization_options_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2 on 2025-04-07 17:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('base', '0002_organization_location_source_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='organization', 15 | options={'verbose_name': 'Organization', 'verbose_name_plural': 'Organizations'}, 16 | ), 17 | migrations.AlterField( 18 | model_name='organization', 19 | name='location_source', 20 | field=models.CharField(choices=[('manual', 'Manual Entry'), ('geocode', 'Geocode from Address'), ('first_checkin', 'Derive from First Intern Check-in'), ('pending', 'Pending Automatic Detection')], default='pending', max_length=20), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /base/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | 5 | 6 | urlpatterns = [ 7 | # Authentication 8 | path('register/', views.register, name='register'), 9 | path('', views.user_login, name='login'), 10 | path('logout/', views.user_logout, name='logout'), 11 | 12 | # Dashboard 13 | path('dashboard/', views.dashboard, name='dashboard'), 14 | path('profile/complete/', views.profile_complete, name='profile_complete'), 15 | path('api/departments/', views.load_departments, name='load_departments'), 16 | # Location APIs 17 | path('update-location/', views.update_location, name='update_location'), 18 | path('location-history/', views.location_history, name='location_history'), 19 | 20 | # Geofencing 21 | path('geofence-violations/', views.geofence_violations, name='geofence_violations'), 22 | 23 | # Intern Management 24 | path('interns/', views.intern_list, name='intern_list'), 25 | path('interns//', views.intern_detail, name='intern_detail'), 26 | ] -------------------------------------------------------------------------------- /base/migrations/0004_department.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2 on 2025-04-07 18:31 2 | 3 | import django.core.validators 4 | import django.db.models.deletion 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('base', '0003_alter_organization_options_and_more'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Department', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=100)), 20 | ('code', models.CharField(max_length=10, unique=True, validators=[django.core.validators.MinLengthValidator(2)])), 21 | ('is_active', models.BooleanField(default=True)), 22 | ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='departments', to='base.organization')), 23 | ], 24 | options={ 25 | 'ordering': ['name'], 26 | 'unique_together': {('organization', 'name')}, 27 | }, 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /base/migrations/0002_organization_location_source_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2 on 2025-04-07 16:58 2 | 3 | import django.contrib.gis.db.models.fields 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('base', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='organization', 16 | name='location_source', 17 | field=models.CharField(choices=[('manual', 'Manual Entry'), ('geocode', 'Geocode from Address'), ('first_checkin', 'Derive from First Intern Check-in')], default='first_checkin', max_length=20), 18 | ), 19 | migrations.AlterField( 20 | model_name='organization', 21 | name='address', 22 | field=models.TextField(blank=True), 23 | ), 24 | migrations.AlterField( 25 | model_name='organization', 26 | name='geofence_radius', 27 | field=models.PositiveIntegerField(default=100, help_text='Radius in meters'), 28 | ), 29 | migrations.AlterField( 30 | model_name='organization', 31 | name='location', 32 | field=django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /base/templates/geofence_violations.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Geofence Violations

6 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for violation in violations %} 19 | 20 | 25 | 26 | 27 | 28 | {% endfor %} 29 | 30 |
InternTimeLocation
21 |
22 |
{{ violation.intern.user.get_full_name }}
23 |
24 |
{{ violation.timestamp|date:"M d, Y H:i" }}{{ violation.address|truncatechars:40 }}
31 |
32 |
33 |
34 | {% endblock %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🛰️ Geofencing HR Intern Monitoring System 2 | 3 | This project is a web-based **Geofencing Application for Human Resources** that enables companies to monitor interns' physical locations in real time. It uses **Django** as the backend framework and integrates **OpenStreetMap** to track and visualize the precise locations of interns using their GPS coordinates (longitude and latitude). 4 | 5 | > ⚠️ **Note:** This project is currently **under active development**. Features and functionality are subject to change. 6 | 7 | --- 8 | 9 | ## 🔍 Project Overview 10 | 11 | **Purpose** 12 | To help HR teams ensure that interns remain within the designated company vicinity during working hours. This system aims to improve attendance accountability and workplace compliance using modern geolocation technologies. 13 | 14 | --- 15 | 16 | ## 🚀 Features 17 | 18 | - 🌐 **Real-time location tracking** of interns using GPS coordinates. 19 | - 🗺️ Integration with **OpenStreetMap** for map rendering and visualization. 20 | - 📍 **Geofencing logic** to detect if an intern leaves or enters the company-defined area. 21 | - 🔔 **Alerts or logs** when interns move outside the permitted geofenced boundary. 22 | - 🧑‍💼 Role-based access for HR admins and interns. 23 | - 📊 Dashboard for monitoring intern statuses. 24 | - 📱 Mobile compatibility for real-time updates via mobile browsers. 25 | 26 | --- 27 | 28 | ## 🧰 Technologies Used 29 | 30 | ### Backend 31 | - **Django** (Python web framework) 32 | - **Django REST Framework** *(optional, if used for APIs)* 33 | 34 | ### Frontend 35 | - **HTML** 36 | - **CSS** 37 | - **JavaScript** 38 | - **Leaflet.js** (for OpenStreetMap integration) 39 | 40 | ### Mapping 41 | - **OpenStreetMap** 42 | - **Leaflet.js** for interactive geolocation visualization 43 | 44 | --- 45 | 46 | ## 📦 Installation 47 | 48 | 1. **Clone the repository** 49 | ```bash 50 | git clone https://github.com/melau-eddy/Geofence-Hr 51 | cd geofencing-hr-monitor 52 | -------------------------------------------------------------------------------- /base/templates/location_history.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Your Location History

6 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% for loc in locations %} 20 | 21 | 22 | 23 | 29 | 30 | 31 | {% endfor %} 32 | 33 |
TimeLocationStatusAccuracy
{{ loc.timestamp|date:"M d, Y H:i" }}{{ loc.address|truncatechars:40 }} 24 | 26 | {% if loc.is_inside_geofence %}Inside{% else %}Outside{% endif %} 27 | 28 | {{ loc.accuracy|floatformat:0 }}m
34 |
35 |
36 |
37 | {% endblock %} -------------------------------------------------------------------------------- /base/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Intern Tracker 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 24 | 25 | 26 | 41 | 42 |
43 | {% block content %}{% endblock %} 44 |
45 | 46 | 51 | 52 | -------------------------------------------------------------------------------- /base/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |
8 | 9 |
10 |

11 | Welcome Back 12 |

13 |

14 | Track your internship journey with us 15 |

16 |
17 | 18 | {% if form.errors %} 19 |
20 |
21 |
22 | 23 |
24 |
25 |

26 | Invalid username or password. Please try again. 27 |

28 |
29 |
30 |
31 | {% endif %} 32 | 33 |
34 | {% csrf_token %} 35 | 36 |
37 |
38 | 39 |
40 |
41 | 42 |
43 | 46 |
47 |
48 |
49 | 50 |
51 |
52 | 53 |
54 | 57 |
58 |
59 |
60 | 61 |
62 |
63 | 65 | 68 |
69 | 70 | 75 |
76 | 77 |
78 | 85 |
86 |
87 | 88 |
89 |

90 | Don't have an account? 91 | 92 | Register here 93 | 94 |

95 |
96 |
97 |
98 | {% endblock %} -------------------------------------------------------------------------------- /geo_test/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for geo_test project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2.20. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-xxdsg2epffv4xvj=of7@a!3qjy(nn!_r^+r_4=4m0q$4!8*u%@' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'jazzmin', 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'django.contrib.gis', 42 | 'base' 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ] 54 | 55 | ROOT_URLCONF = 'geo_test.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'geo_test.wsgi.application' 74 | 75 | # settings.py 76 | GEOPY_USER_AGENT = "base" # Required for Nominatim 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 81 | 82 | # DATABASES = { 83 | # 'default': { 84 | # 'ENGINE': 'django.db.backends.sqlite3', 85 | # 'NAME': BASE_DIR / 'db.sqlite3', 86 | # } 87 | # } 88 | 89 | 90 | DATABASES = { 91 | 'default': { 92 | 'ENGINE': 'django.contrib.gis.db.backends.postgis', 93 | 'NAME': 'intern_tracker', 94 | 'USER': 'admin', 95 | 'PASSWORD': 'admin@user', 96 | 'HOST': 'localhost', 97 | 'PORT': '5432', 98 | } 99 | } 100 | 101 | 102 | # Password validation 103 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 104 | 105 | AUTH_PASSWORD_VALIDATORS = [ 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 108 | }, 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 111 | }, 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 114 | }, 115 | { 116 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 117 | }, 118 | ] 119 | 120 | 121 | # Internationalization 122 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 123 | 124 | LANGUAGE_CODE = 'en-us' 125 | 126 | TIME_ZONE = 'UTC' 127 | 128 | USE_I18N = True 129 | 130 | USE_TZ = True 131 | 132 | 133 | # Static files (CSS, JavaScript, Images) 134 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 135 | 136 | STATIC_URL = 'static/' 137 | 138 | # Default primary key field type 139 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 140 | 141 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 142 | 143 | 144 | LOGIN_REDIRECT_URL = 'dashboard' # Where to redirect after login 145 | LOGOUT_REDIRECT_URL = 'login' # Where to redirect after logout -------------------------------------------------------------------------------- /base/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.2 on 2025-04-07 00:23 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | import django.contrib.gis.db.models.fields 6 | import django.db.models.deletion 7 | import django.utils.timezone 8 | from django.conf import settings 9 | from django.db import migrations, models 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | initial = True 15 | 16 | dependencies = [ 17 | ('auth', '0012_alter_user_first_name_max_length'), 18 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 19 | ] 20 | 21 | operations = [ 22 | migrations.CreateModel( 23 | name='Organization', 24 | fields=[ 25 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 26 | ('name', models.CharField(max_length=255)), 27 | ('location', django.contrib.gis.db.models.fields.PointField(srid=4326)), 28 | ('geofence_radius', models.PositiveIntegerField(help_text='Radius in meters')), 29 | ('address', models.TextField()), 30 | ], 31 | ), 32 | migrations.CreateModel( 33 | name='User', 34 | fields=[ 35 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 36 | ('password', models.CharField(max_length=128, verbose_name='password')), 37 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 38 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 39 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 40 | ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), 41 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 42 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 43 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 44 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 45 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 46 | ('is_intern', models.BooleanField(default=False)), 47 | ('is_supervisor', models.BooleanField(default=False)), 48 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to.', related_name='base_user_set', related_query_name='user', to='auth.group', verbose_name='groups')), 49 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='base_user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), 50 | ], 51 | options={ 52 | 'swappable': 'AUTH_USER_MODEL', 53 | }, 54 | managers=[ 55 | ('objects', django.contrib.auth.models.UserManager()), 56 | ], 57 | ), 58 | migrations.CreateModel( 59 | name='InternProfile', 60 | fields=[ 61 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 62 | ('department', models.CharField(max_length=100)), 63 | ('phone_number', models.CharField(max_length=20)), 64 | ('is_active', models.BooleanField(default=True)), 65 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='intern_profile', to=settings.AUTH_USER_MODEL)), 66 | ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.organization')), 67 | ], 68 | ), 69 | migrations.CreateModel( 70 | name='LocationLog', 71 | fields=[ 72 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 73 | ('point', django.contrib.gis.db.models.fields.PointField(srid=4326)), 74 | ('timestamp', models.DateTimeField(auto_now_add=True)), 75 | ('accuracy', models.FloatField(blank=True, null=True)), 76 | ('address', models.TextField(blank=True, null=True)), 77 | ('is_inside_geofence', models.BooleanField(default=False)), 78 | ('intern', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.internprofile')), 79 | ], 80 | options={ 81 | 'ordering': ['-timestamp'], 82 | }, 83 | ), 84 | ] 85 | -------------------------------------------------------------------------------- /base/templates/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |

8 | 9 | Organization Settings 10 |

11 | 24 |
25 | 26 |
27 |
28 |

Basic Information

29 |

Update your organization's details and location settings.

30 |
31 | 32 |
33 | {% csrf_token %} 34 | 35 |
36 |
37 | 38 | {{ form.name }} 39 | {% if form.name.errors %} 40 |

{{ form.name.errors.as_text }}

41 | {% endif %} 42 |
43 |
44 | 45 |
46 | {{ form.geofence_radius }} 47 |
48 | m 49 |
50 |
51 | {% if form.geofence_radius.errors %} 52 |

{{ form.geofence_radius.errors.as_text }}

53 | {% endif %} 54 |
55 |
56 | 57 |
58 | 59 | {{ form.address }} 60 | {% if form.address.errors %} 61 |

{{ form.address.errors.as_text }}

62 | {% endif %} 63 |
64 | 65 |
66 | 67 |
68 |
69 |
70 |

Click on the map to set your organization's location.

71 | 72 |
73 | 74 |
75 | 76 | Cancel 77 | 78 | 81 |
82 |
83 |
84 |
85 |
86 | 87 | 125 | {% endblock %} -------------------------------------------------------------------------------- /base/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import AbstractUser 3 | from django.contrib.gis.db import models as gis_models 4 | from django.contrib.gis.geos import Point 5 | from django.conf import settings 6 | from geopy.geocoders import Nominatim 7 | from geopy.exc import GeocoderTimedOut, GeocoderServiceError 8 | import logging 9 | from django.core.validators import MinLengthValidator 10 | 11 | class User(AbstractUser): 12 | is_intern = models.BooleanField(default=False) 13 | is_supervisor = models.BooleanField(default=False) 14 | 15 | 16 | class Meta: 17 | # Add this to avoid clashes 18 | swappable = 'AUTH_USER_MODEL' 19 | 20 | # Specify unique related_names 21 | groups = models.ManyToManyField( 22 | 'auth.Group', 23 | verbose_name='groups', 24 | blank=True, 25 | help_text='The groups this user belongs to.', 26 | related_name='base_user_set', # Changed from 'user_set' 27 | related_query_name='user' 28 | ) 29 | user_permissions = models.ManyToManyField( 30 | 'auth.Permission', 31 | verbose_name='user permissions', 32 | blank=True, 33 | help_text='Specific permissions for this user.', 34 | related_name='base_user_set', # Changed from 'user_set' 35 | related_query_name='user' 36 | ) 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | logger = logging.getLogger(__name__) 46 | 47 | 48 | 49 | 50 | class Department(models.Model): 51 | organization = models.ForeignKey( 52 | 'Organization', 53 | on_delete=models.CASCADE, 54 | related_name='departments' 55 | ) 56 | name = models.CharField(max_length=100) 57 | code = models.CharField( 58 | max_length=10, 59 | validators=[MinLengthValidator(2)], 60 | unique=True 61 | ) 62 | is_active = models.BooleanField(default=True) 63 | 64 | class Meta: 65 | ordering = ['name'] 66 | unique_together = ('organization', 'name') 67 | 68 | def __str__(self): 69 | return f"{self.name} ({self.organization})" 70 | 71 | class Organization(models.Model): 72 | name = models.CharField(max_length=255) 73 | location = gis_models.PointField(null=True, blank=True) 74 | geofence_radius = models.PositiveIntegerField( 75 | default=100, # Default 100m radius 76 | help_text="Radius in meters" 77 | ) 78 | address = models.TextField(blank=True) 79 | 80 | LOCATION_SOURCE_CHOICES = [ 81 | ('manual', 'Manual Entry'), 82 | ('geocode', 'Geocode from Address'), 83 | ('first_checkin', 'Derive from First Intern Check-in'), 84 | ('pending', 'Pending Automatic Detection') 85 | ] 86 | location_source = models.CharField( 87 | max_length=20, 88 | choices=LOCATION_SOURCE_CHOICES, 89 | default='pending' 90 | ) 91 | 92 | def __str__(self): 93 | return self.name 94 | 95 | def save(self, *args, **kwargs): 96 | if self.location_source == 'geocode' and self.address and not self.location: 97 | self.geocode_from_address() 98 | super().save(*args, **kwargs) 99 | 100 | def geocode_from_address(self): 101 | try: 102 | geolocator = Nominatim(user_agent="your_app_name") 103 | location = geolocator.geocode(self.address) 104 | if location: 105 | self.location = Point(location.longitude, location.latitude) 106 | self.location_source = 'geocode' 107 | return True 108 | except (GeocoderTimedOut, GeocoderServiceError) as e: 109 | logger.error(f"Geocoding failed for {self.name}: {str(e)}") 110 | return False 111 | 112 | class Meta: 113 | verbose_name = "Organization" 114 | verbose_name_plural = "Organizations" 115 | 116 | 117 | 118 | 119 | # class Organization(models.Model): 120 | # name = models.CharField(max_length=255) 121 | # location = gis_models.PointField(null=True, blank=True) 122 | # geofence_radius = models.PositiveIntegerField( 123 | # default=100, # Default 100m radius 124 | # help_text="Radius in meters" 125 | # ) 126 | # address = models.TextField(blank=True) 127 | 128 | # AUTO_LOCATION_CHOICES = [ 129 | # ('manual', 'Manual Entry'), 130 | # ('geocode', 'Geocode from Address'), 131 | # ('first_checkin', 'Derive from First Intern Check-in') 132 | # ] 133 | # location_source = models.CharField( 134 | # max_length=20, 135 | # choices=AUTO_LOCATION_CHOICES, 136 | # default='first_checkin' 137 | # ) 138 | 139 | # def save(self, *args, **kwargs): 140 | # if self.location_source == 'geocode' and self.address: 141 | # self.geocode_from_address() 142 | # super().save(*args, **kwargs) 143 | 144 | # def geocode_from_address(self): 145 | # geolocator = Nominatim(user_agent="org_locator") 146 | # location = geolocator.geocode(self.address) 147 | # if location: 148 | # self.location = Point(location.longitude, location.latitude) 149 | 150 | class InternProfile(models.Model): 151 | user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,related_name='intern_profile') 152 | department = models.CharField(max_length=100) 153 | phone_number = models.CharField(max_length=20) 154 | organization = models.ForeignKey(Organization, on_delete=models.CASCADE) 155 | is_active = models.BooleanField(default=True) 156 | 157 | def __str__(self): 158 | return f"{self.user.get_full_name()} ({self.department})" 159 | 160 | 161 | 162 | class LocationLog(models.Model): 163 | intern = models.ForeignKey(InternProfile, on_delete=models.CASCADE) 164 | point = gis_models.PointField() 165 | timestamp = models.DateTimeField(auto_now_add=True) 166 | accuracy = models.FloatField(null=True, blank=True) 167 | address = models.TextField(null=True, blank=True) 168 | is_inside_geofence = models.BooleanField(default=False) 169 | 170 | class Meta: 171 | ordering = ['-timestamp'] 172 | 173 | def __str__(self): 174 | status = "Inside" if self.is_inside_geofence else "Outside" 175 | return f"{self.intern} at {self.point} ({status})" -------------------------------------------------------------------------------- /base/forms.py: -------------------------------------------------------------------------------- 1 | # base/forms.py 2 | # from django import forms 3 | # from django.contrib.auth.forms import AuthenticationForm, UserCreationForm 4 | # from .models import User, Organization, InternProfile 5 | # from django.contrib.auth import get_user_model 6 | 7 | # class InternRegistrationForm(UserCreationForm): 8 | # phone_number = forms.CharField(max_length=20) 9 | # department = forms.CharField(max_length=100) 10 | # organization = forms.ModelChoiceField(queryset=Organization.objects.all()) 11 | 12 | # class Meta(UserCreationForm.Meta): 13 | # fields = UserCreationForm.Meta.fields + ('first_name', 'last_name', 'email') 14 | 15 | # def save(self, commit=True): 16 | # user = super().save(commit=False) 17 | # if commit: 18 | # user.save() 19 | # InternProfile.objects.create( 20 | # user=user, 21 | # phone_number=self.cleaned_data['phone_number'], 22 | # department=self.cleaned_data['department'], 23 | # organization=self.cleaned_data['organization'] 24 | # ) 25 | # return user 26 | 27 | # class OrganizationForm(forms.ModelForm): 28 | # class Meta: 29 | # model = Organization 30 | # fields = ['name', 'location', 'geofence_radius', 'address'] 31 | # widgets = { 32 | # 'location': forms.HiddenInput() # We'll handle this via Leaflet 33 | # } 34 | 35 | 36 | # User = get_user_model() 37 | 38 | # class CustomAuthenticationForm(AuthenticationForm): 39 | # username = forms.CharField( 40 | # widget=forms.TextInput(attrs={ 41 | # 'class': 'form-control', 42 | # 'placeholder': 'Username', 43 | # 'autofocus': True 44 | # }) 45 | # ) 46 | # password = forms.CharField( 47 | # widget=forms.PasswordInput(attrs={ 48 | # 'class': 'form-control', 49 | # 'placeholder': 'Password' 50 | # }) 51 | # ) 52 | 53 | # class InternRegistrationForm(UserCreationForm): 54 | # class Meta(UserCreationForm.Meta): 55 | # fields = UserCreationForm.Meta.fields + ('first_name', 'last_name', 'email') 56 | 57 | 58 | # class ProfileCompletionForm(forms.ModelForm): 59 | # class Meta: 60 | # model = InternProfile 61 | # fields = ['department', 'phone_number', 'organization'] 62 | # widgets = { 63 | # 'organization': forms.Select(attrs={'class': 'form-control'}), 64 | # } 65 | 66 | 67 | 68 | from django import forms 69 | from django.contrib.auth.forms import AuthenticationForm, UserCreationForm 70 | from django.contrib.auth import get_user_model 71 | from .models import Organization, InternProfile, Department 72 | from django.contrib.gis.geos import Point 73 | from geopy.geocoders import Nominatim 74 | from geopy.exc import GeocoderTimedOut, GeocoderServiceError 75 | import logging 76 | 77 | logger = logging.getLogger(__name__) 78 | User = get_user_model() 79 | 80 | class CustomAuthenticationForm(AuthenticationForm): 81 | username = forms.CharField( 82 | widget=forms.TextInput(attrs={ 83 | 'class': 'form-control', 84 | 'placeholder': 'Username', 85 | 'autofocus': True 86 | }) 87 | ) 88 | password = forms.CharField( 89 | widget=forms.PasswordInput(attrs={ 90 | 'class': 'form-control', 91 | 'placeholder': 'Password' 92 | }) 93 | ) 94 | 95 | from django import forms 96 | from django.contrib.auth.forms import UserCreationForm 97 | from .models import User, Department, Organization 98 | 99 | class InternRegistrationForm(UserCreationForm): 100 | phone_number = forms.CharField(max_length=20, required=True) 101 | organization = forms.ModelChoiceField( 102 | queryset=Organization.objects.all(), 103 | required=True 104 | ) 105 | department = forms.ModelChoiceField( 106 | queryset=Department.objects.none(), # Will be populated in __init__ 107 | required=True 108 | ) 109 | 110 | class Meta(UserCreationForm.Meta): 111 | model = User 112 | fields = UserCreationForm.Meta.fields + ( 113 | 'first_name', 'last_name', 'email', 114 | 'phone_number', 'organization', 'department' 115 | ) 116 | 117 | def __init__(self, *args, **kwargs): 118 | super().__init__(*args, **kwargs) 119 | 120 | # If organization is already selected (form re-display), show its departments 121 | if 'organization' in self.data: 122 | try: 123 | org_id = int(self.data.get('organization')) 124 | self.fields['department'].queryset = Department.objects.filter( 125 | organization_id=org_id, 126 | is_active=True 127 | ).order_by('name') 128 | except (ValueError, TypeError): 129 | pass 130 | elif self.instance.pk and hasattr(self.instance, 'internprofile'): 131 | self.fields['department'].queryset = self.instance.internprofile.organization.departments.all() 132 | 133 | def save(self, commit=True): 134 | user = super().save(commit=False) 135 | user.is_intern = True 136 | 137 | if commit: 138 | user.save() 139 | # Create intern profile with all additional fields 140 | InternProfile.objects.create( 141 | user=user, 142 | phone_number=self.cleaned_data['phone_number'], 143 | organization=self.cleaned_data['organization'], 144 | department=self.cleaned_data['department'] 145 | ) 146 | return user 147 | 148 | def save(self, commit=True): 149 | user = super().save(commit=False) 150 | user.email = self.cleaned_data['email'] 151 | user.first_name = self.cleaned_data['first_name'] 152 | user.last_name = self.cleaned_data['last_name'] 153 | user.is_intern = True # Automatically set as intern 154 | 155 | if commit: 156 | user.save() 157 | return user 158 | 159 | 160 | class OrganizationForm(forms.ModelForm): 161 | class Meta: 162 | model = Organization 163 | fields = ['name', 'address', 'geofence_radius'] 164 | widgets = { 165 | 'address': forms.Textarea(attrs={'rows': 3}), 166 | 'geofence_radius': forms.NumberInput(attrs={'min': 10}), 167 | } 168 | 169 | def clean_geofence_radius(self): 170 | radius = self.cleaned_data['geofence_radius'] 171 | if radius < 10: 172 | raise forms.ValidationError("Geofence radius must be at least 10 meters") 173 | return radius 174 | 175 | def save(self, commit=True): 176 | organization = super().save(commit=False) 177 | if organization.location_source == 'geocode' and organization.address: 178 | try: 179 | geolocator = Nominatim(user_agent="org_locator") 180 | location = geolocator.geocode(organization.address) 181 | if location: 182 | organization.location = Point(location.longitude, location.latitude) 183 | except (GeocoderTimedOut, GeocoderServiceError) as e: 184 | logger.error(f"Geocoding failed: {str(e)}") 185 | # You might want to handle this differently 186 | 187 | if commit: 188 | organization.save() 189 | return organization -------------------------------------------------------------------------------- /base/templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |

Register

8 |

Create your internship account

9 |
10 | 11 |
12 | {% csrf_token %} 13 |
14 | 15 |
16 |
17 | 18 | 22 | {% if form.first_name.errors %} 23 |

{{ form.first_name.errors.as_text }}

24 | {% endif %} 25 |
26 | 27 |
28 | 29 | 33 | {% if form.last_name.errors %} 34 |

{{ form.last_name.errors.as_text }}

35 | {% endif %} 36 |
37 |
38 | 39 | 40 |
41 | 42 | 46 | {% if form.email.errors %} 47 |

{{ form.email.errors.as_text }}

48 | {% endif %} 49 |
50 | 51 | 52 |
53 | 54 | 58 | {% if form.username.errors %} 59 |

{{ form.username.errors.as_text }}

60 | {% endif %} 61 |
62 | 63 | 64 |
65 | 66 | 70 | {% if form.phone_number.errors %} 71 |

{{ form.phone_number.errors.as_text }}

72 | {% endif %} 73 |
74 | 75 | 76 |
77 | 78 | {{ form.organization }} 79 | {% if form.organization.errors %} 80 |

{{ form.organization.errors.as_text }}

81 | {% endif %} 82 |
83 | 84 |
85 | 86 | {{ form.department }} 87 | {% if form.department.errors %} 88 |

{{ form.department.errors.as_text }}

89 | {% endif %} 90 |
91 | 92 | {% for department in departments %} 93 | 94 | {% endfor %} 95 | 96 | 97 |
98 | 99 | 102 | {% if form.password1.errors %} 103 |

{{ form.password1.errors.as_text }}

104 | {% endif %} 105 |
106 | 107 |
108 | 109 | 112 | {% if form.password2.errors %} 113 |

{{ form.password2.errors.as_text }}

114 | {% endif %} 115 |
116 |
117 | 118 |
119 | 122 |
123 |
124 |
125 |
126 | 127 | 128 | 139 | 140 | 148 | {% endblock %} -------------------------------------------------------------------------------- /base/templates/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |
6 |

7 | Intern Management 8 |

9 | 18 |
19 | 20 |
21 |
22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 | 32 | 35 |
36 |
37 | 38 |
39 | 40 | 41 | 42 | 45 | 48 | 51 | 54 | 57 | 58 | 59 | 60 | {% for intern in interns %} 61 | 62 | 73 | 77 | 83 | 92 | 100 | 101 | {% endfor %} 102 | 103 |
43 | Intern 44 | 46 | Department 47 | 49 | Status 50 | 52 | Last Activity 53 | 55 | Actions 56 |
63 |
64 |
65 | 66 |
67 |
68 |
{{ intern.user.get_full_name }}
69 |
{{ intern.user.email }}
70 |
71 |
72 |
74 |
{{ intern.department }}
75 |
{{ intern.organization.name }}
76 |
78 | 80 | {% if intern.is_active %}Active{% else %}Inactive{% endif %} 81 | 82 | 84 | {% with last_log=intern.locationlog_set.first %} 85 | {% if last_log %} 86 | {{ last_log.timestamp|timesince }} ago 87 | {% else %} 88 | Never 89 | {% endif %} 90 | {% endwith %} 91 | 93 | 94 | View 95 | 96 | 97 | Edit 98 | 99 |
104 |
105 | 106 |
107 | 115 | 143 |
144 |
145 |
146 | {% endblock %} -------------------------------------------------------------------------------- /base/templates/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 | 6 |
7 |
8 |
9 | 10 |
11 |
12 |

{{ intern.user.get_full_name }}

13 |

{{ intern.department }}

14 |
15 |
16 | 17 |
18 |
19 | 20 | {{ organization.name }} 21 |
22 |
23 | 24 | {{ intern.phone_number }} 25 |
26 |
27 | 28 | {{ organization.address }} 29 |
30 |
31 | 32 |
33 |
34 |
35 |

Current Status

36 |

Waiting for location...

37 |
38 |
39 |
40 |

41 |
42 | 43 | 46 |
47 | 48 | 49 |
50 |

51 | Location Tracking 52 |

53 | 54 |
55 | 56 |
57 |

Recent Locations

58 |
59 | {% for loc in locations %} 60 |
61 |
62 | {{ loc.timestamp|date:"H:i" }} 63 | {{ loc.address|truncatechars:30 }} 64 |
65 | 66 | {% if loc.is_inside_geofence %}Inside{% else %}Outside{% endif %} 67 | 68 |
69 | {% endfor %} 70 |
71 |
72 |
73 |
74 | 75 | 212 | 213 | 222 | {% endblock %} -------------------------------------------------------------------------------- /base/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.gis.admin import GISModelAdmin 3 | from django.contrib.auth.admin import UserAdmin 4 | from .models import User, Organization, InternProfile, LocationLog, Department 5 | from django.contrib.gis.geos import Point 6 | from geopy.geocoders import Nominatim 7 | from django.db.models import Count 8 | from django.utils.html import format_html 9 | 10 | from django.contrib import admin 11 | from django.contrib.gis.admin import GISModelAdmin 12 | from django.contrib import messages 13 | 14 | 15 | 16 | 17 | 18 | class DepartmentInline(admin.TabularInline): 19 | model = Department 20 | extra = 1 21 | 22 | 23 | 24 | @admin.register(Department) 25 | class DepartmentAdmin(admin.ModelAdmin): 26 | list_display = ('name', 'code', 'organization', 'is_active') 27 | list_filter = ('organization', 'is_active') 28 | search_fields = ('name', 'code') 29 | 30 | 31 | 32 | 33 | 34 | 35 | # Custom User Admin 36 | @admin.register(User) 37 | class CustomUserAdmin(UserAdmin): 38 | list_display = ('username', 'email', 'first_name', 'last_name', 39 | 'is_intern', 'is_supervisor', 'is_staff') 40 | list_filter = ('is_intern', 'is_supervisor', 'is_staff', 'is_superuser') 41 | fieldsets = ( 42 | (None, {'fields': ('username', 'password')}), 43 | ('Personal Info', {'fields': ('first_name', 'last_name', 'email')}), 44 | ('Permissions', { 45 | 'fields': ('is_active', 'is_staff', 'is_superuser', 46 | 'is_intern', 'is_supervisor', 'groups', 'user_permissions'), 47 | }), 48 | ('Important dates', {'fields': ('last_login', 'date_joined')}), 49 | ) 50 | actions = ['mark_as_intern', 'mark_as_supervisor'] 51 | 52 | def mark_as_intern(self, request, queryset): 53 | queryset.update(is_intern=True) 54 | mark_as_intern.short_description = "Mark selected users as interns" 55 | 56 | def mark_as_supervisor(self, request, queryset): 57 | queryset.update(is_supervisor=True) 58 | mark_as_supervisor.short_description = "Mark selected users as supervisors" 59 | 60 | # Organization Admin with Auto-Location 61 | # @admin.register(Organization) 62 | # class OrganizationAdmin(GISModelAdmin): 63 | # list_display = ('name', 'location_preview', 'geofence_radius', 64 | # 'intern_count', 'location_source') 65 | # list_editable = ('geofence_radius',) 66 | # search_fields = ('name', 'address') 67 | # actions = ['geocode_addresses', 'calculate_optimal_radius'] 68 | # readonly_fields = ('location_source',) 69 | # fieldsets = ( 70 | # (None, { 71 | # 'fields': ('name', 'address', 'location_source') 72 | # }), 73 | # ('Location Settings', { 74 | # 'fields': ('location', 'geofence_radius'), 75 | # 'classes': ('collapse',) 76 | # }), 77 | # ) 78 | 79 | # def location_preview(self, obj): 80 | # if obj.location: 81 | # return format_html( 82 | # '📍 View on Map', 83 | # obj.location.y, obj.location.x 84 | # ) 85 | # return "Not set" 86 | # location_preview.short_description = "Location" 87 | 88 | # def intern_count(self, obj): 89 | # return obj.internprofile_set.count() 90 | # intern_count.short_description = "Interns" 91 | 92 | # def geocode_addresses(self, request, queryset): 93 | # geolocator = Nominatim(user_agent="org_locator") 94 | # for org in queryset: 95 | # if org.address and not org.location: 96 | # try: 97 | # location = geolocator.geocode(org.address) 98 | # if location: 99 | # org.location = Point(location.longitude, location.latitude) 100 | # org.location_source = 'geocode' 101 | # org.save() 102 | # self.message_user(request, f"Geocoded {org.name}") 103 | # except Exception as e: 104 | # self.message_user(request, f"Failed to geocode {org.name}: {str(e)}", level='error') 105 | # geocode_addresses.short_description = "Geocode addresses" 106 | 107 | # def calculate_optimal_radius(self, request, queryset): 108 | # from django.contrib.gis.db.models.functions import Distance 109 | # for org in queryset: 110 | # if org.location: 111 | # checkins = LocationLog.objects.filter( 112 | # intern__organization=org 113 | # ).annotate( 114 | # distance=Distance('point', org.location) 115 | # ).order_by('-distance') 116 | 117 | # if checkins.exists(): 118 | # index = int(len(checkins) * 0.95) # Cover 95% of checkins 119 | # org.geofence_radius = checkins[index].distance.m 120 | # org.save() 121 | # self.message_user(request, f"Updated radius for {org.name}") 122 | # calculate_optimal_radius.short_description = "Calculate optimal radius" 123 | 124 | # def save_model(self, request, obj, form, change): 125 | # if not obj.location_source: 126 | # if obj.location: 127 | # obj.location_source = 'manual' 128 | # elif obj.address: 129 | # obj.location_source = 'geocode' 130 | # else: 131 | # obj.location_source = 'pending' 132 | # super().save_model(request, obj, form, change) 133 | 134 | 135 | 136 | 137 | 138 | 139 | @admin.register(Organization) 140 | class OrganizationAdmin(GISModelAdmin): 141 | list_display = ('name', 'location_status', 'geofence_radius', 'get_intern_count') 142 | list_editable = ('geofence_radius',) 143 | actions = ['geocode_selected'] 144 | fieldsets = ( 145 | (None, { 146 | 'fields': ('name', 'address') 147 | }), 148 | ('Location Settings', { 149 | 'fields': ('location', 'geofence_radius', 'location_source'), 150 | 'classes': ('collapse',) 151 | }), 152 | ) 153 | readonly_fields = ('location_source',) 154 | 155 | def location_status(self, obj): 156 | if obj.location: 157 | return format_html( 158 | '📍 View Map', 159 | obj.location.y, obj.location.x 160 | ) 161 | return "❌ Not located" if obj.address else "—" 162 | location_status.short_description = "Location" 163 | 164 | def get_intern_count(self, obj): 165 | return obj.internprofile_set.count() 166 | get_intern_count.short_description = "Interns" 167 | 168 | def geocode_selected(self, request, queryset): 169 | for org in queryset: 170 | if org.address and not org.location: 171 | if org.geocode_from_address(): 172 | org.save() 173 | self.message_user( 174 | request, 175 | f"Successfully geocoded {org.name}", 176 | messages.SUCCESS 177 | ) 178 | else: 179 | self.message_user( 180 | request, 181 | f"Failed to geocode {org.name}", 182 | messages.ERROR 183 | ) 184 | geocode_selected.short_description = "Geocode selected organizations" 185 | 186 | def save_model(self, request, obj, form, change): 187 | if not obj.location and obj.address: 188 | obj.location_source = 'geocode' 189 | super().save_model(request, obj, form, change) 190 | 191 | 192 | 193 | 194 | # Intern Profile Admin 195 | @admin.register(InternProfile) 196 | class InternProfileAdmin(admin.ModelAdmin): 197 | list_display = ('user', 'organization', 'department', 'phone_number', 'is_active') 198 | list_filter = ('organization', 'department', 'is_active') 199 | search_fields = ('user__username', 'user__first_name', 'user__last_name', 'phone_number') 200 | raw_id_fields = ('user',) 201 | list_editable = ('is_active',) 202 | actions = ['activate_profiles', 'deactivate_profiles'] 203 | 204 | def activate_profiles(self, request, queryset): 205 | queryset.update(is_active=True) 206 | activate_profiles.short_description = "Activate selected profiles" 207 | 208 | def deactivate_profiles(self, request, queryset): 209 | queryset.update(is_active=False) 210 | deactivate_profiles.short_description = "Deactivate selected profiles" 211 | 212 | # Location Log Admin 213 | @admin.register(LocationLog) 214 | class LocationLogAdmin(GISModelAdmin): 215 | list_display = ('intern', 'timestamp', 'status', 'address_short', 'accuracy') 216 | list_filter = ('is_inside_geofence', 'intern__organization', 'timestamp') 217 | search_fields = ('intern__user__username', 'address') 218 | readonly_fields = ('timestamp',) 219 | date_hierarchy = 'timestamp' 220 | 221 | def status(self, obj): 222 | color = 'green' if obj.is_inside_geofence else 'red' 223 | text = 'Inside' if obj.is_inside_geofence else 'Outside' 224 | return format_html( 225 | '{}', 226 | color, text 227 | ) 228 | status.short_description = "Geofence Status" 229 | 230 | def address_short(self, obj): 231 | return obj.address[:50] + '...' if obj.address else '' 232 | address_short.short_description = "Address" 233 | 234 | def save_model(self, request, obj, form, change): 235 | # Auto-set organization location if not set 236 | if not obj.intern.organization.location: 237 | obj.intern.organization.location = obj.point 238 | obj.intern.organization.location_source = 'first_checkin' 239 | obj.intern.organization.save() 240 | super().save_model(request, obj, form, change) -------------------------------------------------------------------------------- /base/templates/detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |

8 | 9 | {{ intern.user.get_full_name }} 10 |

11 | 24 |
25 | 33 |
34 | 35 |
36 | 37 |
38 |
39 |

Profile Information

40 |
41 |
42 |
43 |
44 | 45 |
46 |
47 |

{{ intern.user.get_full_name }}

48 |

{{ intern.department }}

49 |
50 |
51 | 52 |
53 |
54 | 55 |

{{ intern.user.email }}

56 |
57 |
58 | 59 |

{{ intern.phone_number }}

60 |
61 |
62 | 63 |

{{ intern.organization.name }}

64 |
65 |
66 | 67 | 69 | {% if intern.is_active %}Active{% else %}Inactive{% endif %} 70 | 71 |
72 |
73 |
74 |
75 | 76 | 77 |
78 |
79 |

Location Activity

80 |
81 |
82 |
83 |
84 |
85 |
86 | 87 |
88 |
89 |

Total Check-ins

90 |

{{ locations|length }}

91 |
92 |
93 |
94 |
95 |
96 |
97 | 98 |
99 |
100 |

Inside Geofence

101 |

{{ locations|length|add:"-violations" }}

102 |
103 |
104 |
105 |
106 |
107 |
108 | 109 |
110 |
111 |

Violations

112 |

{{ violations }}

113 |
114 |
115 |
116 |
117 | 118 |
119 |
120 |

Recent Locations

121 |
122 | 127 |
128 |
129 |
130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | {% for loc in locations|slice:":5" %} 140 | 141 | 144 | 147 | 157 | 158 | {% endfor %} 159 | 160 |
TimeLocationStatus
142 | {{ loc.timestamp|date:"H:i" }} 143 | 145 | {{ loc.address|truncatechars:30 }} 146 | 148 | 150 | {% if loc.is_inside_geofence %} 151 | Inside 152 | {% else %} 153 | Outside 154 | {% endif %} 155 | 156 |
161 |
162 |
163 | 164 | 165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | 173 | 222 | {% endblock %} -------------------------------------------------------------------------------- /base/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.shortcuts import render, redirect, get_object_or_404 3 | from django.contrib.auth.decorators import login_required 4 | from django.http import JsonResponse 5 | from django.views.decorators.csrf import csrf_exempt 6 | from django.contrib import messages 7 | from django.contrib.gis.geos import Point 8 | from django.contrib.gis.measure import D 9 | from geopy.geocoders import Nominatim 10 | from .models import User, Organization, InternProfile, LocationLog, Department 11 | from .forms import InternRegistrationForm, OrganizationForm, CustomAuthenticationForm 12 | from datetime import datetime, timedelta 13 | from django.contrib.auth import logout, authenticate, login 14 | 15 | 16 | 17 | # Utility Functions 18 | def get_address_from_coords(lat, lng): 19 | """Reverse geocode coordinates to get address""" 20 | geolocator = Nominatim(user_agent="intern_tracker") 21 | location = geolocator.reverse(f"{lat}, {lng}") 22 | return location.address if location else "Unknown location" 23 | 24 | def check_geofence(point, organization): 25 | """Check if point is within organization's geofence""" 26 | return organization.location.distance(point) * 100000 <= organization.geofence_radius 27 | 28 | # Authentication Views 29 | def register(request): 30 | if request.method == 'POST': 31 | form = InternRegistrationForm(request.POST) 32 | if form.is_valid(): 33 | user = form.save() 34 | login(request, user) 35 | return redirect('dashboard') 36 | else: 37 | form = InternRegistrationForm() 38 | 39 | return render(request, 'register.html', {'form': form}) 40 | 41 | def load_departments(request): 42 | """AJAX endpoint for loading departments""" 43 | org_id = request.GET.get('organization') 44 | departments = Department.objects.filter( 45 | organization_id=org_id, 46 | is_active=True 47 | ).order_by('name') 48 | return render(request, 'departments_dropdown.html', {'departments': departments}) 49 | 50 | def user_login(request): 51 | if request.user.is_authenticated: 52 | return redirect('dashboard') 53 | 54 | if request.method == 'POST': 55 | form = CustomAuthenticationForm(request, data=request.POST) 56 | if form.is_valid(): 57 | username = form.cleaned_data.get('username') 58 | password = form.cleaned_data.get('password') 59 | user = authenticate(request, username=username, password=password) 60 | 61 | if user is not None: 62 | login(request, user) 63 | messages.success(request, f"Welcome back, {user.first_name}!") 64 | return redirect('dashboard') 65 | else: 66 | messages.error(request, "Invalid username or password.") 67 | else: 68 | messages.error(request, "Please correct the errors below.") 69 | else: 70 | form = CustomAuthenticationForm() 71 | 72 | return render(request, 'login.html', {'form': form}) 73 | 74 | # Dashboard Views 75 | @login_required 76 | def dashboard(request): 77 | if not hasattr(request.user, 'internprofile'): 78 | return redirect('profile_complete') 79 | 80 | intern = request.user.internprofile 81 | org = intern.organization 82 | today = datetime.now().date() 83 | 84 | # Get today's locations 85 | locations = LocationLog.objects.filter( 86 | intern=intern, 87 | timestamp__date=today 88 | ).order_by('-timestamp')[:10] 89 | 90 | # Geofence violation count 91 | violations_today = LocationLog.objects.filter( 92 | intern=intern, 93 | is_inside_geofence=False, 94 | timestamp__date=today 95 | ).count() 96 | 97 | context = { 98 | 'intern': intern, 99 | 'organization': org, 100 | 'locations': locations, 101 | 'violations_today': violations_today, 102 | 'mapbox_access_token': 'your_mapbox_access_token', 103 | } 104 | return render(request, 'dashboard.html', context) 105 | from django.http import JsonResponse 106 | from django.views.decorators.http import require_GET 107 | from .models import Department 108 | 109 | @require_GET 110 | def get_departments(request): 111 | """API endpoint for fetching departments by organization""" 112 | org_id = request.GET.get('organization') 113 | 114 | if not org_id: 115 | return JsonResponse([], safe=False) 116 | 117 | departments = Department.objects.filter( 118 | organization_id=org_id, 119 | is_active=True 120 | ).order_by('name').values('id', 'name') 121 | 122 | return JsonResponse(list(departments), safe=False) 123 | 124 | def profile_complete(request): 125 | if hasattr(request.user, 'internprofile'): 126 | return redirect('dashboard') 127 | 128 | if request.method == 'POST': 129 | form = ProfileCompletionForm(request.POST, user=request.user) 130 | if form.is_valid(): 131 | profile = form.save(commit=False) 132 | profile.user = request.user 133 | profile.save() 134 | return redirect('dashboard') 135 | else: 136 | form = ProfileCompletionForm(user=request.user) 137 | 138 | # Get initial departments if user already has an organization selected 139 | departments = [] 140 | if hasattr(request.user, 'internprofile') and request.user.internprofile.organization: 141 | departments = Department.objects.filter( 142 | organization=request.user.internprofile.organization, 143 | is_active=True 144 | ).order_by('name') 145 | 146 | return render(request, 'profile_complete.html', { 147 | 'form': form, 148 | 'departments': departments 149 | }) 150 | 151 | # Location Tracking API 152 | @csrf_exempt 153 | @login_required 154 | def update_location(request): 155 | if request.method == 'POST': 156 | try: 157 | data = json.loads(request.body) 158 | lat = float(data['latitude']) 159 | lng = float(data['longitude']) 160 | point = Point(lng, lat, srid=4326) 161 | 162 | intern = request.user.internprofile 163 | org = intern.organization 164 | 165 | # Check geofence status 166 | is_inside = check_geofence(point, org) 167 | address = get_address_from_coords(lat, lng) 168 | 169 | # Save location 170 | LocationLog.objects.create( 171 | intern=intern, 172 | point=point, 173 | accuracy=data.get('accuracy'), 174 | address=address, 175 | is_inside_geofence=is_inside 176 | ) 177 | 178 | return JsonResponse({ 179 | 'status': 'success', 180 | 'is_inside': is_inside, 181 | 'address': address 182 | }) 183 | except Exception as e: 184 | return JsonResponse({'status': 'error', 'message': str(e)}, status=400) 185 | return JsonResponse({'status': 'error'}, status=405) 186 | 187 | # Intern Management 188 | @login_required 189 | def intern_list(request): 190 | if not request.user.is_supervisor: 191 | messages.error(request, "You don't have permission to view this page") 192 | return redirect('dashboard') 193 | 194 | organization = request.user.supervisorprofile.organization 195 | interns = InternProfile.objects.filter(organization=organization) 196 | 197 | return render(request, 'list.html', { 198 | 'interns': interns, 199 | 'organization': organization 200 | }) 201 | 202 | @login_required 203 | def intern_detail(request, pk): 204 | intern = get_object_or_404(InternProfile, pk=pk) 205 | 206 | # Verify supervisor has access to this intern 207 | if (request.user.is_supervisor and 208 | request.user.supervisorprofile.organization != intern.organization): 209 | messages.error(request, "You don't have permission to view this intern") 210 | return redirect('intern_list') 211 | 212 | # Get time filter from query params 213 | time_filter = request.GET.get('time', 'today') 214 | 215 | if time_filter == 'week': 216 | date_filter = datetime.now() - timedelta(days=7) 217 | elif time_filter == 'month': 218 | date_filter = datetime.now() - timedelta(days=30) 219 | else: # today 220 | date_filter = datetime.now() - timedelta(days=1) 221 | 222 | locations = LocationLog.objects.filter( 223 | intern=intern, 224 | timestamp__gte=date_filter 225 | ).order_by('-timestamp') 226 | 227 | violations = locations.filter(is_inside_geofence=False) 228 | 229 | return render(request, 'detail.html', { 230 | 'intern': intern, 231 | 'locations': locations[:50], # Limit to 50 most recent 232 | 'violations': violations.count(), 233 | 'time_filter': time_filter 234 | }) 235 | 236 | # Organization Management 237 | @login_required 238 | def organization_dashboard(request): 239 | if not request.user.is_supervisor: 240 | messages.error(request, "Access denied") 241 | return redirect('dashboard') 242 | 243 | org = request.user.supervisorprofile.organization 244 | interns = InternProfile.objects.filter(organization=org) 245 | 246 | # Stats for dashboard 247 | active_interns = interns.filter(is_active=True).count() 248 | violations_today = LocationLog.objects.filter( 249 | intern__organization=org, 250 | is_inside_geofence=False, 251 | timestamp__date=datetime.now().date() 252 | ).count() 253 | 254 | return render(request, 'dashboard.html', { 255 | 'organization': org, 256 | 'active_interns': active_interns, 257 | 'violations_today': violations_today, 258 | 'mapbox_access_token': 'your_mapbox_access_token' 259 | }) 260 | 261 | @login_required 262 | def edit_organization(request): 263 | if not request.user.is_supervisor: 264 | messages.error(request, "Access denied") 265 | return redirect('dashboard') 266 | 267 | org = request.user.supervisorprofile.organization 268 | 269 | if request.method == 'POST': 270 | form = OrganizationForm(request.POST, instance=org) 271 | if form.is_valid(): 272 | form.save() 273 | messages.success(request, "Organization updated successfully") 274 | return redirect('organization_dashboard') 275 | else: 276 | form = OrganizationForm(instance=org) 277 | 278 | return render(request, 'edit.html', {'form': form}) 279 | 280 | # Reporting Views 281 | @login_required 282 | def location_history(request): 283 | intern = request.user.internprofile 284 | time_filter = request.GET.get('time', 'today') 285 | 286 | if time_filter == 'week': 287 | date_filter = datetime.now() - timedelta(days=7) 288 | elif time_filter == 'month': 289 | date_filter = datetime.now() - timedelta(days=30) 290 | else: # today 291 | date_filter = datetime.now() - timedelta(days=1) 292 | 293 | locations = LocationLog.objects.filter( 294 | intern=intern, 295 | timestamp__gte=date_filter 296 | ).order_by('-timestamp') 297 | 298 | return render(request, 'location_history.html', { 299 | 'locations': locations, 300 | 'time_filter': time_filter 301 | }) 302 | 303 | @login_required 304 | def geofence_violations(request): 305 | if request.user.is_intern: 306 | intern = request.user.internprofile 307 | violations = LocationLog.objects.filter( 308 | intern=intern, 309 | is_inside_geofence=False 310 | ).order_by('-timestamp') 311 | else: # supervisor 312 | org = request.user.supervisorprofile.organization 313 | violations = LocationLog.objects.filter( 314 | intern__organization=org, 315 | is_inside_geofence=False 316 | ).order_by('-timestamp') 317 | 318 | return render(request, 'geofence_violations.html', { 319 | 'violations': violations 320 | }) 321 | 322 | def user_logout(request): 323 | logout(request) 324 | messages.success(request, "You have been successfully logged out.") 325 | return redirect('login') --------------------------------------------------------------------------------