diff --git a/busManager/busManager/settings.py b/busManager/busManager/settings.py
index 1a956cf..44c16eb 100644
--- a/busManager/busManager/settings.py
+++ b/busManager/busManager/settings.py
@@ -87,6 +87,12 @@ if "AZURE_CLIENT_ID" in os.environ:
# "PUBLIC_PATHS": ['/go/',], # Optional, public paths accessible by non-authenticated users
}
+if "TELSTRA_CLIENT_ID" in os.environ:
+ TELSTRA_AUTH = {
+ 'client_id': os.environ.get('TELSTRA_CLIENT_ID'),
+ 'client_secret': os.environ.get('TELSTRA_CLIENT_SECRET')
+ }
+
ROOT_URLCONF = 'busManager.urls'
TEMPLATES = [
diff --git a/busManager/coord/admin.py b/busManager/coord/admin.py
index 796cfc9..42dc63c 100644
--- a/busManager/coord/admin.py
+++ b/busManager/coord/admin.py
@@ -26,6 +26,16 @@ class BusRollMixin:
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)
@@ -137,7 +147,7 @@ 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_emergency_contacts", "email_bus_roll", "email_emergency_contacts"]
+ actions = ["show_bus_roll", "show_emergency_contacts", "sms_traveller_contacts", "email_bus_roll", "email_emergency_contacts"]
inlines = [DriverInline, BusStopInline]
fieldsets = [
(None, {'fields': [
diff --git a/busManager/coord/models.py b/busManager/coord/models.py
index 71aaccd..c805bf6 100644
--- a/busManager/coord/models.py
+++ b/busManager/coord/models.py
@@ -1,6 +1,7 @@
from datetime import datetime
import phonenumbers
+from django.core.exceptions import ValidationError
from django.db import models
@@ -271,9 +272,6 @@ class Traveller(models.Model):
else:
self.address = "Multiple"
- def get_parsed_numbers(self, parents=False, emergency=False):
- return [] # Moved to family model. To remove from traveller model
-
def get_families(self):
return Family.objects.filter(traveller__id__exact=self.id)
@@ -320,6 +318,11 @@ class Family(models.Model):
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
@@ -343,13 +346,19 @@ class Family(models.Model):
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:
- num = phonenumbers.parse(number, "AU")
- if phonenumbers.is_valid_number(num):
- valid_numbers.append(num.national_number)
- return valid_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):
diff --git a/busManager/coord/templates/admin/sms_form.html b/busManager/coord/templates/admin/sms_form.html
index 39b2d97..cf8ff13 100644
--- a/busManager/coord/templates/admin/sms_form.html
+++ b/busManager/coord/templates/admin/sms_form.html
@@ -10,12 +10,16 @@
Emergency B |
{% for item in items %}
-
- | {{ item }} |
- {{ item.parent_A_phone }} |
- {{ item.parent_B_phone }} |
- {{ item.emergency_contact_A_phone }} |
- {{ item.emergency_contact_B_phone }} |
+ {% if item.has_failed_number %}
+
+ {% else %}
+
+ {% endif %}
+ | {{ item.traveller }} |
+ {{ item.parent_A }} |
+ {{ item.parent_B }} |
+ {{ item.contact_A }} |
+ {{ item.contact_B }} |
{% endfor %}
diff --git a/busManager/coord/utils/send_sms.py b/busManager/coord/utils/send_sms.py
index e275924..84bbb01 100644
--- a/busManager/coord/utils/send_sms.py
+++ b/busManager/coord/utils/send_sms.py
@@ -1,6 +1,75 @@
+from django.conf import settings
from django.http import HttpResponseRedirect
from django import forms
from django.shortcuts import render
+import requests
+
+
+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):
@@ -8,32 +77,71 @@ class SMSForm(forms.Form):
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=320, widget=forms.Textarea)
+ 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 'confirm' in request.POST:
- message = request.POST["message"]
- send_to_parents = False
- if request.POST.get("send_to_parents"):
- send_to_parents = True
- send_to_emergency_contacts = False
- if request.POST.get("send_to_emergency_contacts"):
- send_to_emergency_contacts = True
- only_include_active_travellers = False
- if request.POST.get("only_include_active_travellers"):
- only_include_active_travellers = True
- total = 0
- numbers = []
- for traveller in queryset:
- if only_include_active_travellers and not traveller._is_active():
- continue
- numbers.append(traveller.get_parsed_numbers(parents=send_to_parents, emergency=send_to_emergency_contacts))
+ if not settings.TELSTRA_AUTH:
+ send_sms_mixin.message_user(request, "Telstra auth not configured", level="WARNING")
+ return HttpResponseRedirect(request.get_full_path())
- len(numbers)
- send_sms_mixin.message_user(request, f"SMS has been sent to {total} recipients")
+ 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)})
- return render(request, 'admin/sms_form.html', context={'form': form, 'items': queryset})
+ family_set = _family_context(queryset)
+
+ return render(request, 'admin/sms_form.html', context={'form': form, 'items': family_set})