commit 0c3ba727bf811271d6986bc35643a18c24a8b6bf Author: John Mullins Date: Fri Aug 18 18:10:53 2023 +1000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c02ae02 --- /dev/null +++ b/.gitignore @@ -0,0 +1,136 @@ +# Django # +*.log +*.pot +*.pyc +__pycache__ +db.sqlite3 +media + +# Backup files # +*.bak + +# If you are using PyCharm # +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# File-based project format +*.iws + +# IntelliJ +out/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Python # +*.py[cod] +*$py.class + +# Distribution / packaging +.Python build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery +celerybeat-schedule.* + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# sftp configuration file +sftp-config.json + +# Package control specific files Package +Control.last-run +Control.ca-list +Control.ca-bundle +Control.system-ca-bundle +GitHub.sublime-settings + +# Visual Studio Code # +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history + + +/busManager/busManager/settings.py +/busManager/coord/migrations +/busManager/coord/adminClone.py +/busManager/coord/adminCloneOld.py \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ + diff --git a/busManager/busManager/__init__.py b/busManager/busManager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/busManager/busManager/asgi.py b/busManager/busManager/asgi.py new file mode 100644 index 0000000..dc5b877 --- /dev/null +++ b/busManager/busManager/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for busManager project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'busManager.settings') + +application = get_asgi_application() diff --git a/busManager/busManager/settingsTemplate.py b/busManager/busManager/settingsTemplate.py new file mode 100644 index 0000000..f07c456 --- /dev/null +++ b/busManager/busManager/settingsTemplate.py @@ -0,0 +1,125 @@ +""" +Django settings for busManager project. + +Generated by 'django-admin startproject' using Django 4.2.4. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = False + +ALLOWED_HOSTS = ['*'] + + +# Application definition + +INSTALLED_APPS = [ + 'coord.apps.CoordConfig', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'import_export', +] + +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', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'busManager.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / "busManager/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', + ], + }, + }, +] + +WSGI_APPLICATION = 'busManager.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'Australia/Melbourne' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/busManager/busManager/urls.py b/busManager/busManager/urls.py new file mode 100644 index 0000000..44007c2 --- /dev/null +++ b/busManager/busManager/urls.py @@ -0,0 +1,25 @@ +""" +URL configuration for busManager project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +admin.site.site_header = "Bus Portal Admin" + +urlpatterns = [ + path('report/', include("coord.urls")), + path('admin/', admin.site.urls), +] diff --git a/busManager/busManager/wsgi.py b/busManager/busManager/wsgi.py new file mode 100644 index 0000000..584cea5 --- /dev/null +++ b/busManager/busManager/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for busManager project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'busManager.settings') + +application = get_wsgi_application() diff --git a/busManager/coord/__init__.py b/busManager/coord/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/busManager/coord/admin.py b/busManager/coord/admin.py new file mode 100644 index 0000000..5b35405 --- /dev/null +++ b/busManager/coord/admin.py @@ -0,0 +1,199 @@ +import csv + +from django.contrib.admin.utils import unquote +from django.core.exceptions import PermissionDenied +from django.forms import widgets +from django.contrib import admin +from django.http import HttpResponse, Http404 +from django.urls import reverse +from django.utils.translation import gettext_lazy +from django.utils.html import format_html +from django.utils.http import urlencode +from import_export.admin import ImportExportModelAdmin + +from .adminClone import ClonableModelAdmin +from .models import * +from .views import bus_roll + + +class ExportCsvMixin: + def export_as_csv(self, request, queryset): + meta = self.model._meta + field_names = [field.name for field in meta.fields] + + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta) + writer = csv.writer(response) + + writer.writerow(field_names) + for obj in queryset: + row = writer.writerow([getattr(obj, field) for field in field_names]) + + return response + + export_as_csv.short_description = "Export Selected" + + +class BusRollMixin: + + def bus_roll(self, request, queryset): + return bus_roll(request, queryset) + + def email_company(self, request, queryset): + pass + + email_company.short_description = "Email Bus Roll to Company" + + +class HiddenNowTime(widgets.TimeInput): + pass + + +class MyImportExportModelAdmin(ImportExportModelAdmin): + def has_import_permission(self, request): + return request.user.is_superuser + + +@admin.register(Company) +class CompanyAdmin(admin.ModelAdmin): + list_display = ["name", "buses"] + + def buses(self, obj): + count = obj.bus_set.count() + url = ( + reverse("admin:coord_bus_changelist") + + "?" + + urlencode({"company__id": f"{obj.id}"}) + ) + return format_html('{} Buses', url, count) + + +class DriverInline(admin.StackedInline): + model = Driver + extra = 1 + + +class BusStopInline(admin.TabularInline): + model = BusStop + extra = 0 + ordering = ("am_time",) + + +@admin.register(Bus) +class BusesAdmin(MyImportExportModelAdmin, admin.ModelAdmin, BusRollMixin): + list_filter = ["company"] + list_display = ["route_name", "company", "contract_number"] + actions = ["email_company", "bus_roll"] + inlines = [DriverInline, BusStopInline] + + +@admin.register(BusStop) +class BusStopAdmin(MyImportExportModelAdmin, admin.ModelAdmin): + list_filter = ["bus__company", "bus__route_name"] + list_display = ["__str__", "am_time", "pm_time", "address"] + search_fields = ["bus__route_name", "address"] + + # change_form_template = 'admin/change_form_busStops.html' + + def export_as_csv(self, request, queryset): + pass + + +@admin.register(Suburb) +class SuburbsAdmin(MyImportExportModelAdmin, admin.ModelAdmin): + list_filter = ["state"] + + +class TravellerRouteInline(admin.TabularInline): + model = TravellerRoute + extra = 0 + + +@admin.register(Traveller) +class TravellerAdmin(MyImportExportModelAdmin, admin.ModelAdmin): + list_display = ["first_name", "last_name", "school", "year_level", "residential_address", "residential_suburb"] + list_filter = ["school", "eligibility_status", "bus_stops__bus", "residential_suburb"] + search_fields = ["first_name", "last_name", "residential_address"] + inlines = [TravellerRouteInline] + readonly_fields = ["fare_paying"] + fieldsets = [ + (None, { + 'fields': [ + "school", + "first_name", + "last_name", + "dob", + "year_level", + ] + }), + ('Address', { + 'classes': ('collapse',), + 'fields': [ + "residential_address", + "residential_suburb", + "postal_address", + "postal_suburb", + ] + }), + ('Office Use', { + 'classes': ('collapse',), + 'fields': [ + "distance_to_school", + "travel_start_date", + "travel_end_date", + "eligibility_status", + "fare_paying", + "assessment_date", + "application_form_completed", + "parent_notified", + "seat_number", + ] + }), + ('Adult Contacts', { + 'classes': ('collapse',), + 'fields': [ + "parent_A_firstname", + "parent_A_lastname", + "parent_A_phone", + "parent_A_email", + "parent_B_firstname", + "parent_B_lastname", + "parent_B_phone", + "parent_B_email", + "emergency_contact_A_firstname", + "emergency_contact_A_lastname", + "emergency_contact_A_phone", + "emergency_contact_A_relation", + "emergency_contact_B_firstname", + "emergency_contact_B_lastname", + "emergency_contact_B_phone", + "emergency_contact_B_relation" + ] + }), + (None, {'fields': ["notes", "shuttle"]}) + ] + # list_display_links = None + + actions = ["yearly_rollover"] + + def yearly_rollover(self, request, queryset): + pass + + +@admin.register(TravellerRoute) +class TravellerRouteAdmin(MyImportExportModelAdmin, admin.ModelAdmin): + list_display = ["traveller", "busStop"] + + +@admin.register(School) +class SchoolAdmin(MyImportExportModelAdmin, admin.ModelAdmin): + pass + + +@admin.register(Setting) +class SettingAdmin(MyImportExportModelAdmin, admin.ModelAdmin): + list_display = ["name", "value"] + + +admin.site.register(Shuttle) +admin.site.register(Driver) diff --git a/busManager/coord/apps.py b/busManager/coord/apps.py new file mode 100644 index 0000000..397e318 --- /dev/null +++ b/busManager/coord/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CoordConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'coord' diff --git a/busManager/coord/models.py b/busManager/coord/models.py new file mode 100644 index 0000000..f8bb83d --- /dev/null +++ b/busManager/coord/models.py @@ -0,0 +1,282 @@ +from datetime import datetime + +from django.db import models +from django.db.models import Model + + +class Setting(models.Model): + name = models.CharField(max_length=20, unique=True) + value = models.CharField(max_length=20, blank=True) + + def __str__(self): + return self.name + +class Suburb(models.Model): + STATE = [ + ("VIC", "Victoria"), + ("NSW", "New South Wales"), + ("SA", "South Australia"), + ("ACT", "Australia Capital Territory"), + ("QLD", "Queensland"), + ("NT", "Northern Territory"), + ("WA", "Western Australia"), + ("TAS", "Tasmania"), + ] + name = models.CharField(max_length=30, unique=True) + state = models.CharField(max_length=3, choices=STATE) + postcode = models.PositiveSmallIntegerField() + distance = models.PositiveSmallIntegerField(blank=True, null=True) + + class Meta: + ordering = ["name"] + + def __str__(self): + return f"{self.name}, {self.state} {self.postcode}" + + +class School(models.Model): + name = models.CharField(max_length=30, unique=True) + shortName = models.CharField(max_length=10, unique=True) + address = models.CharField(max_length=50) + suburb = models.ForeignKey(Suburb, on_delete=models.CASCADE) + email = models.CharField(max_length=50, blank=True) + phone = models.CharField(max_length=15, blank=True) + principal_name = models.CharField(max_length=50, blank=True) + principal_phone = models.CharField(max_length=15, blank=True) + notes = models.TextField(blank=True) + + def __str__(self): + return self.name + + +class Company(models.Model): + name = models.CharField(max_length=50, unique=True) + contact_name = models.CharField(max_length=50, blank=True) + contact_number = models.CharField(max_length=15, blank=True) + contact_mobile = models.CharField(max_length=15, blank=True) + contact_email = models.CharField(max_length=50, blank=True) + address = models.CharField(max_length=50, blank=True) + suburb = models.ForeignKey(Suburb, on_delete=models.CASCADE) + notes = models.TextField(blank=True) + + class Meta: + verbose_name_plural = "Companies" + + def __str__(self): + return f"{self.name}" + + +class Bus(models.Model): + company = models.ForeignKey(Company, on_delete=models.CASCADE) + route_name = models.CharField(max_length=50, unique=True) + contract_number = models.CharField(max_length=20, blank=True) + registration = models.CharField(max_length=10, blank=True) + seating_capacity = models.SmallIntegerField() + make = models.CharField(max_length=15, blank=True) + model = models.CharField(max_length=15, blank=True) + notes = models.TextField(blank=True) + + class Meta: + verbose_name_plural = "Buses" + ordering = ["route_name"] + + def __str__(self): + return f"{self.route_name}" + + +class Shuttle(models.Model): + bus = models.ForeignKey(Bus, on_delete=models.CASCADE) + name = models.CharField(max_length=20) + school = models.ForeignKey(School, on_delete=models.CASCADE) + + class Meta: + ordering = ["school__name"] + + def __str__(self): + return f"{self.name}, {self.bus.route_name} -> {self.school.name}" + + +class Driver(models.Model): + bus = models.ForeignKey(Bus, on_delete=models.CASCADE) + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + phone_number = models.CharField(max_length=15, blank=True) + + class Meta: + ordering = ["last_name"] + + def __str__(self): + return f"{self.first_name} {self.last_name} - {self.bus.route_name}" + + +class BusStop(models.Model): + bus = models.ForeignKey(Bus, on_delete=models.CASCADE) + am_time = models.TimeField() + pm_time = models.TimeField() + address = models.CharField(max_length=100) + notes = models.TextField(blank=True) + + class Meta: + ordering = ["bus__route_name", "am_time"] + + def get_stop_number(self): + return BusStop.objects.filter(bus=self.bus, am_time__lt=self.am_time).count() + 1 + + def __str__(self): + return f"{self.bus.route_name} #{self.get_stop_number()} - {self.address}" + + +class Traveller(models.Model): + YEAR = [ + ("PS", "PreSchool"), + ("00", "Year 00"), + ("01", "Year 01"), + ("02", "Year 02"), + ("03", "Year 03"), + ("04", "Year 04"), + ("05", "Year 05"), + ("06", "Year 06"), + ("07", "Year 07"), + ("08", "Year 08"), + ("09", "Year 09"), + ("10", "Year 10"), + ("11", "Year 11"), + ("12", "Year 12"), + ("AL", "Adult Learner"), + ] + + ELIGIBILITY_STATUS = [ + ("1", "Eligible"), + ("2", "Fare Payer"), + ("3", "<4.8 Exemption"), + ("4", "Eligible waitlisted"), + ("5", "Ineligible waitlisted"), + ("6", "Kinder Exemption"), + ("7", "Tafe/Post Secondary Exemption"), + ("8", "Other Exemption"), + ] + + RELATIONS = [ + ("1", "Parent"), + ("2", "Step-Parent"), + ("3", "Foster Parent"), + ("4", "Host Family"), + ("5", "Sibling"), + ("6", "Grandparent"), + ("7", "Aunt/Uncle"), + ("8", "Cousin"), + ("9", "Carer"), + ("10", "Case Worker"), + ("11", "Friend/Other"), + ] + + school = models.ForeignKey(School, on_delete=models.PROTECT) + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + dob = models.DateField(blank=True, null=True) + year_level = models.CharField(max_length=2, choices=YEAR) + bus_stops = models.ManyToManyField(BusStop, through='TravellerRoute', blank=True) + distance_to_school = models.PositiveSmallIntegerField(blank=True, null=True) + residential_address = models.CharField(max_length=50, blank=True) + residential_suburb = models.ForeignKey(Suburb, on_delete=models.PROTECT, blank=True, null=True, related_name='residential_suburb') + postal_address = models.CharField(max_length=50, blank=True) + postal_suburb = models.ForeignKey(Suburb, on_delete=models.PROTECT, blank=True, null=True, related_name='postal_suburb') + + travel_start_date = models.DateField(blank=True, null=True) + travel_end_date = models.DateField(blank=True, null=True) + eligibility_status = models.CharField(max_length=1, choices=ELIGIBILITY_STATUS) + assessment_date = models.DateField(blank=True, null=True) + fee_per_term = models.DecimalField(decimal_places=2, max_digits=5, blank=True, null=True) + application_form_completed = models.BooleanField() + parent_notified = models.BooleanField() + seat_number = models.CharField(max_length=5, blank=True) + + parent_A_firstname = models.CharField(max_length=50, blank=True) + parent_A_lastname = models.CharField(max_length=50, blank=True) + parent_A_phone = models.CharField(max_length=15, blank=True) + parent_A_email = models.CharField(max_length=50, blank=True) + parent_B_firstname = models.CharField(max_length=50, blank=True) + parent_B_lastname = models.CharField(max_length=50, blank=True) + parent_B_phone = models.CharField(max_length=15, blank=True) + parent_B_email = models.CharField(max_length=50, blank=True) + emergency_contact_A_firstname = models.CharField(max_length=50, blank=True) + emergency_contact_A_lastname = models.CharField(max_length=50, blank=True) + emergency_contact_A_phone = models.CharField(max_length=15, blank=True) + emergency_contact_A_relation = models.CharField(max_length=50, choices=RELATIONS, blank=True) + emergency_contact_B_firstname = models.CharField(max_length=50, blank=True) + emergency_contact_B_lastname = models.CharField(max_length=50, blank=True) + emergency_contact_B_phone = models.CharField(max_length=15, blank=True) + emergency_contact_B_relation = models.CharField(max_length=50, choices=RELATIONS, blank=True) + notes = models.TextField(blank=True, verbose_name='Admin Notes') + shuttle = models.ForeignKey(Shuttle, on_delete=models.SET_NULL, blank=True, null=True) + + class Meta: + ordering = ["last_name"] + + def __str__(self): + return f"{self.first_name} {self.last_name}" + + def is_active(self): + if not self.travel_end_date: + return True + return datetime(self.travel_end_date.year, self.travel_end_date.month, self.travel_end_date.day) < datetime.today() + + def fare_paying(self): + if self.eligibility_status != "2": + return + cost_setting = Setting.objects.filter(name="TERM_TRAVEL_COST") + + if not cost_setting.exists(): + return "TERM_TRAVEL_COST not configured" + + cost = int(cost_setting.get().value) + + stops = 0 + for stop in TravellerRoute.objects.filter(traveller=self.id): + stops += stop.active_stops() + if stops > 1: + stops = 1 + return f"${str(cost*stops)}" + + +class TravellerRoute(models.Model): + traveller = models.ForeignKey(Traveller, on_delete=models.CASCADE) + busStop = models.ForeignKey(BusStop, on_delete=models.CASCADE) + mon_am = models.BooleanField(default=True) + mon_pm = models.BooleanField(default=True) + tue_am = models.BooleanField(default=True) + tue_pm = models.BooleanField(default=True) + wen_am = models.BooleanField(default=True) + wen_pm = models.BooleanField(default=True) + thu_am = models.BooleanField(default=True) + thu_pm = models.BooleanField(default=True) + fri_am = models.BooleanField(default=True) + fri_pm = models.BooleanField(default=True) + notes = models.TextField(blank=True, verbose_name="Driver Notes") + + def __str__(self): + return f"{self.busStop}" + + def active_stops(self): + stops = 0 + if self.mon_am: + stops += 1 + if self.mon_pm: + stops += 1 + if self.tue_am: + stops += 1 + if self.tue_pm: + stops += 1 + if self.wen_am: + stops += 1 + if self.wen_pm: + stops += 1 + if self.thu_am: + stops += 1 + if self.thu_pm: + stops += 1 + if self.fri_am: + stops += 1 + if self.fri_pm: + stops += 1 + return stops * 0.1 diff --git a/busManager/coord/templates/admin/admin_change_form.html b/busManager/coord/templates/admin/admin_change_form.html new file mode 100644 index 0000000..edc8ef8 --- /dev/null +++ b/busManager/coord/templates/admin/admin_change_form.html @@ -0,0 +1,7 @@ +{% extends 'admin/change_form.html' %} +{% load i18n admin_urls %} + +{% block object-tools-items %} +
  • {{ clone_verbose_name }}
  • + {{ block.super }} +{% endblock %} \ No newline at end of file diff --git a/busManager/coord/templates/admin/clone_form.html b/busManager/coord/templates/admin/clone_form.html new file mode 100644 index 0000000..0dd3e2d --- /dev/null +++ b/busManager/coord/templates/admin/clone_form.html @@ -0,0 +1,46 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls static admin_modify %} + +{% block extrahead %}{{ block.super }} + +{{ media }} +{% endblock %} + +{% block extrastyle %}{{ block.super }}{% endblock %} + +{% block coltype %}colM{% endblock %} + +{% block bodyclass %}{{ block.super }} app-{{ app_label }} model-{{ verbose_name }} change-form{% endblock %} + +{% if not is_popup %} +{% block breadcrumbs %} + +{% endblock %} +{% endif %} + +{% block content %}
    + +{% block after_field_sets %}{% endblock %} + +{% block inline_field_sets %} + +{% endblock %} + +{% block after_related_objects %}{% endblock %} + +{% block submit_buttons_bottom %}{% endblock %} + +{% block admin_change_form_document_ready %} + +{% endblock %} + +
    +{% endblock %} diff --git a/busManager/coord/templates/admin/submit_line.html b/busManager/coord/templates/admin/submit_line.html new file mode 100644 index 0000000..b2b2054 --- /dev/null +++ b/busManager/coord/templates/admin/submit_line.html @@ -0,0 +1,17 @@ +{% load i18n admin_urls %} +
    +{% block submit-row %} +{% if show_save %}{% endif %} +{% if show_save_as_new %}{% endif %} +{% if show_save_and_add_another %}{% endif %} +{% if show_save_and_continue %}{% endif %} +{% if show_close %} + {% url opts|admin_urlname:'changelist' as changelist_url %} + {% translate 'Close' %} +{% endif %} +{% if show_delete_link and original %} + {% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %} + {% translate "Delete" %} +{% endif %} +{% endblock %} +
    diff --git a/busManager/coord/templates/companies.html b/busManager/coord/templates/companies.html new file mode 100644 index 0000000..56a1c96 --- /dev/null +++ b/busManager/coord/templates/companies.html @@ -0,0 +1,12 @@ +

    Company List

    + + {% for item in object_list %} + + + + + + + + {% endfor %} +
    {{ item.name }}{{ item.contact_name }}{{ item.contact_number }}{{ item.contact_email }}{{ item.address }}
    \ No newline at end of file diff --git a/busManager/coord/templates/reports/bus_numbers.html b/busManager/coord/templates/reports/bus_numbers.html new file mode 100644 index 0000000..72a8e1f --- /dev/null +++ b/busManager/coord/templates/reports/bus_numbers.html @@ -0,0 +1,13 @@ +

    Bus Numbers

    + + + + + + {% for bus in buses %} + + + + + {% endfor %} +
    Route NameNumber of Students
    {{ bus.route_name }}{{ bus.num_travellers }}
    \ No newline at end of file diff --git a/busManager/coord/templates/reports/bus_roll.html b/busManager/coord/templates/reports/bus_roll.html new file mode 100644 index 0000000..f875fc4 --- /dev/null +++ b/busManager/coord/templates/reports/bus_roll.html @@ -0,0 +1,80 @@ + + +{% for route in routes %} +

    {{ route.route_name }}

    + {% for stop in route.stops %} +
    + + + + + + + + + + + +
    Stop Number #{{ stop.stop_num }}Pickup TimeDrop-off Time
    {{ stop.name }}{{ stop.am }}{{ stop.pm }}
    + + + + + + + + + + + + + + + + {% for traveller in stop.travellers %} + + + + + + + + + + + + + + + {% endfor %} +
    StudentFareMon AMMon PMTue AMTue PMWed AMWed PMThu AMThu PMFri AMFri PM
    {{ traveller.display }}{{ traveller.isFared }}
    +
    + {% endfor %} +

    +{% endfor %} \ No newline at end of file diff --git a/busManager/coord/templates/reports/bus_roll_old.html b/busManager/coord/templates/reports/bus_roll_old.html new file mode 100644 index 0000000..d283545 --- /dev/null +++ b/busManager/coord/templates/reports/bus_roll_old.html @@ -0,0 +1,33 @@ +{% for route in routes %} +

    {{ route.route_name }}

    + {% for stop in route.stops %} + + Stop Number + Address + Pickup Time + Drop-off Time + + + {{ stop.stop_num }} + {{ stop.name }} + {{ stop.am }} + {{ stop.pm }} + + {% for traveller in stop.travellers %} + + Student + Mon AM + Mon PM + Tue AM + Tue PM + + + {{ traveller.traveller }} + {{ traveller.mon_am }} + {{ traveller.mon_pm }} + {{ traveller.tue_am }} + {{ traveller.tue_pm }} + + {% endfor %} + {% endfor %} +{% endfor %} \ No newline at end of file diff --git a/busManager/coord/templates/reports/emergency_contacts.html b/busManager/coord/templates/reports/emergency_contacts.html new file mode 100644 index 0000000..49f094b --- /dev/null +++ b/busManager/coord/templates/reports/emergency_contacts.html @@ -0,0 +1,50 @@ + + +{% for route in routes %} +

    {{ route.bus }}

    +
    + + + + + + + + + + {% for traveller in route.travellers %} + + + + + + + + + {% endfor %} +
    StudentParent AParent BEmergency Contact AEmergency Contact BDriver notes
    {{ traveller.traveller }} ({{ traveller.traveller.school.shortName }}){{ traveller.parent_a }}{{ traveller.parent_b }}{{ traveller.contact_a }}{{ traveller.contact_b }}{{ traveller.note }}
    +
    +

    +{% endfor %} \ No newline at end of file diff --git a/busManager/coord/tests.py b/busManager/coord/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/busManager/coord/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/busManager/coord/urls.py b/busManager/coord/urls.py new file mode 100644 index 0000000..5633a6f --- /dev/null +++ b/busManager/coord/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("", views.bus_numbers, name="index"), + path("roll", views.bus_roll, name="Student Roll"), + path("clone", views.clone, name="Clone"), + path("contacts", views.emergency_contacts, name="Emergency Contacts") +] diff --git a/busManager/coord/views.py b/busManager/coord/views.py new file mode 100644 index 0000000..a0fd90d --- /dev/null +++ b/busManager/coord/views.py @@ -0,0 +1,109 @@ +from django.contrib.admin.views.decorators import staff_member_required +from django.http import HttpResponse +from django.shortcuts import render +from django.views.generic import ListView +from .models import Company, Bus, Traveller, BusStop, TravellerRoute + + +# def index(request): +# return HttpResponse("Hello, world. You're at the coord index.") + + +@staff_member_required +def bus_numbers(request): + buses = [] + for bus in Bus.objects.all(): + num_travellers = Traveller.objects.filter(bus_stops__bus=bus).count() + buses.append({'route_name': bus.route_name, 'num_travellers': num_travellers}) + return render(request, 'reports/bus_numbers.html', {'buses': buses}) + + +@staff_member_required +def bus_roll(request, queryset=None): + bus_routes = [] + if queryset is None: + buses = Bus.objects.all() + else: + buses = queryset + + for bus in buses: + bus_route = [] + + for bus_stop in BusStop.objects.filter(bus=bus): + traveller_list = [] + for trav_route in TravellerRoute.objects.filter(busStop=bus_stop): + traveller = trav_route.traveller + if not traveller.is_active(): + continue + is_fared = "---" + if traveller.eligibility_status == "2": + is_fared = "Y" + traveller_list.append({ + 'display': f"{traveller} ({traveller.get_year_level_display()}, {traveller.school.shortName})", + 'isFared': is_fared + }) + stop_result = { + 'stop_num': bus_stop.get_stop_number(), + 'name': bus_stop.address, + 'am': bus_stop.am_time, + 'pm': bus_stop.pm_time, + 'travellers': traveller_list + } + # print(traveller_list) + bus_route.append(stop_result) + + bus_routes.append({'route_name': bus.route_name, 'stops': bus_route}) + + return render(request, 'reports/bus_roll.html', {'routes': bus_routes}) + + +@staff_member_required +def emergency_contacts(request, queryset=None): + if queryset is None: + buses = Bus.objects.all() + else: + buses = queryset + + bus_routes = [] + for bus in buses: + traveller_list = [] + for travellerRoute in TravellerRoute.objects.filter(busStop__bus=bus): + traveller = travellerRoute.traveller + parent_a = "" + if travellerRoute.traveller.guardian_A_firstname: + parent_a = f"{traveller.guardian_A_firstname} {traveller.guardian_A_lastname} ({traveller.guardian_A_phone})" + parent_b = "" + if travellerRoute.traveller.guardian_B_firstname: + parent_b = f"{traveller.guardian_B_firstname} {traveller.guardian_B_lastname} ({traveller.guardian_B_phone})" + contact_a = "" + if travellerRoute.traveller.emergency_contact_A_firstname: + contact_a = f"{traveller.emergency_contact_A_firstname} {traveller.emergency_contact_A_lastname} ({traveller.emergency_contact_A_phone})" + contact_b = "" + if travellerRoute.traveller.emergency_contact_B_firstname: + contact_b = f"{traveller.emergency_contact_B_firstname} {traveller.emergency_contact_B_lastname} ({traveller.emergency_contact_B_phone})" + traveller_list.append({ + 'traveller': traveller, + 'parent_a': parent_a, + 'parent_b': parent_b, + 'contact_a': contact_a, + 'contact_b': contact_b, + 'note': travellerRoute.notes + }) + bus_routes.append({'bus': bus, 'travellers': traveller_list}) + + return render(request, 'reports/emergency_contacts.html', {'routes': bus_routes}) + + +@staff_member_required +def clone(request): + options = { + 'verbose_name': "Traveller", + 'app_label': "coord" + } + return render(request, 'admin/clone_form.html', options) + + +@staff_member_required +class CompanyList(ListView): + model = Company + template_name = "companies.html" \ No newline at end of file diff --git a/busManager/manage.py b/busManager/manage.py new file mode 100644 index 0000000..c766d71 --- /dev/null +++ b/busManager/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'busManager.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3e38630 Binary files /dev/null and b/requirements.txt differ