Created new models to replicate out of and split coord app

Moved helpers and views to their respective new apps
This commit is contained in:
st01765
2026-02-05 11:18:08 +11:00
parent ad4fd19cc7
commit b695dd8054
26 changed files with 1536 additions and 10 deletions
+2 -1
View File
@@ -46,7 +46,8 @@ INSTALLED_APPS = [
'rangefilter',
'common',
'transport',
'traveller'
'traveller',
'messaging'
]
if platform.system() == "Linux":
+11 -1
View File
@@ -1,3 +1,13 @@
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
# Register your models here.
from common.models import Suburb
class MyImportExportModelAdmin(ImportExportModelAdmin):
def has_import_permission(self, request):
return request.user.is_superuser
@admin.register(Suburb)
class SuburbsAdmin(MyImportExportModelAdmin, admin.ModelAdmin):
list_filter = ["state"]
+14
View File
@@ -0,0 +1,14 @@
from io import BytesIO
from django.http import HttpResponse
from django.template.loader import get_template
from xhtml2pdf import pisa
def render_to_pdf(template, context):
html = get_template(template).render(context)
result = BytesIO()
pdf = pisa.pisaDocument(BytesIO(html.encode("ISO-8859-1")), result)
if pdf.err:
return HttpResponse("Invalid PDF", status_code=400, content_type='text/plan')
return HttpResponse(result.getvalue(), content_type='application/pdf')
+21 -1
View File
@@ -1,3 +1,23 @@
from django.db import models
# Create your models here.
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}"
+1 -1
View File
@@ -21,7 +21,7 @@ def emergency_contacts(request):
def bus_roll(request):
return render_to_pdf('reports/bus_roll.html', bus_roll_context())
@staff_member_required
def sms_message(request, queryset):
if request.method == 'POST':
form = SMSForm(request.POST)
View File
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class MessagingConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'messaging'
+127
View File
@@ -0,0 +1,127 @@
from datetime import date
from django.core.mail import EmailMessage
from common.documents import render_to_pdf
from transport.context_busroll import bus_roll_context
from transport.context_helpers import emergency_contacts_context
from transport.models import Company
from traveller.context_helpers import school_roll_context
from traveller.models import School
def _getBCC(request):
if request.user.email:
return [request.user.email]
return []
def email_companies_bus_roll(request, query_set=None):
html_template = 'reports/bus_roll.html'
context = bus_roll_context(query_set)
for company in Company.objects.all():
if not company.contact_email:
continue
company_route = []
for route in context.get("routes"):
if route.get("bus").company == company:
company_route.append(route)
if not company_route:
continue
company_context = {'routes': company_route}
pdf = render_to_pdf(html_template, company_context)
subject = "Echuca Schools Bus Roll"
message = f"A new bus roll for {company.name} has been generated"
email_from = "bus.manager@education.vic.gov.au"
recipient = [company.contact_email]
email = EmailMessage(subject, message, email_from, recipient, _getBCC(request))
email.attach(f"school_bus_roll_{date.today()}.pdf", pdf.content)
email.send(fail_silently=True)
return render_to_pdf(html_template, context)
def email_companies_emergency_contacts(request, query_set=None):
html_template = 'reports/emergency_contacts.html'
context = emergency_contacts_context(query_set)
for company in Company.objects.all():
if not company.contact_email:
continue
company_route = []
for route in context.get("routes"):
if route.get("bus").company == company:
company_route.append(route)
if not company_route:
continue
company_context = {'routes': company_route}
pdf = render_to_pdf(html_template, company_context)
subject = "Echuca School Buses Emergency Contacts"
message = f"A new emergency contact list for {company.name} has been generated"
email_from = "bus.manager@education.vic.gov.au"
recipient = [company.contact_email]
email = EmailMessage(subject, message, email_from, recipient, _getBCC(request))
email.attach(f"school_bus_roll_{date.today()}.pdf", pdf.content)
email.send(fail_silently=True)
return render_to_pdf(html_template, context)
def email_school_roll(request, query_set):
html_template = 'reports/school_roll.html'
context = school_roll_context(query_set)
for school in School.objects.all():
if not school.email:
continue
school_route = []
for school_context in context.get("schools"):
if school_context.get("name") == school.name:
school_route.append(school_context)
if not school_route:
continue
school_context = {'schools': school_route}
pdf = render_to_pdf(html_template, school_context)
subject = "Echuca Schools Bus Roll"
message = f"A new bus roll for {school.name} has been generated"
email_from = "bus.manager@education.vic.gov.au"
recipient = [school.email]
email = EmailMessage(subject, message, email_from, recipient, _getBCC(request))
email.attach(f"school_bus_roll_{date.today()}.pdf", pdf.content)
email.send(fail_silently=True)
return render_to_pdf(html_template, context)
def email_school_shuttle_roll(request, queryset):
html_template = 'reports/bus_roll.html'
schools = []
for shuttle in queryset:
if shuttle.school not in schools:
schools.append(shuttle.school)
for school in schools:
buses = []
for shuttle in queryset:
if shuttle.school == school:
buses.append(shuttle.bus)
pdf = render_to_pdf(html_template, bus_roll_context(buses, include_bus_stops=False))
subject = "Echuca Schools Shuttle Roll"
message = f"A new shuttle roll for {school.name} has been generated"
email_from = "bus.manager@education.vic.gov.au"
recipient = [school.email]
email = EmailMessage(subject, message, email_from, recipient, _getBCC(request))
email.attach(f"school_shuttle_roll_{date.today()}.pdf", pdf.content)
email.send(fail_silently=True)
buses = []
for shuttle in queryset:
if shuttle.bus not in buses:
buses.append(shuttle.bus)
return render_to_pdf(html_template, bus_roll_context(buses, include_bus_stops=False))
+162
View File
@@ -0,0 +1,162 @@
import requests
from django import forms
from django.conf import settings
from django.http import HttpResponseRedirect
from django.shortcuts import render
def _get_token():
url = 'https://products.api.telstra.com/v2/oauth/token'
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
data = {
'client_id': settings.TELSTRA_AUTH['client_id'],
'client_secret': settings.TELSTRA_AUTH['client_secret'],
'grant_type': 'client_credentials'
}
result = requests.post(url, data=data, headers=headers)
if result.status_code != 200:
print("Bad request for telstra access_token:" + str(result.status_code))
return None
return result.json()['access_token']
def telstra_api_request(url, data=None, method="POST"):
url = 'https://products.api.telstra.com/' + url
token = _get_token()
headers = {
'Telstra-api-version': '3.x',
'Content-Language': 'en-au',
'Authorization': f'Bearer {token}',
'Accept': 'application/json',
'Accept-Charset': 'utf-8',
'Content-Type': 'application/json'
}
result = requests.request(method, url, json=data, headers=headers)
if result.status_code != 200:
print("Bad request:" + str(result.status_code))
print(result.content)
return False, result.content
return True, result.json()
def _send_message(to, msg):
url = 'messaging/v3/messages'
data = {
'to': to,
'from': _get_virtual_numbers(),
'messageContent': msg
}
result = telstra_api_request(url, data)
return result
def _get_virtual_numbers():
url = 'messaging/v3/virtual-numbers'
success, result = telstra_api_request(url, method='GET')
if not success:
print("No number found")
success, result = telstra_api_request(url, method='POST')
if not success:
return None
return result['virtualNumber']
numbers = result['virtualNumbers']
if numbers is None:
return None
return numbers[0]['virtualNumber']
class SMSForm(forms.Form):
_selected_action = forms.CharField(widget=forms.MultipleHiddenInput)
send_to_parents = forms.BooleanField(required=False)
send_to_emergency_contacts = forms.BooleanField(required=False)
only_include_active_travellers = forms.BooleanField(initial=True, required=False)
message = forms.CharField(label="Message", max_length=160, widget=forms.Textarea)
def _get_numbers(request, queryset):
send_to_parents = bool(request.POST.get("send_to_parents"))
send_to_emergency_contacts = bool(request.POST.get("send_to_emergency_contacts"))
only_include_active_travellers = bool(request.POST.get("only_include_active_travellers"))
numbers = []
for traveller in queryset:
if only_include_active_travellers and not traveller._is_active():
continue
for family in traveller.get_families():
family_numbers, _ = family.get_parsed_numbers(parents=send_to_parents, emergency=send_to_emergency_contacts)
if family_numbers:
numbers = numbers + family_numbers
return list(set(numbers)) # Remove duplicates
def _family_context(queryset):
family_set = []
for traveller in queryset:
families = traveller.get_families()
if len(families) == 0:
family_set.append({
'traveller': traveller.__str__(),
'has_failed_number': True
})
for family in families:
_, failed_numbers = family.get_parsed_numbers(True, True)
family_context = {
'traveller': traveller.__str__(),
'has_failed_number': len(failed_numbers) > 0
}
if family.parent_A_phone:
family_context['parent_A'] = f"{family.parent_A_firstname} {family.parent_A_lastname} ({family.parent_A_phone})"
if family.parent_B_phone:
family_context['parent_B'] = f"{family.parent_B_firstname} {family.parent_B_lastname} ({family.parent_B_phone})"
if family.emergency_contact_A_phone:
family_context['contact_A'] = f"{family.emergency_contact_A_firstname} {family.emergency_contact_A_lastname} ({family.emergency_contact_A_phone})"
if family.emergency_contact_B_phone:
family_context['contact_B'] = f"{family.emergency_contact_B_firstname} {family.emergency_contact_B_lastname} ({family.emergency_contact_B_phone})"
family_set.append(family_context)
return family_set
def send_sms(send_sms_mixin, request, queryset):
if not settings.TELSTRA_AUTH:
send_sms_mixin.message_user(request, "Telstra auth not configured", level="WARNING")
return HttpResponseRedirect(request.get_full_path())
if 'send' in request.POST:
numbers = _get_numbers(request, queryset)
if len(numbers) > 500:
send_sms_mixin.message_user(request, f"SMS failed. Total phone numbers ({len(numbers)}) exceeds 500", level="WARNING")
return HttpResponseRedirect(request.get_full_path())
if len(numbers) == 0:
send_sms_mixin.message_user(request, f"SMS failed. No numbers we selected", level="WARNING")
return HttpResponseRedirect(request.get_full_path())
result = _send_message(numbers, request.POST["message"])
send_sms_mixin.message_user(request, f"SMS has been sent to {len(numbers)} recipients")
return HttpResponseRedirect(request.get_full_path())
form = SMSForm(initial={'_selected_action': queryset.values_list('id', flat=True)})
family_set = _family_context(queryset)
return render(request, 'admin/sms_form.html', context={'form': form, 'items': family_set})
class SMSTestForm(forms.Form):
phone_number = forms.CharField(label="Phone number", max_length=20)
message = forms.CharField(label="Message", max_length=160, widget=forms.Textarea)
def send_sms_test(request):
if not settings.TELSTRA_AUTH:
return None
if request.method == "POST":
_send_message(request.POST["phone_number"], request.POST["message"])
return None
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
@@ -0,0 +1,24 @@
import os
import sys
import django
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Setup Django environment
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "busManager.settings")
django.setup()
from coord.models import Suburb as OldSuburb
from common.models import Suburb as NewSuburb
for old in OldSuburb.objects.all():
NewSuburb.objects.create(
id=old.id, # preserve PK
name=old.name,
state=old.state,
postcode=old.postcode,
distance=old.distance
)
print(f"Copied {OldSuburb.objects.count()} suburbs to common_suburb")
+77 -1
View File
@@ -1,3 +1,79 @@
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
from django.utils.http import urlencode
# Register your models here.
from common.admin import MyImportExportModelAdmin
from transport.admin_mixins import BusRollMixin, ShuttleRollMixin
from transport.models import Driver, BusStop, Company, Bus, Shuttle
@admin.register(Company)
class CompanyAdmin(MyImportExportModelAdmin, admin.ModelAdmin):
list_display = ["name", "contact_name", "email", "buses"]
def buses(self, obj):
count = obj.bus_set.count()
url = (
reverse("admin:coord_bus_changelist")
+ "?"
+ urlencode({"company__id__exact": f"{obj.id}"})
)
return format_html('<a href="{}">{} Buses</a>', url, count)
def email(self, obj):
return format_html('<a href="mailto:{}">{}</a>', obj.contact_email, obj.contact_email)
class DriverInline(admin.StackedInline):
model = Driver
extra = 0
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", "seating_capacity", "route_travellers"]
readonly_fields = ["traveller_count"]
actions = ["show_bus_roll", "show_bus_roll_on_date", "show_emergency_contacts", "sms_traveller_contacts", "email_bus_roll", "email_emergency_contacts"]
inlines = [DriverInline, BusStopInline]
fieldsets = [
(None, {'fields': [
"company", "route_name", "contract_number", "registration",
"traveller_count", "seating_capacity", "make", "model", "notes"
]})
]
def route_travellers(self, obj):
url = (
reverse("admin:coord_traveller_changelist")
+ "?"
+ urlencode({"bus_stops__bus__id__exact": f"{obj.id}"})
)
return format_html('<a href="{}">{} Travellers</a>', url, obj.traveller_count())
# @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"]
@admin.register(Shuttle)
class ShuttleAdmin(MyImportExportModelAdmin, admin.ModelAdmin, ShuttleRollMixin):
list_display = ["__str__", "school", "bus", "shuttle_travellers"]
actions = ["show_shuttle_roll", "email_shuttle_roll"]
def shuttle_travellers(self, obj):
url = (
reverse("admin:coord_traveller_changelist")
+ "?"
+ urlencode({"shuttle__id__exact": f"{obj.id}"})
)
return format_html('<a href="{}">{} Travellers</a>', url, obj.traveller_count())
+54
View File
@@ -0,0 +1,54 @@
from common.documents import render_to_pdf
from messaging.services.email import email_companies_bus_roll, email_companies_emergency_contacts, \
email_school_shuttle_roll
from messaging.services.sms import send_sms
from transport.context_busroll import bus_roll_context
from transport.context_helpers import emergency_contacts_context
from transport.forms import roll_date_selector
from traveller.models import Traveller
class BusRollMixin:
def show_bus_roll(self, request, queryset):
return render_to_pdf('reports/bus_roll.html', bus_roll_context(queryset))
def show_bus_roll_on_date(self, request, queryset):
return roll_date_selector(self, request, queryset)
def show_emergency_contacts(self, request, queryset):
return render_to_pdf('reports/emergency_contacts.html', emergency_contacts_context(queryset))
def sms_traveller_contacts(self, request, queryset):
travellers = None
for bus in queryset:
query = Traveller.objects.filter(bus_stops__bus=bus).filter(is_active=True).distinct()
if travellers is None:
travellers = query
else:
travellers.union(query)
return send_sms(self, request, travellers)
def email_bus_roll(self, request, queryset):
return email_companies_bus_roll(request, queryset)
def email_emergency_contacts(self, request, queryset):
return email_companies_emergency_contacts(request, queryset)
email_bus_roll.short_description = "Email Bus Roll to Company"
email_emergency_contacts.short_description = "Email Emergency Contacts to Company"
class ShuttleRollMixin:
def show_shuttle_roll(self, request, queryset):
if queryset is None:
buses = None
else:
buses = []
for shuttle in queryset:
if shuttle.bus not in buses:
buses.append(shuttle.bus)
return render_to_pdf('reports/bus_roll.html', bus_roll_context(buses, include_bus_stops=False))
def email_shuttle_roll(self, request, queryset):
return email_school_shuttle_roll(request, queryset)
+73
View File
@@ -0,0 +1,73 @@
import datetime
from transport.models import Bus, Shuttle, BusStop
from traveller.models import TravellerRoute, Traveller
def route_paged_context(bus, date=None):
table_header_size = 5
page_max_size = 45
page_size = 3 # Account for traveller numbers at the top of the first page
route_stops = []
for bus_stop in BusStop.objects.filter(bus=bus):
traveller_routes = TravellerRoute.objects.filter(busStop=bus_stop)
traveller_list = []
for trav_route in traveller_routes:
traveller = trav_route.traveller
if not traveller._is_active(date):
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_size = len(traveller_list)
page_break = False
page_size += table_header_size + stop_size
if page_size > page_max_size:
if len(route_stops) > 0: # Don't break the page if it's the first stop
page_break = True
page_size = table_header_size + stop_size
route_stops.append({
'stop_num': bus_stop.get_stop_number(),
'name': bus_stop.address,
'am': bus_stop.am_time,
'pm': bus_stop.pm_time,
'travellers': traveller_list,
'page_break': page_break
})
return route_stops
def shuttle_route_context(shuttle, date=None):
shuttle_travellers = []
for traveller in Traveller.objects.filter(shuttle=shuttle):
if traveller._is_active(date):
shuttle_travellers.append({
'display': f"{traveller} ({traveller.get_year_level_display()}, {traveller.school})",
})
return {'shuttle': shuttle, 'shuttle_travellers': shuttle_travellers, 'traveller_count': shuttle.traveller_count(date)}
def bus_roll_context(queryset=None, include_bus_stops=True, date=None):
bus_routes = []
if queryset is None:
buses = Bus.objects.all()
else:
buses = queryset
for bus in buses:
route_stops = []
if include_bus_stops:
route_stops = route_paged_context(bus=bus, date=date)
shuttle_routes = []
for shuttle in Shuttle.objects.filter(bus=bus):
shuttle_routes.append(shuttle_route_context(shuttle, date))
bus_routes.append({'name': bus.route_name, 'traveller_count': bus.traveller_count(date), 'seating_capacity': bus.seating_capacity, 'bus': bus, 'route_stops': route_stops, 'shuttle_routes': shuttle_routes})
if date is None:
date = datetime.date.today()
return {'routes': bus_routes, 'date': date.strftime('%Y-%m-%d')}
+88
View File
@@ -0,0 +1,88 @@
from transport.models import Bus, Driver, BusStop, Shuttle
from traveller.models import TravellerRoute, Traveller
def bus_summary_context():
bus_routes = []
for bus in Bus.objects.all():
drivers = []
for driver in Driver.objects.filter(bus=bus):
drivers.append(driver)
stops = []
for bus_stop in BusStop.objects.filter(bus=bus):
stops.append(bus_stop)
traveller_count = 0
for travellerRoute in TravellerRoute.objects.filter(busStop__bus=bus):
if travellerRoute.traveller._is_active():
traveller_count += 1
shuttle_name = ""
shuttle_count = 0
for shuttle in Shuttle.objects.filter(bus=bus):
if shuttle_name == "":
shuttle_name = shuttle.school.shortName
else:
shuttle_name += f", {shuttle.school.shortName}"
for traveller in Traveller.objects.filter(shuttle=shuttle):
if traveller._is_active():
shuttle_count += 1
over_capacity = traveller_count > bus.seating_capacity or shuttle_count > bus.seating_capacity
if shuttle_count == 0:
shuttle_count = ""
bus_routes.append({
'bus': bus,
'drivers': drivers,
'stops': stops,
'traveller_count': traveller_count,
'shuttle_name': shuttle_name,
'shuttle_count': shuttle_count,
'over_capacity': over_capacity,
})
return {'routes': bus_routes}
def emergency_contacts_context(queryset=None):
if queryset is None:
buses = Bus.objects.all()
else:
buses = queryset
bus_routes = []
for bus in buses:
drivers = []
for driver in Driver.objects.filter(bus=bus):
drivers.append(driver)
traveller_list = []
for travellerRoute in TravellerRoute.objects.filter(busStop__bus=bus):
traveller = travellerRoute.traveller
if not traveller._is_active():
continue
for family in traveller.get_families():
parent_a = ""
if family.parent_A_firstname:
parent_a = f"{family.parent_A_firstname} {family.parent_A_lastname} ({family.parent_A_phone})"
parent_b = ""
if family.parent_B_firstname:
parent_b = f"{family.parent_B_firstname} {family.parent_B_lastname} ({family.parent_B_phone})"
contact_a = ""
if family.emergency_contact_A_firstname:
contact_a = f"{family.emergency_contact_A_firstname} {family.emergency_contact_A_lastname} ({family.emergency_contact_A_phone})"
contact_b = ""
if family.emergency_contact_B_firstname:
contact_b = f"{family.emergency_contact_B_firstname} {family.emergency_contact_B_lastname} ({family.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, 'drivers': drivers, 'travellers': traveller_list})
return {'routes': bus_routes}
+23
View File
@@ -0,0 +1,23 @@
from datetime import datetime
from django import forms
from django.shortcuts import render
from common.documents import render_to_pdf
from transport.context_busroll import bus_roll_context
class RollDateSelector(forms.Form):
_selected_action = forms.CharField(widget=forms.MultipleHiddenInput)
def roll_date_selector(mixin, request, queryset):
if 'generate' in request.POST:
date = request.POST.get("date")
if date:
date = datetime.strptime(date, '%Y-%m-%d')
else:
date = None
return render_to_pdf('reports/bus_roll.html', bus_roll_context(queryset, date=date))
form = RollDateSelector(initial={'_selected_action': queryset.values_list('id', flat=True)})
return render(request, 'admin/date_selector.html', context={'form': form})
+102 -1
View File
@@ -1,3 +1,104 @@
from django.db import models
# Create your models here.
from common.models import Suburb
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}"
def traveller_count(self, date=None):
count = 0
from traveller.models import Traveller
for traveller in Traveller.objects.filter(bus_stops__bus=self):
if traveller._is_active(date):
count += 1
return count
class Shuttle(models.Model):
bus = models.ForeignKey(Bus, on_delete=models.CASCADE)
school = models.ForeignKey("traveller.School", on_delete=models.CASCADE)
custom_name = models.CharField(max_length=10, blank=True)
# transfer_school = models.ForeignKey(School, related_name='transfer_school', on_delete=models.CASCADE)
am_service = models.BooleanField(default=True)
pm_service = models.BooleanField(default=True)
class Meta:
ordering = ["school__name"]
def __str__(self):
custom_name = self.custom_name
if custom_name:
custom_name = f" ({self.custom_name})"
else:
custom_name = ""
return f"{self.school.shortName} <-> {self.bus.route_name}{custom_name}"
def traveller_count(self, date=None):
count = 0
from traveller.models import Traveller
for traveller in Traveller.objects.filter(shuttle=self):
if traveller._is_active(date):
count += 1
return count
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}"
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}"
+18 -1
View File
@@ -1,3 +1,20 @@
from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import render
# Create your views here.
from common.documents import render_to_pdf
from transport.context_busroll import bus_roll_context
from transport.context_helpers import bus_summary_context, emergency_contacts_context
@staff_member_required
def bus_summary(request):
return render(request, 'reports/bus_summary.html', bus_summary_context())
@staff_member_required
def emergency_contacts(request):
return render_to_pdf('reports/emergency_contacts.html', emergency_contacts_context())
@staff_member_required
def bus_roll(request):
return render_to_pdf('reports/bus_roll.html', bus_roll_context())
+109 -1
View File
@@ -1,3 +1,111 @@
from django.contrib import admin
from django.utils.html import format_html
from rangefilter.filters import DateRangeFilterBuilder
# Register your models here.
from common.admin import MyImportExportModelAdmin
from traveller.adminClone import CloneModelAdmin
from traveller.admin_mixins import SchoolRollMixin, TravellerRollMixin
from traveller.models import Family, TravellerRoute, Traveller, School
class FamilyInline(admin.StackedInline):
model = Family
classes = ['collapse']
extra = 0
clone_parent = "traveller"
class TravellerRouteInline(admin.TabularInline):
model = TravellerRoute
extra = 0
clone_parent = "traveller"
@admin.register(Traveller)
class TravellerAdmin(MyImportExportModelAdmin, CloneModelAdmin, TravellerRollMixin):
list_display = ["first_name", "last_name", "school", "year_level", "is_active", "address", "stop_route", "shuttle", "travel_start_date", "travel_end_date"]
list_filter = [
"is_active", "school", "year_level", "eligibility_status", "bus_stops__bus", "shuttle",
("travel_start_date", DateRangeFilterBuilder(
title="Start date"
)),
("travel_end_date", DateRangeFilterBuilder(
title="End date"
))
]
cloneable_fields = ["last_name"]
search_fields = ["first_name", "last_name", "address"]
inlines = [FamilyInline, TravellerRouteInline]
readonly_fields = ["travel_start_date", "travel_end_date", "created_on", "last_edit", "is_active", "address"]
actions = ["export_to_csv", "send_sms", "confirmation_letter", "letter_creator"]
fieldsets = [
(None, {
'fields': [
"is_active",
"school",
"first_name",
"last_name",
"year_level",
"dob",
"address",
]
}),
('Office Use', {
'classes': ('collapse',),
'fields': [
"distance_to_school",
"travel_start_date",
"travel_end_date",
"eligibility_status",
"term_1_paid",
"term_2_paid",
"term_3_paid",
"term_4_paid",
"assessment_date",
"application_form_completed",
"parent_notified",
"seat_number",
"created_on",
"last_edit",
]
}),
(None, {'fields': ["notes", "shuttle"]})
]
# list_display_links = None
def stop_route(self, obj):
from transport.models import BusStop
stops = BusStop.objects.filter(traveller__id__exact=obj.id)
if stops.count() == 0:
return ""
if stops.count() == 1:
return stops.first()
return "Multiple"
# def save_model(self, request, obj, form, change):
# if obj.is_archived and obj.travel_end_date is None:
# obj.is_archived = False
# super().save_model(request, obj, form, change)
# @admin.register(Family)
class FamilyAdmin(MyImportExportModelAdmin, admin.ModelAdmin):
list_display = ["traveller", "__str__",
"parent_A_firstname", "parent_A_lastname", "parent_A_phone",
"parent_B_firstname", "parent_B_lastname", "parent_B_phone",
"emergency_contact_A_firstname", "emergency_contact_A_lastname", "emergency_contact_A_phone",
"emergency_contact_B_firstname", "emergency_contact_B_lastname", "emergency_contact_B_phone"]
# @admin.register(TravellerRoute)
class TravellerRouteAdmin(MyImportExportModelAdmin, admin.ModelAdmin):
list_display = ["traveller", "busStop"]
@admin.register(School)
class SchoolAdmin(MyImportExportModelAdmin, admin.ModelAdmin, SchoolRollMixin):
list_display = ["__str__", "address", "suburb", "school_email", "phone"]
actions = ["email_travellers_to_school", "show_school_travellers", "export_travellers_to_csv"]
def school_email(self, obj):
return format_html('<a href="mailto:{}">{}</a>', obj.email, obj.email)
+161
View File
@@ -0,0 +1,161 @@
from functools import update_wrapper
from django.contrib.admin import ModelAdmin, helpers
from django.contrib.admin.exceptions import DisallowedModelAdminToField
from django.contrib.admin.options import TO_FIELD_VAR
from django.contrib.admin.utils import flatten_fieldsets, unquote
from django.shortcuts import redirect
from django.urls import reverse, path
from django.core.exceptions import PermissionDenied, FieldDoesNotExist
class CloneModelAdmin(ModelAdmin):
clone_verbose_name = "Clone"
change_form_template = 'admin/admin_change_form.html'
cloneable_fields = []
def get_urls(self):
# Not certain what this wrap() function is exactly. Just copied it from the django admin get_urls function
def wrap(view):
def wrapper(*args, **kwargs):
return self.admin_site.admin_view(view)(*args, **kwargs)
wrapper.model_admin = self
return update_wrapper(wrapper, view)
info = self.opts.app_label, self.opts.model_name
new_urlpatterns = [
path(
"<path:object_id>/clone/",
wrap(self.clone_view),
name="%s_%s_clone" % info,
),
]
original_urlpatterns = super(CloneModelAdmin, self).get_urls()
# Important to add custom urls before the existing ones.
# Last entry is <URLPattern '<path:object_id>/'> which will catch everything not already picked up
return new_urlpatterns + original_urlpatterns
def change_view(self, request, object_id, form_url='', extra_context=None):
url = reverse("admin:{0}_{1}_clone".format(self.opts.app_label, self.opts.model_name), args=[object_id])
extra_context = extra_context or {}
extra_context.update({
'clone_verbose_name': self.clone_verbose_name,
'clone_link': url,
})
return super(CloneModelAdmin, self).change_view(request, object_id, form_url, extra_context)
def clone_view(self, request, object_id, form_url='', extra_context=None):
to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
if to_field:
print("To field in clone_view. What is this? ",to_field)
if to_field and not self.to_field_allowed(request, to_field):
raise DisallowedModelAdminToField(
"The field %s cannot be referenced." % to_field
)
if not self.has_add_permission(request):
raise PermissionDenied
original_obj = self.get_object(request, unquote(object_id), to_field)
if original_obj is None:
return self._get_obj_does_not_exist_redirect(request, self.opts, object_id)
if not self.has_view_or_change_permission(request, original_obj):
raise PermissionDenied
fieldsets = self.get_fieldsets(request, original_obj)
model_form = self.get_form(request, original_obj, change=False, fields=flatten_fieldsets(fieldsets))
if request.method == "POST":
form = model_form(request.POST)
formsets, inline_instances = self._create_formsets(
request,
original_obj,
change=False,
)
form_validated = form.is_valid()
if form_validated:
new_object = self.save_form(request, form, change=False)
self.save_model(request, new_object, form, False)
self.clone_related(request, original_obj, new_object)
return redirect(reverse("admin:{0}_{1}_change".format(self.opts.app_label, self.opts.model_name), args=[new_object.pk]))
else:
new_obj = self.model()
for field in self.cloneable_fields:
setattr(new_obj, field, getattr(original_obj, field))
form = model_form(instance=new_obj)
formsets, inline_instances = self._create_formsets(request, original_obj, change=False)
admin_form = helpers.AdminForm(
form,
list(self.get_fieldsets(request)),
self.get_prepopulated_fields(request),
self.readonly_fields,
model_admin=self
)
media = self.media
inline_formsets = self.get_inline_formsets(request, formsets, inline_instances)
for inline_formset in inline_formsets:
media += inline_formset.media
title = u'{0} {1}'.format(self.clone_verbose_name, original_obj)
context = {
**self.admin_site.each_context(request),
"title": title,
"original": title,
"adminform": admin_form,
"is_popup": "_popup" in request.POST or "_popup" in request.GET,
"show_delete": False,
"media": media,
"inline_admin_formsets": inline_formsets,
"errors": helpers.AdminErrorList(form, formsets),
**(extra_context or {}),
}
context.update(extra_context or {})
return self.render_change_form(
request,
context,
form_url=form_url,
change=False,
add=True
)
def clone_related(self, request, original_obj, new_obj):
for inline in self.inlines:
if not inline.clone_parent:
continue
try:
inline.model._meta.get_field(inline.clone_parent)
except FieldDoesNotExist:
continue
inline_objects = inline.model.objects.filter(**{inline.clone_parent: original_obj})
for inline_object in inline_objects:
inline_object.pk = None
setattr(inline_object, inline.clone_parent, new_obj)
inline_object.save()
class InlineAdminFormSetFakeOriginal(helpers.InlineAdminFormSet):
def __iter__(self):
# the template requires the AdminInlineForm to have an `original`
# attribute, which is the model instance, in order to display the
# 'Delete' checkbox
# we don't have `original` because we are just providing initial
# data to the form, so we attach a "fake original" (something that
# evaluates to True) to fool the template and make is display
# the 'Delete' checkbox
# needless to say this is a terrible hack and will break in future
# django versions :)
for inline_form in super(InlineAdminFormSetFakeOriginal, self).__iter__():
if inline_form.form.initial:
inline_form.original = True
yield inline_form
+60
View File
@@ -0,0 +1,60 @@
import csv
from datetime import date
from django.http import HttpResponse
from common.documents import render_to_pdf
from messaging.services.email import email_school_roll
from messaging.services.sms import send_sms
from traveller.context_helpers import school_roll_context, traveller_route_context, confirmation_letter_context, \
traveller_roll_context
from traveller.models import TravellerRoute
class SchoolRollMixin:
def email_travellers_to_school(self, request, queryset):
return email_school_roll(request, queryset)
def show_school_travellers(self, request, queryset):
return render_to_pdf('reports/school_roll.html', school_roll_context(queryset))
def show_school_travellers_on_date(self, request, queryset):
return render_to_pdf('reports/school_roll.html', school_roll_context(queryset))
def export_travellers_to_csv(self, request, queryset):
traveller_list = []
for school in queryset:
for travellerRoute in TravellerRoute.objects.filter(traveller__school=school):
if not travellerRoute.traveller._is_active():
continue
traveller_list.append(traveller_route_context(travellerRoute))
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f"attachment; filename=traveller_list_{date.today()}.csv"
writer = csv.DictWriter(response, fieldnames=traveller_list[0].keys())
writer.writeheader()
writer.writerows(traveller_list)
return response
class TravellerRollMixin:
def confirmation_letter(self, request, queryset):
return render_to_pdf('mail/confirmation_letter.html', confirmation_letter_context(queryset))
def send_sms(self, request, queryset):
return send_sms(self, request, queryset)
def export_to_csv(self, request, queryset):
traveller_list = traveller_roll_context(queryset)
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f"attachment; filename=traveller_list_{date.today()}.csv"
writer = csv.DictWriter(response, fieldnames=traveller_list[0].keys())
writer.writeheader()
writer.writerows(traveller_list)
return response
+90
View File
@@ -0,0 +1,90 @@
from django.db.models import Q
from transport.models import Bus
from traveller.models import TravellerRoute
def school_roll_context(queryset, date=None):
school_list = []
for school in queryset:
school_routes = []
query = Q(traveller__school=school) | Q(traveller__shuttle__school=school)
for bus in Bus.objects.all():
travellers = []
for trav_route in TravellerRoute.objects.filter(query).filter(busStop__bus=bus).order_by('busStop__am_time'):
traveller = trav_route.traveller
if not traveller._is_active(date):
continue
bus_stop = trav_route.busStop
display_name = f"{traveller} ({traveller.get_year_level_display()})"
if traveller.school != school:
display_name = f"{traveller} ({traveller.get_year_level_display()}, {traveller.school.shortName})"
is_fared = "---"
if traveller.eligibility_status == "2":
is_fared = "Y"
shuttle_name = " "
if traveller.shuttle:
shuttle_name = traveller.shuttle.bus
travellers.append({
'display': display_name,
'isFared': is_fared,
'shuttle': shuttle_name,
'stop': f"#{bus_stop.get_stop_number()} - {bus_stop.address}",
'am_time': bus_stop.am_time,
'pm_time': bus_stop.pm_time
})
if travellers:
school_routes.append({
'bus': bus,
'travellers': travellers
})
school_list.append({"name": school.name, "routes": school_routes})
return {"schools": school_list}
def traveller_route_context(traveller_route):
traveller = traveller_route.traveller
bus_stop = traveller_route.busStop
families = traveller.get_families()
address = ""
for family in families:
if address:
address += ";"
address = address + f"{family.residential_address} {family.residential_suburb}"
return {
'first_name': traveller.first_name,
'last_name': traveller.last_name,
'active': traveller.is_active,
'school': traveller.school,
'dob': traveller.dob,
'year_level': traveller.year_level,
'address': address,
'start_date': traveller.travel_start_date,
'end_date': traveller.travel_end_date,
'eligibility': traveller.get_eligibility_status_display(),
'shuttle': traveller.shuttle,
'route': traveller_route.busStop.bus,
'stop': f"#{bus_stop.get_stop_number()} - {bus_stop.address}",
'pickup': bus_stop.am_time,
'drop-off': bus_stop.pm_time
}
def traveller_roll_context(queryset):
travellers = []
for traveller in queryset:
for traveller_route in TravellerRoute.objects.filter(traveller=traveller):
travellers.append(traveller_route_context(traveller_route))
return travellers
def confirmation_letter_context(queryset):
travellers = []
for traveller in queryset:
for travellerRoute in TravellerRoute.objects.filter(traveller=traveller):
travellers.append({
'traveller': traveller,
'stop': travellerRoute.busStop,
'shuttle': traveller.shuttle,
})
return {'travellers': travellers}
+292 -1
View File
@@ -1,3 +1,294 @@
from datetime import datetime
import phonenumbers
from django.core.exceptions import ValidationError
from django.db import models
# Create your models here.
from common.models import Suburb
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 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", "Ineligible"),
("3", "<4.8 Exemption"),
("4", "Eligible waitlisted"),
("5", "Ineligible waitlisted"),
("6", "Kinder Exemption"),
("7", "Tafe/Post Secondary Exemption"),
("8", "Other Exemption"),
]
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("transport.BusStop", through='TravellerRoute', blank=True)
distance_to_school = models.PositiveSmallIntegerField(blank=True, null=True)
address = models.CharField(max_length=100, default="")
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)
term_1_paid = models.BooleanField(default=False)
term_2_paid = models.BooleanField(default=False)
term_3_paid = models.BooleanField(default=False)
term_4_paid = models.BooleanField(default=False)
application_form_completed = models.BooleanField()
parent_notified = models.BooleanField()
seat_number = models.CharField(max_length=5, blank=True)
created_on = models.DateTimeField(auto_now_add=True, blank=True, null=True)
last_edit = models.DateTimeField(auto_now=True, blank=True, null=True)
is_archived = models.BooleanField(default=False, verbose_name="Archived")
is_active = models.BooleanField(default=False, verbose_name='Active')
notes = models.TextField(blank=True, verbose_name='Admin Notes')
shuttle = models.ForeignKey("transport.Shuttle", on_delete=models.SET_NULL, blank=True, null=True)
class Meta:
ordering = ["last_name", "first_name"]
def __str__(self):
return f"{self.first_name} {self.last_name}"
def save(self, *args, **kwargs):
self._update_active_status()
self._repopulate_address()
super(Traveller, self).save(*args, **kwargs)
def _is_active(self, date=None):
if date is None:
today = datetime.today()
date = datetime(today.year, today.month, today.day)
if not self.travel_start_date or datetime(self.travel_start_date.year, self.travel_start_date.month, self.travel_start_date.day) > date:
return False
if not self.travel_end_date:
return True
end_date = datetime(self.travel_end_date.year, self.travel_end_date.month, self.travel_end_date.day)
return end_date >= date
def _update_active_status(self):
new_start_date = None
new_end_date = None
for travellerRoute in TravellerRoute.objects.filter(traveller=self.id):
route_start = travellerRoute.travel_start_date
route_end = travellerRoute.travel_end_date
if route_start is not None:
if new_start_date is None or new_start_date > route_start:
new_start_date = route_start
if route_end is not None:
if new_end_date is None or new_end_date < route_end:
new_end_date = route_end
self.travel_start_date = new_start_date
self.travel_end_date = new_end_date
self.is_active = self._is_active()
def fare_paying(self):
if self.eligibility_status != "2":
return
cost_setting = 0
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"${round(cost*stops)}"
def _repopulate_address(self):
families = self.get_families()
if families.count() == 0:
self.address = ""
elif families.count() == 1:
family = families.first()
self.address = f"{family.residential_address} {family.residential_suburb}"
else:
self.address = "Multiple"
def get_families(self):
return Family.objects.filter(traveller__id__exact=self.id)
class Family(models.Model):
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"),
]
traveller = models.ForeignKey(Traveller, on_delete=models.CASCADE)
residential_address = models.CharField(max_length=50, blank=True)
residential_suburb = models.ForeignKey(Suburb, on_delete=models.PROTECT, blank=True, null=True,
related_name='family_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='family_postal_suburb')
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)
created_on = models.DateTimeField(auto_now_add=True, blank=True, null=True)
last_edit = models.DateTimeField(auto_now=True, blank=True, null=True)
class Meta:
verbose_name_plural = "Families"
def __str__(self):
return self.parent_names()
def clean(self):
valid_numbers, failed_numbers = self.get_parsed_numbers(True, True)
if len(failed_numbers) > 0:
raise ValidationError(f"Phone number {failed_numbers[0]} not valid")
def parent_names(self):
a_name = self.parent_A_firstname
b_name = self.parent_B_firstname
if a_name:
if b_name:
return f"{a_name} and {b_name}"
return a_name
elif b_name:
return b_name
return ""
def get_parsed_numbers(self, parents=False, emergency=False):
numbers = []
if parents:
if self.parent_A_phone:
numbers.append(self.parent_A_phone)
if self.parent_B_phone:
numbers.append(self.parent_B_phone)
if emergency:
if self.emergency_contact_A_phone:
numbers.append(self.emergency_contact_A_phone)
if self.emergency_contact_B_phone:
numbers.append(self.emergency_contact_B_phone)
valid_numbers = []
failed_numbers = []
for number in numbers:
try:
num = phonenumbers.parse(number, "AU")
if phonenumbers.is_valid_number(num):
valid_numbers.append(f"+{num.country_code}{num.national_number}")
else:
failed_numbers.append(number)
except:
failed_numbers.append(number)
return valid_numbers, failed_numbers
class TravellerRoute(models.Model):
traveller = models.ForeignKey(Traveller, on_delete=models.CASCADE)
busStop = models.ForeignKey("transport.BusStop", on_delete=models.CASCADE)
travel_start_date = models.DateField(blank=True, null=True)
travel_end_date = models.DateField(blank=True, null=True)
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")
created_on = models.DateTimeField(auto_now_add=True, blank=True, null=True)
last_edit = models.DateTimeField(auto_now=True, blank=True, null=True)
def __str__(self):
return f"{self.busStop}"
def save(self, *args, **kwargs):
super(TravellerRoute, self).save(*args, **kwargs)
self.traveller.save()
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
+15 -1
View File
@@ -1,3 +1,17 @@
from django.contrib.admin.views.decorators import staff_member_required
from django.http import HttpResponseRedirect
from django.shortcuts import render
# Create your views here.
from messaging.services.sms import SMSForm
@staff_member_required
def sms_message(request, queryset):
if request.method == 'POST':
form = SMSForm(request.POST)
if form.is_valid():
HttpResponseRedirect(request.get_full_path())
else:
form = SMSForm()
return render(request, 'admin/sms_form.html', context={'form': form, 'travellers': queryset})